Skip to content

Commit

Permalink
Update for Editor Drivers Abstraction (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
askvortsov1 authored Feb 26, 2021
1 parent ccb6fa5 commit a34fdfa
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 145 deletions.
37 changes: 18 additions & 19 deletions js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
258 changes: 133 additions & 125 deletions js/src/forum/addComposerAutocomplete.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import getCaretCoordinates from 'textarea-caret';
import emojiMap from 'simple-emoji-map';

import { extend } from 'flarum/extend';
Expand All @@ -12,20 +11,11 @@ import cdn from './cdn';

export default function addComposerAutocomplete() {
const emojiKeys = Object.keys(emojiMap);
const $container = $('<div class="ComposerBody-emojiDropdownContainer"></div>');
const dropdown = new AutocompleteDropdown();

extend(TextEditor.prototype, 'oncreate', function() {

const $container = $('<div class="ComposerBody-emojiDropdownContainer"></div>');
const dropdown = new AutocompleteDropdown();
const $textarea = this.$('textarea').wrap('<div class="ComposerBody-emojiWrapper"></div>');
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('<div class="ComposerBody-emojiWrapper"></div>');

this.navigator = new KeyboardNavigatable();
this.navigator
Expand All @@ -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 (
<button
key={emoji}
onclick={() => applySuggestion(emoji)}
onmouseenter={function() {
dropdown.setIndex($(this).parent().index() - 1);
}}>
<img alt={emoji} class="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`}/>
{name}
</button>
);
dropdown.hide();
dropdown.active = false;

if (absEmojiStart) {
typed = lastChunk.substring(relEmojiStart).toLowerCase();

const makeSuggestion = function ({ emoji, name, code }) {
return (
<button
key={emoji}
onclick={() => applySuggestion(emoji)}
onmouseenter={function () {
dropdown.setIndex($(this).parent().index() - 1);
}}>
<img alt={emoji} class="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} />
{name}
</button>
);
};

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', (
<TextEditorButton onclick={() => this.attrs.composer.editor.insertAtCursor(':')} icon="far fa-smile">
<TextEditorButton onclick={() => this.attrs.composer.editor.insertAtCursor(' :')} icon="far fa-smile">
{app.translator.trans('flarum-emoji.forum.composer.emoji_tooltip')}
</TextEditorButton>
));
Expand Down

0 comments on commit a34fdfa

Please sign in to comment.