From 9dc383777274dc6a2a4686170f1c519e0f19da2a Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com> Date: Fri, 26 Feb 2021 16:18:32 -0500 Subject: [PATCH] Update for Editor Drivers Abstraction (#34) --- js/package-lock.json | 37 ++-- js/package.json | 1 - js/src/forum/addComposerAutocomplete.js | 258 ++++++++++++------------ 3 files changed, 151 insertions(+), 145 deletions(-) diff --git a/js/package-lock.json b/js/package-lock.json index 9666218..f2c4e0e 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1778,9 +1778,9 @@ "integrity": "sha512-rYWlogJ2q5P78U8Xx1vhsXHcYKu1wFnr7+o6z9QHssZ1SsJLTCkJINZIPHRFWuDreAUK457TkqHpdOXElu0fzA==" }, "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "requires": { "once": "^1.4.0" } @@ -3441,9 +3441,12 @@ "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" }, "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } }, "set-blocking": { "version": "2.0.0", @@ -3500,7 +3503,8 @@ }, "simple-emoji-map": { "version": "0.4.1", - "resolved": "git+https://github.com/davwheat/simple-emoji-map.git#dfa29dae25b4620302b1330cf54e12d4492fedf0", + "resolved": "https://registry.npmjs.org/simple-emoji-map/-/simple-emoji-map-0.4.1.tgz", + "integrity": "sha512-K40UJhKs0VGE4gXj4cV/REnBLIUfPJ5n+eLWwpaOWQmjnJnK1+uyw/KIqFAWemD1kpsL2/khUNYRLe48YH9MKA==", "requires": { "cosmiconfig": "^6.0.0", "emojibase-data": "^5.0.1", @@ -3770,9 +3774,9 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, "terser": { - "version": "4.6.13", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.13.tgz", - "integrity": "sha512-wMvqukYgVpQlymbnNbabVZbtM6PN63AzqexpwJL8tbh/mRT9LE5o+ruVduAGL7D6Fpjl+Q+06U5I9Ul82odAhw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", @@ -3787,15 +3791,15 @@ } }, "terser-webpack-plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", - "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -3809,11 +3813,6 @@ } } }, - "textarea-caret": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", - "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==" - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", diff --git a/js/package.json b/js/package.json index 54aa7ba..3e2e07c 100644 --- a/js/package.json +++ b/js/package.json @@ -4,7 +4,6 @@ "dependencies": { "flarum-webpack-config": "0.1.0-beta.10", "simple-emoji-map": "^0.4.1", - "textarea-caret": "^3.1.0", "twemoji": "^13.0.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.12" diff --git a/js/src/forum/addComposerAutocomplete.js b/js/src/forum/addComposerAutocomplete.js index c4ee949..a6a1bf1 100644 --- a/js/src/forum/addComposerAutocomplete.js +++ b/js/src/forum/addComposerAutocomplete.js @@ -1,4 +1,3 @@ -import getCaretCoordinates from 'textarea-caret'; import emojiMap from 'simple-emoji-map'; import { extend } from 'flarum/extend'; @@ -12,20 +11,11 @@ import cdn from './cdn'; export default function addComposerAutocomplete() { const emojiKeys = Object.keys(emojiMap); + const $container = $('
'); + const dropdown = new AutocompleteDropdown(); - extend(TextEditor.prototype, 'oncreate', function() { - - const $container = $('
'); - const dropdown = new AutocompleteDropdown(); - const $textarea = this.$('textarea').wrap('
'); - let emojiStart; - let typed; - - const applySuggestion = (replacement) => { - this.attrs.composer.editor.replaceBeforeCursor(emojiStart - 1, replacement + ' '); - - dropdown.hide(); - }; + extend(TextEditor.prototype, 'oncreate', function () { + const $editor = this.$('.TextEditor-editor').wrap('
'); this.navigator = new KeyboardNavigatable(); this.navigator @@ -34,131 +24,149 @@ export default function addComposerAutocomplete() { .onDown(() => dropdown.navigate(1)) .onSelect(dropdown.complete.bind(dropdown)) .onCancel(dropdown.hide.bind(dropdown)) - .bindTo($textarea); - - $textarea - .after($container) - .on('click keyup input', function(e) { - // Up, down, enter, tab, escape, left, right. - if ([9, 13, 27, 40, 38, 37, 39].indexOf(e.which) !== -1) return; - - const cursor = this.selectionStart; - - if (this.selectionEnd - cursor > 0) return; - - // Search backwards from the cursor for an ':' symbol. If we find - // one and followed by a whitespace, we will want to show the - // autocomplete dropdown! - const value = this.value; - emojiStart = 0; - for (let i = cursor - 1; i >= 0; i--) { - const character = value.substr(i, 1); - // check what user typed, emoji names only contains alphanumeric, - // underline, '+' and '-' - if (!/[a-z0-9]|\+|\-|_|\:/.test(character)) break; - // make sure ':' followed by a whitespace or newline - if (character === ':' && (i == 0 || /\s/.test(value.substr(i - 1, 1)))) { - emojiStart = i + 1; - break; - } + .bindTo($editor); + + $editor.after($container); + + }); + + extend(TextEditor.prototype, 'buildEditorParams', function (params) { + let relEmojiStart; + let absEmojiStart; + let typed; + + const applySuggestion = (replacement) => { + this.attrs.composer.editor.replaceBeforeCursor(absEmojiStart - 1, replacement + ' '); + + dropdown.hide(); + }; + + params.inputListeners.push(function (e) { + const selection = app.composer.editor.getSelectionRange(); + + const cursor = selection[0]; + + if (selection[1] - cursor > 0) return; + + // Search backwards from the cursor for an ':' symbol. If we find + // one and followed by a whitespace, we will want to show the + // autocomplete dropdown! + const lastChunk = app.composer.editor.getLastNChars(15); + absEmojiStart = 0; + for (let i = lastChunk.length - 1; i >= 0; i--) { + const character = lastChunk.substr(i, 1); + // check what user typed, emoji names only contains alphanumeric, + // underline, '+' and '-' + if (!/[a-z0-9]|\+|\-|_|\:/.test(character)) break; + // make sure ':' preceded by a whitespace or newline + if (character === ':' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) { + relEmojiStart = i + 1; + absEmojiStart = cursor - lastChunk.length + i + 1; + break; } + } - dropdown.hide(); - dropdown.active = false; - - if (emojiStart) { - typed = value.substring(emojiStart, cursor).toLowerCase(); - - const makeSuggestion = function({emoji, name, code}) { - return ( - - ); + dropdown.hide(); + dropdown.active = false; + + if (absEmojiStart) { + typed = lastChunk.substring(relEmojiStart).toLowerCase(); + + const makeSuggestion = function ({ emoji, name, code }) { + return ( + + ); + }; + + const buildSuggestions = () => { + const similarEmoji = []; + + // Build a regular expression to do a fuzzy match of the given input string + const fuzzyRegexp = function (str) { + const reEscape = new RegExp('\\(([' + ('+.*?[]{}()^$|\\'.replace(/(.)/g, '\\$1')) + '])\\)', 'g'); + return new RegExp('(.*)' + (str.toLowerCase().replace(/(.)/g, '($1)(.*?)')).replace(reEscape, '(\\$1)') + '$', 'i'); }; + const regTyped = fuzzyRegexp(typed); - const buildSuggestions = () => { - const similarEmoji = []; - - // Build a regular expression to do a fuzzy match of the given input string - const fuzzyRegexp = function(str) { - const reEscape = new RegExp('\\(([' + ('+.*?[]{}()^$|\\'.replace(/(.)/g, '\\$1')) + '])\\)', 'g'); - return new RegExp('(.*)' + (str.toLowerCase().replace(/(.)/g, '($1)(.*?)')).replace(reEscape, '(\\$1)') + '$', 'i'); - }; - const regTyped = fuzzyRegexp(typed); - - let maxSuggestions = 7; - - const findMatchingEmojis = matcher => { - for (let i = 0; i < emojiKeys.length && maxSuggestions > 0; i++) { - const curEmoji = emojiKeys[i]; - - if (similarEmoji.indexOf(curEmoji) === -1) { - const names = emojiMap[curEmoji]; - for (let name of names) { - if (matcher(name)) { - --maxSuggestions; - similarEmoji.push(curEmoji); - break; - } + let maxSuggestions = 7; + + const findMatchingEmojis = matcher => { + for (let i = 0; i < emojiKeys.length && maxSuggestions > 0; i++) { + const curEmoji = emojiKeys[i]; + + if (similarEmoji.indexOf(curEmoji) === -1) { + const names = emojiMap[curEmoji]; + for (let name of names) { + if (matcher(name)) { + --maxSuggestions; + similarEmoji.push(curEmoji); + break; } } } - }; - - // First, try to find all emojis starting with the given string - findMatchingEmojis(emoji => emoji.indexOf(typed) === 0); - - // If there are still suggestions left, try for some fuzzy matches - findMatchingEmojis(emoji => regTyped.test(emoji)); - - const suggestions = similarEmoji.map(emoji => ({ - emoji, - name: emojiMap[emoji][0], - code: getEmojiIconCode(emoji), - })).map(makeSuggestion); - - if (suggestions.length) { - dropdown.items = suggestions; - m.render($container[0], dropdown.render()); - - dropdown.show(); - const coordinates = getCaretCoordinates(this, emojiStart); - const width = dropdown.$().outerWidth(); - const height = dropdown.$().outerHeight(); - const parent = dropdown.$().offsetParent(); - let left = coordinates.left; - let top = coordinates.top + 15; - if (top + height > parent.height()) { - top = coordinates.top - height - 15; - } - if (left + width > parent.width()) { - left = parent.width() - width; - } - top = Math.max(-$(this).offset().top, top); - left = Math.max(-$(this).offset().left, left); - dropdown.show(left, top); } }; - buildSuggestions(); + // First, try to find all emojis starting with the given string + findMatchingEmojis(emoji => emoji.indexOf(typed) === 0); + + // If there are still suggestions left, try for some fuzzy matches + findMatchingEmojis(emoji => regTyped.test(emoji)); + + const suggestions = similarEmoji.map(emoji => ({ + emoji, + name: emojiMap[emoji][0], + code: getEmojiIconCode(emoji), + })).map(makeSuggestion); + + if (suggestions.length) { + dropdown.items = suggestions; + m.render($container[0], dropdown.render()); + + dropdown.show(); + const coordinates = app.composer.editor.getCaretCoordinates(absEmojiStart); + const width = dropdown.$().outerWidth(); + const height = dropdown.$().outerHeight(); + const parent = dropdown.$().offsetParent(); + let left = coordinates.left; + let top = coordinates.top + 15; + + // Keep the dropdown inside the editor. + if (top + height > parent.height()) { + top = coordinates.top - height - 15; + } + if (left + width > parent.width()) { + left = parent.width() - width; + } - dropdown.setIndex(0); - dropdown.$().scrollTop(0); - dropdown.active = true; - } - }); + // Prevent the dropdown from going off screen on mobile + top = Math.max(-parent.offset().top, top); + left = Math.max(-parent.offset().left, left); + + dropdown.show(left, top); + } + }; + + buildSuggestions(); + + dropdown.setIndex(0); + dropdown.$().scrollTop(0); + dropdown.active = true; + } + }); }); - extend(TextEditor.prototype, 'toolbarItems', function(items) { + extend(TextEditor.prototype, 'toolbarItems', function (items) { items.add('emoji', ( - this.attrs.composer.editor.insertAtCursor(':')} icon="far fa-smile"> + this.attrs.composer.editor.insertAtCursor(' :')} icon="far fa-smile"> {app.translator.trans('flarum-emoji.forum.composer.emoji_tooltip')} ));