| |
| |
| |
|
|
| (function () { |
| 'use strict'; |
|
|
| var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager'); |
|
|
| const eq = t => a => t === a; |
| const isNull = eq(null); |
| const isUndefined = eq(undefined); |
| const isNullable = a => a === null || a === undefined; |
| const isNonNullable = a => !isNullable(a); |
|
|
| const noop = () => { |
| }; |
| const constant = value => { |
| return () => { |
| return value; |
| }; |
| }; |
| const never = constant(false); |
|
|
| class Optional { |
| constructor(tag, value) { |
| this.tag = tag; |
| this.value = value; |
| } |
| static some(value) { |
| return new Optional(true, value); |
| } |
| static none() { |
| return Optional.singletonNone; |
| } |
| fold(onNone, onSome) { |
| if (this.tag) { |
| return onSome(this.value); |
| } else { |
| return onNone(); |
| } |
| } |
| isSome() { |
| return this.tag; |
| } |
| isNone() { |
| return !this.tag; |
| } |
| map(mapper) { |
| if (this.tag) { |
| return Optional.some(mapper(this.value)); |
| } else { |
| return Optional.none(); |
| } |
| } |
| bind(binder) { |
| if (this.tag) { |
| return binder(this.value); |
| } else { |
| return Optional.none(); |
| } |
| } |
| exists(predicate) { |
| return this.tag && predicate(this.value); |
| } |
| forall(predicate) { |
| return !this.tag || predicate(this.value); |
| } |
| filter(predicate) { |
| if (!this.tag || predicate(this.value)) { |
| return this; |
| } else { |
| return Optional.none(); |
| } |
| } |
| getOr(replacement) { |
| return this.tag ? this.value : replacement; |
| } |
| or(replacement) { |
| return this.tag ? this : replacement; |
| } |
| getOrThunk(thunk) { |
| return this.tag ? this.value : thunk(); |
| } |
| orThunk(thunk) { |
| return this.tag ? this : thunk(); |
| } |
| getOrDie(message) { |
| if (!this.tag) { |
| throw new Error(message !== null && message !== void 0 ? message : 'Called getOrDie on None'); |
| } else { |
| return this.value; |
| } |
| } |
| static from(value) { |
| return isNonNullable(value) ? Optional.some(value) : Optional.none(); |
| } |
| getOrNull() { |
| return this.tag ? this.value : null; |
| } |
| getOrUndefined() { |
| return this.value; |
| } |
| each(worker) { |
| if (this.tag) { |
| worker(this.value); |
| } |
| } |
| toArray() { |
| return this.tag ? [this.value] : []; |
| } |
| toString() { |
| return this.tag ? `some(${ this.value })` : 'none()'; |
| } |
| } |
| Optional.singletonNone = new Optional(false); |
|
|
| const exists = (xs, pred) => { |
| for (let i = 0, len = xs.length; i < len; i++) { |
| const x = xs[i]; |
| if (pred(x, i)) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| const map$1 = (xs, f) => { |
| const len = xs.length; |
| const r = new Array(len); |
| for (let i = 0; i < len; i++) { |
| const x = xs[i]; |
| r[i] = f(x, i); |
| } |
| return r; |
| }; |
| const each$1 = (xs, f) => { |
| for (let i = 0, len = xs.length; i < len; i++) { |
| const x = xs[i]; |
| f(x, i); |
| } |
| }; |
|
|
| const Cell = initial => { |
| let value = initial; |
| const get = () => { |
| return value; |
| }; |
| const set = v => { |
| value = v; |
| }; |
| return { |
| get, |
| set |
| }; |
| }; |
|
|
| const last = (fn, rate) => { |
| let timer = null; |
| const cancel = () => { |
| if (!isNull(timer)) { |
| clearTimeout(timer); |
| timer = null; |
| } |
| }; |
| const throttle = (...args) => { |
| cancel(); |
| timer = setTimeout(() => { |
| timer = null; |
| fn.apply(null, args); |
| }, rate); |
| }; |
| return { |
| cancel, |
| throttle |
| }; |
| }; |
|
|
| const insertEmoticon = (editor, ch) => { |
| editor.insertContent(ch); |
| }; |
|
|
| const keys = Object.keys; |
| const hasOwnProperty = Object.hasOwnProperty; |
| const each = (obj, f) => { |
| const props = keys(obj); |
| for (let k = 0, len = props.length; k < len; k++) { |
| const i = props[k]; |
| const x = obj[i]; |
| f(x, i); |
| } |
| }; |
| const map = (obj, f) => { |
| return tupleMap(obj, (x, i) => ({ |
| k: i, |
| v: f(x, i) |
| })); |
| }; |
| const tupleMap = (obj, f) => { |
| const r = {}; |
| each(obj, (x, i) => { |
| const tuple = f(x, i); |
| r[tuple.k] = tuple.v; |
| }); |
| return r; |
| }; |
| const has = (obj, key) => hasOwnProperty.call(obj, key); |
|
|
| const shallow = (old, nu) => { |
| return nu; |
| }; |
| const baseMerge = merger => { |
| return (...objects) => { |
| if (objects.length === 0) { |
| throw new Error(`Can't merge zero objects`); |
| } |
| const ret = {}; |
| for (let j = 0; j < objects.length; j++) { |
| const curObject = objects[j]; |
| for (const key in curObject) { |
| if (has(curObject, key)) { |
| ret[key] = merger(ret[key], curObject[key]); |
| } |
| } |
| } |
| return ret; |
| }; |
| }; |
| const merge = baseMerge(shallow); |
|
|
| const singleton = doRevoke => { |
| const subject = Cell(Optional.none()); |
| const revoke = () => subject.get().each(doRevoke); |
| const clear = () => { |
| revoke(); |
| subject.set(Optional.none()); |
| }; |
| const isSet = () => subject.get().isSome(); |
| const get = () => subject.get(); |
| const set = s => { |
| revoke(); |
| subject.set(Optional.some(s)); |
| }; |
| return { |
| clear, |
| isSet, |
| get, |
| set |
| }; |
| }; |
| const value = () => { |
| const subject = singleton(noop); |
| const on = f => subject.get().each(f); |
| return { |
| ...subject, |
| on |
| }; |
| }; |
|
|
| const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr; |
| const contains = (str, substr, start = 0, end) => { |
| const idx = str.indexOf(substr, start); |
| if (idx !== -1) { |
| return isUndefined(end) ? true : idx + substr.length <= end; |
| } else { |
| return false; |
| } |
| }; |
| const startsWith = (str, prefix) => { |
| return checkRange(str, prefix, 0); |
| }; |
|
|
| var global = tinymce.util.Tools.resolve('tinymce.Resource'); |
|
|
| const DEFAULT_ID = 'tinymce.plugins.emoticons'; |
| const option = name => editor => editor.options.get(name); |
| const register$2 = (editor, pluginUrl) => { |
| const registerOption = editor.options.register; |
| registerOption('emoticons_database', { |
| processor: 'string', |
| default: 'emojis' |
| }); |
| registerOption('emoticons_database_url', { |
| processor: 'string', |
| default: `${ pluginUrl }/js/${ getEmojiDatabase(editor) }${ editor.suffix }.js` |
| }); |
| registerOption('emoticons_database_id', { |
| processor: 'string', |
| default: DEFAULT_ID |
| }); |
| registerOption('emoticons_append', { |
| processor: 'object', |
| default: {} |
| }); |
| registerOption('emoticons_images_url', { |
| processor: 'string', |
| default: 'https://twemoji.maxcdn.com/v/13.0.1/72x72/' |
| }); |
| }; |
| const getEmojiDatabase = option('emoticons_database'); |
| const getEmojiDatabaseUrl = option('emoticons_database_url'); |
| const getEmojiDatabaseId = option('emoticons_database_id'); |
| const getAppendedEmoji = option('emoticons_append'); |
| const getEmojiImageUrl = option('emoticons_images_url'); |
|
|
| const ALL_CATEGORY = 'All'; |
| const categoryNameMap = { |
| symbols: 'Symbols', |
| people: 'People', |
| animals_and_nature: 'Animals and Nature', |
| food_and_drink: 'Food and Drink', |
| activity: 'Activity', |
| travel_and_places: 'Travel and Places', |
| objects: 'Objects', |
| flags: 'Flags', |
| user: 'User Defined' |
| }; |
| const translateCategory = (categories, name) => has(categories, name) ? categories[name] : name; |
| const getUserDefinedEmoji = editor => { |
| const userDefinedEmoticons = getAppendedEmoji(editor); |
| return map(userDefinedEmoticons, value => ({ |
| keywords: [], |
| category: 'user', |
| ...value |
| })); |
| }; |
| const initDatabase = (editor, databaseUrl, databaseId) => { |
| const categories = value(); |
| const all = value(); |
| const emojiImagesUrl = getEmojiImageUrl(editor); |
| const getEmoji = lib => { |
| if (startsWith(lib.char, '<img')) { |
| return lib.char.replace(/src="([^"]+)"/, (match, url) => `src="${ emojiImagesUrl }${ url }"`); |
| } else { |
| return lib.char; |
| } |
| }; |
| const processEmojis = emojis => { |
| const cats = {}; |
| const everything = []; |
| each(emojis, (lib, title) => { |
| const entry = { |
| title, |
| keywords: lib.keywords, |
| char: getEmoji(lib), |
| category: translateCategory(categoryNameMap, lib.category) |
| }; |
| const current = cats[entry.category] !== undefined ? cats[entry.category] : []; |
| cats[entry.category] = current.concat([entry]); |
| everything.push(entry); |
| }); |
| categories.set(cats); |
| all.set(everything); |
| }; |
| editor.on('init', () => { |
| global.load(databaseId, databaseUrl).then(emojis => { |
| const userEmojis = getUserDefinedEmoji(editor); |
| processEmojis(merge(emojis, userEmojis)); |
| }, err => { |
| console.log(`Failed to load emojis: ${ err }`); |
| categories.set({}); |
| all.set([]); |
| }); |
| }); |
| const listCategory = category => { |
| if (category === ALL_CATEGORY) { |
| return listAll(); |
| } |
| return categories.get().bind(cats => Optional.from(cats[category])).getOr([]); |
| }; |
| const listAll = () => all.get().getOr([]); |
| const listCategories = () => [ALL_CATEGORY].concat(keys(categories.get().getOr({}))); |
| const waitForLoad = () => { |
| if (hasLoaded()) { |
| return Promise.resolve(true); |
| } else { |
| return new Promise((resolve, reject) => { |
| let numRetries = 15; |
| const interval = setInterval(() => { |
| if (hasLoaded()) { |
| clearInterval(interval); |
| resolve(true); |
| } else { |
| numRetries--; |
| if (numRetries < 0) { |
| console.log('Could not load emojis from url: ' + databaseUrl); |
| clearInterval(interval); |
| reject(false); |
| } |
| } |
| }, 100); |
| }); |
| } |
| }; |
| const hasLoaded = () => categories.isSet() && all.isSet(); |
| return { |
| listCategories, |
| hasLoaded, |
| waitForLoad, |
| listAll, |
| listCategory |
| }; |
| }; |
|
|
| const emojiMatches = (emoji, lowerCasePattern) => contains(emoji.title.toLowerCase(), lowerCasePattern) || exists(emoji.keywords, k => contains(k.toLowerCase(), lowerCasePattern)); |
| const emojisFrom = (list, pattern, maxResults) => { |
| const matches = []; |
| const lowerCasePattern = pattern.toLowerCase(); |
| const reachedLimit = maxResults.fold(() => never, max => size => size >= max); |
| for (let i = 0; i < list.length; i++) { |
| if (pattern.length === 0 || emojiMatches(list[i], lowerCasePattern)) { |
| matches.push({ |
| value: list[i].char, |
| text: list[i].title, |
| icon: list[i].char |
| }); |
| if (reachedLimit(matches.length)) { |
| break; |
| } |
| } |
| } |
| return matches; |
| }; |
|
|
| const patternName = 'pattern'; |
| const open = (editor, database) => { |
| const initialState = { |
| pattern: '', |
| results: emojisFrom(database.listAll(), '', Optional.some(300)) |
| }; |
| const currentTab = Cell(ALL_CATEGORY); |
| const scan = dialogApi => { |
| const dialogData = dialogApi.getData(); |
| const category = currentTab.get(); |
| const candidates = database.listCategory(category); |
| const results = emojisFrom(candidates, dialogData[patternName], category === ALL_CATEGORY ? Optional.some(300) : Optional.none()); |
| dialogApi.setData({ results }); |
| }; |
| const updateFilter = last(dialogApi => { |
| scan(dialogApi); |
| }, 200); |
| const searchField = { |
| label: 'Search', |
| type: 'input', |
| name: patternName |
| }; |
| const resultsField = { |
| type: 'collection', |
| name: 'results' |
| }; |
| const getInitialState = () => { |
| const body = { |
| type: 'tabpanel', |
| tabs: map$1(database.listCategories(), cat => ({ |
| title: cat, |
| name: cat, |
| items: [ |
| searchField, |
| resultsField |
| ] |
| })) |
| }; |
| return { |
| title: 'Emojis', |
| size: 'normal', |
| body, |
| initialData: initialState, |
| onTabChange: (dialogApi, details) => { |
| currentTab.set(details.newTabName); |
| updateFilter.throttle(dialogApi); |
| }, |
| onChange: updateFilter.throttle, |
| onAction: (dialogApi, actionData) => { |
| if (actionData.name === 'results') { |
| insertEmoticon(editor, actionData.value); |
| dialogApi.close(); |
| } |
| }, |
| buttons: [{ |
| type: 'cancel', |
| text: 'Close', |
| primary: true |
| }] |
| }; |
| }; |
| const dialogApi = editor.windowManager.open(getInitialState()); |
| dialogApi.focus(patternName); |
| if (!database.hasLoaded()) { |
| dialogApi.block('Loading emojis...'); |
| database.waitForLoad().then(() => { |
| dialogApi.redial(getInitialState()); |
| updateFilter.throttle(dialogApi); |
| dialogApi.focus(patternName); |
| dialogApi.unblock(); |
| }).catch(_err => { |
| dialogApi.redial({ |
| title: 'Emojis', |
| body: { |
| type: 'panel', |
| items: [{ |
| type: 'alertbanner', |
| level: 'error', |
| icon: 'warning', |
| text: 'Could not load emojis' |
| }] |
| }, |
| buttons: [{ |
| type: 'cancel', |
| text: 'Close', |
| primary: true |
| }], |
| initialData: { |
| pattern: '', |
| results: [] |
| } |
| }); |
| dialogApi.focus(patternName); |
| dialogApi.unblock(); |
| }); |
| } |
| }; |
|
|
| const register$1 = (editor, database) => { |
| editor.addCommand('mceEmoticons', () => open(editor, database)); |
| }; |
|
|
| const setup = editor => { |
| editor.on('PreInit', () => { |
| editor.parser.addAttributeFilter('data-emoticon', nodes => { |
| each$1(nodes, node => { |
| node.attr('data-mce-resize', 'false'); |
| node.attr('data-mce-placeholder', '1'); |
| }); |
| }); |
| }); |
| }; |
|
|
| const init = (editor, database) => { |
| editor.ui.registry.addAutocompleter('emoticons', { |
| trigger: ':', |
| columns: 'auto', |
| minChars: 2, |
| fetch: (pattern, maxResults) => database.waitForLoad().then(() => { |
| const candidates = database.listAll(); |
| return emojisFrom(candidates, pattern, Optional.some(maxResults)); |
| }), |
| onAction: (autocompleteApi, rng, value) => { |
| editor.selection.setRng(rng); |
| editor.insertContent(value); |
| autocompleteApi.hide(); |
| } |
| }); |
| }; |
|
|
| const register = editor => { |
| const onAction = () => editor.execCommand('mceEmoticons'); |
| editor.ui.registry.addButton('emoticons', { |
| tooltip: 'Emojis', |
| icon: 'emoji', |
| onAction |
| }); |
| editor.ui.registry.addMenuItem('emoticons', { |
| text: 'Emojis...', |
| icon: 'emoji', |
| onAction |
| }); |
| }; |
|
|
| var Plugin = () => { |
| global$1.add('emoticons', (editor, pluginUrl) => { |
| register$2(editor, pluginUrl); |
| const databaseUrl = getEmojiDatabaseUrl(editor); |
| const databaseId = getEmojiDatabaseId(editor); |
| const database = initDatabase(editor, databaseUrl, databaseId); |
| register$1(editor, database); |
| register(editor); |
| init(editor, database); |
| setup(editor); |
| }); |
| }; |
|
|
| Plugin(); |
|
|
| })(); |
|
|