ckeditor5-1.0.x-dev/js/ckeditor5.js

js/ckeditor5.js
/**
 * @file
 * CKEditor 5 implementation of {@link Drupal.editors} API.
 */
(function (Drupal, debounce, CKEditor5, $) {
  /**
   * The CKEDITOR instances.
   *
   * @type {Map}
   */
  Drupal.CKEditor5Instances = new Map();

  /**
   * The callback functions.
   *
   * @type {Map}
   */
  const callbacks = new Map();

  /**
   * List of element ids with the required attribute.
   *
   * @type {Set}
   */
  const required = new Set();

  function findFunc(scope, name) {
    if (!scope) {
      return null;
    }
    const parts = name.includes('.') ? name.split('.') : name;

    if (parts.length > 1) {
      return findFunc(scope[parts.shift()], parts);
    } else {
     return typeof scope[parts[0]] === 'function'
       ? scope[parts[0]]
       : null
    }
  }

  function buildFunc(config) {
    const { func } = config;
    // Assuming a global object.
    let fn = findFunc(window, func.name);
    if (typeof fn === 'function') {
      const result = func.invoke ? fn(...func.args) : fn;
      return result;
    }
    return null;
  }

  /**
   * Converts a string representing regexp to a RegExp object.
   *
   * @param {Object} config
   * @param {string} config.pattern
   *   The regexp pattern that is used to create the RegExp object.
   *
   * @returns {RegExp}
   */
  function buildRegexp(config) {
    const { pattern } = config.regexp;

    const main = pattern.match(/\/(.+)\/.*/)[1];
    const options = pattern.match(/\/.+\/(.*)/)[1];

    return new RegExp(main, options);
  }

  /**
   * Processes an array in config.
   *
   * @param {*} config
   * @returns {*}
   */
  function processArray(config) {
    return config.map((item) => {
      if (typeof item === 'object') {
        return processConfig(item);
      }

      return item;
    })
  }

  function processConfig(config) {
    return Object.entries(config).reduce((processed, [key, value]) => {
      if (typeof value === 'object') {
        if (value.hasOwnProperty('func')) {
          processed[key] = buildFunc(value);
        } else if (value.hasOwnProperty('regexp')) {
          processed[key] = buildRegexp(value);
        } else if (Array.isArray(value)) {
          processed[key] = processArray(value);
        } else {
          processed[key] = processConfig(value);
        }
      } else {
        processed[key] = value;
      }

      return processed;
    }, {});
  }

  /**
   * Set an id to a data-attribute for registering this element instance.
   *
   * @param element
   *
   * @return {string}
   *   The id to use for this element.
   */
  const setElementId = (element) => {
    const id = Math.random().toString().slice(2,9);
    element.setAttribute('data-ckeditor5-id', id);

    return id;
  }

  /**
   * Return a unique selector for the element.
   *
   * @param {HTMLElement} element
   *
   * @return {string}
   *   The id to use for this element.
   */
  const getElementId = (element) =>
    element.getAttribute('data-ckeditor5-id');

  /**
   * Select CKEditor5 plugin classes to include.
   *
   * Found in the CKEditor5 global js object as {package.Class}.
   *
   * @param {Array} plugins
   *  List of package and Class name of plugins
   *
   * @return {Array}
   *   List of JavaScript Classes to add in the extraPlugins property of config.
   */
  function selectPlugins(plugins) {
    return plugins.map((pluginDefinition) => {
      const [build, name] = pluginDefinition.split('.');
      if (CKEditor5[build] && CKEditor5[build][name]) {
        return CKEditor5[build][name];
      } else {
        console.warn(`Failed to load ${build} - ${name}`);
      }
    });
  }

  /**
   * Adds CSS to ensure proper styling of CKEditor 5 inside off-canvas dialogs.
   *
   * @param {HTMLElement} element
   *   The element the editor is attached to.
   */
  const offCanvasCss = (element) => {
    element.parentNode.setAttribute('data-drupal-ck-style-fence', true);

    // Only proceed if the styles haven't been added yet.
    if (!document.querySelector('#ckeditor5-off-canvas-reset')) {
      const prefix = `#drupal-off-canvas [data-drupal-ck-style-fence]`;
      let existingCss = '';

      // Find every existing style that doesn't come from off-canvas resets and
      // copy them to new styles with a prefix targeting CKEditor inside an
      // off-canvas dialog.
      [...document.styleSheets].forEach((sheet) => {
        if (!sheet.href || (sheet.href && sheet.href.indexOf('off-canvas') === -1)) {
          // This is wrapped in a try/catch as Chromium browsers will fail if
          // the stylesheet was provided via a CORS request.
          // @see https://bugs.chromium.org/p/chromium/issues/detail?id=775525
          try {
            const rules = sheet.cssRules;
            [...rules].forEach((rule) => {
              let { cssText } = rule;
              const selector = rule.cssText.split('{')[0];

              // Prefix all selectors added after a comma.
              cssText = cssText.replace(selector, selector.replace(/,/g, `, ${prefix}`));

              // When adding to existingCss, prefix the first selector as well.
              existingCss += `${prefix} ${cssText}`;
            });
          } catch(e) {
            console.warn(`Stylesheet ${sheet.href} not included in CKEditor reset due to the browser's CORS policy.`);
          }
        }
      });

      // Additional styles that need to be explicity added in addition to the
      // prefixed versions of existing css in `existingCss`.
      const addedCss = [
        `${prefix} .ck.ck-content {display:block;min-height:5rem;}`,
        `${prefix} .ck.ck-content * {display:initial;background:initial;color:initial;padding:initial;}`,
        `${prefix} .ck.ck-content li {display:list-item}`,
        `${prefix} .ck.ck-content ol li {list-style-type: decimal}`,
        `${prefix} .ck[contenteditable], ${prefix} .ck[contenteditable] * {-webkit-user-modify: read-write;-moz-user-modify: read-write;}`,
      ];

      // Styles to ensure block elements are displayed as such inside
      // off-canvas dialogs. These are all element types that are styled with
      // ` all: initial;` in the off-canvas reset that should default to being
      // displayed as blocks within CKEditor.
      // @see core/misc/dialog/off-canvas.reset.pcss.css
      const blockSelectors = [
        'h1',
        'h2',
        'h3',
        'h4',
        'h5',
        'h6',
        'p',
        'ol',
        'ul',
        'address',
        'article',
        'aside',
        'blockquote',
        'body',
        'dd',
        'div',
        'dl',
        'dt',
        'fieldset',
        'figcaption',
        'figure',
        'footer',
        'form',
        'header',
        'hgroup',
        'hr',
        'html',
        'legend',
        'main',
        'menu',
        'pre',
        'section',
        'xmp',
      ].map((blockElement) => `${prefix} .ck.ck-content ${blockElement}`)
        .join(', \n');
      const blockCss = `${blockSelectors} { display: block; }`;

      const prefixedCss = [...addedCss, existingCss, blockCss]
        .join('\n');

      // Create a new style tag with the prefixed styles added above.
      const offCanvasCss = document.createElement('style');
      offCanvasCss.innerHTML = prefixedCss;
      offCanvasCss.setAttribute('id', 'ckeditor5-off-canvas-reset');
      document.body.appendChild(offCanvasCss);
    }
  }

  /**
   * @namespace
   */
  Drupal.editors.ckeditor5 = {
    /**
     * Editor attach callback.
     *
     * @param {HTMLElement} element
     *   The element to attach the editor to.
     * @param {string} format
     *   The text format for the editor.
     */
    attach(element, format) {
      const { editorClassic } = CKEditor5;
      const { toolbar, plugins, config: pluginConfig } = format.editorSettings;
      const extraPlugins = selectPlugins(plugins);

      const config = {
        extraPlugins,
        toolbar,
        ...processConfig(pluginConfig),
      }
      // Set the id immediately so that it is available when onChange is called.
      const id = setElementId(element);
      const { ClassicEditor } = editorClassic;

      ClassicEditor.create(element, config)
        .then((editor) => {
          // Save a reference to the initialized instance.
          Drupal.CKEditor5Instances.set(id, editor);

          // CKEditor4 had a feature to remove the required attribute
          // see: https://www.drupal.org/project/drupal/issues/1954968
          if (element.hasAttribute('required')) {
            required.add(id);
            element.removeAttribute('required');
          }

          editor.model.document.on('change:data', () => {
            const callback = callbacks.get(id);
            if (callback) {
              // Marks the field as changed.
              // @see Drupal.editorAttach
              callback();
            }
          });

          const isOffCanvas = element.closest('#drupal-off-canvas');

          if (isOffCanvas) {
            offCanvasCss(element);
          }
        })
        .catch((error) => {
          console.error(error);
        });
    },

    /**
     * Editor detach callback.
     *
     * @param {HTMLElement} element
     *   The element to detach the editor from.
     * @param {string} format
     *   The text format used for the editor.
     * @param {string} trigger
     *   The event trigger for the detach.
     */
    detach(element, format, trigger) {
      const id = getElementId(element);
      const editor = Drupal.CKEditor5Instances.get(id);
      if (!editor) {
        return;
      }
      if (trigger === 'serialize') {
        editor.updateSourceElement();
      } else {
        element.removeAttribute('contentEditable');

        // Prepare variables that will be used when discarding Quickedit changes.
        let textElement = null;
        let originalValue = null;
        const usingQuickEdit = (((Drupal || {}).quickedit || {}).editors || {}).editor;
        if (usingQuickEdit) {
          // The revert() function in QuickEdit's text editor does not work with
          // CKEditor 5, as it is triggered before CKEditor 5 is fully
          // destroyed. This function is overridden so the functionality it
          // provides can happen after the CKEditor destroy() promise is
          // fulfilled.
          // This pulls the necessary values from the QuickEdit Backbone Model
          // before it is destroyed, so they can be used by
          // `editor.destroy().then()` to perform the expected revert.
          Drupal.quickedit.editors.editor.prototype.revert = function () {
            textElement = this.$textElement[0];
            originalValue = this.model.get('originalValue');
          }
        }

        editor
          .destroy()
          .then(() => {
            // If textElement and originalValue are not null, a QuickEdit
            // revert has been requested. Perform the revert here as it
            // can't happen until the CKEditor instance is destroyed.
            if (textElement && originalValue) {
              textElement.innerHTML = originalValue;
            }

            // Clean up stored references.
            Drupal.CKEditor5Instances.delete(id);
            callbacks.delete(id);
            if (required.has(id)) {
              element.setAttribute('required', 'required');
              required.delete(id);
            }
          })
          .catch((error) => {
            console.error(error);
          });
      }
    },

    /**
     * Registers a callback which CKEditor5 will call on change:data event.
     *
     * @param {HTMLElement} element
     *   The element where the change occurred.
     * @param {function} callback
     *   Callback called with the value of the editor.
     */
    onChange(element, callback) {
      callbacks.set(getElementId(element), debounce(callback, 400));
    },

    /**
     * Attaches an inline editor to a DOM element.
     *
     * @param {HTMLElement} element
     *   The element to attach the editor to.
     * @param {object} format
     *   The text format used in the editor.
     * @param {string} [mainToolbarId]
     *   The id attribute for the main editor toolbar, if any.
     * @param {string} [floatedToolbarId]
     *   The id attribute for the floated editor toolbar, if any.
     *
     * @see Drupal.quickedit.editors.editor
     */
    attachInlineEditor(element, format, mainToolbarId, floatedToolbarId) {
      const { editorDecoupled } = CKEditor5;
      const { toolbar, plugins, config: pluginConfig } = format.editorSettings;
      const extraPlugins = selectPlugins(plugins);
      const config = {
        extraPlugins,
        toolbar,
        ...processConfig(pluginConfig),
      }
      const id = setElementId(element);
      const { DecoupledEditor } = editorDecoupled;

      DecoupledEditor.create(element, config)
        .then((editor) => {
          Drupal.CKEditor5Instances.set(id, editor);
          const toolbar = document.getElementById(mainToolbarId);
          toolbar.appendChild(editor.ui.view.toolbar.element);
          editor.model.document.on('change:data', () => {
            const callback = callbacks.get(id);
            if (callback) {
              // Quick Edit requires the current data to update EditorModel.
              // @see Drupal.quickedit.editors.editor
              callback(editor.getData());
            }
          });
        })
        .catch((error) => {
          console.error(error);
        });
    },
  }

  Drupal.ckeditor5 = {
    /**
     * Variable storing the current dialog's save callback.
     *
     * @type {?function}
     */
    saveCallback: null,

    openDialog(url, saveCallback, dialogSettings) {
      // Add a consistent dialog class.
      const classes = dialogSettings.dialogClass
        ? dialogSettings.dialogClass.split(' ')
        : [];
      classes.push('ui-dialog--narrow');
      dialogSettings.dialogClass = classes.join(' ');
      dialogSettings.autoResize = window.matchMedia(
        '(min-width: 600px)',
      ).matches;
      dialogSettings.width = 'auto';

      const $content = $(
        `<div class="ckeditor5-dialog-loading"><span style="top: -40px;" class="ckeditor5-dialog-loading-link">${Drupal.t(
          'Loading...',
        )}</span></div>`,
      );
      $content.appendTo($('body'));

      const ckeditorAjaxDialog = Drupal.ajax({
        dialog: dialogSettings,
        dialogType: 'modal',
        selector: '.ckeditor5-dialog-loading-link',
        url,
        progress: { type: 'throbber' },
        submit: {
          editor_object: {},
        },
      });
      ckeditorAjaxDialog.execute();

      // After a short delay, show "Loading…" message.
      window.setTimeout(() => {
        $content.find('span').animate({ top: '0px' });
      }, 1000);

      // Store the save callback to be executed when this dialog is closed.
      Drupal.ckeditor5.saveCallback = saveCallback;
    },
  };

  // Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
  $(window).on('dialog:beforecreate', (e, dialog, $element, settings) => {
    $('.ckeditor5-dialog-loading').animate({ top: '-40px' }, function () {
      $(this).remove();
    });
  });

  // Respond to dialogs that are saved, sending data back to CKEditor.
  $(window).on('editor:dialogsave', (e, values) => {
    if (Drupal.ckeditor5.saveCallback) {
      Drupal.ckeditor5.saveCallback(values);
    }
  });

  // Respond to dialogs that are closed, removing the current save handler.
  $(window).on('dialog:afterclose', (e, dialog, $element) => {
    if (Drupal.ckeditor5.saveCallback) {
      Drupal.ckeditor5.saveCallback = null;
    }
  });

})(Drupal, Drupal.debounce, CKEditor5, jQuery);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc