text: /**
   * @file
   * Drupal Image plugin.
   *
   * This alters the existing CKEditor image2 widget plugin to:
   * - require a data-entity-type and a data-entity-uuid attribute (which Drupal
   *   uses to track where images are being used)
   * - use a Drupal-native dialog (that is in fact just an alterable Drupal form
   *   like any other) instead of CKEditor's own dialogs.
   *
   * @see \Drupal\editor\Form\EditorImageDialog
   *
   * @ignore
   */
  
  (function($, Drupal, CKEDITOR) {
    /**
     * Gets the focused widget, if of the type specific for this plugin.
     *
     * @param {CKEDITOR.editor} editor
     *   A CKEditor instance.
     *
     * @return {?CKEDITOR.plugins.widget}
     *   The focused image2 widget instance, or null.
     */
    function getFocusedWidget(editor) {
      const widget = editor.widgets.focused;
  
      if (widget && widget.name === 'image') {
        return widget;
      }
  
      return null;
    }
  
    /**
     * Integrates the drupalimage widget with the drupallink plugin.
     *
     * Makes images linkable.
     *
     * @param {CKEDITOR.editor} editor
     *   A CKEditor instance.
     */
    function linkCommandIntegrator(editor) {
      // Nothing to integrate with if the drupallink plugin is not loaded.
      if (!editor.plugins.drupallink) {
        return;
      }
  
      CKEDITOR.plugins.drupallink.registerLinkableWidget('image');
  
      // Override default behavior of 'drupalunlink' command.
      editor.getCommand('drupalunlink').on('exec', function(evt) {
        const widget = getFocusedWidget(editor);
  
        // Override 'drupalunlink' only when link truly belongs to the widget. If
        // wrapped inline widget in a link, let default unlink work.
        // @see https://dev.ckeditor.com/ticket/11814
        if (!widget || !widget.parts.link) {
          return;
        }
  
        widget.setData('link', null);
  
        // Selection (which is fake) may not change if unlinked image in focused
        // widget, i.e. if captioned image. Let's refresh command state manually
        // here.
        this.refresh(editor, editor.elementPath());
  
        evt.cancel();
      });
  
      // Override default refresh of 'drupalunlink' command.
      editor.getCommand('drupalunlink').on('refresh', function(evt) {
        const widget = getFocusedWidget(editor);
  
        if (!widget) {
          return;
        }
  
        // Note that widget may be wrapped in a link, which
        // does not belong to that widget (#11814).
        this.setState(
          widget.data.link || widget.wrapper.getAscendant('a')
            ? CKEDITOR.TRISTATE_OFF
            : CKEDITOR.TRISTATE_DISABLED,
        );
  
        evt.cancel();
      });
    }
  
    CKEDITOR.plugins.add('drupalimage', {
      requires: 'image2',
      icons: 'drupalimage',
      hidpi: true,
  
      beforeInit(editor) {
        // Override the image2 widget definition to require and handle the
        // additional data-entity-type and data-entity-uuid attributes.
        editor.on('widgetDefinition', event => {
          const widgetDefinition = event.data;
          if (widgetDefinition.name !== 'image') {
            return;
          }
  
          // First, convert requiredContent & allowedContent from the string
          // format that image2 uses for both to formats that are better suited
          // for extending, so that both this basic drupalimage plugin and Drupal
          // modules can easily extend it.
          // @see http://docs.ckeditor.com/#!/api/CKEDITOR.filter.allowedContentRules
          // Mapped from image2's allowedContent. Unlike image2, we don't allow
          // <figure>, <figcaption>, <div> or <p>  in our downcast, so we omit
          // those. For the <img> tag, we list all attributes it lists, but omit
          // the classes, because the listed classes are for alignment, and for
          // alignment we use the data-align attribute.
          widgetDefinition.allowedContent = {
            img: {
              attributes: {
                '!src': true,
                '!alt': true,
                width: true,
                height: true,
              },
              classes: {},
            },
          };
          // Mapped from image2's requiredContent: "img[src,alt]". This does not
          // use the object format unlike above, but a CKEDITOR.style instance,
          // because requiredContent does not support the object format.
          // @see https://www.drupal.org/node/2585173#comment-10456981
          widgetDefinition.requiredContent = new CKEDITOR.style({
            element: 'img',
            attributes: {
              src: '',
              alt: '',
            },
          });
  
          // Extend requiredContent & allowedContent.
          // CKEDITOR.style is an immutable object: we cannot modify its
          // definition to extend requiredContent. Hence we get the definition,
          // modify it, and pass it to a new CKEDITOR.style instance.
          //const requiredContent = widgetDefinition.requiredContent.getDefinition();
          //requiredContent.attributes['data-entity-type'] = '';
          //requiredContent.attributes['data-entity-uuid'] = '';
          //widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent);
          widgetDefinition.allowedContent.img.attributes[
            '!data-entity-type'
          ] = true;
          widgetDefinition.allowedContent.img.attributes[
            '!data-entity-uuid'
          ] = true;
  
          // Override downcast(): since we only accept <img> in our upcast method,
          // the element is already correct. We only need to update the element's
          // data-entity-uuid attribute.
          widgetDefinition.downcast = function(element) {
            element.attributes['data-entity-type'] = this.data[
              'data-entity-type'
            ] ? this.data['data-entity-type'] : 'image';
            element.attributes['data-entity-uuid'] = this.data[
              'data-entity-uuid'
            ] ? this.data['data-entity-uuid'] : 0;
          };
  
          // We want to upcast <img> elements to a DOM structure required by the
          // image2 widget; we only accept an <img> tag, and that <img> tag MAY
          // have a data-entity-type and a data-entity-uuid attribute.
          widgetDefinition.upcast = function(element, data) {
            if (element.name !== 'img') {
              return;
            }
            // Don't initialize on pasted fake objects.
            if (element.attributes['data-cke-realelement']) {
              return;
            }
  
            // Parse the data-entity-type attribute.
            data['data-entity-type'] = element.attributes['data-entity-type'];
            // Parse the data-entity-uuid attribute.
            data['data-entity-uuid'] = element.attributes['data-entity-uuid'];
  
            return element;
          };
  
          // Overrides default implementation. Used to populate the "classes"
          // property of the widget's "data" property, which is used for the
          // "widget styles" functionality
          // (http://docs.ckeditor.com/#!/guide/dev_styles-section-widget-styles).
          // Is applied to whatever the main element of the widget is (<figure> or
          // <img>). The classes in image2_captionedClass are always added due to
          // a bug in CKEditor. In the case of drupalimage, we don't ever want to
          // add that class, because the widget template already contains it.
          // @see http://dev.ckeditor.com/ticket/13888
          // @see https://www.drupal.org/node/2268941
          const originalGetClasses = widgetDefinition.getClasses;
          widgetDefinition.getClasses = function() {
            const classes = originalGetClasses.call(this);
            const captionedClasses = (
              this.editor.config.image2_captionedClass || ''
            ).split(/\s+/);
  
            if (captionedClasses.length && classes) {
              for (let i = 0; i < captionedClasses.length; i++) {
                if (captionedClasses[i] in classes) {
                  delete classes[captionedClasses[i]];
                }
              }
            }
  
            return classes;
          };
  
          // Protected; keys of the widget data to be sent to the Drupal dialog.
          // Keys in the hash are the keys for image2's data, values are the keys
          // that the Drupal dialog uses.
          widgetDefinition._mapDataToDialog = {
            src: 'src',
            alt: 'alt',
            width: 'width',
            height: 'height',
            'data-entity-type': 'data-entity-type',
            'data-entity-uuid': 'data-entity-uuid',
          };
  
          // Protected; transforms widget's data object to the format used by the
          // \Drupal\editor\Form\EditorImageDialog dialog, keeping only the data
          // listed in widgetDefinition._dataForDialog.
          widgetDefinition._dataToDialogValues = function(data) {
            const dialogValues = {};
            const map = widgetDefinition._mapDataToDialog;
            Object.keys(widgetDefinition._mapDataToDialog).forEach(key => {
              dialogValues[map[key]] = data[key];
            });
            return dialogValues;
          };
  
          // Protected; the inverse of _dataToDialogValues.
          widgetDefinition._dialogValuesToData = function(dialogReturnValues) {
            const data = {};
            const map = widgetDefinition._mapDataToDialog;
            Object.keys(widgetDefinition._mapDataToDialog).forEach(key => {
              if (dialogReturnValues.hasOwnProperty(map[key])) {
                data[key] = dialogReturnValues[map[key]];
              }
            });
            return data;
          };
  
          // Protected; creates Drupal dialog save callback.
          widgetDefinition._createDialogSaveCallback = function(editor, widget) {
            return function(dialogReturnValues) {
              const firstEdit = !widget.ready;
  
              // Dialog may have blurred the widget. Re-focus it first.
              if (!firstEdit) {
                widget.focus();
              }
  
              editor.fire('saveSnapshot');
  
              // Pass `true` so DocumentFragment will also be returned.
              const container = widget.wrapper.getParent(true);
              const image = widget.parts.image;
  
              // Set the updated widget data, after the necessary conversions from
              // the dialog's return values.
              // Note: on widget#setData this widget instance might be destroyed.
              const data = widgetDefinition._dialogValuesToData(
                dialogReturnValues.attributes,
              );
              widget.setData(data);
  
              // Retrieve the widget once again. It could've been destroyed
              // when shifting state, so might deal with a new instance.
              widget = editor.widgets.getByElement(image);
  
              // It's first edit, just after widget instance creation, but before
              // it was inserted into DOM. So we need to retrieve the widget
              // wrapper from inside the DocumentFragment which we cached above
              // and finalize other things (like ready event and flag).
              if (firstEdit) {
                editor.widgets.finalizeCreation(container);
              }
  
              setTimeout(() => {
                // (Re-)focus the widget.
                widget.focus();
                // Save snapshot for undo support.
                editor.fire('saveSnapshot');
              });
  
              return widget;
            };
          };
  
          const originalInit = widgetDefinition.init;
          widgetDefinition.init = function() {
            originalInit.call(this);
  
            // Update data.link object with attributes if the link has been
            // discovered.
            // @see plugins/image2/plugin.js/init() in CKEditor; this is similar.
            if (this.parts.link) {
              this.setData(
                'link',
                CKEDITOR.plugins.image2.getLinkAttributesParser()(
                  editor,
                  this.parts.link,
                ),
              );
            }
          };
        });
  
        // Add a widget#edit listener to every instance of image2 widget in order
        // to handle its editing with a Drupal-native dialog.
        // This includes also a case just after the image was created
        // and dialog should be opened for it for the first time.
        editor.widgets.on('instanceCreated', event => {
          const widget = event.data;
  
          if (widget.name !== 'image') {
            return;
          }
  
          widget.on('edit', event => {
            // Cancel edit event to break image2's dialog binding
            // (and also to prevent automatic insertion before opening dialog).
            event.cancel();
  
            // Open drupalimage dialog.
            editor.execCommand('editdrupalimage', {
              existingValues: widget.definition._dataToDialogValues(widget.data),
              saveCallback: widget.definition._createDialogSaveCallback(
                editor,
                widget,
              ),
              // Drupal.t() will not work inside CKEditor plugins because CKEditor
              // loads the JavaScript file instead of Drupal. Pull translated
              // strings from the plugin settings that are translated server-side.
              dialogTitle: widget.data.src
                ? editor.config.drupalImage_dialogTitleEdit
                : editor.config.drupalImage_dialogTitleAdd,
            });
          });
        });
  
        // Register the "editdrupalimage" command, which essentially just replaces
        // the "image" command's CKEditor dialog with a Drupal-native dialog.
        editor.addCommand('editdrupalimage', {
          allowedContent:
            'img[alt,!src,width,height,!data-entity-type,!data-entity-uuid]',
          requiredContent: 'img[alt,src,data-entity-type,data-entity-uuid]',
          modes: { wysiwyg: 1 },
          canUndo: true,
          exec(editor, data) {
            const dialogSettings = {
              title: data.dialogTitle,
              dialogClass: 'editor-image-dialog',
            };
            Drupal.ckeditor.openDialog(
              editor,
              Drupal.url(`editor/dialog/image/${editor.config.drupal.format}`),
              data.existingValues,
              data.saveCallback,
              dialogSettings,
            );
          },
        });
  
        // Register the toolbar button.
        if (editor.ui.addButton) {
          editor.ui.addButton('DrupalImage', {
            label: Drupal.t('Image'),
            // Note that we use the original image2 command!
            command: 'image',
          });
        }
      },
  
      afterInit(editor) {
        linkCommandIntegrator(editor);
      },
    });
  
    // Override image2's integration with the official CKEditor link plugin:
    // integrate with the drupallink plugin instead.
    CKEDITOR.plugins.image2.getLinkAttributesParser = function() {
      return CKEDITOR.plugins.drupallink.parseLinkAttributes;
    };
    CKEDITOR.plugins.image2.getLinkAttributesGetter = function() {
      return CKEDITOR.plugins.drupallink.getLinkAttributes;
    };
  
    // Expose an API for other plugins to interact with drupalimage widgets.
    CKEDITOR.plugins.drupalimage = {
      getFocusedWidget,
    };
  })(jQuery, Drupal, CKEDITOR);
  