core/lib/Drupal/Core/Ajax/AjaxResponse.php | 2 +- core/modules/ckeditor/ckeditor.admin.inc | 2 +- core/modules/ckeditor/ckeditor.module | 2 +- core/modules/ckeditor/css/ckeditor.admin.css | 4 +- core/modules/ckeditor/js/ckeditor.js | 6 +- .../ckeditor/js/plugins/drupalimage/plugin.js | 119 +++++++++++++++++++ .../lib/Drupal/ckeditor/CKEditorPluginBase.php | 22 +++- .../Drupal/ckeditor/CKEditorPluginInterface.php | 21 ++++ .../lib/Drupal/ckeditor/CKEditorPluginManager.php | 11 ++ .../Plugin/ckeditor/plugin/DrupalImage.php | 62 ++++++++++ .../ckeditor/Plugin/ckeditor/plugin/Internal.php | 10 +- .../Plugin/ckeditor/plugin/StylesCombo.php | 3 +- .../ckeditor/Plugin/editor/editor/CKEditor.php | 14 ++- .../Drupal/ckeditor/Tests/CKEditorLoadingTest.php | 2 + .../ckeditor/Tests/CKEditorPluginManagerTest.php | 4 +- .../lib/Drupal/ckeditor/Tests/CKEditorTest.php | 6 +- .../ckeditor_test/Plugin/ckeditor/plugin/Llama.php | 18 +-- core/modules/editor/css/editor.css | 8 ++ core/modules/editor/editor.module | 37 +++++- core/modules/editor/editor.pages.inc | 110 +++++++++++++++++ core/modules/editor/js/editor.js | 123 ++++++++++++++++++-- .../lib/Drupal/editor/Ajax/EditorModalSave.php | 45 +++++++ .../lib/Drupal/editor/Ajax/EditorModalTitle.php | 44 +++++++ .../lib/Drupal/editor/Plugin/EditorManager.php | 1 + .../lib/Drupal/editor/Tests/EditorLoadingTest.php | 2 + .../lib/Drupal/editor/Tests/EditorManagerTest.php | 1 + 26 files changed, 633 insertions(+), 46 deletions(-) diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php index 83124e8..1231f36 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -133,7 +133,7 @@ protected function ajaxRender(Request $request) { $scripts = drupal_add_js(); if (!empty($scripts['settings'])) { $settings = drupal_merge_js_settings($scripts['settings']['data']); - $this->addCommand(new SettingsCommand($settings, TRUE)); + $this->addCommand(new SettingsCommand($settings, TRUE), TRUE); } $commands = $this->commands; diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc index 0dff9f5..7e6bda7 100644 --- a/core/modules/ckeditor/ckeditor.admin.inc +++ b/core/modules/ckeditor/ckeditor.admin.inc @@ -57,7 +57,7 @@ function theme_ckeditor_settings_toolbar($variables) { $value = $button['image_alternative' . $rtl]; } elseif (isset($button['image'])) { - $value = theme('image', array('uri' => $button['image' . $rtl], 'title' => $button['label'])); + $value = '' . theme('image', array('uri' => $button['image' . $rtl], 'title' => $button['label'])) . ''; } else { $value = '?'; diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module index c566f3a..d9f4808 100644 --- a/core/modules/ckeditor/ckeditor.module +++ b/core/modules/ckeditor/ckeditor.module @@ -20,7 +20,7 @@ function ckeditor_library_info() { 'title' => 'Drupal behavior to enable CKEditor on textareas.', 'version' => VERSION, 'js' => array( - $module_path . '/js/ckeditor.js' => array(), + $module_path . '/js/ckeditor.js' => array('group' => JS_DEFAULT), array('data' => $settings, 'type' => 'setting'), ), 'dependencies' => array( diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css index 272e3ea..41af514 100644 --- a/core/modules/ckeditor/css/ckeditor.admin.css +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -85,8 +85,10 @@ ul.ckeditor-buttons li .cke-icon-only { text-indent: -9999px; width: 16px; } -ul.ckeditor-buttons li a:focus { +ul.ckeditor-buttons li a:focus, +ul.ckeditor-multiple-buttons li a:focus { z-index: 11; /* Ensure focused buttons show their outline on all sides. */ + outline: 1px dotted #333; } ul.ckeditor-buttons li:first-child a { border-top-left-radius: 2px; /* LTR */ diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js index b8318be..b95295e 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) { @@ -15,6 +15,10 @@ Drupal.editors.ckeditor = { } delete format.editorSettings.drupalExternalPlugins; } + // Also pass settings that are Drupal-specific. + format.editorSettings.drupal = { + format: format.format + }; return !!CKEDITOR.replace(element, format.editorSettings); }, 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..508c98f --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js @@ -0,0 +1,119 @@ +/** + * @file + * Drupal Image plugin. + */ + +(function ($, Drupal, drupalSettings, CKEDITOR) { + +"use strict"; + +CKEDITOR.plugins.add('drupalimage', { + init: function (editor) { + var pluginName = 'drupalimage'; + + // Register the toolbar button. + editor.ui.addButton('DrupalImage', { + label: editor.lang.common.image, + command: 'image', + icon: drupalSettings.basePath + drupalSettings.ckeditor.modulePath + '/js/plugins/drupalimage/image.png' + }); + + // Register the image command. + editor.addCommand('image', { + allowedContent: 'img[class,id,lang,longdesc,title]', + requiredContent: 'img[alt,src,width,height,class]', + exec: function (editor) { + var imageElement = getSelectedImage(editor); + var imageDOMElement = null; + if (imageElement && imageElement.$) { + imageDOMElement = imageElement.$; + } + // Width and height are populated by actual dimensions. + var existingValues = { + width: imageDOMElement ? imageDOMElement.width : '', + 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 = function(returnValues) { + // 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); + } + } + } + } + }; + var modalSettings = { + dialogClass: 'editor-image-dialog', + width: 500 + }; + Drupal.editor.modalOpen(drupalSettings.basePath + 'editor/image/nojs/' + editor.config.drupal.format, existingValues, saveCallback, modalSettings); + }, + modes: { wysiwyg : 1 }, + canUndo: true + }); + + // 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 }; + } + }); + } + } +}); + +/** + * 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; + } +} + +})(jQuery, Drupal, drupalSettings, CKEDITOR); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php index 4050be1..71d93a4 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,24 @@ function isInternal() { return FALSE; } + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getDependencies(). + */ + function getDependencies() { + return array(); + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getLibraries(). + */ + function getLibraries(Editor $editor) { + return array(); + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getConfig(). + */ + function getConfig(Editor $editor) { + return array(); + } } diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php index 601cafb..c4cd4a5 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php @@ -41,6 +41,27 @@ 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 a list of libraries this plugin requires. + * + * These libraries will be attached to the text_format element on which the + * editor is being loaded. + * + * @return array + * An array of libraries suitable for usage in a render API #attached + * property. + */ + public function getLibraries(Editor $editor); + + /** * 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 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA $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 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA } $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/DrupalImage.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/DrupalImage.php new file mode 100644 index 0000000..bec31af --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/DrupalImage.php @@ -0,0 +1,62 @@ + 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..4988893 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(). @@ -47,7 +48,7 @@ public function getConfig(Editor $editor) { $config = array( 'customConfig' => '', // Don't load CKEditor's config.js file. 'pasteFromWordPromptCleanup' => TRUE, - 'removeDialogTabs' => 'image:Link;image:advanced;link:advanced', + 'removeDialogTabs' => 'link:advanced', 'resize_dir' => 'vertical', 'keystrokes' => array( // 0x11000 is CKEDITOR.CTRL, see http://docs.ckeditor.com/#!/api/CKEDITOR-property-CTRL. @@ -194,11 +195,6 @@ public function getButtons() { 'label' => t('HTML block format'), 'image_alternative' => '' . t('Format') . '', ), - // "image" plugin. - 'Image' => array( - 'label' => t('Image'), - 'image_alternative' => $button('image'), - ), // "table" plugin. 'Table' => array( 'label' => t('Table'), 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/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php index e4c0948..1efc5c7 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php @@ -124,7 +124,10 @@ public function getJSSettings(Editor $editor) { 'toolbar' => $this->buildToolbarJSSetting($editor), 'contentsCss' => $this->buildContentsCssJSSetting($editor), 'extraPlugins' => implode(',', array_keys($external_plugins)), + // @todo: Remove default image plugin entirely from Drupal CKEditor build. + 'removePlugins' => 'image', 'language' => $language_interface->langcode, + 'allowedContent' => true, ); // Finally, set Drupal-specific CKEditor settings. @@ -139,9 +142,18 @@ public function getJSSettings(Editor $editor) { * Implements \Drupal\editor\Plugin\EditorInterface::getLibraries(). */ public function getLibraries(Editor $editor) { - return array( + // The main CKEditor library. + $libraries = array( array('ckeditor', 'drupal.ckeditor'), ); + // Add any libraries needed by non-internal plugins. + $manager = drupal_container()->get('plugin.manager.ckeditor.plugin'); + $external_plugins = $manager->getEnabledPlugins($editor); + foreach ($external_plugins as $plugin_id => $file) { + $plugin = $manager->createInstance($plugin_id); + $libraries = array_merge($libraries, $plugin->getLibraries($editor)); + } + return $libraries; } /** diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php index 308906a..755a505 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php @@ -90,6 +90,7 @@ function testLoading() { $ckeditor_plugin = drupal_container()->get('plugin.manager.editor')->createInstance('ckeditor'); $editor = entity_load('editor', 'filtered_html'); $expected = array('formats' => array('filtered_html' => array( + 'format' => 'filtered_html', 'editor' => 'ckeditor', 'editorSettings' => $ckeditor_plugin->getJSSettings($editor), ))); @@ -116,6 +117,7 @@ function testLoading() { $this->drupalGet('node/add/article'); list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck(); $expected = array('formats' => array('filtered_html' => array( + 'format' => 'filtered_html', 'editor' => 'ckeditor', 'editorSettings' => $ckeditor_plugin->getJSSettings($editor), ))); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php index fc56910..f68b6eb 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('internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.'); + $this->assertIdentical(array('drupalimage', 'internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.'); $this->assertIdentical(array(), $this->manager->getEnabledPlugins($editor), 'Only built-in plugins are enabled.'); $this->assertIdentical(array('internal' => NULL), $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.'); @@ -82,7 +82,7 @@ function testEnabledPlugins() { // Case 2: CKEditor plugins are available. $plugin_ids = array_keys($this->manager->getDefinitions()); sort($plugin_ids); - $this->assertIdentical(array('internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.'); + $this->assertIdentical(array('drupalimage', 'internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.'); $this->assertIdentical(array(), $this->manager->getEnabledPlugins($editor), 'Only the internal plugins are enabled.'); $this->assertIdentical(array('internal' => NULL), $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.'); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php index a71a725..31e4529 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php @@ -79,7 +79,9 @@ function testGetJSSettings() { 'toolbar' => $this->getDefaultToolbarConfig(), 'contentsCss' => $this->getDefaultContentsCssConfig(), 'extraPlugins' => '', + 'removePlugins' => 'image', 'language' => 'en', + 'allowedContent' => TRUE, 'drupalExternalPlugins' => array(), ); $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for default configuration.'); @@ -97,6 +99,8 @@ function testGetJSSettings() { $expected_config['toolbar'][] = '/'; $expected_config['format_tags'] = 'p;h4;h5;h6'; $expected_config['extraPlugins'] = 'llama_contextual,llama_contextual_and_button'; + $expected_config['removePlugins'] = 'image'; + $expected_config['allowedContent'] = TRUE; $expected_config['drupalExternalPlugins']['llama_contextual'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js'); $expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js'); $expected_config['contentsCss'][] = file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css'); @@ -227,7 +231,7 @@ protected function getDefaultInternalConfig() { return array( 'customConfig' => '', 'pasteFromWordPromptCleanup' => TRUE, - 'removeDialogTabs' => 'image:Link;image:advanced;link:advanced', + 'removeDialogTabs' => 'link:advanced', 'resize_dir' => 'vertical', 'keystrokes' => array(array(0x110000 + 75, 'link'), array(0x110000 + 76, NULL)), ); diff --git a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/ckeditor/plugin/Llama.php b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/ckeditor/plugin/Llama.php index d2e0732..029099e 100644 --- a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/ckeditor/plugin/Llama.php +++ b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/ckeditor/plugin/Llama.php @@ -7,8 +7,8 @@ namespace Drupal\ckeditor_test\Plugin\ckeditor\plugin; +use Drupal\ckeditor\CKEditorPluginBase; use Drupal\ckeditor\CKEditorPluginInterface; -use Drupal\Component\Plugin\PluginBase; use Drupal\Core\Annotation\Plugin; use Drupal\Core\Annotation\Translation; use Drupal\editor\Plugin\Core\Entity\Editor; @@ -31,14 +31,7 @@ * module = "ckeditor_test" * ) */ -class Llama extends PluginBase implements CKEditorPluginInterface { - - /** - * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal(). - */ - function isInternal() { - return FALSE; - } +class Llama extends CKEditorPluginBase { /** * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getFile(). @@ -47,11 +40,4 @@ function getFile() { return drupal_get_path('module', 'ckeditor_test') . '/js/llama.js'; } - /** - * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getButtons(). - */ - public function getConfig(Editor $editor) { - return array(); - } - } diff --git a/core/modules/editor/css/editor.css b/core/modules/editor/css/editor.css new file mode 100644 index 0000000..4f71f1a --- /dev/null +++ b/core/modules/editor/css/editor.css @@ -0,0 +1,8 @@ +/** + * @file + * Styles for text editors. + */ +.editor-image-dialog { + width: 80%; + max-width: 500px; +} diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index 6db6210..55f7cbb 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -34,6 +34,22 @@ function editor_help($path, $arg) { } /** + * Implements hook_menu(). + */ +function editor_menu() { + $items['editor/image/%/%filter_format'] = array( + 'title' => 'Insert image', + 'description' => 'Displays the modal dialog for inserting or editing an image in a text editor.', + 'page callback' => 'editor_image_modal', + 'page arguments' => array(2, 3), + 'access callback' => 'filter_access', + 'access arguments' => array(3), + 'file' => 'editor.pages.inc', + ); + return $items; +} + +/** * Implements hook_menu_alter(). * * Rewrites the menu entries for filter module that relate to the configuration @@ -69,7 +85,10 @@ function editor_library_info() { 'title' => 'Text Editor', 'version' => VERSION, 'js' => array( - $path . '/js/editor.js' => array(), + $path . '/js/editor.js' => array('group' => JS_DEFAULT), + ), + 'css' => array( + $path . '/css/editor.css' => array('group' => CSS_DEFAULT), ), 'dependencies' => array( array('system', 'jquery'), @@ -79,6 +98,22 @@ function editor_library_info() { ), ); + // This library ensures that the Drupal AJAX and jQuery UI libraries are + // loaded for dialogs. The JS/CSS for dialogs is included directly in the + // drupal.editor library. + $libraries['drupal.editor-dialog'] = array( + 'title' => 'Text Editor Dialogs', + 'version' => VERSION, + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'drupalSettings'), + array('system', 'drupal.ajax'), + array('system', 'jquery.ui.dialog'), + array('editor', 'drupal.editor'), + ), + ); + return $libraries; } diff --git a/core/modules/editor/editor.pages.inc b/core/modules/editor/editor.pages.inc new file mode 100644 index 0000000..04a74e3 --- /dev/null +++ b/core/modules/editor/editor.pages.inc @@ -0,0 +1,110 @@ +' . theme('status_messages') . $output . ''; + $javascript = drupal_add_js(); + + $response = new AjaxResponse(); + if ($form['#title']) { + $response->addCommand(new EditorModalTitle($form['#title'])); + } + $response->addCommand(new HtmlCommand('#editor-modal', $output)); + + return $response; +} + +/** + * Form callback; Display the form for inserting/editing an image. + */ +function editor_image_form($form, $form_state, $format) { + $input = $form_state['input']['editor_object']; + + $form['#tree'] = TRUE; + + // The #title attribute on the form will be set as the modal title. + $form['#title'] = t('Insert/Edit Image'); + + // Everything under the "attributes" key is merged directly into the + // generate img tag's attributes. + $form['attributes']['src'] = array( + '#title' => t('URL'), + '#type' => 'textfield', + '#default_value' => isset($input['src']) ? $input['src'] : '', + ); + $form['attributes']['alt'] = array( + '#title' => t('Alternative text'), + '#type' => 'textfield', + '#default_value' => isset($input['alt']) ? $input['alt'] : '', + ); + $form['attributes']['width'] = array( + '#title' => t('Width'), + '#type' => 'textfield', + '#default_value' => isset($input['width']) ? $input['width'] : '', + '#size' => 8, + '#maxlength' => 8, + '#field_suffix' => ' ' . t('px'), + ); + $form['attributes']['height'] = array( + '#title' => t('Height'), + '#type' => 'textfield', + '#default_value' => isset($input['height']) ? $input['height'] : '', + '#size' => 8, + '#maxlength' => 8, + '#field_suffix' => ' ' . t('px'), + ); + + $form['modal_actions'] = array( + '#type' => 'actions', + ); + $form['modal_actions']['save_modal'] = array( + '#type' => 'submit', + '#value' => t('Save'), + // No regular submit-handler. This form only works via JavaScript. + '#submit' => array(), + '#ajax' => array( + 'callback' => 'editor_image_form_submit', + ), + ); + + return $form; +} + +/** + * Submit handler for editor_image_form_submit(). + */ +function editor_image_form_submit($form, $form_state) { + $response = new AjaxResponse(); + $response->addCommand(new EditorModalSave($form_state['values'])); + return $response; +} diff --git a/core/modules/editor/js/editor.js b/core/modules/editor/js/editor.js index 32da290..8da8a6b 100644 --- a/core/modules/editor/js/editor.js +++ b/core/modules/editor/js/editor.js @@ -31,7 +31,7 @@ Drupal.behaviors.editor = { // Directly attach this editor, if the text format is enabled. if (settings.editor.formats[activeFormatID]) { - Drupal.editorAttach(field, settings.editor.formats[activeFormatID]); + Drupal.editor.attach(field, settings.editor.formats[activeFormatID]); } // Attach onChange handler to text format selector element. @@ -46,11 +46,11 @@ Drupal.behaviors.editor = { // Detach the current editor (if any) and attach a new editor. if (settings.editor.formats[activeFormatID]) { - Drupal.editorDetach(field, settings.editor.formats[activeFormatID]); + Drupal.editor.detach(field, settings.editor.formats[activeFormatID]); } activeFormatID = newFormatID; if (settings.editor.formats[activeFormatID]) { - Drupal.editorAttach(field, settings.editor.formats[activeFormatID]); + Drupal.editor.attach(field, settings.editor.formats[activeFormatID]); } }); } @@ -60,7 +60,7 @@ Drupal.behaviors.editor = { if (event.isDefaultPrevented()) { return; } - Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize'); + Drupal.editor.detach(field, settings.editor.formats[activeFormatID], 'serialize'); }); }); }, @@ -84,7 +84,7 @@ Drupal.behaviors.editor = { var activeFormatID = $this.val(); var field = behavior.findFieldForFormatSelector($this); - Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger); + Drupal.editor.detach(field, settings.editor.formats[activeFormatID], trigger); }); }, @@ -94,15 +94,116 @@ Drupal.behaviors.editor = { } }; -Drupal.editorAttach = function (field, format) { - if (format.editor) { - Drupal.editors[format.editor].attach(field, format); +Drupal.editor = { + + /** + * Attaches an editor to a field with a certain active text format. + * + * @param DOMNode field + * A textual field. + * @param format + * An object depending on the field's active text format, with the following + * keys: + * - format: A text format ID. + * - editor: The plugin ID of the corresponding text editor. + * - editorSettings: An object containing text editor settings. + */ + attach: function (field, format) { + if (format.editor) { + Drupal.editors[format.editor].attach(field, format); + } + }, + + /** + * Detaches an editor from a field with a certain trigger. + * + * @param DOMNode field + * A textual field. + * @param format + * See Drupal.editor.attach(). + * @param string trigger + * See Drupal.detachBehaviors(). + * + * @see Drupal.editor.attach() + * @see Drupal.detachBehaviors() + */ + detach: function (field, format, trigger) { + if (format.editor) { + Drupal.editors[format.editor].detach(field, format, trigger); + } } + }; -Drupal.editorDetach = function (field, format, trigger) { - if (format.editor) { - Drupal.editors[format.editor].detach(field, format, trigger); +/** + * Open a Drupal-based modal dialog. + * + * @param url + * The URL of the page that will provide the modal contents. + * @param title + * The title of the modal dialog. + * @param values + * Existing values to be populated into the form. + * @param callback + * When this dialog is completed, this function will receive the returned + * data. + */ +Drupal.editor.modalOpen = function (url, values, callback, callerSettings) { + // Remove any existing modal. + var $modal = $('#editor-modal'); + if ($modal.length) { + Drupal.detachBehaviors($modal[0]); + $modal.remove(); + } + // Create a new modal dialog container. + $modal = $('
').hide().appendTo('body'); + var $ajaxElement = $('').attr('href', url); + var modalSettings = {}; + $.extend(modalSettings, { + modal: true + }, callerSettings); + + $modal.data('saveCallback', callback); + $modal.html($ajaxElement); + $modal.dialog(modalSettings); + + // Perform the AJAX request to load the form. + var base = 'editor-modal'; + var ajaxSettings = { + event: 'click', + submit: { + js: true, + editor_object: values + } + }; + + Drupal.ajax[base] = new Drupal.ajax(base, $ajaxElement[0], ajaxSettings); + $ajaxElement.triggerHandler('click'); + + Drupal.settings.editor.activeModal = $modal; +}; + +/** + * Close a modal dialog and save its returned configuration + */ +Drupal.editor.modalClose = function (data) { + Drupal.settings.editor.activeModal.data('saveCallback')(data); + Drupal.settings.editor.activeModal.dialog('close'); +}; + +/** + * Command to close and save an open modal dialog. + */ +Drupal.ajax.prototype.commands.editorModalSave = function (ajax, response, status) { + Drupal.editor.modalClose(response.values); +}; + +/** + * Command to set the title of the current modal dialog. + */ +Drupal.ajax.prototype.commands.editorModalTitle = function (ajax, response, status) { + if (Drupal.settings.editor.activeModal) { + Drupal.settings.editor.activeModal.dialog('option', 'title', response.title); } }; diff --git a/core/modules/editor/lib/Drupal/editor/Ajax/EditorModalSave.php b/core/modules/editor/lib/Drupal/editor/Ajax/EditorModalSave.php new file mode 100644 index 0000000..f84b7ec --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Ajax/EditorModalSave.php @@ -0,0 +1,45 @@ +values = $values; + } + + /** + * Implements \Drupal\Core\Ajax\CommandInterface::render(). + */ + public function render() { + return array( + 'command' => 'editorModalSave', + 'values' => $this->values, + ); + } + +} diff --git a/core/modules/editor/lib/Drupal/editor/Ajax/EditorModalTitle.php b/core/modules/editor/lib/Drupal/editor/Ajax/EditorModalTitle.php new file mode 100644 index 0000000..13cf7f0 --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Ajax/EditorModalTitle.php @@ -0,0 +1,44 @@ +title = $title; + } + + /** + * Implements \Drupal\Core\Ajax\CommandInterface::render(). + */ + public function render() { + return array( + 'command' => 'editorModalTitle', + 'title' => $this->title, + ); + } + +} diff --git a/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php b/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php index 74a5eab..671b8d9 100644 --- a/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php +++ b/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php @@ -72,6 +72,7 @@ public function getAttachments(array $format_ids) { // JavaScript settings. $settings[$format_id] = array( + 'format' => $format_id, 'editor' => $editor->editor, 'editorSettings' => $plugin->getJSSettings($editor), ); diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditorLoadingTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditorLoadingTest.php index 755c2c0..a2da611 100644 --- a/core/modules/editor/lib/Drupal/editor/Tests/EditorLoadingTest.php +++ b/core/modules/editor/lib/Drupal/editor/Tests/EditorLoadingTest.php @@ -96,6 +96,7 @@ function testLoading() { $this->drupalGet('node/add/article'); list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck(); $expected = array('formats' => array('full_html' => array( + 'format' => 'full_html', 'editor' => 'unicorn', 'editorSettings' => array('ponyModeEnabled' => TRUE), ))); @@ -122,6 +123,7 @@ function testLoading() { $this->drupalGet('node/add/article'); list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck(); $expected = array('formats' => array('plain_text' => array( + 'format' => 'plain_text', 'editor' => 'unicorn', 'editorSettings' => array('ponyModeEnabled' => TRUE), ))); diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditorManagerTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditorManagerTest.php index 3a83b8a..022d9e0 100644 --- a/core/modules/editor/lib/Drupal/editor/Tests/EditorManagerTest.php +++ b/core/modules/editor/lib/Drupal/editor/Tests/EditorManagerTest.php @@ -102,6 +102,7 @@ function testManager() { 'type' => 'setting', 'data' => array('editor' => array('formats' => array( 'full_html' => array( + 'format' => 'full_html', 'editor' => 'unicorn', 'editorSettings' => $unicorn_plugin->getJSSettings($editor), )