mirror of
https://github.com/flarum/core.git
synced 2025-07-30 21:20:24 +02:00
Merge remote-tracking branch 'extensions_emoji/REWRITE'
This commit is contained in:
19
extensions/emoji/.editorconfig
Normal file
19
extensions/emoji/.editorconfig
Normal file
@@ -0,0 +1,19 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{php,xml,json}]
|
||||
indent_size = 4
|
18
extensions/emoji/.gitattributes
vendored
Normal file
18
extensions/emoji/.gitattributes
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitmodules export-ignore
|
||||
.github export-ignore
|
||||
.travis export-ignore
|
||||
.travis.yml export-ignore
|
||||
.editorconfig export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
phpunit.xml export-ignore
|
||||
tests export-ignore
|
||||
|
||||
js/dist/* -diff
|
||||
js/dist/* linguist-generated
|
||||
js/dist-typings/* linguist-generated
|
||||
js/yarn.lock -diff
|
||||
|
||||
* text=auto eol=lf
|
15
extensions/emoji/.github/workflows/backend.yml
vendored
Normal file
15
extensions/emoji/.github/workflows/backend.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Emoji PHP
|
||||
|
||||
on: [workflow_dispatch, push, pull_request]
|
||||
|
||||
# The reusable workflow definitions will be moved to the `flarum/framework` repo soon.
|
||||
# This will break your current script.
|
||||
# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure.
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: flarum/.github/.github/workflows/REUSABLE_backend.yml@main
|
||||
with:
|
||||
enable_backend_testing: false
|
||||
|
||||
backend_directory: .
|
21
extensions/emoji/.github/workflows/frontend.yml
vendored
Normal file
21
extensions/emoji/.github/workflows/frontend.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Emoji JS
|
||||
|
||||
on: [workflow_dispatch, push, pull_request]
|
||||
|
||||
# The reusable workflow definitions will be moved to the `flarum/framework` repo soon.
|
||||
# This will break your current script.
|
||||
# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure.
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: flarum/.github/.github/workflows/REUSABLE_frontend.yml@main
|
||||
with:
|
||||
enable_bundlewatch: false
|
||||
enable_prettier: true
|
||||
enable_typescript: false
|
||||
|
||||
frontend_directory: ./js
|
||||
main_git_branch: master
|
||||
|
||||
secrets:
|
||||
bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
|
12
extensions/emoji/.gitignore
vendored
Normal file
12
extensions/emoji/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/vendor
|
||||
composer.lock
|
||||
composer.phar
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
tests/.phpunit.result.cache
|
||||
/tests/integration/tmp
|
||||
.vagrant
|
||||
.idea/*
|
||||
.vscode
|
||||
js/coverage-ts
|
14
extensions/emoji/.styleci.yml
Normal file
14
extensions/emoji/.styleci.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
preset: recommended
|
||||
|
||||
enabled:
|
||||
- logical_not_operators_with_successor_space
|
||||
|
||||
disabled:
|
||||
- align_double_arrow
|
||||
- blank_line_after_opening_tag
|
||||
- multiline_array_trailing_comma
|
||||
- new_with_braces
|
||||
- phpdoc_align
|
||||
- phpdoc_order
|
||||
- phpdoc_separation
|
||||
- phpdoc_types
|
59
extensions/emoji/CHANGELOG.md
Normal file
59
extensions/emoji/CHANGELOG.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
## [1.2.0](https://github.com/flarum/emoji/compare/v1.1.1...v1.2.0)
|
||||
|
||||
No changes.
|
||||
|
||||
## [1.1.1](https://github.com/flarum/emoji/compare/v1.1.0...v1.1.1)
|
||||
|
||||
### Fixed
|
||||
* fix: errors with composer reuse by @davwheat in https://github.com/flarum/emoji/pull/42
|
||||
|
||||
## [1.1.0](https://github.com/flarum/emoji/compare/v1.0.0...v1.1.0)
|
||||
|
||||
### Changed
|
||||
- Format with prettier, bump deps (https://github.com/flarum/markdown/pulls/37)
|
||||
- Update JS imports to new namespaced style (https://github.com/flarum/markdown/pulls/39)
|
||||
|
||||
### Fixed
|
||||
- Twemoji frontend broken rendering with image markdown syntax (https://github.com/flarum/markdown/pulls/40)
|
||||
|
||||
## [1.0.0](https://github.com/flarum/emoji/compare/v0.1.0-beta.16...v1.0.0)
|
||||
|
||||
### Changed
|
||||
- Compatibility with Flarum v1.0.0.
|
||||
|
||||
## [0.1.0-beta.16](https://github.com/flarum/emoji/compare/v0.1.0-beta.15...v0.1.0-beta.16)
|
||||
|
||||
### Changed
|
||||
- Updated admin category from discussion to feature (https://github.com/flarum/emoji/pull/35)
|
||||
- Moved locale files from translation pack to extension (https://github.com/flarum/emoji/pull/32)
|
||||
- Editor Driver Abstractions (https://github.com/flarum/emoji/pull/34)
|
||||
|
||||
### Fixes
|
||||
- Dropdown going off screen ([259b934](https://github.com/flarum/emoji/commit/259b934af706e1d8763b8efc8d0e67bbf92cf45f))
|
||||
|
||||
## [0.1.0-beta.15](https://github.com/flarum/emoji/compare/v0.1.0-beta.14...v0.1.0-beta.15)
|
||||
|
||||
### Changed
|
||||
- Updated composer.json for new admin area.
|
||||
|
||||
## [0.1.0-beta.14](https://github.com/flarum/emoji/compare/v0.1.0-beta.13...v0.1.0-beta.14)
|
||||
|
||||
### Changed
|
||||
- Updated mithril to version 2
|
||||
- Load language strings correctly on en-/disable
|
||||
- Updated JS dependencies
|
||||
- Implemented lazy loading for images
|
||||
- Change CDN to jsDelivr
|
||||
|
||||
## [0.1.0-beta.13](https://github.com/flarum/emoji/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
||||
|
||||
### Changed
|
||||
- Use different CDN for loading Twemoji assets (#24)
|
||||
- Updated JS dependencies
|
||||
|
||||
## [0.1.0-beta.10](https://github.com/flarum/emoji/compare/v0.1.0-beta.8...v0.1.0-beta.10)
|
||||
|
||||
### Added
|
||||
- Latest and greatest emojis with Twemoji 12 (#20)
|
22
extensions/emoji/LICENSE
Normal file
22
extensions/emoji/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
54
extensions/emoji/composer.json
Normal file
54
extensions/emoji/composer.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "flarum/emoji",
|
||||
"description": "Convert text and unicode emoji into Twemoji.",
|
||||
"type": "flarum-extension",
|
||||
"keywords": [
|
||||
"formatting"
|
||||
],
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/flarum/core/issues",
|
||||
"source": "https://github.com/flarum/emoji",
|
||||
"forum": "https://discuss.flarum.org"
|
||||
},
|
||||
"homepage": "https://flarum.org",
|
||||
"funding": [
|
||||
{
|
||||
"type": "website",
|
||||
"url": "https://flarum.org/donate/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.2"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
},
|
||||
"flarum-extension": {
|
||||
"title": "Emoji",
|
||||
"category": "feature",
|
||||
"icon": {
|
||||
"image": "icon.svg",
|
||||
"backgroundColor": "#FECC4D"
|
||||
}
|
||||
},
|
||||
"flarum-cli": {
|
||||
"modules": {
|
||||
"admin": false,
|
||||
"forum": true,
|
||||
"js": true,
|
||||
"jsCommon": false,
|
||||
"css": true,
|
||||
"gitConf": true,
|
||||
"githubActions": true,
|
||||
"prettier": true,
|
||||
"typescript": false,
|
||||
"bundlewatch": false,
|
||||
"backendTesting": false,
|
||||
"editorConfig": true,
|
||||
"styleci": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
extensions/emoji/extend.php
Normal file
32
extensions/emoji/extend.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Extend;
|
||||
use s9e\TextFormatter\Configurator;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
->js(__DIR__.'/js/dist/forum.js')
|
||||
->css(__DIR__.'/less/forum.less'),
|
||||
|
||||
(new Extend\Formatter)
|
||||
->configure(function (Configurator $config) {
|
||||
$config->Emoticons->add(':)', '🙂');
|
||||
$config->Emoticons->add(':D', '😃');
|
||||
$config->Emoticons->add(':P', '😛');
|
||||
$config->Emoticons->add(':(', '🙁');
|
||||
$config->Emoticons->add(':|', '😐');
|
||||
$config->Emoticons->add(';)', '😉');
|
||||
$config->Emoticons->add(':\'(', '😢');
|
||||
$config->Emoticons->add(':O', '😮');
|
||||
$config->Emoticons->add('>:(', '😡');
|
||||
}),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
];
|
1
extensions/emoji/icon.svg
Normal file
1
extensions/emoji/icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 47.5 47.5" style="enable-background:new 0 0 47.5 47.5;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,38 38,38 38,0 0,0 0,38 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,47.5)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,19)" id="g20"><path id="path22" style="fill:#ffcc4d;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-9.389 -7.611,-17 -17,-17 -9.389,0 -17,7.611 -17,17 0,9.389 7.611,17 17,17 C -7.611,17 0,9.389 0,0"/></g><g transform="translate(19,16)" id="g24"><path id="path26" style="fill:#664500;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -3.623,0 -6.027,0.422 -9,1 -0.679,0.131 -2,0 -2,-2 0,-4 4.595,-9 11,-9 6.404,0 11,5 11,9 C 11,1 9.679,1.132 9,1 6.027,0.422 3.623,0 0,0"/></g><g transform="translate(11,24)" id="g28"><path id="path30" style="fill:#664500;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,0 0,2 2,2 4,2 4,0 4,0 l 0,-2 c 0,0 0,-2 -2,-2 -2,0 -2,2 -2,2 l 0,2 z"/></g><g transform="translate(23,24)" id="g32"><path id="path34" style="fill:#664500;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,0 0,2 2,2 4,2 4,0 4,0 l 0,-2 c 0,0 0,-2 -2,-2 -2,0 -2,2 -2,2 l 0,2 z"/></g><g transform="translate(10,15)" id="g36"><path id="path38" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0 3,-1 9,-1 6,0 9,1 9,1 0,0 -2,-4 -9,-4 -7,0 -9,4 -9,4"/></g></g></g></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
9
extensions/emoji/js/.gitignore
vendored
Normal file
9
extensions/emoji/js/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
node_modules
|
6
extensions/emoji/js/.prettierrc.json
Normal file
6
extensions/emoji/js/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
3
extensions/emoji/js/dist/forum.js
generated
vendored
Normal file
3
extensions/emoji/js/dist/forum.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
extensions/emoji/js/dist/forum.js.LICENSE.txt
generated
vendored
Normal file
1
extensions/emoji/js/dist/forum.js.LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */
|
1
extensions/emoji/js/dist/forum.js.map
generated
vendored
Normal file
1
extensions/emoji/js/dist/forum.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
extensions/emoji/js/forum.js
Normal file
1
extensions/emoji/js/forum.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './src/forum';
|
11553
extensions/emoji/js/package-lock.json
generated
Normal file
11553
extensions/emoji/js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
extensions/emoji/js/package.json
Normal file
30
extensions/emoji/js/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@flarum/emoji",
|
||||
"prettier": "@flarum/prettier-config",
|
||||
"dependencies": {
|
||||
"simple-emoji-map": "^0.4.1",
|
||||
"twemoji": "^13.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"@flarum/prettier-config": "^1.0.0",
|
||||
"flarum-tsconfig": "^1.0.2",
|
||||
"typescript": "^4.5.4",
|
||||
"typescript-coverage-report": "^0.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production",
|
||||
"format": "prettier --write src",
|
||||
"format-check": "prettier --check src",
|
||||
"analyze": "cross-env ANALYZER=true yarn build",
|
||||
"clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
|
||||
"build-typings": "npm run clean-typings && cp -r src/@types dist-typings/@types && tsc",
|
||||
"check-typings": "tsc --noEmit --emitDeclarationOnly false",
|
||||
"check-typings-coverage": "typescript-coverage-report"
|
||||
}
|
||||
}
|
177
extensions/emoji/js/src/forum/addComposerAutocomplete.js
Normal file
177
extensions/emoji/js/src/forum/addComposerAutocomplete.js
Normal file
@@ -0,0 +1,177 @@
|
||||
import emojiMap from 'simple-emoji-map';
|
||||
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import TextEditor from 'flarum/common/components/TextEditor';
|
||||
import TextEditorButton from 'flarum/common/components/TextEditorButton';
|
||||
import KeyboardNavigatable from 'flarum/forum/utils/KeyboardNavigatable';
|
||||
|
||||
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
|
||||
import getEmojiIconCode from './helpers/getEmojiIconCode';
|
||||
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 $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-emojiWrapper"></div>');
|
||||
|
||||
this.navigator = new KeyboardNavigatable();
|
||||
this.navigator
|
||||
.when(() => dropdown.active)
|
||||
.onUp(() => dropdown.navigate(-1))
|
||||
.onDown(() => dropdown.navigate(1))
|
||||
.onSelect(dropdown.complete.bind(dropdown))
|
||||
.onCancel(dropdown.hide.bind(dropdown))
|
||||
.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(() => {
|
||||
const selection = this.attrs.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 = this.attrs.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 (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);
|
||||
|
||||
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 = this.attrs.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;
|
||||
}
|
||||
|
||||
// Prevent the dropdown from going off screen on mobile
|
||||
top = Math.max(-(parent.offset().top - $(document).scrollTop()), 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) {
|
||||
items.add(
|
||||
'emoji',
|
||||
<TextEditorButton onclick={() => this.attrs.composer.editor.insertAtCursor(' :')} icon="far fa-smile">
|
||||
{app.translator.trans('flarum-emoji.forum.composer.emoji_tooltip')}
|
||||
</TextEditorButton>
|
||||
);
|
||||
});
|
||||
}
|
5
extensions/emoji/js/src/forum/cdn.js
Normal file
5
extensions/emoji/js/src/forum/cdn.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import twemoji from 'twemoji';
|
||||
|
||||
export const version = /([0-9]+).[0-9]+.[0-9]+/g.exec(twemoji.base)[1];
|
||||
|
||||
export default `https://cdn.jsdelivr.net/gh/twitter/twemoji@${version}/assets/`;
|
@@ -0,0 +1,82 @@
|
||||
import Fragment from 'flarum/common/Fragment';
|
||||
|
||||
export default class AutocompleteDropdown extends Fragment {
|
||||
items = [];
|
||||
active = false;
|
||||
index = 0;
|
||||
keyWasJustPressed = false;
|
||||
|
||||
view() {
|
||||
return (
|
||||
<ul className="Dropdown-menu EmojiDropdown">
|
||||
<li className="Dropdown-header">{app.translator.trans('flarum-emoji.forum.composer.type_to_search_text')}</li>
|
||||
{this.items.map((item) => (
|
||||
<li key={item.attrs.key}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
show(left, top) {
|
||||
this.$()
|
||||
.show()
|
||||
.css({
|
||||
left: left + 'px',
|
||||
top: top + 'px',
|
||||
});
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$().hide();
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
navigate(delta) {
|
||||
this.keyWasJustPressed = true;
|
||||
this.setIndex(this.index + delta, true);
|
||||
clearTimeout(this.keyWasJustPressedTimeout);
|
||||
this.keyWasJustPressedTimeout = setTimeout(() => (this.keyWasJustPressed = false), 500);
|
||||
}
|
||||
|
||||
complete() {
|
||||
this.$('li:not(.Dropdown-header)').eq(this.index).find('button').click();
|
||||
}
|
||||
|
||||
setIndex(index, scrollToItem) {
|
||||
if (this.keyWasJustPressed && !scrollToItem) return;
|
||||
|
||||
const $dropdown = this.$();
|
||||
const $items = $dropdown.find('li:not(.Dropdown-header)');
|
||||
let rangedIndex = index;
|
||||
|
||||
if (rangedIndex < 0) {
|
||||
rangedIndex = $items.length - 1;
|
||||
} else if (rangedIndex >= $items.length) {
|
||||
rangedIndex = 0;
|
||||
}
|
||||
|
||||
this.index = rangedIndex;
|
||||
|
||||
const $item = $items.removeClass('active').eq(rangedIndex).addClass('active');
|
||||
|
||||
if (scrollToItem) {
|
||||
const dropdownScroll = $dropdown.scrollTop();
|
||||
const dropdownTop = $dropdown.offset().top;
|
||||
const dropdownBottom = dropdownTop + $dropdown.outerHeight();
|
||||
const itemTop = $item.offset().top;
|
||||
const itemBottom = itemTop + $item.outerHeight();
|
||||
|
||||
let scrollTop;
|
||||
if (itemTop < dropdownTop) {
|
||||
scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);
|
||||
} else if (itemBottom > dropdownBottom) {
|
||||
scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);
|
||||
}
|
||||
|
||||
if (typeof scrollTop !== 'undefined') {
|
||||
$dropdown.stop(true).animate({ scrollTop }, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
extensions/emoji/js/src/forum/helpers/getEmojiIconCode.js
Normal file
23
extensions/emoji/js/src/forum/helpers/getEmojiIconCode.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */ /*
|
||||
https://github.com/twitter/twemoji/blob/gh-pages/LICENSE
|
||||
*/
|
||||
|
||||
import twemoji from 'twemoji';
|
||||
|
||||
// avoid using a string literal like '\u200D' here because minifiers expand it inline
|
||||
const U200D = String.fromCharCode(0x200d);
|
||||
|
||||
// avoid runtime RegExp creation for not so smart,
|
||||
// not JIT based, and old browsers / engines
|
||||
const UFE0Fg = /\uFE0F/g;
|
||||
|
||||
/**
|
||||
* Used to both remove the possible variant
|
||||
* and to convert utf16 into code points.
|
||||
* If there is a zero-width-joiner (U+200D), leave the variants in.
|
||||
* @param string the raw text of the emoji match
|
||||
* @return string the code point
|
||||
*/
|
||||
export default function getEmojiIconCode(emoji) {
|
||||
return twemoji.convert.toCodePoint(emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji);
|
||||
}
|
13
extensions/emoji/js/src/forum/index.js
Normal file
13
extensions/emoji/js/src/forum/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import app from 'flarum/forum/app';
|
||||
|
||||
import addComposerAutocomplete from './addComposerAutocomplete';
|
||||
import renderEmoji from './renderEmoji';
|
||||
|
||||
app.initializers.add('flarum-emoji', () => {
|
||||
// After typing ':' in the composer, show a dropdown suggesting a bunch of
|
||||
// emoji that the user could use.
|
||||
addComposerAutocomplete();
|
||||
|
||||
// render emoji as image in Posts content and title.
|
||||
renderEmoji();
|
||||
});
|
59
extensions/emoji/js/src/forum/renderEmoji.js
Normal file
59
extensions/emoji/js/src/forum/renderEmoji.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import twemoji from 'twemoji';
|
||||
|
||||
import { override } from 'flarum/common/extend';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
|
||||
import base from './cdn';
|
||||
|
||||
const options = {
|
||||
base,
|
||||
attributes: () => ({
|
||||
loading: 'lazy',
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses an HTML string into a `<body>` node containing the HTML content.
|
||||
*
|
||||
* Vanilla JS implementation of jQuery's `$.parseHTML()`,
|
||||
* sourced from http://youmightnotneedjquery.com/
|
||||
*/
|
||||
function parseHTML(str) {
|
||||
const tmp = document.implementation.createHTMLDocument();
|
||||
tmp.body.innerHTML = str;
|
||||
|
||||
return tmp.body;
|
||||
}
|
||||
|
||||
export default function renderEmoji() {
|
||||
override(Post.prototype, 'contentHtml', function (original) {
|
||||
const contentHtml = original();
|
||||
|
||||
if (this.oldContentHtml !== contentHtml) {
|
||||
// We need to parse the HTML string into a DOM node, then give it to Twemoji.
|
||||
//
|
||||
// This prevents some issues with the default find-replace that would be performed
|
||||
// on a string passed to `Twemoji.parse()`.
|
||||
//
|
||||
// The parse function can only handle a single DOM node provided, so we need to
|
||||
// wrap it in a node. In our `parseHTML` implementation, we wrap it in a `<body>`
|
||||
// element. This gets stripped below.
|
||||
//
|
||||
// See https://github.com/flarum/core/issues/2958
|
||||
const emojifiedDom = twemoji.parse(parseHTML(contentHtml), options);
|
||||
|
||||
// Steal the HTML string inside the emojified DOM `<body>` tag.
|
||||
this.emojifiedContentHtml = emojifiedDom.innerHTML;
|
||||
|
||||
this.oldContentHtml = contentHtml;
|
||||
}
|
||||
|
||||
return this.emojifiedContentHtml;
|
||||
});
|
||||
|
||||
override(s9e.TextFormatter, 'preview', (original, text, element) => {
|
||||
original(text, element);
|
||||
|
||||
twemoji.parse(element, options);
|
||||
});
|
||||
}
|
1
extensions/emoji/js/webpack.config.js
Normal file
1
extensions/emoji/js/webpack.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('flarum-webpack-config')();
|
2980
extensions/emoji/js/yarn.lock
Normal file
2980
extensions/emoji/js/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
38
extensions/emoji/less/forum.less
Normal file
38
extensions/emoji/less/forum.less
Normal file
@@ -0,0 +1,38 @@
|
||||
img.emoji {
|
||||
height: 1.5em;
|
||||
margin: 0 .1em;
|
||||
vertical-align: -0.3em;
|
||||
}
|
||||
|
||||
.EmojiDropdown {
|
||||
max-width: 500px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
margin: 5px 0 !important;
|
||||
|
||||
> li > button {
|
||||
color: @text-color;
|
||||
font-weight: bold;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 45px;
|
||||
|
||||
.emoji {
|
||||
float: left;
|
||||
margin-left: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
.Dropdown-header {
|
||||
color: @muted-more-color;
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
padding-bottom: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.ComposerBody-emojiWrapper {
|
||||
position: relative;
|
||||
}
|
13
extensions/emoji/locale/en.yml
Normal file
13
extensions/emoji/locale/en.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
flarum-emoji:
|
||||
|
||||
##
|
||||
# UNIQUE KEYS - The following keys are used in only one location each.
|
||||
##
|
||||
|
||||
# Translations in this namespace are used by the forum user interface.
|
||||
forum:
|
||||
|
||||
# These translations are used by the composer (emoji autocompletion function).
|
||||
composer:
|
||||
emoji_tooltip: Insert emoji
|
||||
type_to_search_text: Type to search for an emoji
|
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$emojioneToTwemojiMap = [
|
||||
'1f468-2764-1f468' => '1f468-200d-2764-fe0f-200d-1f468',
|
||||
'1f469-2764-1f469' => '1f469-200d-2764-fe0f-200d-1f469',
|
||||
'1f468-2764-1f48b-1f468' => '1f468-200d-2764-fe0f-200d-1f48b-200d-1f468',
|
||||
'1f469-2764-1f48b-1f469' => '1f469-200d-2764-fe0f-200d-1f48b-200d-1f469',
|
||||
'1f468-1f468-1f466' => '1f468-200d-1f468-200d-1f466',
|
||||
'1f468-1f468-1f466-1f466' => '1f468-200d-1f468-200d-1f466-200d-1f466',
|
||||
'1f468-1f468-1f467' => '1f468-200d-1f468-200d-1f467',
|
||||
'1f468-1f468-1f467-1f466' => '1f468-200d-1f468-200d-1f467-200d-1f466',
|
||||
'1f468-1f468-1f467-1f467' => '1f468-200d-1f468-200d-1f467-200d-1f467',
|
||||
'1f468-1f469-1f466-1f466' => '1f468-200d-1f469-200d-1f466-200d-1f466',
|
||||
'1f468-1f469-1f467' => '1f468-200d-1f469-200d-1f467',
|
||||
'1f468-1f469-1f467-1f466' => '1f468-200d-1f469-200d-1f467-200d-1f466',
|
||||
'1f468-1f469-1f467-1f467' => '1f468-200d-1f469-200d-1f467-200d-1f467',
|
||||
'1f469-1f469-1f466' => '1f469-200d-1f469-200d-1f466',
|
||||
'1f469-1f469-1f466-1f466' => '1f469-200d-1f469-200d-1f466-200d-1f466',
|
||||
'1f469-1f469-1f467' => '1f469-200d-1f469-200d-1f467',
|
||||
'1f469-1f469-1f467-1f466' => '1f469-200d-1f469-200d-1f467-200d-1f466',
|
||||
'1f469-1f469-1f467-1f467' => '1f469-200d-1f469-200d-1f467-200d-1f467',
|
||||
'1f441-1f5e8' => '1f441-200d-1f5e8', // as always PITA
|
||||
'1f3f3-1f308' => '1f3f3-fe0f-200d-1f308',
|
||||
|
||||
// https://github.com/twitter/twemoji/issues/192
|
||||
'1f91d-1f3fb' => '1f91d',
|
||||
'1f91d-1f3fc' => '1f91d',
|
||||
'1f91d-1f3fd' => '1f91d',
|
||||
'1f91d-1f3fe' => '1f91d',
|
||||
'1f91d-1f3ff' => '1f91d',
|
||||
'1f93c-1f3fb' => '1f93c',
|
||||
'1f93c-1f3fc' => '1f93c',
|
||||
'1f93c-1f3fd' => '1f93c',
|
||||
'1f93c-1f3fe' => '1f93c',
|
||||
'1f93c-1f3ff' => '1f93c',
|
||||
];
|
||||
|
||||
$fromCodePoint = function ($code) {
|
||||
$num = intval($code, 16);
|
||||
|
||||
if ($num <= 0x7F) {
|
||||
return chr($num);
|
||||
}
|
||||
|
||||
if ($num <= 0x7FF) {
|
||||
return chr(($num >> 6) + 192).chr(($num & 63) + 128);
|
||||
}
|
||||
|
||||
if ($num <= 0xFFFF) {
|
||||
return chr(($num >> 12) + 224).chr((($num >> 6) & 63) + 128).chr(($num & 63) + 128);
|
||||
}
|
||||
|
||||
return chr(($num >> 18) + 240).chr((($num >> 12) & 63) + 128).chr((($num >> 6) & 63) + 128).chr(($num & 63) + 128);
|
||||
};
|
||||
|
||||
$convertEmojioneToTwemoji = function ($code) use ($emojioneToTwemojiMap) {
|
||||
if (isset($emojioneToTwemojiMap[$code])) {
|
||||
return $emojioneToTwemojiMap[$code];
|
||||
}
|
||||
|
||||
return ltrim($code, '0');
|
||||
};
|
||||
|
||||
$posts = $schema->getConnection()->table('posts')
|
||||
->select('id', 'content')
|
||||
->where('content', 'like', '%<EMOJI%')
|
||||
->cursor();
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$content = preg_replace_callback(
|
||||
'/<EMOJI seq="(.+?)">.+?<\/EMOJI>/',
|
||||
function ($m) use ($convertEmojioneToTwemoji, $fromCodePoint) {
|
||||
$code = $convertEmojioneToTwemoji($m[1]);
|
||||
$codepoints = explode('-', $code);
|
||||
|
||||
return implode('', array_map($fromCodePoint, $codepoints));
|
||||
},
|
||||
$post->content
|
||||
);
|
||||
|
||||
$schema->getConnection()->table('posts')
|
||||
->where('id', $post->id)
|
||||
->update(['content' => $content]);
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
// not implemented
|
||||
}
|
||||
];
|
Reference in New Issue
Block a user