diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js index b8318be..928f149 100644 --- a/core/modules/ckeditor/js/ckeditor.js +++ b/core/modules/ckeditor/js/ckeditor.js @@ -5,7 +5,7 @@ Drupal.editors.ckeditor = { attach: function (element, format) { - var externalPlugins = format.editorSettings.externalPlugins; + var externalPlugins = format.editorSettings.drupalExternalPlugins; // Register and load additional CKEditor plugins as necessary. if (externalPlugins) { for (var pluginName in externalPlugins) { diff --git a/core/modules/ckeditor/js/plugins/drupaldialog/plugin.js b/core/modules/ckeditor/js/plugins/drupaldialog/plugin.js new file mode 100644 index 0000000..b02ef8c --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupaldialog/plugin.js @@ -0,0 +1,26 @@ +/** + * @file Drupal Dialog plugin. + * + * This file is a placeholder until we can implements true Drupal-side dialog + * handling, pending the patch at + * + * Note that this file follows coding standards for the CKEditor project rather + * than traditional Drupal coding standards for JavaScript. This keeps + * compatibility high when adapting code between the built-in plugins and the + * custom plugins that Drupal provides. + * + * @see http://dev.ckeditor.com/wiki/CodingStyle + */ + +(function() +{ + +CKEDITOR.plugins.add( 'drupaldialog', +{ + init : function( editor ) + { + var pluginName = 'drupaldialog'; + } +}); + +})(); diff --git a/core/modules/ckeditor/js/plugins/drupalimage/image.png b/core/modules/ckeditor/js/plugins/drupalimage/image.png new file mode 100644 index 0000000..3e07ac8 --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupalimage/image.png Binary files differ diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js new file mode 100644 index 0000000..10e1557 --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js @@ -0,0 +1,319 @@ +/** + * @file Drupal Image plugin. + * + * Note that this file follows coding standards for the CKEditor project rather + * than traditional Drupal coding standards for JavaScript. This keeps + * compatibility high when adapting code between the built-in plugins and the + * custom plugins that Drupal provides. + * + * @see http://dev.ckeditor.com/wiki/CodingStyle + */ + +(function() +{ + +CKEDITOR.plugins.add( 'drupalimage', +{ + // @todo: Remove dependency on normal image dialog, build Drupal-based dialog. + requires: [ 'dialog' ], + + init : function( editor ) + { + var pluginName = 'drupalimage'; + + // Register the normal image dialog. + CKEDITOR.dialog.add( 'image', Drupal.settings.basePath + Drupal.settings.ckeditor.modulePath + '/lib/ckeditor/plugins/image/dialogs/image.js' ); + + // Register the toolbar button. + editor.ui.addButton( 'DrupalImage', + { + label : editor.lang.common.image, + command : 'image', + icon : Drupal.settings.basePath + Drupal.settings.ckeditor.modulePath + '/js/plugins/drupalimage/image.png', + }); + + // Register the image command. + editor.addCommand( 'image', new CKEDITOR.dialogCommand( 'image' ) ); + + // Double clicking an image opens its properties. + editor.on( 'doubleclick', function( evt ) + { + var element = evt.data.element; + + if ( element.is( 'img' ) && !element.data( 'cke-realelement' ) && !element.isReadOnly() ) + evt.data.dialog = 'image'; + }); + + // If the "menu" plugin is loaded, register the menu items. + if ( editor.addMenuItems ) + { + editor.addMenuItems( + { + image : + { + label : editor.lang.image.menu, + command : 'image', + group : 'image' + } + }); + } + + // If the "contextmenu" plugin is loaded, register the listeners. + if ( editor.contextMenu ) + { + editor.contextMenu.addListener( function( element, selection ) + { + if ( getSelectedImage( editor, element ) ) + return { image : CKEDITOR.TRISTATE_OFF }; + }); + } + + // Customize the built-in dialog for images. + CKEDITOR.on( 'dialogDefinition', function( evt ) { + if ( evt.data.name == 'image' ) + { + var infoPane = evt.data.definition.getContents('info'); + + // Remove unneeded options that write style attributes. + infoPane.remove('txtHSpace'); + infoPane.remove('txtVSpace'); + infoPane.remove('txtBorder'); + + // Add option for center alignment if supported. + var alignOption = infoPane.get('cmbAlign'); + if ( CKEDITOR.config.drupalimage_justifyClasses ) + alignOption.items.push([ editor.lang.common.alignCenter, 'center' ]); + + // Adjust the alignment options to use our alignment classes. + alignOption.setup = function ( type, element ) { + // Set alignment value based on active alignment state. + var value = ''; + var possibleButtons = [ 'left', 'center', 'right', 'block' ]; + for (var n = 0; n < 4; n++) { + var currentCommand = editor.getCommand('justify' + possibleButtons[n]); + if ( currentCommand && currentCommand.state === CKEDITOR.TRISTATE_ON ) + value = possibleButtons[n]; + } + if ( value ) + this.setValue( value ); + this.setupValue = value; + } + alignOption.commit = function( type, element, internalCommit ) { + // Constants from image plugin dialog.js. + var IMAGE = 1, LINK = 2, PREVIEW = 4, CLEANUP = 8; + var value = this.getValue(); + switch ( type ) + { + case IMAGE: + if ( value !== this.setupValue ) + { + if ( value ) + { + var alignCommand = editor.getCommand('justify' + value ); + if ( alignCommand.state === CKEDITOR.TRISTATE_OFF ) + alignCommand.exec(); + } + else if ( this.setupValue ) + { + var alignCommand = editor.getCommand('justify' + this.setupValue ); + if ( alignCommand.state === CKEDITOR.TRISTATE_ON ) + alignCommand.exec(); + } + } + break; + case PREVIEW: + setImageAlignment( element, value ); + break; + case CLEANUP: + setImageAlignment( element, false ); + break; + } + } + } + }); + + }, + afterInit : function( editor ) + { + // Use normal height/width attributes for images instead of styles (#5547). + // We may be able to remove this code once Drupal provides its own modal + // dialogs for manipulating images, but resizing an image via resize handles + // may make it necessary still. + var dataProcessor = editor.dataProcessor; + var htmlFilter = dataProcessor && dataProcessor.htmlFilter; + htmlFilter.addRules( + { + elements : + { + img : function( element ) + { + var style = element.attributes.style, match, width, height; + if ( style ) + { + // Get the width from the style. + match = /(?:^|\s)width\s*:\s*(\d+)px/i.exec( style ), + width = match && match[1]; + // Get the height from the style. + match = /(?:^|\s)height\s*:\s*(\d+)px/i.exec( style ); + height = match && match[1]; + if ( width.length ) + { + style = style.replace( /(?:^|\s)width\s*:\s*(\d+)px;?/i , '' ); + element.attributes.width = width; + } + if ( height.length ) + { + style = style.replace( /(?:^|\s)height\s*:\s*(\d+)px;?/i , '' ); + element.attributes.height = height; + } + if (style) + element.attributes.style = style; + else + delete element.attributes.style; + } + } + } + } + ); + + // Customize the behavior of the alignment commands. (#7430) + setupAlignCommand( 'left' ); + setupAlignCommand( 'right' ); + setupAlignCommand( 'center' ); + setupAlignCommand( 'block' ); + + function setupAlignCommand( value ) + { + var command = editor.getCommand( 'justify' + value ); + if ( command ) + { + command.on( 'exec', function( evt ) + { + var img = getSelectedImage( editor ), align; + if ( img ) + { + // Remove the state of the previous alignment button. + previousAlignment = getImageAlignment( img ); + if (previousAlignment) + { + var previousCommand = editor.getCommand( 'justify' + previousAlignment ); + if ( previousCommand.state === CKEDITOR.TRISTATE_ON) + previousCommand.setState(CKEDITOR.TRISTATE_OFF); + } + // Set alignment and activate the current alignment button. + if ( setImageAlignment( img, value ) ) { + command.setState(CKEDITOR.TRISTATE_ON); + } + evt.cancel(); + } + }); + + command.on( 'refresh', function( evt ) + { + var img = getSelectedImage( editor ), align; + if ( img ) + { + align = getImageAlignment( img ); + + this.setState( + ( align == value ) ? CKEDITOR.TRISTATE_ON : + ( CKEDITOR.config.drupalimage_justifyClasses || value == 'right' || value == 'left' ) ? CKEDITOR.TRISTATE_OFF : + CKEDITOR.TRISTATE_DISABLED ); + + evt.cancel(); + } + }); + } + } + } +}); + +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 getImageAlignment( element ) +{ + // Get alignment based on precedence of display used in browsers: + // 1) inline style, 2) class style, then 3) align attribute. + var align = element.getStyle( 'float' ); + + if ( align == 'inherit' || align == 'none' ) + align = 0; + + if ( !align && CKEDITOR.config.drupalimage_justifyClasses ) { + var justifyClasses = CKEDITOR.config.drupalimage_justifyClasses; + var justifyNames = [ 'left', 'center', 'right', 'block' ]; + for (var classPosition = 0; classPosition < 4; classPosition++) { + if (element.hasClass(justifyClasses[classPosition])) + align = justifyNames[classPosition]; + } + } + + if ( !align ) + align = element.getAttribute( 'align' ); + + return align; +} + +function setImageAlignment( img, value ) +{ + align = getImageAlignment( img ); + if ( CKEDITOR.config.drupalimage_justifyClasses ) + { + var justifyClasses = CKEDITOR.config.drupalimage_justifyClasses; + var justifyNames = [ 'left', 'center', 'right', 'block' ]; + var justifyOldPosition = CKEDITOR.tools.indexOf( justifyNames, align ); + var justifyOldClassName = justifyOldPosition === -1 ? null : justifyClasses[justifyOldPosition]; + var justifyNewPosition = CKEDITOR.tools.indexOf( justifyNames, value ); + var justifyNewClassName = justifyNewPosition === -1 ? null : justifyClasses[justifyNewPosition]; + } + + // If this image is already aligned, remove existing alignment. + if ( align ) + { + img.removeStyle( 'float' ); + img.removeAttribute( 'align' ); + if ( justifyOldClassName ) + img.removeClass( justifyOldClassName ); + } + + // If changing the alignment to a new value, set the new style. + if ( value && align !== value ) + { + if ( justifyNewClassName ) + img.addClass( justifyNewClassName ); + else + img.setStyle( 'float', value ); + } + + return align !== value; +} + +})(); + +/** + * List of classes to use for aligning images. Each class will be used when + * an image is selected and the normal justify toolbar buttons are clicked. The + * array of classes should contain 4 members, in the following order: left, + * center, right, justify. If the list of classes is null, CSS style attributes + * will be used instead. + * + * @type Array + * @default true + * @example + * // Disable the list of classes and use styles instead. + * config.drupalimage_justifyClasses = null; + */ +CKEDITOR.config.drupalimage_justifyClasses = [ 'align-left', 'align-center', 'align-right', 'full-width' ]; diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php index 4050be1..cbe4499 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php @@ -30,7 +30,7 @@ * @see CKEditorPluginContextualInterface * @see CKEditorPluginConfigurableInterface */ -abstract class CKEditorPluginBase extends PluginBase implements CKEditorPluginInterface, CKEditorPluginButtonsInterface { +abstract class CKEditorPluginBase extends PluginBase implements CKEditorPluginInterface { /** * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal(). @@ -39,4 +39,11 @@ return FALSE; } + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getDependencies(). + */ + function getDependencies() { + return array(); + } + } diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php index 601cafb..6357655 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php @@ -41,6 +41,15 @@ public function isInternal(); /** + * Returns a list of plugins this plugin requires. + * + * @return array + * An unindexed array of plugin names this plugin requires. Each plugin is + * is identified by its annotated ID. + */ + public function getDependencies(); + + /** * Returns the Drupal root-relative file path to the plugin JavaScript file. * * Note: this does not use a Drupal library because this uses CKEditor's API, diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php index 57a3a4b..ae797b1 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php @@ -61,6 +61,7 @@ $plugins = array_keys($this->getDefinitions()); $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); $enabled_plugins = array(); + $additional_plugins = array(); foreach ($plugins as $plugin_id) { $plugin = $this->createInstance($plugin_id); @@ -70,19 +71,29 @@ } $enabled = FALSE; + // Enable this plugin if it provides a button that has been enabled. if ($plugin instanceof CKEditorPluginButtonsInterface) { $plugin_buttons = array_keys($plugin->getButtons()); $enabled = (count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0); } + // Otherwise enable this plugin if it declares itself as enabled. if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) { $enabled = $plugin->isEnabled($editor); } if ($enabled) { $enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile(); + // Check if this plugin has dependencies that also need to be enabled. + $additional_plugins = array_merge($additional_plugins, array_diff($plugin->getDependencies(), $additional_plugins)); } } + // Add the list of dependent plugins. + foreach ($additional_plugins as $plugin_id) { + $plugin = $this->createInstance($plugin_id); + $enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile(); + } + // Always return plugins in the same order. asort($enabled_plugins); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/DrupalDialog.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/DrupalDialog.php new file mode 100644 index 0000000..ebbe722 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/DrupalDialog.php @@ -0,0 +1,40 @@ + array( + 'label' => t('Image'), + 'image' => drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/image.png', + ), + ); + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php index 86fe66f..16e58c6 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php @@ -8,6 +8,7 @@ namespace Drupal\ckeditor\Plugin\ckeditor\plugin; use Drupal\ckeditor\CKEditorPluginBase; +use Drupal\ckeditor\CKEditorPluginButtonsInterface; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Annotation\Plugin; use Drupal\Core\Annotation\Translation; @@ -22,7 +23,7 @@ * module = "ckeditor" * ) */ -class Internal extends CKEditorPluginBase { +class Internal extends CKEditorPluginBase implements CKEditorPluginButtonsInterface { /** * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal(). @@ -193,11 +194,6 @@ 'Format' => array( 'label' => t('HTML block format'), 'image_alternative' => '' . t('Format') . '', - ), - // "image" plugin. - 'Image' => array( - 'label' => t('Image'), - 'image_alternative' => $button('image'), ), // "table" plugin. 'Table' => array( diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php index 9f09a11..ec00a9a 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php @@ -8,6 +8,7 @@ namespace Drupal\ckeditor\Plugin\ckeditor\plugin; use Drupal\ckeditor\CKEditorPluginBase; +use Drupal\ckeditor\CKEditorPluginButtonsInterface; use Drupal\ckeditor\CKEditorPluginConfigurableInterface; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Annotation\Plugin; @@ -23,7 +24,7 @@ * module = "ckeditor" * ) */ -class StylesCombo extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface { +class StylesCombo extends CKEditorPluginBase implements CKEditorPluginButtonsInterface, CKEditorPluginConfigurableInterface { /** * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal(). diff --git a/core/modules/ckeditor/lib/StylesCombo.php b/core/modules/ckeditor/lib/StylesCombo.php new file mode 100644 index 0000000..9f09a11 --- /dev/null +++ b/core/modules/ckeditor/lib/StylesCombo.php @@ -0,0 +1,159 @@ +settings['toolbar']['buttons'])); + if (in_array('Styles', $toolbar_buttons)) { + $styles = $editor->settings['plugins']['stylescombo']['styles']; + $config['stylesSet'] = $this->generateStylesSetSetting($styles); + } + + return $config; + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons(). + */ + public function getButtons() { + return array( + 'Styles' => array( + 'label' => t('Font style'), + 'image_alternative' => '' . t('Styles') . '', + ), + ); + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginConfigurableInterface::settingsForm(). + */ + public function settingsForm(array $form, array &$form_state, Editor $editor) { + // Defaults. + $config = array('styles' => ''); + if (isset($editor->settings['plugins']['stylescombo'])) { + $config = $editor->settings['plugins']['stylescombo']; + } + + $form['styles'] = array( + '#title' => t('Styles'), + '#title_display' => 'invisible', + '#type' => 'textarea', + '#default_value' => $config['styles'], + '#description' => t('A list of classes that will be provided in the "Styles" dropdown. Enter one class on each line in the format: element.class|Label. Example: h1.title|Title.
These styles should be available in your theme\'s CSS file.'), + '#attached' => array( + 'library' => array(array('ckeditor', 'drupal.ckeditor.stylescombo.admin')), + ), + '#element_validate' => array( + array($this, 'validateStylesValue'), + ), + ); + + return $form; + } + + /** + * #element_validate handler for the "styles" element in settingsForm(). + */ + public function validateStylesValue(array $element, array &$form_state) { + if ($this->generateStylesSetSetting($element['#value']) === FALSE) { + form_error($element, t('The provided list of styles is syntactically incorrect.')); + } + } + + /** + * Builds the "stylesSet" configuration part of the CKEditor JS settings. + * + * @see getConfig() + * + * @param string $styles + * The "styles" setting. + * @return array|FALSE + * An array containing the "stylesSet" configuration, or FALSE when the + * syntax is invalid. + */ + protected function generateStylesSetSetting($styles) { + $styles_set = array(); + + // Early-return when empty. + $styles = trim($styles); + if (empty($styles)) { + return $styles_set; + } + + $styles = str_replace(array("\r\n", "\r"), "\n", $styles); + foreach (explode("\n", $styles) as $style) { + $style = trim($style); + + // Ignore empty lines in between non-empty lines. + if (empty($style)) { + continue; + } + + // Validate syntax: element.class[.class...]|label pattern expected. + if (!preg_match('@^ *[a-zA-Z0-9]+ *(\\.[a-zA-Z0-9_-]+ *)+\\| *.+ *$@', $style)) { + return FALSE; + } + + // Parse. + list($selector, $label) = explode('|', $style); + $classes = explode('.', $selector); + $element = array_shift($classes); + + // Build the data structure CKEditor's stylescombo plugin expects. + // @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles + $styles_set[] = array( + 'name' => trim($label), + 'element' => trim($element), + 'attributes' => array( + 'class' => implode(' ', array_map('trim', $classes)) + ), + ); + } + return $styles_set; + } + +}