/**
 * HugeRTE version 1.0.9 (2025-03-15)
 * Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
 * Copyright (c) 2024 HugeRTE contributors
 * Licensed under the MIT license (https://github.com/hugerte/hugerte/blob/main/LICENSE.TXT)
 */

(function () {
    'use strict';

    var global$1 = hugerte.util.Tools.resolve('hugerte.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 = hugerte.util.Tools.resolve('hugerte.Resource');

    const DEFAULT_ID = 'hugerte.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 onSetupEditable = editor => api => {
      const nodeChanged = () => {
        api.setEnabled(editor.selection.isEditable());
      };
      editor.on('NodeChange', nodeChanged);
      nodeChanged();
      return () => {
        editor.off('NodeChange', nodeChanged);
      };
    };
    const register = editor => {
      const onAction = () => editor.execCommand('mceEmoticons');
      editor.ui.registry.addButton('emoticons', {
        tooltip: 'Emojis',
        icon: 'emoji',
        onAction,
        onSetup: onSetupEditable(editor)
      });
      editor.ui.registry.addMenuItem('emoticons', {
        text: 'Emojis...',
        icon: 'emoji',
        onAction,
        onSetup: onSetupEditable(editor)
      });
    };

    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);
        return { getAllEmojis: () => database.waitForLoad().then(() => database.listAll()) };
      });
    };

    Plugin();

})();
