editor_advanced_link-8.x-1.8/js/ckeditor5_plugins/editorAdvancedLink/src/editoradvancedlinkediting.js

js/ckeditor5_plugins/editorAdvancedLink/src/editoradvancedlinkediting.js
// eslint-disable-next-line import/no-extraneous-dependencies
import { Plugin } from 'ckeditor5/src/core';
// eslint-disable-next-line import/no-extraneous-dependencies
import { findAttributeRange } from 'ckeditor5/src/typing';
import { additionalFormElements } from './utils';

export default class EditorAdvancedLinkEditing extends Plugin {
  /**
   * @inheritdoc
   */
  static get pluginName() {
    return 'EditorAdvancedLinkEditing';
  }

  init() {
    const editorAdvancedLinkConfig =
      this.editor.config.get('editorAdvancedLink');

    if (!editorAdvancedLinkConfig.options) {
      this.enabledModelNames = [];
      return;
    }
    const enabledViewAttributes = Object.values(
      editorAdvancedLinkConfig.options,
    );

    this.enabledModelNames = Object.keys(additionalFormElements).filter(
      (modelName) => {
        return enabledViewAttributes.includes(
          additionalFormElements[modelName].viewAttribute,
        );
      },
    );

    // Setup link attribute on all inline nodes.
    this.enabledModelNames.forEach((modelName) => {
      this._allowAndConvertExtraAttribute(
        modelName,
        additionalFormElements[modelName].viewAttribute,
      );
      this._registerUnlinkCommand(modelName);
      this._refreshExtraAttributeValue(modelName);
    });

    // Update linking commands.
    this._registerLinkCommand(
      Object.keys(additionalFormElements),
    );
  }

  _allowAndConvertExtraAttribute(modelName, attributeName) {
    const { editor } = this;

    // Allow link attribute on all inline nodes.
    editor.model.schema.extend('$text', { allowAttributes: modelName });

    // Model -> View (DOM)
    editor.conversion.for('downcast').attributeToElement({
      model: modelName,
      view: (value, { writer }) => {
        if (!value) {
          return;
        }

        const linkViewElement = writer.createAttributeElement(
          'a',
          {
            [attributeName]: value,
          },
          { priority: 5 },
        );

        writer.setCustomProperty('link', true, linkViewElement);
        return linkViewElement;
      },
    });

    // View (DOM/DATA) -> Model
    let view = {
      name: 'a',
      attributes: {
        [attributeName]: true,
      },
    };

    // Fixes warning: https://ckeditor.com/docs/ckeditor5/latest/support/error-codes.html#error-matcher-pattern-deprecated-attributes-class-key.
    if (attributeName === 'class') {
      view = {
        name: 'a',
        classes: true
      };
    }

    editor.conversion.for('upcast').elementToAttribute({
      view,
      model: {
        key: modelName,
        value: (viewElement) => {
          return viewElement.getAttribute(attributeName);
        }
      },
    });
  }

  _registerLinkCommand(modelNames) {
    const { editor } = this;
    const linkCommand = editor.commands.get('link');
    let linkCommandExecuting = false;

    linkCommand.on('execute', (evt, args) => {
      if (linkCommandExecuting) {
        linkCommandExecuting = false;
        return;
      }

      // If the additional attribute was passed, we stop the default execution
      // of the LinkCommand. We're going to create Model#change() block for undo
      // and execute the LinkCommand together with setting the extra attribute.
      evt.stop();

      // Prevent infinite recursion by keeping records of when link command is
      // being executed by this function.
      linkCommandExecuting = true;

      // If no advanced_attributes passed in event arguments (eg decorator updated), then get values from state.
      let advancedAttributeValues = [];
      const decoratorsArgIndex = 1;
      if (args && args[decoratorsArgIndex] && !args[decoratorsArgIndex]['advanced_attributes']) {
          this.enabledModelNames.forEach((attribute) => {
          advancedAttributeValues[attribute] = evt.source[attribute];
        });
        args[decoratorsArgIndex]['advanced_attributes'] = advancedAttributeValues;
      }
      else {
        advancedAttributeValues = args[decoratorsArgIndex]['advanced_attributes'];
      }
      args[decoratorsArgIndex]['advanced_attributes'] = advancedAttributeValues;

      const model = this.editor.model;
      const selection = model.document.selection;
      const displayedText = args[args.length - 1] || args[decoratorsArgIndex]['advanced_attributes']['displayedText'];

      // Wrapping the original command execution in a model.change() block to make sure there's a single undo step
      // when the extra attribute is added.
      model.change((writer) => {

        // Returns a link range based on selection:
        // Selection.getAttribute('linkHref') or
        // Based on finding a link range by href attribute at selection's first position.
        const getCurrentLinkRange = (model, selection, hrefSourceValue) => {
          const position = selection.getFirstPosition();

          // When selection is inside text with `linkHref` attribute or text of link.
          const range = findAttributeRange(position, 'linkHref', hrefSourceValue, model);
          return range;
        };

        // Returns a text of a link range.
        // If the returned value is `undefined`, the range contains elements other than text nodes.
        const extractTextFromLinkRange = (range) => {
          let text = '';
          for (const item of range.getItems()) {
            if (!item.is('$text') && !item.is('$textProxy')) {
              return;
            }
            text += item.data;
          }
          return text;
        }

        const updateLinkTextIfNeeded = (range, displayedText) => {
          const linkText = extractTextFromLinkRange(range);
          if (!linkText || typeof displayedText == "object") {
            // In case 'target' attribute is updated, args do no pass any values
            // for displayed text.
            return range;
          }

          // In a scenario where the displayedText is blank, fall back on the
          // linkText, and if that is empty, use the href from args[0].
          let newText = displayedText || linkText || args[0];
          let newRange = writer.createRange(range.start, range.start.getShiftedBy(newText.length));
          return newRange;
        };

        const updateAttributes = (range, removeSelection) => {
          this.enabledModelNames.forEach((attribute) => {
            if (advancedAttributeValues[attribute]) {
              writer.setAttribute(attribute, advancedAttributeValues[attribute], range);
            } else {
              writer.removeAttribute(attribute, range);
            }

            if (removeSelection) {
              writer.setSelection(range.end);
              const { plugins } = this.editor;
              if (plugins.has('TwoStepCaretMovement')) {
                // After replacing the text of the link, we need to move the caret to the end of the link,
                // override it's gravity to forward to prevent keeping e.g. bold attribute on the caret
                // which was previously inside the link.
                //
                // If the plugin is not available, the caret will be placed at the end of the link and the
                // bold attribute will be kept even if command moved caret outside the link.
                plugins.get('TwoStepCaretMovement')._handleForwardMovement();
              } else {
                // Remove any attributes to prevent link splitting.
                writer.removeSelectionAttribute(attribute);
              }
            }
          });
        };

        editor.execute('link', ...args);

        if (selection.isCollapsed) {
          // The user has clicked somewhere within the link, so we need to
          // calculate the range of characters the attributes should apply
          // to.
          const currentHref = args[0] || evt.source.value;

          let range = getCurrentLinkRange(model, selection, currentHref);
          if (!range) {
            console.info('No link range found');
            return;
          }

          // In CKEditor v45, a new displayText input is present in the
          // link widget. So we need to recalculate the range in case the
          // text has changed.
          range = updateLinkTextIfNeeded(range, displayedText);
          updateAttributes(range, true);
        } else {
          if (this.enabledModelNames.length > 0) {
            const ranges = model.schema.getValidRanges(selection.getRanges(), this.enabledModelNames[0]);
            for (const range of ranges) {
              updateAttributes(range);
            }
          }
        }
      });
    },
    { priority: 'high' });
  }

  _registerUnlinkCommand(modelName) {
    const { editor } = this;
    const unlinkCommand = editor.commands.get('unlink');
    const { model } = this.editor;
    const { selection } = model.document;

    let isUnlinkingInProgress = false;

    // Make sure all changes are in a single undo step so cancel the original unlink first in the high priority.
    unlinkCommand.on('execute', (evt) => {
        // This single block wraps all changes that should be in a single undo step.
        model.change(() => {
          // The actual integration that removes the extra attribute.
          model.change((writer) => {
            // Get ranges to unlink.
            let ranges;

            if (selection.isCollapsed) {
              ranges = [
                findAttributeRange(
                  selection.getFirstPosition(),
                  modelName,
                  selection.getAttribute(modelName),
                  model,
                ),
              ];
            } else {
              ranges = model.schema.getValidRanges(
                selection.getRanges(),
                modelName,
              );
            }

            // Remove the extra attribute from specified ranges.
            // eslint-disable-next-line max-nested-callbacks
            ranges.forEach((range) => {
              writer.removeAttribute(modelName, range);
            });
          });
        });
      },
      { priority: 'highest' },
    );
  }

  _refreshExtraAttributeValue(modelName) {
    const { editor } = this;
    const linkCommand = editor.commands.get('link');
    const { model } = this.editor;
    const { selection } = model.document;

    linkCommand.set(modelName, null);
    model.document.on('change', (event, args) => {
      linkCommand[modelName] = selection.getAttribute(modelName);
    });
  }
}

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

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