Commit 33850359 authored by Ben Mullins's avatar Ben Mullins
Browse files

Issue #3248228 by lauriii, Wim Leers: Unable to change selection after linking...

Issue #3248228 by lauriii, Wim Leers: Unable to change selection after linking inline media when manual decorators have been defined
parent da12af14
Loading
Loading
Loading
Loading
+1 −1

File changed.

Preview size limit exceeded, changes collapsed.

+155 −1
Original line number Diff line number Diff line
/* eslint-disable import/no-extraneous-dependencies */
/* cspell:words drupallinkmediaediting linkediting */
/* cspell:words drupallinkmediaediting linkediting linkimageediting linkcommand */
import { Plugin } from 'ckeditor5/src/core';
import { Matcher } from 'ckeditor5/src/engine';
import { toMap } from 'ckeditor5/src/utils';

/**
 * Returns the first drupal-media element in a given view element.
@@ -192,6 +194,128 @@ function editingDowncastMediaLink() {
  };
}

/**
 * Returns a converter that enables manual decorators on linked Drupal Media.
 *
 * @see \Drupal\editor\EditorXssFilter\Standard
 *
 * @param {module:link/link~LinkDecoratorDefinition} decorator
 *   The link decorator.
 * @return {function}
 *   Function attaching event listener to dispatcher.
 *
 * @private
 */
function downcastMediaLinkManualDecorator(decorator) {
  return (dispatcher) => {
    dispatcher.on(
      `attribute:${decorator.id}:drupalMedia`,
      (evt, data, conversionApi) => {
        const mediaContainer = conversionApi.mapper.toViewElement(data.item);

        // Scenario 1: `<figure>` element that contains `<a>`, generated by
        // `dataDowncast`.
        let mediaLink = Array.from(mediaContainer.getChildren()).find(
          (child) => child.name === 'a',
        );

        // Scenario 2: `<drupal-media>` wrapped with `<a>`, generated by
        // `editingDowncast`.
        if (!mediaLink && mediaContainer.is('element', 'a')) {
          mediaLink = mediaContainer;
        } else {
          mediaLink = Array.from(mediaContainer.getAncestors()).find(
            (ancestor) => ancestor.name === 'a',
          );
        }

        // The <a> element was removed by the time this converter is executed.
        // It may happen when the base `linkHref` and decorator attributes are
        // removed at the same time.
        if (!mediaLink) {
          return;
        }

        // eslint-disable-next-line no-restricted-syntax
        for (const [key, val] of toMap(decorator.attributes)) {
          conversionApi.writer.setAttribute(key, val, mediaLink);
        }

        if (decorator.classes) {
          conversionApi.writer.addClass(decorator.classes, mediaLink);
        }

        // Add support for `style` attribute in manual decorators to remain
        // consistent with CKEditor 5. This only works with text formats that
        // have no HTMl filtering enabled.
        // eslint-disable-next-line no-restricted-syntax
        for (const key in decorator.styles) {
          if (Object.prototype.hasOwnProperty.call(decorator.styles, key)) {
            conversionApi.writer.setStyle(
              key,
              decorator.styles[key],
              mediaLink,
            );
          }
        }
      },
    );
  };
}

/**
 * Returns a converter that applies manual decorators to linked Drupal Media.
 *
 * @param {module:core/editor/editor~Editor} editor
 *   The editor.
 * @param {module:link/link~LinkDecoratorDefinition} decorator
 *   The link decorator.
 * @return {function}
 *   Function attaching event listener to dispatcher.
 *
 * @private
 */
function upcastMediaLinkManualDecorator(editor, decorator) {
  return (dispatcher) => {
    dispatcher.on(
      'element:a',
      (evt, data, conversionApi) => {
        const viewLink = data.viewItem;
        const drupalMediaInLink = getFirstMedia(viewLink);

        // We need to check whether Drupal Media is inside a link because the
        // converter handles only manual decorators for linked Drupal Media.
        if (!drupalMediaInLink) {
          return;
        }

        const matcher = new Matcher(decorator._createPattern());
        const result = matcher.match(viewLink);

        // The link element does not have required attributes or/and proper
        // values.
        if (!result) {
          return;
        }

        // Check whether we can consume those attributes.
        if (!conversionApi.consumable.consume(viewLink, result.match)) {
          return;
        }

        // At this stage we can assume that we have the `<drupalMedia>` element.
        const modelElement = data.modelCursor.nodeBefore;

        conversionApi.writer.setAttribute(decorator.id, true, modelElement);
      },
      { priority: 'high' },
    );
    // Using the same priority as the media link upcast converter guarantees
    // that the linked `<drupalMedia>` was already converted.
    // @see upcastMediaLink().
  };
}

/**
 * Model to view and view to model conversions for linked media elements.
 *
@@ -226,5 +350,35 @@ export default class DrupalLinkMediaEditing extends Plugin {
    editor.conversion.for('upcast').add(upcastMediaLink());
    editor.conversion.for('editingDowncast').add(editingDowncastMediaLink());
    editor.conversion.for('dataDowncast').add(dataDowncastMediaLink());

    this._enableManualDecorators();
  }

  /**
   * Processes transformed manual link decorators and attaches proper converters
   * that will work when linking Drupal Media.
   *
   * @see module:link/linkimageediting~LinkImageEditing
   * @see module:link/linkcommand~LinkCommand
   * @see module:link/utils~ManualDecorator
   *
   * @private
   */
  _enableManualDecorators() {
    const editor = this.editor;
    const command = editor.commands.get('link');

    // eslint-disable-next-line no-restricted-syntax
    for (const decorator of command.manualDecorators) {
      editor.model.schema.extend('drupalMedia', {
        allowAttributes: decorator.id,
      });
      editor.conversion
        .for('downcast')
        .add(downcastMediaLinkManualDecorator(decorator));
      editor.conversion
        .for('upcast')
        .add(upcastMediaLinkManualDecorator(editor, decorator));
    }
  }
}
+1 −0
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ export default class DrupalMediaEditing extends Plugin {
      allowWhere: '$block',
      isObject: true,
      isContent: true,
      isBlock: true,
      allowAttributes: Object.keys(this.attrs),
    });
  }
+25 −0
Original line number Diff line number Diff line
ckeditor5_manual_decorator_test_openInNewTab:
  ckeditor5:
    plugins: []
    config:
      link:
        decorators:
          openInNewTab:
            mode: 'manual'
            label: 'Open in a new tab'
            attributes:
              target: '_blank'
              rel: 'noopener noreferrer'
            classes: ['link-new-tab']
          pinkColor:
            mode: 'manual'
            label: 'Pink color'
            styles:
              color: 'pink'
  drupal:
    label: Open in new tab
    elements:
      - <a target="_blank" rel="noopener noreferrer" class>
    conditions:
      plugins:
        - ckeditor5_link
+6 −0
Original line number Diff line number Diff line
name: CKEditor 5 Manual Decorator Test
type: module
description: "Provides configuration for CKEditor 5 link plugin manual decorator."
package: Testing
dependencies:
  - ckeditor5:ckeditor5
Loading