diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 1287fb2c..9b19af97 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; import opusDecodeController from "../../lib/opusDecodeController"; import { RichTextProcessor } from "../../lib/richtextprocessor"; import rootScope from '../../lib/rootScope'; -import { cancelEvent, CLICK_EVENT_NAME, findUpClassName, getRichValue, isInputEmpty, placeCaretAtEnd, serializeNodes } from "../../helpers/dom"; +import { blurActiveElement, cancelEvent, CLICK_EVENT_NAME, findUpClassName, getRichValue, getSelectedNodes, isInputEmpty, isSelectionSingle, markdownTags, MarkdownType, placeCaretAtEnd, serializeNodes } from "../../helpers/dom"; import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu'; import emoticonsDropdown from "../emoticonsDropdown"; import PopupCreatePoll from "../popupCreatePoll"; @@ -23,12 +23,252 @@ import { toast } from "../toast"; import { wrapReply } from "../wrappers"; import InputField from '../inputField'; import { MessageEntity } from '../../layer'; +import ButtonIcon from '../buttonIcon'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply'; +export class MarkupTooltip { + public container: HTMLElement; + private wrapper: HTMLElement; + private buttons: {[type in MarkdownType]: HTMLElement} = {} as any; + private linkBackButton: HTMLElement; + private hideTimeout: number; + private inputs: HTMLElement[] = []; + private addedListener = false; + private waitingForMouseUp = false; + private linkInput: HTMLInputElement; + private savedRange: Range; + + private init() { + this.container = document.createElement('div'); + this.container.classList.add('markup-tooltip', 'z-depth-1', 'hide'); + + this.wrapper = document.createElement('div'); + this.wrapper.classList.add('markup-tooltip-wrapper'); + + const tools1 = document.createElement('div'); + const tools2 = document.createElement('div'); + tools1.classList.add('markup-tooltip-tools'); + tools2.classList.add('markup-tooltip-tools'); + + const arr = ['bold', 'italic', 'underline', 'strikethrough', 'monospace', 'link'] as (keyof MarkupTooltip['buttons'])[]; + arr.forEach(c => { + const button = ButtonIcon(c, {noRipple: true}); + tools1.append(this.buttons[c] = button); + + if(c !== 'link') { + button.addEventListener('click', () => { + appImManager.chatInputC.applyMarkdown(c); + }); + } else { + button.addEventListener('click', () => { + this.container.classList.add('is-link'); + + if(button.classList.contains('active')) { + const startContainer = this.savedRange.startContainer; + const anchor = startContainer.parentElement as HTMLAnchorElement; + this.linkInput.value = anchor.href; + } else { + this.linkInput.value = ''; + } + }); + } + }); + + this.linkBackButton = ButtonIcon('back', {noRipple: true}); + this.linkInput = document.createElement('input'); + this.linkInput.placeholder = 'Enter URL...'; + this.linkInput.classList.add('input-clear'); + this.linkInput.addEventListener('keydown', (e) => { + if(e.code == 'Enter') { + const valid = !this.linkInput.value.length || RichTextProcessor.matchUrl(this.linkInput.value);///^(http)|(https):\/\//i.test(this.linkInput.value); + if(!valid) { + if(this.linkInput.classList.contains('error')) { + this.linkInput.classList.remove('error'); + void this.linkInput.offsetLeft; // reflow + } + + this.linkInput.classList.add('error'); + } else { + cancelEvent(e); + this.resetSelection(); + appImManager.chatInputC.applyMarkdown('link', this.linkInput.value); + this.hide(); + } + } else { + this.linkInput.classList.remove('error'); + } + }); + + this.linkBackButton.addEventListener('click', () => { + this.container.classList.remove('is-link'); + //input.value = ''; + this.resetSelection(); + }); + + const delimiter1 = document.createElement('span'); + const delimiter2 = document.createElement('span'); + delimiter1.classList.add('markup-tooltip-delimiter'); + delimiter2.classList.add('markup-tooltip-delimiter'); + tools1.insertBefore(delimiter1, this.buttons.link); + tools2.append(this.linkBackButton, delimiter2, this.linkInput); + //tools1.insertBefore(delimiter2, this.buttons.link.nextSibling); + + this.wrapper.append(tools1, tools2); + this.container.append(this.wrapper); + document.body.append(this.container); + } + + private resetSelection() { + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(this.savedRange); + this.inputs[0].focus(); + } + + public hide() { + if(this.init) return; + + this.container.classList.remove('is-visible'); + document.removeEventListener('mouseup', this.onMouseUp); + if(this.hideTimeout) clearTimeout(this.hideTimeout); + this.hideTimeout = window.setTimeout(() => { + this.hideTimeout = undefined; + this.container.classList.add('hide'); + this.container.classList.remove('is-link'); + }, 200); + } + + public getActiveMarkupButton() { + const nodes = getSelectedNodes(); + const parents = [...new Set(nodes.map(node => node.parentNode))]; + if(parents.length > 1) return undefined; + + const node = parents[0] as HTMLElement; + let currentMarkup: HTMLElement; + for(const type in markdownTags) { + const tag = markdownTags[type as MarkdownType]; + if(node.matches(tag.match)) { + currentMarkup = this.buttons[type as MarkdownType]; + break; + } + } + + return currentMarkup; + } + + public setActiveMarkupButton() { + const activeButton = this.getActiveMarkupButton(); + + for(const i in this.buttons) { + // @ts-ignore + const button = this.buttons[i]; + if(button != activeButton) { + button.classList.remove('active'); + } + } + + if(activeButton) { + activeButton.classList.add('active'); + } + + return activeButton; + } + + public show() { + if(this.init) { + this.init(); + this.init = null; + } + + const selection = document.getSelection(); + + if(!selection.toString().trim().length) { + this.hide(); + return; + } + + if(this.hideTimeout !== undefined) { + clearTimeout(this.hideTimeout); + } + + const range = this.savedRange = selection.getRangeAt(0); + + const activeButton = this.setActiveMarkupButton(); + + this.container.classList.remove('is-link'); + const isFirstShow = this.container.classList.contains('hide'); + if(isFirstShow) { + this.container.classList.remove('hide'); + this.container.classList.add('no-transition'); + } + + const selectionRect = range.getBoundingClientRect(); + //const containerRect = this.container.getBoundingClientRect(); + const sizesRect = this.container.firstElementChild.firstElementChild.getBoundingClientRect(); + const top = selectionRect.top - sizesRect.height - 8; + const left = selectionRect.left + (selectionRect.width - sizesRect.width) / 2; + //const top = selectionRect.top - 44 - 8; + + this.container.style.transform = `translate3d(${left}px, ${top}px, 0)`; + + if(isFirstShow) { + void this.container.offsetLeft; // reflow + this.container.classList.remove('no-transition'); + } + + this.container.classList.add('is-visible'); + + console.log('selection', selectionRect, activeButton); + } + + private onMouseUp = (e: Event) => { + if(findUpClassName(e.target, 'markup-tooltip')) return; + this.hide(); + document.removeEventListener('mouseup', this.onMouseUp); + }; + + public setMouseUpEvent() { + if(this.waitingForMouseUp) return; + this.waitingForMouseUp = true; + document.addEventListener('mouseup', (e) => { + this.waitingForMouseUp = false; + this.show(); + + document.addEventListener('mouseup', this.onMouseUp); + }, {once: true}); + } + + public handleSelection(input: HTMLElement) { + this.inputs.push(input); + + if(this.addedListener) return; + this.addedListener = true; + document.addEventListener('selectionchange', (e) => { + if(document.activeElement == this.linkInput) { + return; + } + + if(!this.inputs.includes(document.activeElement as HTMLElement)) { + this.hide(); + return; + } + + const selection = document.getSelection(); + + if(!selection.toString().trim().length) { + this.hide(); + return; + } + + this.setMouseUpEvent(); + }); + } +} + export class ChatInput { public pageEl = document.getElementById('page-chats') as HTMLDivElement; public messageInput: HTMLDivElement/* HTMLInputElement */; @@ -82,7 +322,13 @@ export class ChatInput { readonly executedHistory: string[] = []; private canUndoFromHTML = ''; + public markupTooltip: MarkupTooltip; + constructor() { + if(!isTouchSupported) { + this.markupTooltip = new MarkupTooltip(); + } + this.attachMessageInputField(); this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement; @@ -314,7 +560,24 @@ export class ChatInput { }); } + this.messageInput.addEventListener('beforeinput', (e: Event) => { + // * validate due to manual formatting through browser's context menu + const inputType = (e as InputEvent).inputType; + //console.log('message beforeinput event', e); + + if(inputType.indexOf('format') === 0) { + //console.log('message beforeinput format', e, inputType, this.messageInput.innerHTML); + const markdownType = inputType.split('format')[1].toLowerCase() as MarkdownType; + if(this.applyMarkdown(markdownType)) { + cancelEvent(e); // * cancel legacy markdown event + } + } + }); this.messageInput.addEventListener('input', this.onMessageInput); + + if(this.markupTooltip) { + this.markupTooltip.handleSelection(this.messageInput); + } } private onDocumentPaste = (e: ClipboardEvent) => { @@ -377,107 +640,110 @@ export class ChatInput { } }; - private handleMarkdownShortcut = (e: KeyboardEvent) => { - const formatKeys: {[key: string]: string | (() => void)} = { - 'B': 'Bold', - 'I': 'Italic', - 'U': 'Underline', - 'S': 'Strikethrough', - 'M': () => document.execCommand('fontName', false, 'monospace') + public applyMarkdown(type: MarkdownType, href?: string) { + const commandsMap: Partial<{[key in typeof type]: string | (() => void)}> = { + bold: 'Bold', + italic: 'Italic', + underline: 'Underline', + strikethrough: 'Strikethrough', + monospace: () => document.execCommand('fontName', false, 'monospace'), + link: href ? () => document.execCommand('createLink', false, href) : () => document.execCommand('unlink', false, null) }; - for(const key in formatKeys) { - const good = e.code == ('Key' + key); - if(good) { - const getSelectedNodes = () => { - const nodes: Node[] = []; - const selection = window.getSelection(); - for(let i = 0; i < selection.rangeCount; ++i) { - const range = selection.getRangeAt(i); - let {startContainer, endContainer} = range; - if(endContainer.nodeType != 3) endContainer = endContainer.firstChild; - - while(startContainer && startContainer != endContainer) { - nodes.push(startContainer.nodeType == 3 ? startContainer : startContainer.firstChild); - startContainer = startContainer.nextSibling; - } - - if(nodes[nodes.length - 1] != endContainer) { - nodes.push(endContainer); - } - } + if(!commandsMap[type]) { + return false; + } - // * filter null's due to
- return nodes.filter(node => !!node); - }; - - const saveExecuted = this.prepareDocumentExecute(); - const executed: any[] = []; - /** - * * clear previous formatting, due to Telegram's inability to handle several entities - */ - const checkForSingle = () => { - const nodes = getSelectedNodes(); - console.log('Using formatting:', formatKeys[key], nodes, this.executedHistory); - - const parents = [...new Set(nodes.map(node => node.parentNode))]; - //const differentParents = !!nodes.find(node => node.parentNode != firstParent); - const differentParents = parents.length > 1; - - let notSingle = false; - if(differentParents) { - notSingle = true; - } else { - const node = nodes[0]; - if(node && (node.parentNode as HTMLElement) != this.messageInput && (node.parentNode.parentNode as HTMLElement) != this.messageInput) { - notSingle = true; - } - } + const command = commandsMap[type]; - if(notSingle) { - if(key == 'M') { - executed.push(document.execCommand('styleWithCSS', false, 'true')); - } + //type = 'monospace'; - executed.push(document.execCommand('unlink', false, null)); - executed.push(document.execCommand('removeFormat', false, null)); - // @ts-ignore - executed.push(typeof(formatKeys[key]) === 'function' ? formatKeys[key]() : document.execCommand(formatKeys[key], false, null)); + const saveExecuted = this.prepareDocumentExecute(); + const executed: any[] = []; + /** + * * clear previous formatting, due to Telegram's inability to handle several entities + */ + const checkForSingle = () => { + const nodes = getSelectedNodes(); + //console.log('Using formatting:', commandsMap[type], nodes, this.executedHistory); - if(key == 'M') { - executed.push(document.execCommand('styleWithCSS', false, 'false')); - } - } - }; - - if(key == 'M') { - let haveMonospace = false; - executed.push(document.execCommand('styleWithCSS', false, 'true')); + const parents = [...new Set(nodes.map(node => node.parentNode))]; + //const differentParents = !!nodes.find(node => node.parentNode != firstParent); + const differentParents = parents.length > 1; - const selection = window.getSelection(); - if(!selection.isCollapsed) { - const range = selection.getRangeAt(0); - // @ts-ignore - if(range.commonAncestorContainer.parentNode.tagName == 'SPAN' || range.commonAncestorContainer.tagName == 'SPAN') { - haveMonospace = true; - } - } + let notSingle = false; + if(differentParents) { + notSingle = true; + } else { + const node = nodes[0]; + if(node && (node.parentNode as HTMLElement) != this.messageInput && (node.parentNode.parentNode as HTMLElement) != this.messageInput) { + notSingle = true; + } + } - executed.push(document.execCommand('removeFormat', false, null)); - - if(!haveMonospace) { - // @ts-ignore - executed.push(typeof(formatKeys[key]) === 'function' ? formatKeys[key]() : document.execCommand(formatKeys[key], false, null)); - } + if(notSingle) { + //if(type === 'monospace') { + executed.push(document.execCommand('styleWithCSS', false, 'true')); + //} + + executed.push(document.execCommand('unlink', false, null)); + executed.push(document.execCommand('removeFormat', false, null)); + executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); + //if(type === 'monospace') { executed.push(document.execCommand('styleWithCSS', false, 'false')); - } else { - // @ts-ignore - executed.push(typeof(formatKeys[key]) === 'function' ? formatKeys[key]() : document.execCommand(formatKeys[key], false, null)); + //} + } + }; + + //if(type === 'monospace') { + let haveThisType = false; + executed.push(document.execCommand('styleWithCSS', false, 'true')); + + const selection = window.getSelection(); + if(!selection.isCollapsed) { + const range = selection.getRangeAt(0); + const tag = markdownTags[type]; + + const node = range.commonAncestorContainer; + if((node.parentNode as HTMLElement).matches(tag.match) || (node instanceof HTMLElement && node.matches(tag.match))) { + haveThisType = true; } - - checkForSingle(); - saveExecuted(); + } + + executed.push(document.execCommand('removeFormat', false, null)); + + if(!haveThisType) { + executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); + } + + executed.push(document.execCommand('styleWithCSS', false, 'false')); + /* } else { + executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); + } */ + + checkForSingle(); + saveExecuted(); + if(this.markupTooltip) { + this.markupTooltip.setActiveMarkupButton(); + } + + return true; + } + + private handleMarkdownShortcut = (e: KeyboardEvent) => { + const formatKeys: {[key: string]: MarkdownType} = { + 'B': 'bold', + 'I': 'italic', + 'U': 'underline', + 'S': 'strikethrough', + 'M': 'monospace' + }; + + for(const key in formatKeys) { + const good = e.code == ('Key' + key); + if(good) { + this.applyMarkdown(formatKeys[key]); cancelEvent(e); // cancel legacy event break; } @@ -509,7 +775,19 @@ export class ChatInput { } }; - private onMessageInput = (/* e: Event */) => { + private onMessageInput = (e?: Event) => { + // * validate due to manual formatting through browser's context menu + /* const inputType = (e as InputEvent).inputType; + console.log('message input event', e); + if(inputType == 'formatBold') { + console.log('message input format', this.messageInput.innerHTML); + cancelEvent(e); + } + + if(!isSelectionSingle()) { + alert('not single'); + } */ + //console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes))); const value = this.messageInput.innerText; @@ -605,6 +883,7 @@ export class ChatInput { } this.chatInput.classList.add('is-locked'); + blurActiveElement(); this.recorder.start().then(() => { this.recordCanceled = false; diff --git a/src/components/inputField.ts b/src/components/inputField.ts index b822170e..8258ddf7 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -18,11 +18,8 @@ let init = () => { let entities = RichTextProcessor.parseEntities(text); //console.log('messageInput paste', text, entities); entities = entities.filter(e => e._ == 'messageEntityEmoji' || e._ == 'messageEntityLinebreak'); - if(RichTextProcessor.emojiSupported) { // * fix safari emoji - entities = entities.filter(e => e._ != 'messageEntityEmoji'); - } //text = RichTextProcessor.wrapEmojiText(text); - text = RichTextProcessor.wrapRichText(text, {entities, noLinks: true}); + text = RichTextProcessor.wrapRichText(text, {entities, noLinks: true, wrappingDraft: true}); // console.log('messageInput paste after', text); diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 28b2a91b..d1bcba47 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -1,5 +1,6 @@ import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config"; import { isTouchSupported } from "./touchSupport"; +import { isSafari } from "./userAgent"; /* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean { if(!element) { @@ -120,34 +121,46 @@ export function getRichValue(field: HTMLElement) { MOUNT_CLASS_TO && (MOUNT_CLASS_TO.getRichValue = getRichValue); -const markdownTags = [{ - tagName: 'STRONG', - markdown: '**' -}, { - tagName: 'B', // * legacy (Ctrl+B) - markdown: '**' -}, { - tagName: 'U', // * legacy (Ctrl+I) - markdown: '_-_' -}, { - tagName: 'I', // * legacy (Ctrl+I) - markdown: '__' -}, { - tagName: 'EM', - markdown: '__' -}, { - tagName: 'CODE', - markdown: '`' -}, { - tagName: 'PRE', - markdown: '``' -}, { - tagName: 'DEL', - markdown: '~~' -}, { - tagName: 'A', - markdown: (node: HTMLElement) => `[${(node.parentElement as HTMLAnchorElement).href}](${node.nodeValue})` -}]; +const markdownTypes = { + bold: '**', + underline: '_-_', + italic: '__', + monospace: '`', + pre: '``', + strikethrough: '~~' +}; + +export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link'; +export type MarkdownTag = { + match: string, + markdown: string | ((node: HTMLElement) => string) +}; +export const markdownTags: {[type in MarkdownType]: MarkdownTag} = { + bold: { + match: '[style*="font-weight"]', + markdown: markdownTypes.bold + }, + underline: { + match: isSafari ? '[style="text-decoration: underline;"]' : '[style="text-decoration-line: underline;"]', + markdown: markdownTypes.underline + }, + italic: { + match: '[style="font-style: italic;"]', + markdown: markdownTypes.italic + }, + monospace: { + match: '[style="font-family: monospace;"]', + markdown: markdownTypes.monospace + }, + strikethrough: { + match: isSafari ? '[style="text-decoration: line-through;"]' : '[style="text-decoration-line: line-through;"]', + markdown: markdownTypes.strikethrough + }, + link: { + match: 'A', + markdown: (node: HTMLElement) => `[${(node.parentElement as HTMLAnchorElement).href}](${node.nodeValue})` + } +}; export function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number) { if(node.nodeType == 3) { // TEXT if(selNode === node) { @@ -156,8 +169,17 @@ export function getRichElementValue(node: HTMLElement, lines: string[], line: st } else { let markdown: string; if(node.parentNode) { - const tagName = node.parentElement.tagName; - const markdownTag = markdownTags.find(m => m.tagName == tagName); + const parentElement = node.parentElement; + + let markdownTag: MarkdownTag; + for(const type in markdownTags) { + const tag = markdownTags[type as MarkdownType]; + if(parentElement.matches(tag.match)) { + markdownTag = tag; + break; + } + } + if(markdownTag) { if(typeof(markdownTag.markdown) === 'function') { line.push(markdownTag.markdown(node)); @@ -476,3 +498,43 @@ export const detachClickEvent = (elem: HTMLElement, callback: (e: TouchEvent | M elem.removeEventListener(CLICK_EVENT_NAME, callback, options); } }; + +export const getSelectedNodes = () => { + const nodes: Node[] = []; + const selection = window.getSelection(); + for(let i = 0; i < selection.rangeCount; ++i) { + const range = selection.getRangeAt(i); + let {startContainer, endContainer} = range; + if(endContainer.nodeType != 3) endContainer = endContainer.firstChild; + + while(startContainer && startContainer != endContainer) { + nodes.push(startContainer.nodeType == 3 ? startContainer : startContainer.firstChild); + startContainer = startContainer.nextSibling; + } + + if(nodes[nodes.length - 1] != endContainer) { + nodes.push(endContainer); + } + } + + // * filter null's due to
+ return nodes.filter(node => !!node); +}; + +export const isSelectionSingle = (input: Element = document.activeElement) => { + const nodes = getSelectedNodes(); + const parents = [...new Set(nodes.map(node => node.parentNode))]; + const differentParents = parents.length > 1; + + let single = true; + if(differentParents) { + single = false; + } else { + const node = nodes[0]; + if(node && node.parentNode != input && node.parentNode.parentNode != input) { + single = false; + } + } + + return single; +}; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index a56bd4c5..0cf11afe 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -39,7 +39,7 @@ import apiManager from '../mtproto/mtprotoworker'; import { MOUNT_CLASS_TO } from '../mtproto/mtproto_config'; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from '../rootScope'; -import { attachClickEvent, cancelEvent, CLICK_EVENT_NAME, findUpClassName, findUpTag, placeCaretAtEnd, whichChild } from "../../helpers/dom"; +import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, CLICK_EVENT_NAME, findUpClassName, findUpTag, placeCaretAtEnd, whichChild } from "../../helpers/dom"; import apiUpdatesManager from './apiUpdatesManager'; import appChatsManager, { Channel, Chat } from "./appChatsManager"; import appDialogsManager from "./appDialogsManager"; @@ -703,7 +703,9 @@ export class AppImManager { AppMediaViewer.buttons.close.click(); } else */ /* if(appSidebarRight.historyTabIDs.slice(-1)[0] == AppSidebarRight.SLIDERITEMSIDS.forward) { appSidebarRight.forwardTab.closeBtn.click(); - } else */ if(this.chatSelection.isSelecting) { + } else */if(this.chatInputC.markupTooltip && this.chatInputC.markupTooltip.container.classList.contains('is-visible')) { + this.chatInputC.markupTooltip.hide(); + } else if(this.chatSelection.isSelecting) { this.chatSelection.cancelSelection(); } else if(this.columnEl.classList.contains('is-helper-active')) { this.chatInputC.replyElements.cancelBtn.click(); @@ -1108,8 +1110,10 @@ export class AppImManager { //this.lazyLoadQueue.clear(); // clear input + cancelSelection(); this.chatInputC.clearInput(); - this.chatInputC.replyElements.cancelBtn.click(); + this.chatInputC.clearHelper(); + //this.chatInputC.replyElements.cancelBtn.click(); // clear messages if(bubblesToo) { diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index cf4ee681..05da901b 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -6,6 +6,7 @@ import { MOUNT_CLASS_TO } from './mtproto/mtproto_config'; import { MessageEntity } from '../layer'; import { copy } from '../helpers/object'; import { encodeEntities } from '../helpers/string'; +import { isSafari } from '../helpers/userAgent'; const EmojiHelper = { emojiMap: (code: string) => { return code; }, @@ -379,7 +380,7 @@ namespace RichTextProcessor { noLinks: true, noLinebreaks: true, noCommands: true, - noEmphasis: true, + wrappingDraft: true, fromBot: boolean, noTextFormat: true, passEntities: Partial<{ @@ -433,7 +434,8 @@ namespace RichTextProcessor { ' href="', contextUrl.replace('{1}', encodeURIComponent(username)), '">', - encodeEntities(entityText), + wrapRichNestedText(entityText, entity.nested, options), + //encodeEntities(entityText), '' ) break; @@ -448,7 +450,7 @@ namespace RichTextProcessor { '', - encodeEntities(entityText), + wrapRichNestedText(entityText, entity.nested, options), '' ); break; @@ -492,7 +494,7 @@ namespace RichTextProcessor { case 'messageEntityTextUrl': let inner: string; let url: string; - if (entity._ == 'messageEntityTextUrl') { + if(entity._ == 'messageEntityTextUrl') { url = (entity as MessageEntity.messageEntityTextUrl).url; url = wrapUrl(url, true); inner = wrapRichNestedText(entityText, entity.nested, options); @@ -519,6 +521,11 @@ namespace RichTextProcessor { break; case 'messageEntityEmoji': + if(options.wrappingDraft && emojiSupported) { // * fix safari emoji + html.push(encodeEntities(entityText)); + break; + } + html.push(emojiSupported ? // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера `${encodeEntities(entityText)}` : `${encodeEntities(entityText)}`); @@ -554,29 +561,27 @@ namespace RichTextProcessor { html.push(wrapRichNestedText(entityText, entity.nested, options)); break; } - - const tag = options.noEmphasis ? 'b' : 'strong'; - html.push( - `<${tag}>`, - wrapRichNestedText(entityText, entity.nested, options), - `` - ); + + if(options.wrappingDraft) { + html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); + } else { + html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); + } break; } - case 'messageEntityItalic': { if(options.noTextFormat) { html.push(wrapRichNestedText(entityText, entity.nested, options)); break; } - - const tag = options.noEmphasis ? 'i' : 'em'; - html.push( - `<${tag}>`, - wrapRichNestedText(entityText, entity.nested, options), - `` - ); + + if(options.wrappingDraft) { + html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); + } else { + html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); + } + break; } @@ -589,19 +594,21 @@ namespace RichTextProcessor { break; case 'messageEntityStrike': - html.push( - '', - wrapRichNestedText(entityText, entity.nested, options), - '' - ); + if(options.wrappingDraft) { + const styleName = isSafari ? 'text-decoration' : 'text-decoration-line'; + html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); + } else { + html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); + } break; case 'messageEntityUnderline': - html.push( - '', - wrapRichNestedText(entityText, entity.nested, options), - '' - ); + if(options.wrappingDraft) { + const styleName = isSafari ? 'text-decoration' : 'text-decoration-line'; + html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); + } else { + html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); + } break; case 'messageEntityCode': @@ -610,11 +617,16 @@ namespace RichTextProcessor { break; } - html.push( - '', - encodeEntities(entityText), - '' - ); + if(options.wrappingDraft) { + html.push(`${encodeEntities(entityText)}`); + } else { + html.push( + '', + encodeEntities(entityText), + '' + ); + } + break; case 'messageEntityPre': @@ -728,14 +740,11 @@ namespace RichTextProcessor { } let entities = (options.entities || []).slice(); - if(emojiSupported) { // * fix safari emoji - entities = entities.filter(e => e._ != 'messageEntityEmoji'); - } return wrapRichText(text, { entities, noLinks: true, - noEmphasis: true, + wrappingDraft: true, passEntities: { messageEntityTextUrl: true } diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 66a04f32..051f9c14 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -1324,4 +1324,99 @@ $chat-helper-size: 39px; border-bottom: 1px solid white; } } -} \ No newline at end of file +} + +.markup-tooltip { + $widthRegular: 218px; + $widthLink: 420px; + $padding: 7px; + + background: #fff; + border-radius: $border-radius-medium; + transform: translateZ(0); + opacity: 0; + transition: opacity var(--layer-transition), transform var(--layer-transition), width var(--layer-transition); + position: fixed; + left: 0; + top: 0; + height: 44px; + width: $widthRegular; + overflow: hidden; + + &-wrapper { + position: absolute; + left: 0; + top: 0; + display: flex; + align-items: center; + justify-content: start; + //width: 420px; + width: #{$widthRegular + $widthLink}; + height: 100%; + transform: translateX(0); + transition: transform var(--layer-transition); + } + + &-tools { + display: flex; + align-items: center; + justify-content: space-between; + padding: $padding; + + &:first-child { + width: $widthRegular; + } + + &:last-child { + width: $widthLink; + + .markup-tooltip-delimiter { + margin-left: .25rem; + margin-right: .75rem; + } + } + } + + &-delimiter { + width: 1px; + height: 25px; + background-color: #DADCE0; + } + + .btn-icon { + border-radius: $border-radius !important; + width: 30px; + height: 30px; + padding: 0; + + &.active { + color: #fff!important; + background-color: $color-blue!important; + } + } + + &:not(.is-visible) { + pointer-events: none; + } + + &.is-visible { + opacity: 1; + } + + &.is-link { + width: $widthLink; + } + + &.is-link &-wrapper { + transform: translateX(#{-$widthRegular}); + } + + .input-clear { + flex: 1 1 auto; + text-overflow: ellipsis; + } + + &.no-transition { + transition: none; + } +} diff --git a/src/scss/partials/_fonts.scss b/src/scss/partials/_fonts.scss index f4385281..d9ee2ed0 100644 --- a/src/scss/partials/_fonts.scss +++ b/src/scss/partials/_fonts.scss @@ -2,13 +2,13 @@ @font-face { font-family: "#{$tgico-font-family}"; - src: url("#{$tgico-font-path}/#{$tgico-font-family}.eot?owpifk"); - src: url("#{$tgico-font-path}/#{$tgico-font-family}.eot?owpifk#iefix") + src: url("#{$tgico-font-path}/#{$tgico-font-family}.eot?5dnghg"); + src: url("#{$tgico-font-path}/#{$tgico-font-family}.eot?5dnghg#iefix") format("embedded-opentype"), - url("#{$tgico-font-path}/#{$tgico-font-family}.ttf?owpifk") + url("#{$tgico-font-path}/#{$tgico-font-family}.ttf?5dnghg") format("truetype"), - url("#{$tgico-font-path}/#{$tgico-font-family}.woff?owpifk") format("woff"), - url("#{$tgico-font-path}/#{$tgico-font-family}.svg?owpifk##{$tgico-font-family}") + url("#{$tgico-font-path}/#{$tgico-font-family}.woff?5dnghg") format("woff"), + url("#{$tgico-font-path}/#{$tgico-font-family}.svg?5dnghg##{$tgico-font-family}") format("svg"); font-weight: normal; font-style: normal; @@ -34,348 +34,418 @@ -moz-osx-font-smoothing: grayscale; } -.tgico-select:before { + +.tgico-dragmedia:before { content: "\e900"; } -.tgico-info2:before { +.tgico-dragfiles:before { content: "\e901"; } -.tgico-clouddownload:before { +.tgico-link:before { content: "\e902"; } -.tgico-readchats:before { +.tgico-monospace:before { content: "\e903"; } -.tgico-noncontacts:before { +.tgico-strikethrough:before { content: "\e904"; } -.tgico-bots:before { +.tgico-underline:before { content: "\e905"; } -.tgico-muted:before { +.tgico-italic:before { content: "\e906"; } -.tgico-favourites:before { +.tgico-bold:before { content: "\e907"; } -.tgico-tip:before { +.tgico-botcom:before { content: "\e908"; } -.tgico-loginlogodesktop:before { +.tgico-calendarfilter:before { content: "\e909"; } -.tgico-loginlogomobile:before { +.tgico-email:before { content: "\e90a"; } -.tgico-calendar:before { +.tgico-passwordoff:before { content: "\e90b"; } -.tgico-keyboard:before { +.tgico-commentssticker:before { content: "\e90c"; } -.tgico-gifs:before { +.tgico-comments:before { content: "\e90d"; } -.tgico-stickers:before { +.tgico-previous:before { content: "\e90e"; } -.tgico-deleteleft:before { +.tgico-next:before { content: "\e90f"; } -.tgico-folder:before { +.tgico-mention:before { content: "\e910"; } -.tgico-revote:before { +.tgico-down:before { content: "\e911"; } -.tgico-livelocation:before { +.tgico-pinlist:before { content: "\e912"; } -.tgico-microphone2:before { +.tgico-replace:before { content: "\e913"; } -.tgico-colorize:before { +.tgico-schedule:before { content: "\e914"; } -.tgico-poll:before { +.tgico-zoomout:before { content: "\e915"; } -.tgico-minus:before { +.tgico-zoomin:before { content: "\e916"; } -.tgico-nosound:before { +.tgico-select:before { content: "\e917"; } -.tgico-microphone:before { +.tgico-info2:before { content: "\e918"; } -.tgico-largeplay:before { +.tgico-clouddownload:before { content: "\e919"; } -.tgico-largepause:before { +.tgico-readchats:before { content: "\e91a"; } -.tgico-newchannel:before { +.tgico-noncontacts:before { content: "\e91b"; } -.tgico-newgroup:before { +.tgico-bots:before { content: "\e91c"; } -.tgico-newprivate:before { +.tgico-muted:before { content: "\e91d"; } -.tgico-chatsplaceholder:before { +.tgico-favourites:before { content: "\e91e"; } -.tgico-newchat_filled:before { +.tgico-tip:before { content: "\e91f"; } -.tgico-addmember_filled:before { +.tgico-loginlogodesktop:before { content: "\e920"; } -.tgico-delete:before { +.tgico-loginlogomobile:before { content: "\e921"; } -.tgico-delete_filled:before { +.tgico-calendar:before { content: "\e922"; } -.tgico-send2:before { +.tgico-keyboard:before { content: "\e923"; } -.tgico-avatar_deletedaccount:before { +.tgico-gifs:before { content: "\e924"; } -.tgico-avatar_archivedchats:before { +.tgico-stickers:before { content: "\e925"; } -.tgico-avatar_savedmessages:before { +.tgico-deleteleft:before { content: "\e926"; } -.tgico-pinnedchat:before { +.tgico-folder:before { content: "\e927"; } -.tgico-channelviews:before { +.tgico-revote:before { content: "\e928"; } -.tgico-sendingerror:before { +.tgico-livelocation:before { content: "\e929"; } -.tgico-sending:before { +.tgico-microphone2:before { content: "\e92a"; } -.tgico-check:before { +.tgico-colorize:before { content: "\e92b"; } -.tgico-checks:before { +.tgico-poll:before { content: "\e92c"; } -.tgico-radioon:before { +.tgico-minus:before { content: "\e92d"; } -.tgico-radiooff:before { +.tgico-nosound:before { content: "\e92e"; } -.tgico-checkboxempty:before { +.tgico-microphone:before { content: "\e92f"; } -.tgico-checkboxblock:before { +.tgico-largeplay:before { content: "\e930"; } -.tgico-checkboxon:before { +.tgico-largepause:before { content: "\e931"; } -.tgico-eye2:before { +.tgico-newchannel:before { content: "\e932"; } -.tgico-eye1:before { +.tgico-newgroup:before { content: "\e933"; } -.tgico-fullscreen:before { +.tgico-newprivate:before { content: "\e934"; } -.tgico-smallscreen:before { +.tgico-chatsplaceholder:before { content: "\e935"; } -.tgico-flag:before { +.tgico-newchat_filled:before { content: "\e936"; } -.tgico-lamp:before { +.tgico-addmember_filled:before { content: "\e937"; } -.tgico-sport:before { +.tgico-delete:before { content: "\e938"; } -.tgico-car:before { +.tgico-delete_filled:before { content: "\e939"; } -.tgico-eats:before { +.tgico-send2:before { content: "\e93a"; } -.tgico-animals:before { +.tgico-avatar_deletedaccount:before { content: "\e93b"; } -.tgico-smile:before { +.tgico-avatar_archivedchats:before { content: "\e93c"; } -.tgico-unpin:before { +.tgico-avatar_savedmessages:before { content: "\e93d"; } -.tgico-send:before { +.tgico-pinnedchat:before { content: "\e93e"; } -.tgico-unread:before { +.tgico-channelviews:before { content: "\e93f"; } -.tgico-settings:before { +.tgico-sendingerror:before { content: "\e940"; } -.tgico-edit:before { +.tgico-sending:before { content: "\e941"; } -.tgico-download:before { +.tgico-check:before { content: "\e942"; } -.tgico-cameraadd:before { +.tgico-checks:before { content: "\e943"; } -.tgico-camera:before { +.tgico-radioon:before { content: "\e944"; } -.tgico-permissions:before { +.tgico-radiooff:before { content: "\e945"; } -.tgico-admin:before { +.tgico-checkboxempty:before { content: "\e946"; } -.tgico-stop:before { +.tgico-checkboxblock:before { content: "\e947"; } -.tgico-username:before { +.tgico-checkboxon:before { content: "\e948"; } -.tgico-location:before { +.tgico-eye2:before { content: "\e949"; } -.tgico-info:before { +.tgico-eye1:before { content: "\e94a"; } -.tgico-deleteuser:before { +.tgico-FullScreen:before { content: "\e94b"; } -.tgico-adduser:before { +.tgico-smallscreen:before { content: "\e94c"; } -.tgico-recent:before { +.tgico-flag:before { content: "\e94d"; } -.tgico-channel:before { +.tgico-lamp:before { content: "\e94e"; } -.tgico-document:before { +.tgico-sport:before { content: "\e94f"; } -.tgico-activesessions:before { +.tgico-car:before { content: "\e950"; } -.tgico-logout:before { +.tgico-eats:before { content: "\e951"; } -.tgico-help:before { +.tgico-animals:before { content: "\e952"; } -.tgico-play:before { +.tgico-smile:before { content: "\e953"; } -.tgico-pause:before { +.tgico-unpin:before { content: "\e954"; } -.tgico-reply:before { +.tgico-send:before { content: "\e955"; } -.tgico-forward:before { +.tgico-unread:before { content: "\e956"; } -.tgico-next:before { +.tgico-settings:before { content: "\e957"; } -.tgico-unlock:before { +.tgico-edit:before { content: "\e958"; } -.tgico-lock:before { +.tgico-download:before { content: "\e959"; } -.tgico-data:before { +.tgico-cameraadd:before { content: "\e95a"; } -.tgico-user:before { +.tgico-camera:before { content: "\e95b"; } -.tgico-group:before { +.tgico-permissions:before { content: "\e95c"; } -.tgico-mute:before { +.tgico-admin:before { content: "\e95d"; } -.tgico-unmute:before { +.tgico-stop:before { content: "\e95e"; } -.tgico-photo:before { +.tgico-username:before { content: "\e95f"; } -.tgico-language:before { +.tgico-location:before { content: "\e960"; } -.tgico-message:before { +.tgico-info:before { content: "\e961"; } -.tgico-pin:before { +.tgico-deleteuser:before { content: "\e962"; } -.tgico-attach:before { +.tgico-adduser:before { content: "\e963"; } -.tgico-phone:before { +.tgico-recent:before { content: "\e964"; } -.tgico-savedmessages:before { +.tgico-channel:before { content: "\e965"; } -.tgico-checkbox:before { +.tgico-document:before { content: "\e966"; } -.tgico-copy:before { +.tgico-activesessions:before { content: "\e967"; } -.tgico-unarchive:before { +.tgico-logout:before { content: "\e968"; } -.tgico-archive:before { +.tgico-help:before { content: "\e969"; } -.tgico-check1:before { +.tgico-play:before { content: "\e96a"; } -.tgico-up:before { +.tgico-pause:before { content: "\e96b"; } -.tgico-down:before { +.tgico-reply:before { content: "\e96c"; } -.tgico-close:before { +.tgico-forward:before { content: "\e96d"; } -.tgico-add:before { +.tgico-next:before { content: "\e96e"; } -.tgico-back:before { +.tgico-unlock:before { content: "\e96f"; } -.tgico-more:before { +.tgico-lock:before { content: "\e970"; } -.tgico-menu:before { +.tgico-data:before { content: "\e971"; } -.tgico-search:before { +.tgico-user:before { content: "\e972"; } +.tgico-group:before { + content: "\e973"; +} +.tgico-mute:before { + content: "\e974"; +} +.tgico-unmute:before { + content: "\e975"; +} +.tgico-photo:before { + content: "\e976"; +} +.tgico-language:before { + content: "\e977"; +} +.tgico-message:before { + content: "\e978"; +} +.tgico-pin:before { + content: "\e979"; +} +.tgico-attach:before { + content: "\e97a"; +} +.tgico-phone:before { + content: "\e97b"; +} +.tgico-savedmessages:before { + content: "\e97c"; +} +.tgico-checkbox:before { + content: "\e97d"; +} +.tgico-copy:before { + content: "\e97e"; +} +.tgico-unarchive:before { + content: "\e97f"; +} +.tgico-archive:before { + content: "\e980"; +} +.tgico-check1:before { + content: "\e981"; +} +.tgico-up:before { + content: "\e982"; +} +.tgico-down:before { + content: "\e983"; +} +.tgico-close:before { + content: "\e984"; +} +.tgico-add:before { + content: "\e985"; +} +.tgico-back:before { + content: "\e986"; +} +.tgico-more:before { + content: "\e987"; +} +.tgico-menu:before { + content: "\e988"; +} +.tgico-search:before { + content: "\e989"; +} diff --git a/src/scss/partials/_ico.scss b/src/scss/partials/_ico.scss index 5ce2becb..7613e604 100644 --- a/src/scss/partials/_ico.scss +++ b/src/scss/partials/_ico.scss @@ -13,118 +13,141 @@ $tgico-font-path: "assets/fonts" !default; .replace(/\.(.+?):before\{content:"(.+?);\}/g, `$$$1: "\\$2;\n`); */ -$tgico-select: "\e900"; -$tgico-info2: "\e901"; -$tgico-clouddownload: "\e902"; -$tgico-readchats: "\e903"; -$tgico-noncontacts: "\e904"; -$tgico-bots: "\e905"; -$tgico-muted: "\e906"; -$tgico-favourites: "\e907"; -$tgico-tip: "\e908"; -$tgico-loginlogodesktop: "\e909"; -$tgico-loginlogomobile: "\e90a"; -$tgico-calendar: "\e90b"; -$tgico-keyboard: "\e90c"; -$tgico-gifs: "\e90d"; -$tgico-stickers: "\e90e"; -$tgico-deleteleft: "\e90f"; -$tgico-folder: "\e910"; -$tgico-revote: "\e911"; -$tgico-livelocation: "\e912"; -$tgico-microphone2: "\e913"; -$tgico-colorize: "\e914"; -$tgico-poll: "\e915"; -$tgico-minus: "\e916"; -$tgico-nosound: "\e917"; -$tgico-microphone: "\e918"; -$tgico-largeplay: "\e919"; -$tgico-largepause: "\e91a"; -$tgico-newchannel: "\e91b"; -$tgico-newgroup: "\e91c"; -$tgico-newprivate: "\e91d"; -$tgico-chatsplaceholder: "\e91e"; -$tgico-newchat_filled: "\e91f"; -$tgico-addmember_filled: "\e920"; -$tgico-delete: "\e921"; -$tgico-delete_filled: "\e922"; -$tgico-send2: "\e923"; -$tgico-avatar_deletedaccount: "\e924"; -$tgico-avatar_archivedchats: "\e925"; -$tgico-avatar_savedmessages: "\e926"; -$tgico-pinnedchat: "\e927"; -$tgico-channelviews: "\e928"; -$tgico-sendingerror: "\e929"; -$tgico-sending: "\e92a"; -$tgico-check: "\e92b"; -$tgico-checks: "\e92c"; -$tgico-radioon: "\e92d"; -$tgico-radiooff: "\e92e"; -$tgico-checkboxempty: "\e92f"; -$tgico-checkboxblock: "\e930"; -$tgico-checkboxon: "\e931"; -$tgico-eye2: "\e932"; -$tgico-eye1: "\e933"; -$tgico-fullscreen: "\e934"; -$tgico-smallscreen: "\e935"; -$tgico-flag: "\e936"; -$tgico-lamp: "\e937"; -$tgico-sport: "\e938"; -$tgico-car: "\e939"; -$tgico-eats: "\e93a"; -$tgico-animals: "\e93b"; -$tgico-smile: "\e93c"; -$tgico-unpin: "\e93d"; -$tgico-send: "\e93e"; -$tgico-unread: "\e93f"; -$tgico-settings: "\e940"; -$tgico-edit: "\e941"; -$tgico-download: "\e942"; -$tgico-cameraadd: "\e943"; -$tgico-camera: "\e944"; -$tgico-permissions: "\e945"; -$tgico-admin: "\e946"; -$tgico-stop: "\e947"; -$tgico-username: "\e948"; -$tgico-location: "\e949"; -$tgico-info: "\e94a"; -$tgico-deleteuser: "\e94b"; -$tgico-adduser: "\e94c"; -$tgico-recent: "\e94d"; -$tgico-channel: "\e94e"; -$tgico-document: "\e94f"; -$tgico-activesessions: "\e950"; -$tgico-logout: "\e951"; -$tgico-help: "\e952"; -$tgico-play: "\e953"; -$tgico-pause: "\e954"; -$tgico-reply: "\e955"; -$tgico-forward: "\e956"; -$tgico-next: "\e957"; -$tgico-unlock: "\e958"; -$tgico-lock: "\e959"; -$tgico-data: "\e95a"; -$tgico-user: "\e95b"; -$tgico-group: "\e95c"; -$tgico-mute: "\e95d"; -$tgico-unmute: "\e95e"; -$tgico-photo: "\e95f"; -$tgico-language: "\e960"; -$tgico-message: "\e961"; -$tgico-pin: "\e962"; -$tgico-attach: "\e963"; -$tgico-phone: "\e964"; -$tgico-savedmessages: "\e965"; -$tgico-checkbox: "\e966"; -$tgico-copy: "\e967"; -$tgico-unarchive: "\e968"; -$tgico-archive: "\e969"; -$tgico-check1: "\e96a"; -$tgico-up: "\e96b"; -$tgico-down: "\e96c"; -$tgico-close: "\e96d"; -$tgico-add: "\e96e"; -$tgico-back: "\e96f"; -$tgico-more: "\e970"; -$tgico-menu: "\e971"; -$tgico-search: "\e972"; +$tgico-dragmedia: "\e900"; +$tgico-dragfiles: "\e901"; +$tgico-link: "\e902"; +$tgico-monospace: "\e903"; +$tgico-strikethrough: "\e904"; +$tgico-underline: "\e905"; +$tgico-italic: "\e906"; +$tgico-bold: "\e907"; +$tgico-botcom: "\e908"; +$tgico-calendarfilter: "\e909"; +$tgico-email: "\e90a"; +$tgico-passwordoff: "\e90b"; +$tgico-commentssticker: "\e90c"; +$tgico-comments: "\e90d"; +$tgico-previous: "\e90e"; +$tgico-next: "\e90f"; +$tgico-mention: "\e910"; +$tgico-down: "\e911"; +$tgico-pinlist: "\e912"; +$tgico-replace: "\e913"; +$tgico-schedule: "\e914"; +$tgico-zoomout: "\e915"; +$tgico-zoomin: "\e916"; +$tgico-select: "\e917"; +$tgico-info2: "\e918"; +$tgico-clouddownload: "\e919"; +$tgico-readchats: "\e91a"; +$tgico-noncontacts: "\e91b"; +$tgico-bots: "\e91c"; +$tgico-muted: "\e91d"; +$tgico-favourites: "\e91e"; +$tgico-tip: "\e91f"; +$tgico-loginlogodesktop: "\e920"; +$tgico-loginlogomobile: "\e921"; +$tgico-calendar: "\e922"; +$tgico-keyboard: "\e923"; +$tgico-gifs: "\e924"; +$tgico-stickers: "\e925"; +$tgico-deleteleft: "\e926"; +$tgico-folder: "\e927"; +$tgico-revote: "\e928"; +$tgico-livelocation: "\e929"; +$tgico-microphone2: "\e92a"; +$tgico-colorize: "\e92b"; +$tgico-poll: "\e92c"; +$tgico-minus: "\e92d"; +$tgico-nosound: "\e92e"; +$tgico-microphone: "\e92f"; +$tgico-largeplay: "\e930"; +$tgico-largepause: "\e931"; +$tgico-newchannel: "\e932"; +$tgico-newgroup: "\e933"; +$tgico-newprivate: "\e934"; +$tgico-chatsplaceholder: "\e935"; +$tgico-newchat_filled: "\e936"; +$tgico-addmember_filled: "\e937"; +$tgico-delete: "\e938"; +$tgico-delete_filled: "\e939"; +$tgico-send2: "\e93a"; +$tgico-avatar_deletedaccount: "\e93b"; +$tgico-avatar_archivedchats: "\e93c"; +$tgico-avatar_savedmessages: "\e93d"; +$tgico-pinnedchat: "\e93e"; +$tgico-channelviews: "\e93f"; +$tgico-sendingerror: "\e940"; +$tgico-sending: "\e941"; +$tgico-check: "\e942"; +$tgico-checks: "\e943"; +$tgico-radioon: "\e944"; +$tgico-radiooff: "\e945"; +$tgico-checkboxempty: "\e946"; +$tgico-checkboxblock: "\e947"; +$tgico-checkboxon: "\e948"; +$tgico-eye2: "\e949"; +$tgico-eye1: "\e94a"; +$tgico-fullscreen: "\e94b"; +$tgico-smallscreen: "\e94c"; +$tgico-flag: "\e94d"; +$tgico-lamp: "\e94e"; +$tgico-sport: "\e94f"; +$tgico-car: "\e950"; +$tgico-eats: "\e951"; +$tgico-animals: "\e952"; +$tgico-smile: "\e953"; +$tgico-unpin: "\e954"; +$tgico-send: "\e955"; +$tgico-unread: "\e956"; +$tgico-settings: "\e957"; +$tgico-edit: "\e958"; +$tgico-download: "\e959"; +$tgico-cameraadd: "\e95a"; +$tgico-camera: "\e95b"; +$tgico-permissions: "\e95c"; +$tgico-admin: "\e95d"; +$tgico-stop: "\e95e"; +$tgico-username: "\e95f"; +$tgico-location: "\e960"; +$tgico-info: "\e961"; +$tgico-deleteuser: "\e962"; +$tgico-adduser: "\e963"; +$tgico-recent: "\e964"; +$tgico-channel: "\e965"; +$tgico-document: "\e966"; +$tgico-activesessions: "\e967"; +$tgico-logout: "\e968"; +$tgico-help: "\e969"; +$tgico-play: "\e96a"; +$tgico-pause: "\e96b"; +$tgico-reply: "\e96c"; +$tgico-forward: "\e96d"; +$tgico-next: "\e96e"; +$tgico-unlock: "\e96f"; +$tgico-lock: "\e970"; +$tgico-data: "\e971"; +$tgico-user: "\e972"; +$tgico-group: "\e973"; +$tgico-mute: "\e974"; +$tgico-unmute: "\e975"; +$tgico-photo: "\e976"; +$tgico-language: "\e977"; +$tgico-message: "\e978"; +$tgico-pin: "\e979"; +$tgico-attach: "\e97a"; +$tgico-phone: "\e97b"; +$tgico-savedmessages: "\e97c"; +$tgico-checkbox: "\e97d"; +$tgico-copy: "\e97e"; +$tgico-unarchive: "\e97f"; +$tgico-archive: "\e980"; +$tgico-check1: "\e981"; +$tgico-up: "\e982"; +$tgico-down: "\e983"; +$tgico-close: "\e984"; +$tgico-add: "\e985"; +$tgico-back: "\e986"; +$tgico-more: "\e987"; +$tgico-menu: "\e988"; +$tgico-search: "\e989"; diff --git a/src/scss/partials/_input.scss b/src/scss/partials/_input.scss index 7afe5d9c..04e1c06b 100644 --- a/src/scss/partials/_input.scss +++ b/src/scss/partials/_input.scss @@ -152,4 +152,32 @@ input:focus, button:focus { outline: none; +} + +.input-clear { + outline: none; + border: none; + padding: 0; + + &.error { + animation: input-shake .2s ease-in-out forwards; + } +} + +@keyframes input-shake { + 0% { + transform: translateX(0); + } + + 25% { + transform: translateX(-.5rem); + } + + 75% { + transform: translateX(.5rem); + } + + 100% { + transform: translateX(0); + } } \ No newline at end of file