X-Git-Url: http://www.aleph1.co.uk/gitweb/?p=yaffs-website;a=blobdiff_plain;f=web%2Fcore%2Fmodules%2Fckeditor%2Fjs%2Fplugins%2Fdrupalimagecaption%2Fplugin.es6.js;fp=web%2Fcore%2Fmodules%2Fckeditor%2Fjs%2Fplugins%2Fdrupalimagecaption%2Fplugin.es6.js;h=1d51bcb82d486240307929e1e0a0d7476458dd8f;hp=0000000000000000000000000000000000000000;hb=9917807b03b64faf00f6a1f29dcb6eafc454efa5;hpb=aea91e65e895364e460983b890e295aa5d5540a5 diff --git a/web/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.es6.js b/web/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.es6.js new file mode 100644 index 000000000..1d51bcb82 --- /dev/null +++ b/web/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.es6.js @@ -0,0 +1,296 @@ +/** + * @file + * Drupal Image Caption plugin. + * + * This alters the existing CKEditor image2 widget plugin, which is already + * altered by the Drupal Image plugin, to: + * - allow for the data-caption and data-align attributes to be set + * - mimic the upcasting behavior of the caption_filter filter. + * + * @ignore + */ + +(function (CKEDITOR) { + CKEDITOR.plugins.add('drupalimagecaption', { + requires: 'drupalimage', + + beforeInit(editor) { + // Disable default placeholder text that comes with CKEditor's image2 + // plugin: it has an inferior UX (it requires the user to manually delete + // the place holder text). + editor.lang.image2.captionPlaceholder = ''; + + // 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. + const placeholderText = editor.config.drupalImageCaption_captionPlaceholderText; + + // Override the image2 widget definition to handle the additional + // data-align and data-caption attributes. + editor.on('widgetDefinition', (event) => { + const widgetDefinition = event.data; + if (widgetDefinition.name !== 'image') { + return; + } + + // Only perform the downcasting/upcasting for to the enabled filters. + const captionFilterEnabled = editor.config.drupalImageCaption_captionFilterEnabled; + const alignFilterEnabled = editor.config.drupalImageCaption_alignFilterEnabled; + + // Override default features definitions for drupalimagecaption. + CKEDITOR.tools.extend(widgetDefinition.features, { + caption: { + requiredContent: 'img[data-caption]', + }, + align: { + requiredContent: 'img[data-align]', + }, + }, true); + + // 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-align'] = ''; + requiredContent.attributes['data-caption'] = ''; + widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent); + widgetDefinition.allowedContent.img.attributes['!data-align'] = true; + widgetDefinition.allowedContent.img.attributes['!data-caption'] = true; + + // Override allowedContent setting for the 'caption' nested editable. + // This must match what caption_filter enforces. + // @see \Drupal\filter\Plugin\Filter\FilterCaption::process() + // @see \Drupal\Component\Utility\Xss::filter() + widgetDefinition.editables.caption.allowedContent = 'a[!href]; em strong cite code br'; + + // Override downcast(): ensure we *only* output , but also ensure + // we include the data-entity-type, data-entity-uuid, data-align and + // data-caption attributes. + const originalDowncast = widgetDefinition.downcast; + widgetDefinition.downcast = function (element) { + const img = findElementByName(element, 'img'); + originalDowncast.call(this, img); + + const caption = this.editables.caption; + const captionHtml = caption && caption.getData(); + const attrs = img.attributes; + + if (captionFilterEnabled) { + // If image contains a non-empty caption, serialize caption to the + // data-caption attribute. + if (captionHtml) { + attrs['data-caption'] = captionHtml; + } + } + if (alignFilterEnabled) { + if (this.data.align !== 'none') { + attrs['data-align'] = this.data.align; + } + } + + // If img is wrapped with a link, we want to return that link. + if (img.parent.name === 'a') { + return img.parent; + } + + return img; + }; + + // We want to upcast elements to a DOM structure required by the + // image2 widget. Depending on a case it may be: + // - just an tag (non-captioned, not-centered image), + // - tag in a paragraph (non-captioned, centered image), + // -
tag (captioned image). + // We take the same attributes into account as downcast() does. + const originalUpcast = widgetDefinition.upcast; + widgetDefinition.upcast = function (element, data) { + if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) { + return; + } + // Don't initialize on pasted fake objects. + else if (element.attributes['data-cke-realelement']) { + return; + } + + element = originalUpcast.call(this, element, data); + const attrs = element.attributes; + + if (element.parent.name === 'a') { + element = element.parent; + } + + let retElement = element; + let caption; + + // We won't need the attributes during editing: we'll use widget.data + // to store them (except the caption, which is stored in the DOM). + if (captionFilterEnabled) { + caption = attrs['data-caption']; + delete attrs['data-caption']; + } + if (alignFilterEnabled) { + data.align = attrs['data-align']; + delete attrs['data-align']; + } + data['data-entity-type'] = attrs['data-entity-type']; + delete attrs['data-entity-type']; + data['data-entity-uuid'] = attrs['data-entity-uuid']; + delete attrs['data-entity-uuid']; + + if (captionFilterEnabled) { + // Unwrap from

wrapper created by HTML parser for a captioned + // image. The captioned image will be transformed to

, so we + // don't want the

anymore. + if (element.parent.name === 'p' && caption) { + let index = element.getIndex(); + const splitBefore = index > 0; + const splitAfter = index + 1 < element.parent.children.length; + + if (splitBefore) { + element.parent.split(index); + } + index = element.getIndex(); + if (splitAfter) { + element.parent.split(index + 1); + } + + element.parent.replaceWith(element); + retElement = element; + } + + // If this image has a caption, create a full

structure. + if (caption) { + const figure = new CKEDITOR.htmlParser.element('figure'); + caption = new CKEDITOR.htmlParser.fragment.fromHtml(caption, 'figcaption'); + + // Use Drupal's data-placeholder attribute to insert a CSS-based, + // translation-ready placeholder for empty captions. Note that it + // also must to be done for new instances (see + // widgetDefinition._createDialogSaveCallback). + caption.attributes['data-placeholder'] = placeholderText; + + element.replaceWith(figure); + figure.add(element); + figure.add(caption); + figure.attributes.class = editor.config.image2_captionedClass; + retElement = figure; + } + } + + if (alignFilterEnabled) { + // If this image doesn't have a caption (or the caption filter is + // disabled), but it is centered, make sure that it's wrapped with + //

, which will become a part of the widget. + if (data.align === 'center' && (!captionFilterEnabled || !caption)) { + const p = new CKEDITOR.htmlParser.element('p'); + element.replaceWith(p); + p.add(element); + // Apply the class for centered images. + p.addClass(editor.config.image2_alignClasses[1]); + retElement = p; + } + } + + // Return the upcasted element (,

or

). + return retElement; + }; + + // Protected; keys of the widget data to be sent to the Drupal dialog. + // Append to the values defined by the drupalimage plugin. + // @see core/modules/ckeditor/js/plugins/drupalimage/plugin.js + CKEDITOR.tools.extend(widgetDefinition._mapDataToDialog, { + align: 'data-align', + 'data-caption': 'data-caption', + hasCaption: 'hasCaption', + }); + + // Override Drupal dialog save callback. + const originalCreateDialogSaveCallback = widgetDefinition._createDialogSaveCallback; + widgetDefinition._createDialogSaveCallback = function (editor, widget) { + const saveCallback = originalCreateDialogSaveCallback.call(this, editor, widget); + + return function (dialogReturnValues) { + // Ensure hasCaption is a boolean. image2 assumes it always works + // with booleans; if this is not the case, then + // CKEDITOR.plugins.image2.stateShifter() will incorrectly mark + // widget.data.hasCaption as "changed" (e.g. when hasCaption === 0 + // instead of hasCaption === false). This causes image2's "state + // shifter" to enter the wrong branch of the algorithm and blow up. + dialogReturnValues.attributes.hasCaption = !!dialogReturnValues.attributes.hasCaption; + + const actualWidget = saveCallback(dialogReturnValues); + + // By default, the template of captioned widget has no + // data-placeholder attribute. Note that it also must be done when + // upcasting existing elements (see widgetDefinition.upcast). + if (dialogReturnValues.attributes.hasCaption) { + actualWidget.editables.caption.setAttribute('data-placeholder', placeholderText); + + // Some browsers will add a
tag to a newly created DOM + // element with no content. Remove this
if it is the only + // thing in the caption. Our placeholder support requires the + // element be entirely empty. See filter-caption.css. + const captionElement = actualWidget.editables.caption.$; + if (captionElement.childNodes.length === 1 && captionElement.childNodes.item(0).nodeName === 'BR') { + captionElement.removeChild(captionElement.childNodes.item(0)); + } + } + }; + }; + // Low priority to ensure drupalimage's event handler runs first. + }, null, null, 20); + }, + + afterInit(editor) { + const disableButtonIfOnWidget = function (evt) { + const widget = editor.widgets.focused; + if (widget && widget.name === 'image') { + this.setState(CKEDITOR.TRISTATE_DISABLED); + evt.cancel(); + } + }; + + // Disable alignment buttons if the align filter is not enabled. + if (editor.plugins.justify && !editor.config.drupalImageCaption_alignFilterEnabled) { + let cmd; + const commands = ['justifyleft', 'justifycenter', 'justifyright', 'justifyblock']; + for (let n = 0; n < commands.length; n++) { + cmd = editor.getCommand(commands[n]); + cmd.contextSensitive = 1; + cmd.on('refresh', disableButtonIfOnWidget, null, null, 4); + } + } + }, + }); + + /** + * Finds an element by its name. + * + * Function will check first the passed element itself and then all its + * children in DFS order. + * + * @param {CKEDITOR.htmlParser.element} element + * The element to search. + * @param {string} name + * The element name to search for. + * + * @return {?CKEDITOR.htmlParser.element} + * The found element, or null. + */ + function findElementByName(element, name) { + if (element.name === name) { + return element; + } + + let found = null; + element.forEach((el) => { + if (el.name === name) { + found = el; + // Stop here. + return false; + } + }, CKEDITOR.NODE_ELEMENT); + return found; + } +}(CKEDITOR));