core/modules/ckeditor/ckeditor.module | 33 ++- .../ckeditor/js/plugins/drupalimage/plugin.js | 137 ++++++++----- .../js/plugins/drupalimagecaption/plugin.js | 212 ++++++++++++++++++++ .../js/plugins/drupalimagecaption/theme.js | 207 +++++++++++++++++++ .../ckeditor/js/plugins/drupallink/plugin.js | 8 +- .../Plugin/CKEditorPlugin/DrupalImageCaption.php | 90 +++++++++ .../lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php | 2 +- .../ckeditor/Tests/CKEditorPluginManagerTest.php | 4 +- .../lib/Drupal/editor/Form/EditorImageDialog.php | 30 +++ core/modules/system/css/system.theme.css | 6 + core/themes/bartik/bartik.info.yml | 2 + core/themes/bartik/css/ckeditor-iframe.css | 35 ++++ 12 files changed, 703 insertions(+), 63 deletions(-) diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module index 4ca9956..200fd4a 100644 --- a/core/modules/ckeditor/ckeditor.module +++ b/core/modules/ckeditor/ckeditor.module @@ -5,6 +5,8 @@ * Provides integration with the CKEditor WYSIWYG editor. */ +use Drupal\editor\Plugin\Core\Entity\Editor; + /** * Implements hook_library_info(). */ @@ -71,9 +73,19 @@ function ckeditor_library_info() { array('system', 'drupal.debounce'), ), ); + $libraries['drupal.ckeditor.drupalimagecaption-theme'] = array( + 'title' => 'Theming support for the imagecaption plugin.', + 'version' => 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.1', + 'version' => '4.2-dev — d8-imagecaption branch commit fff0881eb3fe228c7877e29da7f48ea93797f5fa', 'js' => array( 'core/assets/vendor/ckeditor/ckeditor.js' => array( 'preprocess' => FALSE, @@ -97,6 +109,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 = filter_format_load($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. * * Themes may specify iframe-specific CSS files for use with CKEditor by diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js index 3dd92c4..6e5e98c 100644 --- a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js +++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js @@ -15,60 +15,100 @@ 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; - for (var key = 0; key < imageDOMElement.attributes.length; key++) { - attribute = imageDOMElement.attributes.item(key); - existingValues[attribute.nodeName.toLowerCase()] = attribute.nodeValue; - } - } + var saveCallback = null; + var dialogTitle; - function saveCallback (returnValues) { - // 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; + saveCallback = override.saveCallback; + dialogTitle = override.dialogTitle; + } + // 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(); - // Create a new image element if needed. - if (!imageElement && returnValues.attributes.src) { - imageElement = editor.document.createElement('img'); - imageElement.setAttribute('alt', ''); - editor.insertElement(imageElement); + // 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; } - // Delete the image if the src was removed. - if (imageElement && !returnValues.attributes.src) { - imageElement.remove(); + // 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; + for (var key = 0; key < imageDOMElement.attributes.length; key++) { + attribute = imageDOMElement.attributes.item(key); + existingValues[attribute.nodeName.toLowerCase()] = attribute.nodeValue; + } + + dialogTitle = editor.config.drupalImage_dialogTitleEdit; } - // Update the image properties. + // The 'drupalimage' command is being executed to add a new image. else { - for (var key in returnValues.attributes) { - if (returnValues.attributes.hasOwnProperty(key)) { - // Update the property if a value is specified. - if (returnValues.attributes[key].length > 0) { - imageElement.setAttribute(key, returnValues.attributes[key]); - } - // Delete the property if set to an empty string. - else { - imageElement.removeAttribute(key); + 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; + } + } + + saveCallback = function (returnValues) { + // Save snapshot for undo support. + editor.fire('saveSnapshot'); + + // Create a new image element if needed. + if (!imageElement && returnValues.attributes.src) { + imageElement = editor.document.createElement('img'); + imageElement.setAttribute('alt', ''); + editor.insertElement(imageElement); + } + // Delete the image if the src was removed. + if (imageElement && !returnValues.attributes.src) { + imageElement.remove(); + } + // Update the image properties. + else { + for (var key in returnValues.attributes) { + if (returnValues.attributes.hasOwnProperty(key)) { + // Update the property if a value is specified. + if (returnValues.attributes[key].length > 0) { + imageElement.setAttribute(key, returnValues.attributes[key]); + } + // Delete the property if set to an empty string. + else { + imageElement.removeAttribute(key); + } } } } - } + }; } // 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' }; @@ -98,7 +138,7 @@ CKEDITOR.plugins.add('drupalimage', { if (editor.addMenuItems) { editor.addMenuItems({ image: { - label: editor.lang.image.menu, + label: Drupal.t('Image'), command : 'drupalimage', group: 'image' } @@ -108,7 +148,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 }; } }); @@ -116,21 +156,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); diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js new file mode 100644 index 0000000..b5fae0b --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js @@ -0,0 +1,212 @@ +/** + * @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: '', + 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({ + 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') + }); + + // Remove the original element attributes. + image.removeAttributes(['data-caption', 'data-align']); + image.removeStyle('float'); + }, + + // Called after initialization and on "data" changes. + data: function () { + this.parts.image.setAttribute('src', this.data.src); + this.parts.image.setAttribute('alt', this.data.alt); + this.parts.image.setAttribute('width', this.data.width); + this.parts.image.setAttribute('height', this.data.height); + + // 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 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; + } + }, + + _insertSaveCallback: function (returnValues) { + // We can't create an image with an empty "src" attribute. + if (returnValues.attributes.src.length === 0) { + return; + } + + // Build the HTML for the widget. + var html = '' + + '' + + '
' + + ''; + + // 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
. + if (this.element.is('figure')) { + // The image is wrapped in
, but it should no longer be. + if (!this.data.hasCaption && this.data.data_align === null) { + // Destroy this widget, so we can unwrap the . + editor.widgets.destroy(this); + // Unwrap from
. + 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
, 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
. + else if (this.element.is('img')) { + // The image is not wrapped in
, but it should be. + if (this.data.hasCaption || this.data.data_align !== null) { + // Destroy this widget, so we can wrap the . + editor.widgets.destroy(this); + // Replace the widget's element (the ) with the template (a + //
wrapping an ) and then replace the the template's + // default by our 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); + } + else { + // Nothing remains to be done, this is already taken care of in + // originalDataFn(). + } + } + }; + }); + + // Upcast to
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
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 . + 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.data.data_caption = caption; + + // We downcast to just the element. + el = el.getFirst('img'); + } + + // Call the original downcast to setup the + // meta data accordingly. + return originalDowncastFn.call(this, el) || el; + }; + }); + + // Generate a
-wrapped 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; + } + + // Build the HTML for the widget. + var html = '
to be transformed back to + // an . Hence, set the data-caption and data-align attributes + // on the newly inserted . + if (returnValues.hasCaption) { + html += ' data-caption=""'; + } + if (returnValues.attributes.data_align && returnValues.attributes.data_align !== 'none') { + html += ' data-align="' + returnValues.attributes.data_align + '"'; + } + html += '/>'; + html += '
'; + 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(); + }; + var override = { + imageDOMElement: null, + existingValues: { hasCaption: false, data_align: '' }, + saveCallback: saveCallback, + dialogTitle: editor.config.drupalImage_dialogTitleAdd + }; + editor.execCommand('drupalimage', override); + }; + }); + } + }); +}); + +})(CKEDITOR); diff --git a/core/modules/ckeditor/js/plugins/drupallink/plugin.js b/core/modules/ckeditor/js/plugins/drupallink/plugin.js index 510f82a..496e2e6 100644 --- a/core/modules/ckeditor/js/plugins/drupallink/plugin.js +++ b/core/modules/ckeditor/js/plugins/drupallink/plugin.js @@ -112,12 +112,12 @@ CKEDITOR.plugins.add('drupallink', { // Add buttons for link and unlink. if (editor.ui.addButton) { editor.ui.addButton('DrupalLink', { - label: editor.lang.link.toolbar, + label: Drupal.t('Link'), command: 'drupallink', icon: this.path.replace(/plugin\.js.*/, 'link.png') }); editor.ui.addButton('DrupalUnlink', { - label: editor.lang.link.unlink, + label: Drupal.t('Unlink'), command: 'drupalunlink', icon: this.path.replace(/plugin\.js.*/, 'unlink.png') }); @@ -138,14 +138,14 @@ CKEDITOR.plugins.add('drupallink', { if (editor.addMenuItems) { editor.addMenuItems({ link: { - label: editor.lang.link.menu, + label: Drupal.t('Link'), command: 'drupallink', group: 'link', order: 1 }, unlink: { - label: editor.lang.link.unlink, + label: Drupal.t('Unlink'), command: 'drupalunlink', group: 'link', order: 5 diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php new file mode 100644 index 0000000..782a522 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php @@ -0,0 +1,90 @@ +format)) { + $filters = filter_format_load($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; + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php index 5b79bf2..ffc8627 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php @@ -296,8 +296,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); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php index 3d31c04..af9974b 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php @@ -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->getEnabledPlugins($editor), 'Only the internal plugins are enabled.'); $this->assertIdentical(array('internal' => NULL) + $enabled_plugins, $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.'); diff --git a/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php index 0fa6249..da1b9db 100644 --- a/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php +++ b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php @@ -91,6 +91,36 @@ 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 in-place edit the image's caption: show a toggle. + if (isset($input['hasCaption'])) { + $form['caption'] = array( + '#title' => t('Caption'), + '#type' => 'checkbox', + '#default_value' => $input['hasCaption'] === 'true', + '#parents' => array('hasCaption'), + ); + } + + // When Drupal core's filter_caption is being used, the text editor may + // offer the ability to change the alignment. + if (isset($input['data_align'])) { + $form['align'] = array( + '#title' => t('Align'), + '#type' => 'radios', + '#options' => array( + 'none' => t('None'), + 'left' => t('Left'), + 'center' => t('Center'), + 'right' => t('Right'), + ), + '#default_value' => $input['data_align'] === '' ? 'none' : $input['data_align'], + '#wrapper_attributes' => array('class' => array('container-inline')), + '#attributes' => array('class' => array('container-inline')), + '#parents' => array('attributes', 'data_align'), + ); + } + $form['actions'] = array( '#type' => 'actions', ); diff --git a/core/modules/system/css/system.theme.css b/core/modules/system/css/system.theme.css index 4fb073f..98ead61 100644 --- a/core/modules/system/css/system.theme.css +++ b/core/modules/system/css/system.theme.css @@ -163,6 +163,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; diff --git a/core/themes/bartik/bartik.info.yml b/core/themes/bartik/bartik.info.yml index 43e3e9c..6851a12 100644 --- a/core/themes/bartik/bartik.info.yml +++ b/core/themes/bartik/bartik.info.yml @@ -11,6 +11,8 @@ stylesheets: - css/colors.css print: - css/print.css +ckeditor_stylesheets: + - css/ckeditor-iframe.css regions: header: Header help: Help diff --git a/core/themes/bartik/css/ckeditor-iframe.css b/core/themes/bartik/css/ckeditor-iframe.css new file mode 100644 index 0000000..f6c6495 --- /dev/null +++ b/core/themes/bartik/css/ckeditor-iframe.css @@ -0,0 +1,35 @@ +/** + * @todo Figure out if we want to duplicate what's in Bartik's style.css here, + * or if Bartik's css should @import this file. Probably the latter. + */ + +/* -------------- Captions -------------- */ + +.caption > * { + background: #F3F3F3; + padding: 0.5ex; + border: 1px solid #CCC; +} + +.caption > figcaption { + border: 1px solid #CCC; + border-top: none; + padding-top: 0.5ex; + font-size: small; + text-align: center; +} + +/* Override Bartik's default blockquote and pre styles when captioned. */ +.caption-pre > pre, +.caption-blockquote > blockquote { + margin: 0; +} +.caption-blockquote > figcaption::before { + content: "— "; +} +.caption-blockquote > figcaption { + text-align: left; +} +[dir="rtl"] .caption-blockquote > figcaption { + text-align: right; +}