Eduard Kuzmenko
4 years ago
16 changed files with 476 additions and 208 deletions
@ -0,0 +1,32 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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