Skip to content
Snippets Groups Projects
Commit 577a41d4 authored by Angie Byron's avatar Angie Byron
Browse files

Issue #2027181 by Wim Leers, seutje, Bojhan: Use a CKEditor Widget to create a...

Issue #2027181 by Wim Leers, seutje, Bojhan: Use a CKEditor Widget to create a stellar UX for captioning and aligning images.
parent 19df7986
No related branches found
No related tags found
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
Showing
with 1629 additions and 906 deletions
CKEditor 4 Changelog
====================
## CKEditor 4.3
## CKEditor 4.2.1
## CKEditor 4.2
**Important Notes:**
......
/**
/**
* This is a Drupal-optimized build of CKEditor.
*
* You may re-use it at any time at http://ckeditor.com/builder to build
......@@ -25,7 +25,7 @@ var CKBUILDER_CONFIG = {
'contents.css',
'styles.js',
'samples',
'skins/moono/readme.md',
'skins/moono/readme.md'
],
plugins : {
'about' : 1,
......@@ -65,14 +65,7 @@ var CKBUILDER_CONFIG = {
'showborders' : 1,
'tableresize' : 1,
'sharedspace' : 1,
'sourcedialog' : 1
// @todo D8: CKEditor Widgets is not available in 4.1 RC, and we're not yet
// using this, so it's commented out for now. However, it will be readded in
// the nearby future.
// 'widget' : 1,
// 'widgetblockquote' : 1,
// 'widgetcaption' : 1,
// 'widgettime' : 1,
// 'widgetvideo' : 1
'sourcedialog' : 1,
'widget' : 1
}
};
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -5,6 +5,8 @@
* Provides integration with the CKEditor WYSIWYG editor.
*/
use Drupal\editor\Entity\Editor;
/**
* Implements hook_help().
*/
......@@ -29,7 +31,7 @@ function ckeditor_help($path, $arg) {
}
}
/*
/**
* Implements hook_library_info().
*/
function ckeditor_library_info() {
......@@ -107,9 +109,19 @@ function ckeditor_library_info() {
array('system', 'drupalSettings'),
),
);
$libraries['drupal.ckeditor.drupalimagecaption-theme'] = array(
'title' => 'Theming support for the imagecaption plugin.',
'version' => Drupal::VERSION,
'js' => array(
$module_path . '/js/plugins/drupalimagecaption/theme.js' => array(),
),
'dependencies' => array(
array('ckeditor', 'ckeditor'),
),
);
$libraries['ckeditor'] = array(
'title' => 'Loads the main CKEditor library.',
'version' => '4.2',
'version' => '4.3-dev — d8-imagecaption branch commit 887d81ac1824008b690e439a1b29eb4f13b51212',
'js' => array(
'core/assets/vendor/ckeditor/ckeditor.js' => array(
'preprocess' => FALSE,
......@@ -132,6 +144,25 @@ function ckeditor_theme() {
);
}
/**
* Implements hook_ckeditor_css_alter().
*/
function ckeditor_ckeditor_css_alter(array &$css, Editor $editor) {
$filters = array();
if (!empty($editor->format)) {
$filters = entity_load('filter_format', $editor->format)
->filters()
->getAll();
}
// Add the filter caption CSS if the text format associated with this text
// editor uses the filter_caption filter. This is used by the included
// CKEditor DrupalImageCaption plugin.
if (isset($filters['filter_caption']) && $filters['filter_caption']->status) {
$css[] = drupal_get_path('module', 'filter') . '/css/filter.caption.css';
}
}
/**
* Retrieves the default theme's CKEditor stylesheets defined in the .info file.
*
......
......@@ -15,34 +15,14 @@ CKEDITOR.plugins.add('drupalimage', {
requiredContent: 'img[alt,src,width,height]',
modes: { wysiwyg : 1 },
canUndo: true,
exec: function (editor) {
var imageElement = getSelectedImage(editor);
exec: function (editor, override) {
var imageDOMElement = null;
var existingValues = {};
if (imageElement && imageElement.$) {
imageDOMElement = imageElement.$;
// Width and height are populated by actual dimensions.
existingValues.width = imageDOMElement ? imageDOMElement.width : '';
existingValues.height = imageDOMElement ? imageDOMElement.height : '';
// Populate all other attributes by their specified attribute values.
var attribute = null, attributeName;
for (var key = 0; key < imageDOMElement.attributes.length; key++) {
attribute = imageDOMElement.attributes.item(key);
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] = imageElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
}
}
var dialogTitle;
var saveCallback = function (returnValues) {
var selection = editor.getSelection();
var imageElement = selection.getSelectedElement();
function saveCallback (returnValues) {
editor.fire('saveSnapshot');
// Create a new image element if needed.
......@@ -75,13 +55,76 @@ CKEDITOR.plugins.add('drupalimage', {
// Save snapshot for undo support.
editor.fire('saveSnapshot');
};
// Allow CKEditor Widget plugins to execute DrupalImage's 'drupalimage'
// command. In this case, they need to provide the DOM element for the
// image (because this plugin wouldn't know where to find it), its
// existing values (because they're stored within the Widget in whatever
// way it sees fit) and a save callback (again because the Widget may
// store the returned values in whatever way it sees fit).
if (override) {
imageDOMElement = override.imageDOMElement;
existingValues = override.existingValues;
dialogTitle = override.dialogTitle;
if (override.saveCallback) {
saveCallback = override.saveCallback;
}
}
// Otherwise, retrieve the selected image and allow it to be edited, or
// if no image is selected: insert a new one.
else {
var selection = editor.getSelection();
var imageElement = selection.getSelectedElement();
// If the 'drupalimage' command is being applied to a CKEditor widget,
// then edit that Widget instead.
if (imageElement && imageElement.type === CKEDITOR.NODE_ELEMENT && imageElement.hasAttribute('data-widget-wrapper')) {
editor.widgets.focused.edit();
return;
}
// Otherwise, check if the 'drupalimage' command is being applied to
// an existing image tag, and then open a dialog to edit it.
else if (isImage(imageElement) && imageElement.$) {
imageDOMElement = imageElement.$;
// Width and height are populated by actual dimensions.
existingValues.width = imageDOMElement ? imageDOMElement.width : '';
existingValues.height = imageDOMElement ? imageDOMElement.height : '';
// Populate all other attributes by their specified attribute values.
var attribute = null, attributeName;
for (var key = 0; key < imageDOMElement.attributes.length; key++) {
attribute = imageDOMElement.attributes.item(key);
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] = imageElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
}
dialogTitle = editor.config.drupalImage_dialogTitleEdit;
}
// The 'drupalimage' command is being executed to add a new image.
else {
dialogTitle = editor.config.drupalImage_dialogTitleAdd;
// Allow other plugins to override the image insertion: they must
// listen to this event and cancel the event to do so.
if (!editor.fire('drupalimageinsert')) {
return;
}
}
}
// 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: imageDOMElement ? editor.config.drupalImage_dialogTitleEdit : editor.config.drupalImage_dialogTitleAdd,
title: dialogTitle,
dialogClass: 'editor-image-dialog'
};
......@@ -121,7 +164,7 @@ CKEDITOR.plugins.add('drupalimage', {
// If the "contextmenu" plugin is loaded, register the listeners.
if (editor.contextMenu) {
editor.contextMenu.addListener(function (element, selection) {
if (getSelectedImage(editor, element)) {
if (isImage(element)) {
return { image: CKEDITOR.TRISTATE_OFF };
}
});
......@@ -129,21 +172,8 @@ CKEDITOR.plugins.add('drupalimage', {
}
});
/**
* Finds an img tag anywhere in the current editor selection.
*/
function getSelectedImage (editor, element) {
if (!element) {
var sel = editor.getSelection();
var selectedText = sel.getSelectedText().replace(/^\s\s*/, '').replace(/\s\s*$/, '');
var isElement = sel.getType() === CKEDITOR.SELECTION_ELEMENT;
var isEmptySelection = sel.getType() === CKEDITOR.SELECTION_TEXT && selectedText.length === 0;
element = (isElement || isEmptySelection) && sel.getSelectedElement();
}
if (element && element.is('img') && !element.data('cke-realelement') && !element.isReadOnly()) {
return element;
}
function isImage (element) {
return element && element.is('img') && !element.data('cke-realelement') && !element.isReadOnly();
}
})(jQuery, Drupal, drupalSettings, CKEDITOR);
/**
* @file
* Drupal Image Caption plugin.
*
* Integrates the Drupal Image plugin with the caption_filter filter if enabled.
*/
(function (CKEDITOR) {
"use strict";
CKEDITOR.plugins.add('drupalimagecaption', {
requires: 'widget',
init: function (editor) {
/**
* Override drupalimage plugin's image insertion mechanism with our own, to
* ensure a widget is inserted, rather than a simple image (Widget's auto-
* discovery only runs upon init).
*/
editor.on('drupalimageinsert', function (event) {
editor.execCommand('widgetDrupalimagecaption');
event.cancel();
});
// Register the widget with a unique name "drupalimagecaption".
editor.widgets.add('drupalimagecaption', {
allowedContent: 'img[!src,alt,width,height,!data-caption,!data-align]',
template: '<img src="" />',
parts: {
image: 'img'
},
// Initialization method called for every widget instance being
// upcasted.
init: function () {
var image = this.parts.image;
// Save the initial widget data.
this.setData({
'data-editor-file-uuid': image.getAttribute('data-editor-file-uuid'),
src: image.getAttribute('src'),
width: image.getAttribute('width') || '',
height: image.getAttribute('height') || '',
alt: image.getAttribute('alt') || '',
data_caption: image.getAttribute('data-caption'),
data_align: image.getAttribute('data-align'),
hasCaption: image.hasAttribute('data-caption')
});
image.removeStyle('float');
},
// Called after initialization and on "data" changes.
data: function () {
if (this.data['data-editor-file-uuid'] !== null) {
this.parts.image.setAttribute('data-editor-file-uuid', this.data['data-editor-file-uuid']);
this.parts.image.setAttribute('data-cke-saved-data-editor-file-uuid', this.data['data-editor-file-uuid']);
}
this.parts.image.setAttribute('src', this.data.src);
this.parts.image.setAttribute('data-cke-saved-src', this.data.src);
this.parts.image.setAttribute('alt', this.data.alt);
this.parts.image.setAttribute('data-cke-saved-alt', this.data.alt);
this.parts.image.setAttribute('width', this.data.width);
this.parts.image.setAttribute('data-cke-saved-width', this.data.width);
this.parts.image.setAttribute('height', this.data.height);
this.parts.image.setAttribute('data-cke-saved-height', this.data.height);
if (this.data.hasCaption) {
this.parts.image.setAttribute('data-caption', this.data.data_caption);
this.parts.image.setAttribute('data-cke-saved-data-caption', this.data.data_caption);
}
else {
this.parts.image.removeAttributes(['data-caption', 'data-cke-saved-data-caption']);
}
if (this.data.data_align !== null) {
this.parts.image.setAttribute('data-align', this.data.data_align);
this.parts.image.setAttribute('data-cke-saved-data-align', this.data.data_align);
}
else {
this.parts.image.removeAttributes(['data-align', 'data-cke-saved-data-align']);
}
// Float the wrapper too.
if (this.data.data_align === null) {
this.wrapper.removeStyle('float');
this.wrapper.removeStyle('text-align');
}
else if (this.data.data_align === 'center') {
this.wrapper.setStyle('float', 'none');
this.wrapper.setStyle('text-align', 'center');
}
else {
this.wrapper.setStyle('float', this.data.data_align);
this.wrapper.removeStyle('text-align');
}
},
// Check the elements that need to be converted to widgets.
upcast: function (el) {
// Upcast all <img> elements that are alone inside a block element.
if (el.name === 'img') {
if (CKEDITOR.dtd.$block[el.parent.name] && el.parent.children.length === 1) {
return true;
}
}
},
// Convert the element back to its desired output representation.
downcast: function (el) {
if (this.data.hasCaption) {
el.attributes['data-caption'] = this.data.data_caption;
}
if (this.data.data_align) {
el.attributes['data-align'] = this.data.data_align;
}
},
_selectionWillCreateInlineImage: function () {
// Returns node or first of its ancestors
// which is a block or block limit.
function getBlockParent( node, root ) {
var path = new CKEDITOR.dom.elementPath( node, root );
return path.block || path.blockLimit;
}
var range = editor.getSelection().getRanges()[ 0 ],
startEl = getBlockParent( range.startContainer, range.root ),
endEl = getBlockParent( range.endContainer, range.root );
var insideStartEl = range.checkBoundaryOfElement( startEl, CKEDITOR.START );
var insideEndEl = range.checkBoundaryOfElement( endEl, CKEDITOR.END );
return !(insideStartEl && insideEndEl);
},
_insertSaveCallback: function (returnValues) {
// We can't create an image with an empty "src" attribute.
if (returnValues.attributes.src.length === 0) {
return;
}
editor.fire('saveSnapshot');
// Build the HTML for the widget.
var html = '<img ';
for (var attr in returnValues.attributes) {
if (returnValues.attributes.hasOwnProperty(attr) && !attr.match(/^data_/)) {
html += attr + '="' + returnValues.attributes[attr] + '" ';
html += 'data-cke-saved-' + attr + '="' + returnValues.attributes[attr] + '" ';
}
}
if (returnValues.hasCaption) {
html += 'data-caption="" ';
html += ' data-cke-saved-data-caption=""';
}
if (returnValues.attributes.data_align && returnValues.attributes.data_align !== 'none') {
html += 'data-align="' + returnValues.attributes.data_align + '" ';
html += ' data-cke-saved-data-align="' + returnValues.attributes.data_align + '"';
}
html += ' />';
var el = new CKEDITOR.dom.element.createFromHtml(html, editor.document);
editor.insertElement(editor.widgets.wrapElement(el, 'drupalimagecaption'));
// Save snapshot for undo support.
editor.fire('saveSnapshot');
// Initialize and focus the widget.
var widget = editor.widgets.initOn(el, 'drupalimagecaption');
widget.focus();
},
insert: function () {
var override = {
imageDOMElement: null,
existingValues: { hasCaption: false, data_align: '' },
saveCallback: this._insertSaveCallback,
dialogTitle: editor.config.drupalImage_dialogTitleAdd
};
if (this._selectionWillCreateInlineImage()) {
override.existingValues.isInline = this._selectionWillCreateInlineImage();
delete override.saveCallback;
}
editor.execCommand('drupalimage', override);
},
edit: function () {
var that = this;
var saveCallback = function (returnValues) {
editor.fire('saveSnapshot');
// Set the updated widget data.
that.setData({
'data-editor-file-uuid': returnValues.attributes['data-editor-file-uuid'],
src: returnValues.attributes.src,
width: returnValues.attributes.width,
height: returnValues.attributes.height,
alt: returnValues.attributes.alt,
hasCaption: !!returnValues.hasCaption,
data_caption: returnValues.hasCaption ? that.data.data_caption : '',
data_align: returnValues.attributes.data_align === 'none' ? null : returnValues.attributes.data_align
});
// Save snapshot for undo support.
editor.fire('saveSnapshot');
};
var override = {
imageDOMElement: this.parts.image.$,
existingValues: this.data,
saveCallback: saveCallback,
dialogTitle: this.data.src === '' ? editor.config.drupalImage_dialogTitleAdd : editor.config.drupalImage_dialogTitleEdit
};
editor.execCommand('drupalimage', override);
}
});
},
afterInit: function (editor) {
function setupAlignCommand (value) {
var command = editor.getCommand('justify' + value);
if (command) {
if (value in { right: 1, left: 1, center: 1 }) {
command.on('exec', function (event) {
var widget = getSelectedWidget(editor);
if (widget && widget.name === 'drupalimagecaption') {
widget.setData({ data_align: value });
event.cancel();
}
});
}
command.on('refresh', function (event) {
var widget = getSelectedWidget(editor),
allowed = { left: 1, center: 1, right: 1 },
align;
if (widget) {
align = widget.data.data_align;
this.setState(
(align === value) ? CKEDITOR.TRISTATE_ON : (value in allowed) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
event.cancel();
}
});
}
}
function getSelectedWidget (editor) {
var widget = editor.widgets.focused;
if (widget && widget.name === 'drupalimagecaption') {
return widget;
}
return null;
}
// Customize the behavior of the alignment commands.
setupAlignCommand('left');
setupAlignCommand('right');
setupAlignCommand('center');
}
});
})(CKEDITOR);
/**
* @file
* Drupal Image Caption plugin theme override.
*/
(function (CKEDITOR) {
"use strict";
CKEDITOR.on('instanceCreated', function (event) {
var editor = event.editor;
// Listen to widget definitions and customize them as needed. It's
// basically rewriting parts of the definition.
editor.on('widgetDefinition', function (event) {
var widgetDefinition = event.data;
// Customize the "drupalimagecaption" widget definition.
if (widgetDefinition.name === 'drupalimagecaption') {
widgetDefinition.template =
'<figure class="caption caption-img">' +
'<img src="" data-caption="" data-align="center" />' +
'<figcaption></figcaption>' +
'</figure>';
// Define the editables created by the overridden upcasting.
widgetDefinition.editables = {
caption: 'figcaption'
};
// Define the additional parts created by the overridden upcasting.
widgetDefinition.parts.caption = 'figcaption';
// Override "data" so we can make the new widget structure
// behave according to changes on data.
widgetDefinition.data = CKEDITOR.tools.override(widgetDefinition.data, function (originalDataFn) {
return function () {
// Call the original "data" implementation.
originalDataFn.apply(this, arguments);
// The image is wrapped in <figure>.
if (this.element.is('figure')) {
// The image is wrapped in <figure>, but it should no longer be.
if (!this.data.hasCaption && this.data.data_align === null) {
// Destroy this widget, so we can unwrap the <img>.
editor.widgets.destroy(this);
// Unwrap <img> from <figure>.
this.parts.image.replace(this.element);
// Reinitialize this widget with the current data.
editor.widgets.initOn(this.parts.image, 'drupalimagecaption', this.data);
}
// The image is wrapped in <figure>, as it should be; update it.
else {
// Set the caption visibility.
this.parts.caption.setStyle('display', this.data.hasCaption ? '' : 'none');
// Set the alignment, if any.
this.element.removeClass('caption-left');
this.element.removeClass('caption-center');
this.element.removeClass('caption-right');
if (this.data.data_align) {
this.element.addClass('caption-' + this.data.data_align);
}
}
}
// The image is not wrapped in <figure>.
else if (this.element.is('img')) {
// The image is not wrapped in <figure>, but it should be.
if (this.data.hasCaption || this.data.data_align !== null) {
// Destroy this widget, so we can wrap the <img>.
editor.widgets.destroy(this);
// Replace the widget's element (the <img>) with the template (a
// <figure> wrapping an <img>) and then replace the the template's
// default <img> by our <img> so we won't lose attributes. We must
// do this manually because upcast() won't run.
var figure = CKEDITOR.dom.element.createFromHtml(this.template.output(), editor.document);
figure.replace(this.element);
this.element.replace(figure.findOne('img'));
// Reinitialize this widget with the current data.
editor.widgets.initOn(figure, 'drupalimagecaption', this.data);
}
}
};
});
// Upcast to <figure> if data-caption or data-align is set.
widgetDefinition.upcast = CKEDITOR.tools.override(widgetDefinition.upcast, function (originalUpcastFn) {
return function (el) {
// Execute the original upcast first. If "true", this is an
// element to be upcasted.
if (originalUpcastFn.apply(this, arguments)) {
var figure;
var captionValue = el.attributes['data-caption'];
var alignValue = el.attributes['data-align'];
// Wrap image in <figure> only if data-caption or data-align is set.
if (captionValue !== undefined || alignValue !== undefined) {
var classes = 'caption caption-img';
if (alignValue !== null) {
classes += ' caption-' + alignValue;
}
figure = el.wrapWith(new CKEDITOR.htmlParser.element('figure', { 'class' : classes }));
var caption = CKEDITOR.htmlParser.fragment.fromHtml(captionValue || '', 'figcaption');
figure.add(caption);
}
return figure || el;
}
};
});
// Downcast to <img>.
widgetDefinition.downcast = CKEDITOR.tools.override(widgetDefinition.downcast, function (originalDowncastFn) {
return function (el) {
if (el.name === 'figure') {
// Update data with the current caption.
var caption = el.getFirst('figcaption');
caption = caption ? caption.getHtml() : '';
this.setData({
data_caption: caption
});
// We downcast to just the <img> element.
el = el.getFirst('img');
}
// Call the original downcast to setup the <img>
// meta data accordingly.
return originalDowncastFn.call(this, el) || el;
};
});
// Generate a <figure>-wrapped <img> if either data-caption or data-align
// are set for a newly created image.
widgetDefinition.insert = CKEDITOR.tools.override(widgetDefinition.downcast, function (originalInsertFn) {
return function () {
var saveCallback = function (returnValues) {
// We can't create an image with an empty "src" attribute.
if (returnValues.attributes.src.length === 0) {
return;
}
// Normalize the "data_align" attribute and the "hasCaption" value.
if (returnValues.attributes.data_align === '' || returnValues.attributes.data_align === 'none') {
returnValues.attributes.data_align = null;
}
if (typeof returnValues.hasCaption === 'number') {
returnValues.hasCaption = !!returnValues.hasCaption;
}
// Use the original save callback if the image has neither a caption
// nor alignment.
if (returnValues.hasCaption === false && returnValues.attributes.data_align === null) {
widgetDefinition._insertSaveCallback.apply(this, arguments);
return;
}
editor.fire('saveSnapshot');
// Build the HTML for the widget.
var html = '<figure class="caption caption-img';
if (returnValues.attributes.data_align && returnValues.attributes.data_align !== 'none') {
html += ' caption-' + returnValues.attributes.data_align;
}
html += '"><img ';
for (var attr in returnValues.attributes) {
if (returnValues.attributes.hasOwnProperty(attr) && !attr.match(/^data_/)) {
html += attr + '="' + returnValues.attributes[attr] + '" ';
html += 'data-cke-saved-' + attr + '="' + returnValues.attributes[attr] + '" ';
}
}
// The init() method will run on this and if it does not find
// data-caption or data-align attributes, the subsequent call to the
// data() method will cause the <figure> to be transformed back to
// an <img>. Hence, set the data-caption and data-align attributes
// on the newly inserted <img>.
if (returnValues.hasCaption) {
html += ' data-caption=""';
html += ' data-cke-saved-data-caption=""';
}
if (returnValues.attributes.data_align && returnValues.attributes.data_align !== 'none') {
html += ' data-align="' + returnValues.attributes.data_align + '"';
html += ' data-cke-saved-data-align="' + returnValues.attributes.data_align + '"';
}
html += '/>';
html += '<figcaption data-placeholder="' + Drupal.t('Enter caption here') + '"></figcaption>';
html += '</figure>';
var el = new CKEDITOR.dom.element.createFromHtml(html, editor.document);
editor.insertElement(editor.widgets.wrapElement(el, 'drupalimagecaption'));
// Save snapshot for undo support.
editor.fire('saveSnapshot');
// Initialize and focus the widget.
var widget = editor.widgets.initOn(el, 'drupalimagecaption');
widget.focus();
};
var override = {
imageDOMElement: null,
existingValues: { hasCaption: false, data_align: '' },
saveCallback: saveCallback,
dialogTitle: editor.config.drupalImage_dialogTitleAdd
};
if (this._selectionWillCreateInlineImage()) {
override.existingValues.isInline = this._selectionWillCreateInlineImage();
delete override.saveCallback;
}
editor.execCommand('drupalimage', override);
};
});
}
});
});
})(CKEDITOR);
<?php
/**
* @file
* Contains \Drupal\ckeditor\Plugin\CKEditorPlugin\DrupalImageWidget.
*/
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\editor\Entity\Editor;
use Drupal\Core\Annotation\Translation;
use Drupal\ckeditor\CKEditorPluginInterface;
use Drupal\ckeditor\CKEditorPluginContextualInterface;
use Drupal\ckeditor\Annotation\CKEditorPlugin;
/**
* Defines the "drupalimagecaption" plugin.
*
* @CKEditorPlugin(
* id = "drupalimagecaption",
* label = @Translation("Drupal image caption widget"),
* module = "ckeditor"
* )
*/
class DrupalImageCaption extends PluginBase implements CKEditorPluginInterface, CKEditorPluginContextualInterface {
/**
* {@inheritdoc}
*/
public function isInternal() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getDependencies(Editor $editor) {
return array();
}
/**
* {@inheritdoc}
*/
public function getLibraries(Editor $editor) {
return array(
array('ckeditor', 'drupal.ckeditor.drupalimagecaption-theme'),
);
}
/**
* {@inheritdoc}
*/
public function getFile() {
return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimagecaption/plugin.js';
}
/**
* {@inheritdoc}
*/
public function getConfig(Editor $editor) {
return array();
}
/**
* {@inheritdoc}
*/
function isEnabled(Editor $editor) {
$filters = array();
if (!empty($editor->format)) {
$filters = entity_load('filter_format', $editor->format)
->filters()
->getAll();
}
// Automatically enable this plugin if the text format associated with this
// text editor uses the filter_caption filter and the DrupalImage button is
// enabled.
if (isset($filters['filter_caption']) && $filters['filter_caption']->status) {
foreach ($editor->settings['toolbar']['buttons'] as $row) {
if (in_array('DrupalImage', $row)) {
return TRUE;
}
}
}
return FALSE;
}
}
......@@ -383,8 +383,8 @@ public function buildContentsCssJSSetting(EditorEntity $editor) {
drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css',
drupal_get_path('module', 'system') . '/css/system.module.css',
);
$css = array_merge($css, _ckeditor_theme_css());
drupal_alter('ckeditor_css', $css, $editor);
$css = array_merge($css, _ckeditor_theme_css());
$css = array_map('file_create_url', $css);
return array_values($css);
......
......@@ -69,7 +69,7 @@ function testEnabledPlugins() {
// Case 1: no CKEditor plugins.
$definitions = array_keys($this->manager->getDefinitions());
sort($definitions);
$this->assertIdentical(array('drupalimage', 'drupallink', 'internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.');
$this->assertIdentical(array('drupalimage', 'drupalimagecaption', 'drupallink', 'internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.');
$enabled_plugins = array(
'drupalimage' => 'core/modules/ckeditor/js/plugins/drupalimage/plugin.js',
'drupallink' => 'core/modules/ckeditor/js/plugins/drupallink/plugin.js',
......@@ -86,7 +86,7 @@ function testEnabledPlugins() {
// Case 2: CKEditor plugins are available.
$plugin_ids = array_keys($this->manager->getDefinitions());
sort($plugin_ids);
$this->assertIdentical(array('drupalimage', 'drupallink', 'internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.');
$this->assertIdentical(array('drupalimage', 'drupalimagecaption', 'drupallink', 'internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.');
$this->assertIdentical($enabled_plugins, $this->manager->getEnabledPluginFiles($editor), 'Only the internal plugins are enabled.');
$this->assertIdentical(array('internal' => NULL) + $enabled_plugins, $this->manager->getEnabledPluginFiles($editor, TRUE), 'Only the "internal" plugin is enabled.');
......
......@@ -7,19 +7,17 @@
namespace Drupal\editor\Form;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormBase;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\editor\Ajax\EditorDialogSave;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\file\FileInterface;
/**
* Provides an image dialog for text editors.
*/
class EditorImageDialog implements FormInterface {
class EditorImageDialog extends FormBase {
/**
* {@inheritdoc}
......@@ -37,7 +35,10 @@ public function getFormID() {
public function buildForm(array $form, array &$form_state, FilterFormat $filter_format = NULL) {
// The default values are set directly from $_POST, provided by the
// editor plugin opening the dialog.
$input = isset($form_state['input']['editor_object']) ? $form_state['input']['editor_object'] : array();
if (!isset($form_state['image_element'])) {
$form_state['image_element'] = isset($form_state['input']['editor_object']) ? $form_state['input']['editor_object'] : array();
}
$image_element = $form_state['image_element'];
$form['#tree'] = TRUE;
$form['#attached']['library'][] = array('editor', 'drupal.editor.dialog');
......@@ -55,11 +56,11 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
}
$max_filesize = min(parse_size($editor->image_upload['max_size']), file_upload_max_size());
$existing_file = isset($input['data-editor-file-uuid']) ? entity_load_by_uuid('file', $input['data-editor-file-uuid']) : NULL;
$existing_file = isset($image_element['data-editor-file-uuid']) ? entity_load_by_uuid('file', $image_element['data-editor-file-uuid']) : NULL;
$fid = $existing_file ? $existing_file->id() : NULL;
$form['fid'] = array(
'#title' => t('Image'),
'#title' => $this->t('Image'),
'#type' => 'managed_file',
'#upload_location' => $editor->image_upload['scheme'] . '://' .$editor->image_upload['directory'],
'#default_value' => $fid ? array($fid) : NULL,
......@@ -72,9 +73,9 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
);
$form['attributes']['src'] = array(
'#title' => t('URL'),
'#title' => $this->t('URL'),
'#type' => 'textfield',
'#default_value' => isset($input['src']) ? $input['src'] : '',
'#default_value' => isset($image_element['src']) ? $image_element['src'] : '',
'#maxlength' => 2048,
'#required' => TRUE,
);
......@@ -83,28 +84,30 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
// otherwise show a (file URL) text form item.
if ($editor->image_upload['status'] === '1') {
$form['attributes']['src']['#access'] = FALSE;
$form['attributes']['src']['#required'] = FALSE;
}
else {
$form['fid']['#access'] = FALSE;
$form['fid']['#required'] = FALSE;
}
$form['attributes']['alt'] = array(
'#title' => t('Alternative text'),
'#title' => $this->t('Alternative text'),
'#type' => 'textfield',
'#default_value' => isset($input['alt']) ? $input['alt'] : '',
'#default_value' => isset($image_element['alt']) ? $image_element['alt'] : '',
'#maxlength' => 2048,
);
$form['dimensions'] = array(
'#type' => 'item',
'#title' => t('Image size'),
'#title' => $this->t('Image size'),
'#field_prefix' => '<div class="container-inline">',
'#field_suffix' => '</div>',
);
$form['dimensions']['width'] = array(
'#title' => t('Width'),
'#title' => $this->t('Width'),
'#title_display' => 'invisible',
'#type' => 'number',
'#default_value' => isset($input['width']) ? $input['width'] : '',
'#default_value' => isset($image_element['width']) ? $image_element['width'] : '',
'#size' => 8,
'#maxlength' => 8,
'#min' => 1,
......@@ -114,10 +117,10 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
'#parents' => array('attributes', 'width'),
);
$form['dimensions']['height'] = array(
'#title' => t('Height'),
'#title' => $this->t('Height'),
'#title_display' => 'invisible',
'#type' => 'number',
'#default_value' => isset($input['height']) ? $input['height'] : '',
'#default_value' => isset($image_element['height']) ? $image_element['height'] : '',
'#size' => 8,
'#maxlength' => 8,
'#min' => 1,
......@@ -127,12 +130,54 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
'#parents' => array('attributes', 'height'),
);
// When Drupal core's filter_caption is being used, the text editor may
// offer the ability to change the alignment.
if (isset($image_element['data_align'])) {
$form['align'] = array(
'#title' => $this->t('Align'),
'#type' => 'radios',
'#options' => array(
'none' => $this->t('None'),
'left' => $this->t('Left'),
'center' => $this->t('Center'),
'right' => $this->t('Right'),
),
'#default_value' => $image_element['data_align'] === '' ? 'none' : $image_element['data_align'],
'#wrapper_attributes' => array('class' => array('container-inline')),
'#attributes' => array('class' => array('container-inline')),
'#parents' => array('attributes', 'data_align'),
);
}
// When Drupal core's filter_caption is being used, the text editor may
// offer the ability to in-place edit the image's caption: show a toggle.
if (isset($image_element['hasCaption'])) {
$form['caption'] = array(
'#title' => $this->t('Caption'),
'#type' => 'checkbox',
'#default_value' => $image_element['hasCaption'] === 'true',
'#parents' => array('hasCaption'),
);
}
$has_align_or_caption = isset($image_element['data_align']) || isset($image_element['hasCaption']);
if ($has_align_or_caption && isset($image_element['isInline']) && $image_element['isInline'] === 'true') {
$form['align']['#type'] = 'item';
$form['align']['#description'] = t('Inline images cannot be aligned.');
unset($form['align']['#default_value']);
$form['caption']['#type'] = 'item';
$form['caption']['#description'] = $this->t('Inline images cannot be captioned.');
$form['caption']['#wrapper_attributes'] = array('class' => array('container-inline'));
$form['caption']['#attributes'] = array('class' => array('container-inline'));
}
$form['actions'] = array(
'#type' => 'actions',
);
$form['actions']['save_modal'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#value' => $this->t('Save'),
// No regular submit-handler. This form only works via JavaScript.
'#submit' => array(),
'#ajax' => array(
......@@ -144,12 +189,6 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, array &$form_state) {
}
/**
* {@inheritdoc}
*/
......
......@@ -8,6 +8,7 @@
*/
.caption {
display: table;
margin: 0;
}
.caption > * {
display: block;
......@@ -19,6 +20,16 @@
max-width: none;
}
/**
* While editing and whenever the caption is empty, show a placeholder.
*
* Based on http://codepen.io/flesler/pen/AEIFc.
*/
.caption > figcaption[contenteditable=true]:empty:before {
content: attr(data-placeholder);
font-style: italic;
}
/**
* Caption alignment.
*/
......
......@@ -110,6 +110,12 @@ abbr.form-required, abbr.tabledrag-changed, abbr.ajax-changed {
.container-inline label:after {
content: ':';
}
.form-type-radios .container-inline label:after {
content: none;
}
.form-type-radios .container-inline .form-type-radio {
margin: 0 1em;
}
.container-inline .form-actions,
.container-inline.form-actions {
margin-top: 0;
......
......@@ -11,6 +11,8 @@ stylesheets:
- css/colors.css
print:
- css/print.css
ckeditor_stylesheets:
- css/ckeditor-iframe.css
regions:
header: Header
help: Help
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment