plugin.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. /**
  2. * TinyMCE version 6.4.2 (2023-04-26)
  3. */
  4. (function () {
  5. 'use strict';
  6. var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager');
  7. const eq = t => a => t === a;
  8. const isNull = eq(null);
  9. const isUndefined = eq(undefined);
  10. const isNullable = a => a === null || a === undefined;
  11. const isNonNullable = a => !isNullable(a);
  12. const noop = () => {
  13. };
  14. const constant = value => {
  15. return () => {
  16. return value;
  17. };
  18. };
  19. const never = constant(false);
  20. class Optional {
  21. constructor(tag, value) {
  22. this.tag = tag;
  23. this.value = value;
  24. }
  25. static some(value) {
  26. return new Optional(true, value);
  27. }
  28. static none() {
  29. return Optional.singletonNone;
  30. }
  31. fold(onNone, onSome) {
  32. if (this.tag) {
  33. return onSome(this.value);
  34. } else {
  35. return onNone();
  36. }
  37. }
  38. isSome() {
  39. return this.tag;
  40. }
  41. isNone() {
  42. return !this.tag;
  43. }
  44. map(mapper) {
  45. if (this.tag) {
  46. return Optional.some(mapper(this.value));
  47. } else {
  48. return Optional.none();
  49. }
  50. }
  51. bind(binder) {
  52. if (this.tag) {
  53. return binder(this.value);
  54. } else {
  55. return Optional.none();
  56. }
  57. }
  58. exists(predicate) {
  59. return this.tag && predicate(this.value);
  60. }
  61. forall(predicate) {
  62. return !this.tag || predicate(this.value);
  63. }
  64. filter(predicate) {
  65. if (!this.tag || predicate(this.value)) {
  66. return this;
  67. } else {
  68. return Optional.none();
  69. }
  70. }
  71. getOr(replacement) {
  72. return this.tag ? this.value : replacement;
  73. }
  74. or(replacement) {
  75. return this.tag ? this : replacement;
  76. }
  77. getOrThunk(thunk) {
  78. return this.tag ? this.value : thunk();
  79. }
  80. orThunk(thunk) {
  81. return this.tag ? this : thunk();
  82. }
  83. getOrDie(message) {
  84. if (!this.tag) {
  85. throw new Error(message !== null && message !== void 0 ? message : 'Called getOrDie on None');
  86. } else {
  87. return this.value;
  88. }
  89. }
  90. static from(value) {
  91. return isNonNullable(value) ? Optional.some(value) : Optional.none();
  92. }
  93. getOrNull() {
  94. return this.tag ? this.value : null;
  95. }
  96. getOrUndefined() {
  97. return this.value;
  98. }
  99. each(worker) {
  100. if (this.tag) {
  101. worker(this.value);
  102. }
  103. }
  104. toArray() {
  105. return this.tag ? [this.value] : [];
  106. }
  107. toString() {
  108. return this.tag ? `some(${ this.value })` : 'none()';
  109. }
  110. }
  111. Optional.singletonNone = new Optional(false);
  112. const exists = (xs, pred) => {
  113. for (let i = 0, len = xs.length; i < len; i++) {
  114. const x = xs[i];
  115. if (pred(x, i)) {
  116. return true;
  117. }
  118. }
  119. return false;
  120. };
  121. const map$1 = (xs, f) => {
  122. const len = xs.length;
  123. const r = new Array(len);
  124. for (let i = 0; i < len; i++) {
  125. const x = xs[i];
  126. r[i] = f(x, i);
  127. }
  128. return r;
  129. };
  130. const each$1 = (xs, f) => {
  131. for (let i = 0, len = xs.length; i < len; i++) {
  132. const x = xs[i];
  133. f(x, i);
  134. }
  135. };
  136. const Cell = initial => {
  137. let value = initial;
  138. const get = () => {
  139. return value;
  140. };
  141. const set = v => {
  142. value = v;
  143. };
  144. return {
  145. get,
  146. set
  147. };
  148. };
  149. const last = (fn, rate) => {
  150. let timer = null;
  151. const cancel = () => {
  152. if (!isNull(timer)) {
  153. clearTimeout(timer);
  154. timer = null;
  155. }
  156. };
  157. const throttle = (...args) => {
  158. cancel();
  159. timer = setTimeout(() => {
  160. timer = null;
  161. fn.apply(null, args);
  162. }, rate);
  163. };
  164. return {
  165. cancel,
  166. throttle
  167. };
  168. };
  169. const insertEmoticon = (editor, ch) => {
  170. editor.insertContent(ch);
  171. };
  172. const keys = Object.keys;
  173. const hasOwnProperty = Object.hasOwnProperty;
  174. const each = (obj, f) => {
  175. const props = keys(obj);
  176. for (let k = 0, len = props.length; k < len; k++) {
  177. const i = props[k];
  178. const x = obj[i];
  179. f(x, i);
  180. }
  181. };
  182. const map = (obj, f) => {
  183. return tupleMap(obj, (x, i) => ({
  184. k: i,
  185. v: f(x, i)
  186. }));
  187. };
  188. const tupleMap = (obj, f) => {
  189. const r = {};
  190. each(obj, (x, i) => {
  191. const tuple = f(x, i);
  192. r[tuple.k] = tuple.v;
  193. });
  194. return r;
  195. };
  196. const has = (obj, key) => hasOwnProperty.call(obj, key);
  197. const shallow = (old, nu) => {
  198. return nu;
  199. };
  200. const baseMerge = merger => {
  201. return (...objects) => {
  202. if (objects.length === 0) {
  203. throw new Error(`Can't merge zero objects`);
  204. }
  205. const ret = {};
  206. for (let j = 0; j < objects.length; j++) {
  207. const curObject = objects[j];
  208. for (const key in curObject) {
  209. if (has(curObject, key)) {
  210. ret[key] = merger(ret[key], curObject[key]);
  211. }
  212. }
  213. }
  214. return ret;
  215. };
  216. };
  217. const merge = baseMerge(shallow);
  218. const singleton = doRevoke => {
  219. const subject = Cell(Optional.none());
  220. const revoke = () => subject.get().each(doRevoke);
  221. const clear = () => {
  222. revoke();
  223. subject.set(Optional.none());
  224. };
  225. const isSet = () => subject.get().isSome();
  226. const get = () => subject.get();
  227. const set = s => {
  228. revoke();
  229. subject.set(Optional.some(s));
  230. };
  231. return {
  232. clear,
  233. isSet,
  234. get,
  235. set
  236. };
  237. };
  238. const value = () => {
  239. const subject = singleton(noop);
  240. const on = f => subject.get().each(f);
  241. return {
  242. ...subject,
  243. on
  244. };
  245. };
  246. const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr;
  247. const contains = (str, substr, start = 0, end) => {
  248. const idx = str.indexOf(substr, start);
  249. if (idx !== -1) {
  250. return isUndefined(end) ? true : idx + substr.length <= end;
  251. } else {
  252. return false;
  253. }
  254. };
  255. const startsWith = (str, prefix) => {
  256. return checkRange(str, prefix, 0);
  257. };
  258. var global = tinymce.util.Tools.resolve('tinymce.Resource');
  259. const DEFAULT_ID = 'tinymce.plugins.emoticons';
  260. const option = name => editor => editor.options.get(name);
  261. const register$2 = (editor, pluginUrl) => {
  262. const registerOption = editor.options.register;
  263. registerOption('emoticons_database', {
  264. processor: 'string',
  265. default: 'emojis'
  266. });
  267. registerOption('emoticons_database_url', {
  268. processor: 'string',
  269. default: `${ pluginUrl }/js/${ getEmojiDatabase(editor) }${ editor.suffix }.js`
  270. });
  271. registerOption('emoticons_database_id', {
  272. processor: 'string',
  273. default: DEFAULT_ID
  274. });
  275. registerOption('emoticons_append', {
  276. processor: 'object',
  277. default: {}
  278. });
  279. registerOption('emoticons_images_url', {
  280. processor: 'string',
  281. default: 'https://twemoji.maxcdn.com/v/13.0.1/72x72/'
  282. });
  283. };
  284. const getEmojiDatabase = option('emoticons_database');
  285. const getEmojiDatabaseUrl = option('emoticons_database_url');
  286. const getEmojiDatabaseId = option('emoticons_database_id');
  287. const getAppendedEmoji = option('emoticons_append');
  288. const getEmojiImageUrl = option('emoticons_images_url');
  289. const ALL_CATEGORY = 'All';
  290. const categoryNameMap = {
  291. symbols: 'Symbols',
  292. people: 'People',
  293. animals_and_nature: 'Animals and Nature',
  294. food_and_drink: 'Food and Drink',
  295. activity: 'Activity',
  296. travel_and_places: 'Travel and Places',
  297. objects: 'Objects',
  298. flags: 'Flags',
  299. user: 'User Defined'
  300. };
  301. const translateCategory = (categories, name) => has(categories, name) ? categories[name] : name;
  302. const getUserDefinedEmoji = editor => {
  303. const userDefinedEmoticons = getAppendedEmoji(editor);
  304. return map(userDefinedEmoticons, value => ({
  305. keywords: [],
  306. category: 'user',
  307. ...value
  308. }));
  309. };
  310. const initDatabase = (editor, databaseUrl, databaseId) => {
  311. const categories = value();
  312. const all = value();
  313. const emojiImagesUrl = getEmojiImageUrl(editor);
  314. const getEmoji = lib => {
  315. if (startsWith(lib.char, '<img')) {
  316. return lib.char.replace(/src="([^"]+)"/, (match, url) => `src="${ emojiImagesUrl }${ url }"`);
  317. } else {
  318. return lib.char;
  319. }
  320. };
  321. const processEmojis = emojis => {
  322. const cats = {};
  323. const everything = [];
  324. each(emojis, (lib, title) => {
  325. const entry = {
  326. title,
  327. keywords: lib.keywords,
  328. char: getEmoji(lib),
  329. category: translateCategory(categoryNameMap, lib.category)
  330. };
  331. const current = cats[entry.category] !== undefined ? cats[entry.category] : [];
  332. cats[entry.category] = current.concat([entry]);
  333. everything.push(entry);
  334. });
  335. categories.set(cats);
  336. all.set(everything);
  337. };
  338. editor.on('init', () => {
  339. global.load(databaseId, databaseUrl).then(emojis => {
  340. const userEmojis = getUserDefinedEmoji(editor);
  341. processEmojis(merge(emojis, userEmojis));
  342. }, err => {
  343. console.log(`Failed to load emojis: ${ err }`);
  344. categories.set({});
  345. all.set([]);
  346. });
  347. });
  348. const listCategory = category => {
  349. if (category === ALL_CATEGORY) {
  350. return listAll();
  351. }
  352. return categories.get().bind(cats => Optional.from(cats[category])).getOr([]);
  353. };
  354. const listAll = () => all.get().getOr([]);
  355. const listCategories = () => [ALL_CATEGORY].concat(keys(categories.get().getOr({})));
  356. const waitForLoad = () => {
  357. if (hasLoaded()) {
  358. return Promise.resolve(true);
  359. } else {
  360. return new Promise((resolve, reject) => {
  361. let numRetries = 15;
  362. const interval = setInterval(() => {
  363. if (hasLoaded()) {
  364. clearInterval(interval);
  365. resolve(true);
  366. } else {
  367. numRetries--;
  368. if (numRetries < 0) {
  369. console.log('Could not load emojis from url: ' + databaseUrl);
  370. clearInterval(interval);
  371. reject(false);
  372. }
  373. }
  374. }, 100);
  375. });
  376. }
  377. };
  378. const hasLoaded = () => categories.isSet() && all.isSet();
  379. return {
  380. listCategories,
  381. hasLoaded,
  382. waitForLoad,
  383. listAll,
  384. listCategory
  385. };
  386. };
  387. const emojiMatches = (emoji, lowerCasePattern) => contains(emoji.title.toLowerCase(), lowerCasePattern) || exists(emoji.keywords, k => contains(k.toLowerCase(), lowerCasePattern));
  388. const emojisFrom = (list, pattern, maxResults) => {
  389. const matches = [];
  390. const lowerCasePattern = pattern.toLowerCase();
  391. const reachedLimit = maxResults.fold(() => never, max => size => size >= max);
  392. for (let i = 0; i < list.length; i++) {
  393. if (pattern.length === 0 || emojiMatches(list[i], lowerCasePattern)) {
  394. matches.push({
  395. value: list[i].char,
  396. text: list[i].title,
  397. icon: list[i].char
  398. });
  399. if (reachedLimit(matches.length)) {
  400. break;
  401. }
  402. }
  403. }
  404. return matches;
  405. };
  406. const patternName = 'pattern';
  407. const open = (editor, database) => {
  408. const initialState = {
  409. pattern: '',
  410. results: emojisFrom(database.listAll(), '', Optional.some(300))
  411. };
  412. const currentTab = Cell(ALL_CATEGORY);
  413. const scan = dialogApi => {
  414. const dialogData = dialogApi.getData();
  415. const category = currentTab.get();
  416. const candidates = database.listCategory(category);
  417. const results = emojisFrom(candidates, dialogData[patternName], category === ALL_CATEGORY ? Optional.some(300) : Optional.none());
  418. dialogApi.setData({ results });
  419. };
  420. const updateFilter = last(dialogApi => {
  421. scan(dialogApi);
  422. }, 200);
  423. const searchField = {
  424. label: 'Search',
  425. type: 'input',
  426. name: patternName
  427. };
  428. const resultsField = {
  429. type: 'collection',
  430. name: 'results'
  431. };
  432. const getInitialState = () => {
  433. const body = {
  434. type: 'tabpanel',
  435. tabs: map$1(database.listCategories(), cat => ({
  436. title: cat,
  437. name: cat,
  438. items: [
  439. searchField,
  440. resultsField
  441. ]
  442. }))
  443. };
  444. return {
  445. title: 'Emojis',
  446. size: 'normal',
  447. body,
  448. initialData: initialState,
  449. onTabChange: (dialogApi, details) => {
  450. currentTab.set(details.newTabName);
  451. updateFilter.throttle(dialogApi);
  452. },
  453. onChange: updateFilter.throttle,
  454. onAction: (dialogApi, actionData) => {
  455. if (actionData.name === 'results') {
  456. insertEmoticon(editor, actionData.value);
  457. dialogApi.close();
  458. }
  459. },
  460. buttons: [{
  461. type: 'cancel',
  462. text: 'Close',
  463. primary: true
  464. }]
  465. };
  466. };
  467. const dialogApi = editor.windowManager.open(getInitialState());
  468. dialogApi.focus(patternName);
  469. if (!database.hasLoaded()) {
  470. dialogApi.block('Loading emojis...');
  471. database.waitForLoad().then(() => {
  472. dialogApi.redial(getInitialState());
  473. updateFilter.throttle(dialogApi);
  474. dialogApi.focus(patternName);
  475. dialogApi.unblock();
  476. }).catch(_err => {
  477. dialogApi.redial({
  478. title: 'Emojis',
  479. body: {
  480. type: 'panel',
  481. items: [{
  482. type: 'alertbanner',
  483. level: 'error',
  484. icon: 'warning',
  485. text: 'Could not load emojis'
  486. }]
  487. },
  488. buttons: [{
  489. type: 'cancel',
  490. text: 'Close',
  491. primary: true
  492. }],
  493. initialData: {
  494. pattern: '',
  495. results: []
  496. }
  497. });
  498. dialogApi.focus(patternName);
  499. dialogApi.unblock();
  500. });
  501. }
  502. };
  503. const register$1 = (editor, database) => {
  504. editor.addCommand('mceEmoticons', () => open(editor, database));
  505. };
  506. const setup = editor => {
  507. editor.on('PreInit', () => {
  508. editor.parser.addAttributeFilter('data-emoticon', nodes => {
  509. each$1(nodes, node => {
  510. node.attr('data-mce-resize', 'false');
  511. node.attr('data-mce-placeholder', '1');
  512. });
  513. });
  514. });
  515. };
  516. const init = (editor, database) => {
  517. editor.ui.registry.addAutocompleter('emoticons', {
  518. trigger: ':',
  519. columns: 'auto',
  520. minChars: 2,
  521. fetch: (pattern, maxResults) => database.waitForLoad().then(() => {
  522. const candidates = database.listAll();
  523. return emojisFrom(candidates, pattern, Optional.some(maxResults));
  524. }),
  525. onAction: (autocompleteApi, rng, value) => {
  526. editor.selection.setRng(rng);
  527. editor.insertContent(value);
  528. autocompleteApi.hide();
  529. }
  530. });
  531. };
  532. const register = editor => {
  533. const onAction = () => editor.execCommand('mceEmoticons');
  534. editor.ui.registry.addButton('emoticons', {
  535. tooltip: 'Emojis',
  536. icon: 'emoji',
  537. onAction
  538. });
  539. editor.ui.registry.addMenuItem('emoticons', {
  540. text: 'Emojis...',
  541. icon: 'emoji',
  542. onAction
  543. });
  544. };
  545. var Plugin = () => {
  546. global$1.add('emoticons', (editor, pluginUrl) => {
  547. register$2(editor, pluginUrl);
  548. const databaseUrl = getEmojiDatabaseUrl(editor);
  549. const databaseId = getEmojiDatabaseId(editor);
  550. const database = initDatabase(editor, databaseUrl, databaseId);
  551. register$1(editor, database);
  552. register(editor);
  553. init(editor, database);
  554. setup(editor);
  555. });
  556. };
  557. Plugin();
  558. })();