Eduard Kuzmenko
4 years ago
16 changed files with 476 additions and 208 deletions
@ -0,0 +1,32 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import EventListenerBase from "../../helpers/eventListenerBase"; |
||||||
|
import rootScope from "../../lib/rootScope"; |
||||||
|
import SetTransition from "../singleTransition"; |
||||||
|
|
||||||
|
export default class AutocompleteHelper extends EventListenerBase<{ |
||||||
|
hidden: () => void, |
||||||
|
visible: () => void, |
||||||
|
}> { |
||||||
|
protected container: HTMLElement; |
||||||
|
|
||||||
|
constructor(appendTo: HTMLElement) { |
||||||
|
super(false); |
||||||
|
|
||||||
|
this.container = document.createElement('div'); |
||||||
|
this.container.classList.add('autocomplete-helper', 'z-depth-1'); |
||||||
|
|
||||||
|
appendTo.append(this.container); |
||||||
|
} |
||||||
|
|
||||||
|
public toggle(hide?: boolean) { |
||||||
|
hide = hide === undefined ? this.container.classList.contains('is-visible') : hide; |
||||||
|
SetTransition(this.container, 'is-visible', !hide, rootScope.settings.animationsEnabled ? 200 : 0, () => { |
||||||
|
this.dispatchEvent(hide ? 'hidden' : 'visible'); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,160 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import fastSmoothScroll from "../fastSmoothScroll"; |
||||||
|
import { cancelEvent } from "./cancelEvent"; |
||||||
|
import { attachClickEvent, detachClickEvent } from "./clickEvent"; |
||||||
|
import findUpAsChild from "./findUpAsChild"; |
||||||
|
import findUpClassName from "./findUpClassName"; |
||||||
|
|
||||||
|
type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight'; |
||||||
|
const HANDLE_EVENT = 'keydown'; |
||||||
|
const ACTIVE_CLASS_NAME = 'active'; |
||||||
|
|
||||||
|
export default function attachListNavigation({list, type, onSelect, once}: { |
||||||
|
list: HTMLElement, |
||||||
|
type: 'xy' | 'x' | 'y', |
||||||
|
onSelect: (target: Element) => void | boolean, |
||||||
|
once: boolean, |
||||||
|
}) { |
||||||
|
const keyNames: Set<ArrowKey> = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']); |
||||||
|
|
||||||
|
let target: Element; |
||||||
|
const getCurrentTarget = () => { |
||||||
|
return target || list.querySelector('.' + ACTIVE_CLASS_NAME) || list.firstElementChild; |
||||||
|
}; |
||||||
|
|
||||||
|
const setCurrentTarget = (_target: Element) => { |
||||||
|
if(target === _target) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let hadTarget = false; |
||||||
|
if(target) { |
||||||
|
hadTarget = true; |
||||||
|
target.classList.remove(ACTIVE_CLASS_NAME); |
||||||
|
} |
||||||
|
|
||||||
|
target = _target; |
||||||
|
target.classList.add(ACTIVE_CLASS_NAME); |
||||||
|
|
||||||
|
if(hadTarget && scrollable) { |
||||||
|
fastSmoothScroll(scrollable, target as HTMLElement, 'center', undefined, undefined, undefined, 100, type === 'x' ? 'x' : 'y'); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const getNextTargetX = (currentTarget: Element, isNext: boolean) => { |
||||||
|
let nextTarget: Element; |
||||||
|
if(isNext) nextTarget = currentTarget.nextElementSibling || list.firstElementChild; |
||||||
|
else nextTarget = currentTarget.previousElementSibling || list.lastElementChild; |
||||||
|
|
||||||
|
return nextTarget; |
||||||
|
}; |
||||||
|
|
||||||
|
const getNextTargetY = (currentTarget: Element, isNext: boolean) => { |
||||||
|
const property = isNext ? 'nextElementSibling' : 'previousElementSibling'; |
||||||
|
const endProperty = isNext ? 'firstElementChild' : 'lastElementChild'; |
||||||
|
const currentRect = currentTarget.getBoundingClientRect(); |
||||||
|
|
||||||
|
let nextTarget = currentTarget[property] || list[endProperty]; |
||||||
|
while(nextTarget !== currentTarget) { |
||||||
|
const targetRect = nextTarget.getBoundingClientRect(); |
||||||
|
if(targetRect.x === currentRect.x && targetRect.y !== currentRect.y) { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
nextTarget = nextTarget[property] || list[endProperty]; |
||||||
|
} |
||||||
|
|
||||||
|
return nextTarget; |
||||||
|
}; |
||||||
|
|
||||||
|
let handleArrowKey: (currentTarget: Element, key: ArrowKey) => Element; |
||||||
|
if(type === 'xy') { // flex-direction: row; flex-wrap: wrap;
|
||||||
|
handleArrowKey = (currentTarget, key) => { |
||||||
|
if(key === 'ArrowUp' || key === 'ArrowDown') return getNextTargetY(currentTarget, key === 'ArrowDown'); |
||||||
|
else return getNextTargetX(currentTarget, key === 'ArrowRight'); |
||||||
|
}; |
||||||
|
} else { // flex-direction: row | column;
|
||||||
|
handleArrowKey = (currentTarget, key) => getNextTargetX(currentTarget, key === 'ArrowRight' || key === 'ArrowDown'); |
||||||
|
} |
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => { |
||||||
|
if(!keyNames.has(e.key as any)) { |
||||||
|
if(e.key === 'Enter') { |
||||||
|
cancelEvent(e); |
||||||
|
fireSelect(getCurrentTarget()); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
cancelEvent(e); |
||||||
|
|
||||||
|
if(list.childElementCount > 1) { |
||||||
|
let currentTarget = getCurrentTarget(); |
||||||
|
currentTarget = handleArrowKey(currentTarget, e.key as any); |
||||||
|
setCurrentTarget(currentTarget); |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
}; |
||||||
|
|
||||||
|
const scrollable = findUpClassName(list, 'scrollable'); |
||||||
|
list.classList.add('navigable-list'); |
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => { |
||||||
|
const target = findUpAsChild(e.target, list) as HTMLElement; |
||||||
|
if(!target) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setCurrentTarget(target); |
||||||
|
}; |
||||||
|
|
||||||
|
const onClick = (e: Event) => { |
||||||
|
cancelEvent(e); // cancel keyboard closening
|
||||||
|
|
||||||
|
const target = findUpAsChild(e.target, list) as HTMLElement; |
||||||
|
if(!target) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setCurrentTarget(target); |
||||||
|
fireSelect(getCurrentTarget()); |
||||||
|
}; |
||||||
|
|
||||||
|
const fireSelect = (target: Element) => { |
||||||
|
const canContinue = onSelect(target); |
||||||
|
if(canContinue !== undefined ? !canContinue : once) { |
||||||
|
detach(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const detach = () => { |
||||||
|
// input.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
|
||||||
|
document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true}); |
||||||
|
list.removeEventListener('mousemove', onMouseMove); |
||||||
|
detachClickEvent(list, onClick); |
||||||
|
}; |
||||||
|
|
||||||
|
const resetTarget = () => { |
||||||
|
setCurrentTarget(list.firstElementChild); |
||||||
|
}; |
||||||
|
|
||||||
|
resetTarget(); |
||||||
|
|
||||||
|
// const input = document.activeElement as HTMLElement;
|
||||||
|
// input.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
|
||||||
|
document.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false}); |
||||||
|
list.addEventListener('mousemove', onMouseMove, {passive: true}); |
||||||
|
attachClickEvent(list, onClick); |
||||||
|
|
||||||
|
return { |
||||||
|
detach, |
||||||
|
resetTarget |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,123 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
* |
||||||
|
* Originally from: |
||||||
|
* https://github.com/zhukov/webogram
|
||||||
|
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com> |
||||||
|
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import { MessageEntity } from "../../layer"; |
||||||
|
|
||||||
|
export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link'; |
||||||
|
export type MarkdownTag = { |
||||||
|
match: string, |
||||||
|
entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl'; |
||||||
|
}; |
||||||
|
export const markdownTags: {[type in MarkdownType]: MarkdownTag} = { |
||||||
|
bold: { |
||||||
|
match: '[style*="font-weight"], b', |
||||||
|
entityName: 'messageEntityBold' |
||||||
|
}, |
||||||
|
underline: { |
||||||
|
match: '[style*="underline"], u', |
||||||
|
entityName: 'messageEntityUnderline' |
||||||
|
}, |
||||||
|
italic: { |
||||||
|
match: '[style*="italic"], i', |
||||||
|
entityName: 'messageEntityItalic' |
||||||
|
}, |
||||||
|
monospace: { |
||||||
|
match: '[style*="monospace"], [face="monospace"]', |
||||||
|
entityName: 'messageEntityPre' |
||||||
|
}, |
||||||
|
strikethrough: { |
||||||
|
match: '[style*="line-through"], strike', |
||||||
|
entityName: 'messageEntityStrike' |
||||||
|
}, |
||||||
|
link: { |
||||||
|
match: 'A', |
||||||
|
entityName: 'messageEntityTextUrl' |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export default function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number, entities?: MessageEntity[], offset = {offset: 0}) { |
||||||
|
if(node.nodeType === 3) { // TEXT
|
||||||
|
if(selNode === node) { |
||||||
|
const value = node.nodeValue; |
||||||
|
line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset)); |
||||||
|
} else { |
||||||
|
const nodeValue = node.nodeValue; |
||||||
|
line.push(nodeValue); |
||||||
|
|
||||||
|
if(entities && nodeValue.trim()) { |
||||||
|
if(node.parentNode) { |
||||||
|
const parentElement = node.parentElement; |
||||||
|
|
||||||
|
for(const type in markdownTags) { |
||||||
|
const tag = markdownTags[type as MarkdownType]; |
||||||
|
const closest = parentElement.closest(tag.match + ', [contenteditable]'); |
||||||
|
if(closest && closest.getAttribute('contenteditable') === null) { |
||||||
|
if(tag.entityName === 'messageEntityTextUrl') { |
||||||
|
entities.push({ |
||||||
|
_: tag.entityName as any, |
||||||
|
url: (parentElement as HTMLAnchorElement).href, |
||||||
|
offset: offset.offset, |
||||||
|
length: nodeValue.length |
||||||
|
}); |
||||||
|
} else { |
||||||
|
entities.push({ |
||||||
|
_: tag.entityName as any, |
||||||
|
offset: offset.offset, |
||||||
|
length: nodeValue.length |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
offset.offset += nodeValue.length; |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if(node.nodeType !== 1) { // NON-ELEMENT
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const isSelected = (selNode === node); |
||||||
|
const isBlock = node.tagName === 'DIV' || node.tagName === 'P'; |
||||||
|
if(isBlock && line.length || node.tagName === 'BR') { |
||||||
|
lines.push(line.join('')); |
||||||
|
line.splice(0, line.length); |
||||||
|
} else if(node.tagName === 'IMG') { |
||||||
|
const alt = (node as HTMLImageElement).alt; |
||||||
|
if(alt) { |
||||||
|
line.push(alt); |
||||||
|
offset.offset += alt.length; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(isSelected && !selOffset) { |
||||||
|
line.push('\x01'); |
||||||
|
} |
||||||
|
|
||||||
|
let curChild = node.firstChild as HTMLElement; |
||||||
|
while(curChild) { |
||||||
|
getRichElementValue(curChild, lines, line, selNode, selOffset, entities, offset); |
||||||
|
curChild = curChild.nextSibling as any; |
||||||
|
} |
||||||
|
|
||||||
|
if(isSelected && selOffset) { |
||||||
|
line.push('\x01'); |
||||||
|
} |
||||||
|
|
||||||
|
if(isBlock && line.length) { |
||||||
|
lines.push(line.join('')); |
||||||
|
line.splice(0, line.length); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
* |
||||||
|
* Originally from: |
||||||
|
* https://github.com/zhukov/webogram
|
||||||
|
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com> |
||||||
|
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
||||||
|
*/ |
||||||
|
|
||||||
|
import { MessageEntity } from "../../layer"; |
||||||
|
import RichTextProcessor from "../../lib/richtextprocessor"; |
||||||
|
import getRichElementValue from "./getRichElementValue"; |
||||||
|
|
||||||
|
export default function getRichValueWithCaret(field: HTMLElement, withEntities = true) { |
||||||
|
const lines: string[] = []; |
||||||
|
const line: string[] = []; |
||||||
|
|
||||||
|
const sel = window.getSelection(); |
||||||
|
var selNode |
||||||
|
var selOffset |
||||||
|
if(sel && sel.rangeCount) { |
||||||
|
const range = sel.getRangeAt(0); |
||||||
|
if(range.startContainer && |
||||||
|
range.startContainer == range.endContainer && |
||||||
|
range.startOffset == range.endOffset) { |
||||||
|
selNode = range.startContainer; |
||||||
|
selOffset = range.startOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const entities: MessageEntity[] = withEntities ? [] : undefined; |
||||||
|
getRichElementValue(field, lines, line, selNode, selOffset, entities); |
||||||
|
|
||||||
|
if(line.length) { |
||||||
|
lines.push(line.join('')); |
||||||
|
} |
||||||
|
|
||||||
|
let value = lines.join('\n'); |
||||||
|
const caretPos = value.indexOf('\x01'); |
||||||
|
if(caretPos != -1) { |
||||||
|
value = value.substr(0, caretPos) + value.substr(caretPos + 1); |
||||||
|
} |
||||||
|
value = value.replace(/\u00A0/g, ' '); |
||||||
|
|
||||||
|
if(entities) { |
||||||
|
RichTextProcessor.combineSameEntities(entities); |
||||||
|
} |
||||||
|
|
||||||
|
return {value, entities, caretPos}; |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
/* |
||||||
|
* https://github.com/morethanwords/tweb |
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
||||||
|
*/ |
||||||
|
|
||||||
|
.autocomplete-helper { |
||||||
|
--border-radius: #{$border-radius-medium}; |
||||||
|
position: absolute !important; |
||||||
|
bottom: calc(100% + .625rem); |
||||||
|
overflow: hidden; |
||||||
|
padding: 0 !important; |
||||||
|
border-radius: var(--border-radius) !important; |
||||||
|
|
||||||
|
&:not(.is-visible) { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
@include animation-level(2) { |
||||||
|
&.is-visible { |
||||||
|
animation: fade-out-opacity .2s ease-in-out forwards; |
||||||
|
|
||||||
|
&:not(.backwards) { |
||||||
|
animation: fade-in-opacity .2s ease-in-out forwards; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue