Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

View file

@ -0,0 +1,230 @@
/**
* @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) {
"use strict";
CKEDITOR.plugins.add('drupalimage', {
requires: 'image2',
beforeInit: function (editor) {
// Override the image2 widget definition to require and handle the
// additional data-entity-type and data-entity-uuid attributes.
editor.on('widgetDefinition', function (event) {
var widgetDefinition = event.data;
if (widgetDefinition.name !== 'image') {
return;
}
// Override requiredContent & allowedContent.
widgetDefinition.requiredContent = 'img[alt,src,width,height,data-entity-type,data-entity-uuid]';
widgetDefinition.allowedContent.img.attributes += ',!data-entity-type,!data-entity-uuid';
// We don't allow <figure>, <figcaption>, <div> or <p> in our downcast.
delete widgetDefinition.allowedContent.figure;
delete widgetDefinition.allowedContent.figcaption;
delete widgetDefinition.allowedContent.div;
delete widgetDefinition.allowedContent.p;
// Override the 'link' part, to completely disable image2's link
// support: http://dev.ckeditor.com/ticket/11341.
widgetDefinition.parts.link = 'This is a nonsensical selector to disable this functionality completely';
// 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-uuid'] = this.data['data-entity-uuid'];
};
// 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.
else 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;
};
// 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) {
var dialogValues = {};
var map = widgetDefinition._mapDataToDialog;
Object.keys(widgetDefinition._mapDataToDialog).forEach(function (key) {
dialogValues[map[key]] = data[key];
});
return dialogValues;
};
// Protected; the inverse of _dataToDialogValues.
widgetDefinition._dialogValuesToData = function (dialogReturnValues) {
var data = {};
var map = widgetDefinition._mapDataToDialog;
Object.keys(widgetDefinition._mapDataToDialog).forEach(function (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) {
var 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.
var container = widget.wrapper.getParent(true);
var 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.
var 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(function () {
// (Re-)focus the widget.
widget.focus();
// Save snapshot for undo support.
editor.fire('saveSnapshot');
});
return widget;
};
};
});
// 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', function (event) {
var widget = event.data;
if (widget.name !== 'image') {
return;
}
widget.on('edit', function (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,width,height,data-entity-type,data-entity-uuid]',
modes: {wysiwyg: 1},
canUndo: true,
exec: function (editor, data) {
var 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',
icon: this.path + '/image.png'
});
}
},
// Disable image2's integration with the link/drupallink plugins: don't
// allow the widget itself to become a link. Support for that may be added
// by an text filter that adds a data- attribute specifically for that.
afterInit: function (editor) {
if (editor.plugins.drupallink) {
var cmd = editor.getCommand('drupallink');
// Needs to be refreshed on selection changes.
cmd.contextSensitive = 1;
// Disable command and cancel event when the image widget is selected.
cmd.on('refresh', function (evt) {
var widget = editor.widgets.focused;
if (widget && widget.name === 'image') {
this.setState(CKEDITOR.TRISTATE_DISABLED);
evt.cancel();
}
});
}
}
});
})(jQuery, Drupal, CKEDITOR);

View file

@ -0,0 +1,247 @@
/**
* @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) {
"use strict";
CKEDITOR.plugins.add('drupalimagecaption', {
requires: 'drupalimage',
beforeInit: function (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.
var placeholderText = editor.config.drupalImageCaption_captionPlaceholderText;
// Override the image2 widget definition to handle the additional
// data-align and data-caption attributes.
editor.on('widgetDefinition', function (event) {
var widgetDefinition = event.data;
if (widgetDefinition.name !== 'image') {
return;
}
// Only perform the downcasting/upcasting for to the enabled filters.
var captionFilterEnabled = editor.config.drupalImageCaption_captionFilterEnabled;
var 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);
// Override requiredContent & allowedContent.
widgetDefinition.requiredContent = 'img[alt,src,width,height,data-entity-type,data-entity-uuid,data-align,data-caption]';
widgetDefinition.allowedContent.img.attributes += ',data-align,data-caption';
// 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 <img>, but also ensure
// we include the data-entity-type, data-entity-uuid, data-align and
// data-caption attributes.
widgetDefinition.downcast = function (element) {
// Find an image element in the one being downcasted (can be itself).
var img = findElementByName(element, 'img');
var caption = this.editables.caption;
var captionHtml = caption && caption.getData();
var 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;
}
}
attrs['data-entity-type'] = this.data['data-entity-type'];
attrs['data-entity-uuid'] = this.data['data-entity-uuid'];
return img;
};
// We want to upcast <img> elements to a DOM structure required by the
// image2 widget. Depending on a case it may be:
// - just an <img> tag (non-captioned, not-centered image),
// - <img> tag in a paragraph (non-captioned, centered image),
// - <figure> tag (captioned image).
// We take the same attributes into account as downcast() does.
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;
}
var attrs = element.attributes;
var retElement = element;
var 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 <p> wrapper created by HTML parser for a captioned
// image. The captioned image will be transformed to <figure>, so we
// don't want the <p> anymore.
if (element.parent.name === 'p' && caption) {
var index = element.getIndex();
var splitBefore = index > 0;
var 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 <figure> structure.
if (caption) {
var 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
// <p>, which will become a part of the widget.
if (data.align === 'center' && (!captionFilterEnabled || !caption)) {
var 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 (<img>, <figure> or <p>).
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.
var originalCreateDialogSaveCallback = widgetDefinition._createDialogSaveCallback;
widgetDefinition._createDialogSaveCallback = function (editor, widget) {
var 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;
var 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);
}
};
};
}, null, null, 20); // Low priority to ensure drupalimage's event handler runs first.
}
});
/**
* 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
* @param {string} name
*
* @return {CKEDITOR.htmlParser.element}
*/
function findElementByName(element, name) {
if (element.name === name) {
return element;
}
var found = null;
element.forEach(function (el) {
if (el.name === name) {
found = el;
// Stop here.
return false;
}
}, CKEDITOR.NODE_ELEMENT);
return found;
}
})(CKEDITOR);

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

View file

@ -0,0 +1,232 @@
/**
* @file
* Drupal Link plugin.
*
* @ignore
*/
(function ($, Drupal, drupalSettings, CKEDITOR) {
"use strict";
CKEDITOR.plugins.add('drupallink', {
init: function (editor) {
// Add the commands for link and unlink.
editor.addCommand('drupallink', {
allowedContent: 'a[!href,target]',
requiredContent: 'a[href]',
modes: {wysiwyg: 1},
canUndo: true,
exec: function (editor) {
var linkElement = getSelectedLink(editor);
var linkDOMElement = null;
// Set existing values based on selected element.
var existingValues = {};
if (linkElement && linkElement.$) {
linkDOMElement = linkElement.$;
// Populate an array with the link's current attributes.
var attribute = null;
var attributeName;
for (var attrIndex = 0; attrIndex < linkDOMElement.attributes.length; attrIndex++) {
attribute = linkDOMElement.attributes.item(attrIndex);
attributeName = attribute.nodeName.toLowerCase();
// Don't consider data-cke-saved- attributes; they're just there to
// work around browser quirks.
if (attributeName.substring(0, 15) === 'data-cke-saved-') {
continue;
}
// Store the value for this attribute, unless there's a
// data-cke-saved- alternative for it, which will contain the quirk-
// free, original value.
existingValues[attributeName] = linkElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
}
}
// Prepare a save callback to be used upon saving the dialog.
var saveCallback = function (returnValues) {
editor.fire('saveSnapshot');
// Create a new link element if needed.
if (!linkElement && returnValues.attributes.href) {
var selection = editor.getSelection();
var range = selection.getRanges(1)[0];
// Use link URL as text with a collapsed cursor.
if (range.collapsed) {
// Shorten mailto URLs to just the email address.
var text = new CKEDITOR.dom.text(returnValues.attributes.href.replace(/^mailto:/, ''), editor.document);
range.insertNode(text);
range.selectNodeContents(text);
}
// Ignore a disabled target attribute.
if (returnValues.attributes.target === 0) {
delete returnValues.attributes.target;
}
// Create the new link by applying a style to the new text.
var style = new CKEDITOR.style({element: 'a', attributes: returnValues.attributes});
style.type = CKEDITOR.STYLE_INLINE;
style.applyToRange(range);
range.select();
// Set the link so individual properties may be set below.
linkElement = getSelectedLink(editor);
}
// Update the link properties.
else if (linkElement) {
for (var attrName in returnValues.attributes) {
if (returnValues.attributes.hasOwnProperty(attrName)) {
// Update the property if a value is specified.
if (returnValues.attributes[attrName].length > 0) {
var value = returnValues.attributes[attrName];
linkElement.data('cke-saved-' + attrName, value);
linkElement.setAttribute(attrName, value);
}
// Delete the property if set to an empty string.
else {
linkElement.removeAttribute(attrName);
}
}
}
}
// Save snapshot for undo support.
editor.fire('saveSnapshot');
};
// 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.
var dialogSettings = {
title: linkElement ? editor.config.drupalLink_dialogTitleEdit : editor.config.drupalLink_dialogTitleAdd,
dialogClass: 'editor-link-dialog'
};
// Open the dialog for the edit form.
Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/link/' + editor.config.drupal.format), existingValues, saveCallback, dialogSettings);
}
});
editor.addCommand('drupalunlink', {
contextSensitive: 1,
startDisabled: 1,
allowedContent: 'a[!href]',
requiredContent: 'a[href]',
exec: function (editor) {
var style = new CKEDITOR.style({element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1});
editor.removeStyle(style);
},
refresh: function (editor, path) {
var element = path.lastElement && path.lastElement.getAscendant('a', true);
if (element && element.getName() === 'a' && element.getAttribute('href') && element.getChildCount()) {
this.setState(CKEDITOR.TRISTATE_OFF);
}
else {
this.setState(CKEDITOR.TRISTATE_DISABLED);
}
}
});
// CTRL + K.
editor.setKeystroke(CKEDITOR.CTRL + 75, 'drupallink');
// Add buttons for link and unlink.
if (editor.ui.addButton) {
editor.ui.addButton('DrupalLink', {
label: Drupal.t('Link'),
command: 'drupallink',
icon: this.path + '/link.png'
});
editor.ui.addButton('DrupalUnlink', {
label: Drupal.t('Unlink'),
command: 'drupalunlink',
icon: this.path + '/unlink.png'
});
}
editor.on('doubleclick', function (evt) {
var element = getSelectedLink(editor) || evt.data.element;
if (!element.isReadOnly()) {
if (element.is('a')) {
editor.getSelection().selectElement(element);
editor.getCommand('drupallink').exec();
}
}
});
// If the "menu" plugin is loaded, register the menu items.
if (editor.addMenuItems) {
editor.addMenuItems({
link: {
label: Drupal.t('Edit Link'),
command: 'drupallink',
group: 'link',
order: 1
},
unlink: {
label: Drupal.t('Unlink'),
command: 'drupalunlink',
group: 'link',
order: 5
}
});
}
// If the "contextmenu" plugin is loaded, register the listeners.
if (editor.contextMenu) {
editor.contextMenu.addListener(function (element, selection) {
if (!element || element.isReadOnly()) {
return null;
}
var anchor = getSelectedLink(editor);
if (!anchor) {
return null;
}
var menu = {};
if (anchor.getAttribute('href') && anchor.getChildCount()) {
menu = {link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF};
}
return menu;
});
}
}
});
/**
* Get the surrounding link element of current selection.
*
* The following selection will all return the link element.
*
* @example
* <a href="#">li^nk</a>
* <a href="#">[link]</a>
* text[<a href="#">link]</a>
* <a href="#">li[nk</a>]
* [<b><a href="#">li]nk</a></b>]
* [<a href="#"><b>li]nk</b></a>
*
* @param {CKEDITOR.editor} editor
*
* @return {?bool}
*/
function getSelectedLink(editor) {
var selection = editor.getSelection();
var selectedElement = selection.getSelectedElement();
if (selectedElement && selectedElement.is('a')) {
return selectedElement;
}
var range = selection.getRanges(true)[0];
if (range) {
range.shrink(CKEDITOR.SHRINK_TEXT);
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
}
return null;
}
})(jQuery, Drupal, drupalSettings, CKEDITOR);

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B