core/modules/ckeditor/ckeditor.admin.inc | 146 +++++++++ core/modules/ckeditor/ckeditor.api.php | 60 ++++ core/modules/ckeditor/ckeditor.info | 6 + core/modules/ckeditor/ckeditor.module | 114 +++++++ core/modules/ckeditor/css/ckeditor-iframe.css | 18 ++ core/modules/ckeditor/css/ckeditor-rtl.css | 18 ++ core/modules/ckeditor/css/ckeditor.admin.css | 173 +++++++++++ core/modules/ckeditor/css/ckeditor.css | 56 ++++ core/modules/ckeditor/js/ckeditor.admin.js | 101 ++++++ core/modules/ckeditor/js/ckeditor.js | 32 ++ .../lib/Drupal/ckeditor/CKEditorPluginBase.php | 42 +++ .../ckeditor/CKEditorPluginButtonsInterface.php | 56 ++++ .../CKEditorPluginConfigurableInterface.php | 45 +++ .../ckeditor/CKEditorPluginContextualInterface.php | 39 +++ .../Drupal/ckeditor/CKEditorPluginInterface.php | 69 +++++ .../lib/Drupal/ckeditor/CKEditorPluginManager.php | 158 ++++++++++ .../lib/Drupal/ckeditor/CkeditorBundle.php | 26 ++ .../ckeditor/Plugin/ckeditor/plugin/Internal.php | 321 ++++++++++++++++++++ .../ckeditor/Plugin/editor/editor/CKEditor.php | 200 ++++++++++++ .../Drupal/ckeditor/Tests/CKEditorAdminTest.php | 147 +++++++++ .../Drupal/ckeditor/Tests/CKEditorLoadingTest.php | 149 +++++++++ .../ckeditor/Tests/CKEditorPluginManagerTest.php | 123 ++++++++ .../lib/Drupal/ckeditor/Tests/CKEditorTest.php | 193 ++++++++++++ .../ckeditor/tests/modules/ckeditor_test.info | 6 + .../ckeditor/tests/modules/ckeditor_test.module | 15 + .../ckeditor_test/Plugin/ckeditor/plugin/Llama.php | 57 ++++ .../Plugin/ckeditor/plugin/LlamaButton.php | 44 +++ .../Plugin/ckeditor/plugin/LlamaContextual.php | 47 +++ .../ckeditor/plugin/LlamaContextualAndButton.php | 80 +++++ core/modules/editor/editor.module | 41 +-- .../lib/Drupal/editor/Plugin/EditorInterface.php | 4 +- .../lib/Drupal/editor/Tests/EditorAdminTest.php | 26 +- 32 files changed, 2579 insertions(+), 33 deletions(-) diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc new file mode 100644 index 0000000..f7e414d --- /dev/null +++ b/core/modules/ckeditor/ckeditor.admin.inc @@ -0,0 +1,146 @@ +direction ? 'rtl' : 'ltr'; + + // Create lists of active and disabled buttons. + $editor = $variables['editor']; + $plugins = $variables['plugins']; + $buttons = array(); + $variables['multiple_buttons'] = array(); + foreach ($plugins as $plugin => $buttons) { + foreach ($buttons as $button_name => $button) { + $button['name'] = $button_name; + if (!empty($button['multiple'])) { + $variables['multiple_buttons'][$button_name] = $button; + } + $buttons[$button_name] = $button; + } + } + $variables['active_buttons'] = array(); + foreach ($editor->settings['toolbar']['buttons'] as $row_number => $row) { + foreach ($row as $button_name) { + if (isset($buttons[$button_name])) { + $variables['active_buttons'][$row_number][] = $buttons[$button_name]; + if (empty($buttons[$button_name]['multiple'])) { + unset($buttons[$button_name]); + } + } + } + } + $variables['disabled_buttons'] = array_diff_key($buttons, $variables['multiple_buttons']); +} + +/** + * Displays the toolbar configuration for CKEditor. + */ +function theme_ckeditor_settings_toolbar($variables) { + $editor = $variables['editor']; + $plugins = $variables['plugins']; + $rtl = $variables['language_direction'] === 'rtl' ? '_rtl' : ''; + + $build_button_item = function($button, $rtl) { + // Value of the button item. + if (isset($button['image_alternative'])) { + $value = $button['image_alternative' . $rtl]; + } + elseif (isset($button['image'])) { + $value = theme('image', array('uri' => $button['image' . $rtl], 'title' => $button['label'])); + } + else { + $value = '?'; + } + + // Set additional attribute on the button if it can occur multiple times. + if (!empty($button['multiple'])) { + $button['attributes']['class'][] = 'ckeditor-multiple-button'; + } + + // Build the button item. + $button_item = array( + 'value' => $value, + 'data-button-name' => $button['name'], + ); + if (!empty($button['attributes'])) { + $button_item = array_merge($button_item, $button['attributes']); + } + + return $button_item; + }; + + // Assemble items to be added to active button rows. + $active_buttons = array(); + foreach ($variables['active_buttons'] as $row_number => $row_buttons) { + foreach ($row_buttons as $button) { + $active_buttons[$row_number][] = $build_button_item($button, $rtl); + } + } + // Assemble list of disabled buttons (which are always a single row). + $disabled_buttons = array(); + foreach ($variables['disabled_buttons'] as $button) { + $disabled_buttons[] = $build_button_item($button, $rtl); + } + // Assemble list of multiple buttons that may be added multiple times. + $multiple_buttons = array(); + foreach ($variables['multiple_buttons'] as $button_name => $button) { + $multiple_buttons[] = $build_button_item($button, $rtl); + } + + $print_buttons = function($buttons) { + $output = ''; + foreach ($buttons as $button) { + $value = $button['value']; + unset($button['value']); + $attributes = (string) new Attribute($button); + $output .= '' . $value . ''; + } + return $output; + }; + + // We don't use theme_item_list() below in case there are no buttons in the + // active or disabled list, as theme_item_list() will not print an empty UL. + $output = ''; + $output .= '' . t('Active toolbar') . ''; + $output .= '
'; + foreach ($active_buttons as $button_row) { + $output .= ''; + } + if (empty($active_buttons)) { + $output .= ''; + } + + $output .= '
'; + $output .= '-'; + $output .= '+'; + $output .= '
'; + + $output .= '
'; + + $output .= '' . t('Available buttons') . ''; + $output .= '
'; + $output .= ''; + $output .= '' . t('Dividers') . ': '; + $output .= ''; + $output .= '
'; + + return $output; +} diff --git a/core/modules/ckeditor/ckeditor.api.php b/core/modules/ckeditor/ckeditor.api.php new file mode 100644 index 0000000..0b4f343 --- /dev/null +++ b/core/modules/ckeditor/ckeditor.api.php @@ -0,0 +1,60 @@ + array( + 'modulePath' => drupal_get_path('module', 'ckeditor'), + ), + ); + $libraries['drupal.ckeditor'] = array( + 'title' => 'Drupal behavior to enable CKEditor on textareas.', + 'version' => VERSION, + 'js' => array( + $module_path . '/js/ckeditor.js' => array(), + array('data' => $settings, 'type' => 'setting'), + ), + 'dependencies' => array( + array('editor', 'drupal.editor'), + array('ckeditor', 'ckeditor'), + ), + ); + $libraries['drupal.ckeditor.css'] = array( + 'title' => 'Formatting CSS for common classes used in CKEditor.', + 'version' => VERSION, + 'css' => array( + $module_path . '/css/ckeditor.css' => array(), + ), + ); + $libraries['drupal.ckeditor.admin'] = array( + 'title' => 'Drupal behavior to enable CKEditor on textareas.', + 'version' => VERSION, + 'js' => array( + $module_path . '/js/ckeditor.admin.js' => array(), + ), + 'css' => array( + $module_path . '/css/ckeditor.admin.css' => array(), + 'core/misc/ckeditor/skins/moono/editor.css' => array(), + ), + 'dependencies' => array( + array('system', 'jquery.once'), + array('system', 'jquery.ui.sortable'), + array('system', 'jquery.ui.draggable'), + ), + ); + $libraries['ckeditor'] = array( + 'title' => 'Loads the main CKEditor library.', + 'version' => '4.0.1', + 'js' => array( + 'core/misc/ckeditor/ckeditor.js' => array(), + ), + ); + + return $libraries; +} + +/** + * Implements hook_theme(). + */ +function ckeditor_theme() { + return array( + 'ckeditor_settings_toolbar' => array( + 'file' => 'ckeditor.admin.inc', + 'variables' => array('editor' => NULL, 'plugins' => NULL), + ), + ); +} + +/** + * Implements hook_page_build(). + */ +function ckeditor_page_build(&$page) { + // Add our CSS file that adds common needed classes, such as align-left, + // align-right, underline, indent, etc. + $page['#attached']['library'][] = array('ckeditor', 'drupal.ckeditor.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 + * including a "ckeditor_stylesheets" key in the theme .info file. + * + * @code + * ckeditor_stylesheets[] = css/ckeditor-iframe.css + * @endcode + */ +function _ckeditor_theme_css($theme = NULL) { + $css = array(); + if (!isset($theme)) { + $theme = variable_get('theme_default'); + } + if ($theme_path = drupal_get_path('theme', $theme)) { + $info = system_get_info('theme', $theme); + if (isset($info['ckeditor_stylesheets'])) { + $css = $info['ckeditor_stylesheets']; + foreach ($css as $key => $path) { + $css[$key] = $theme_path . '/' . $path; + } + } + if (isset($info['base theme'])) { + $css = array_merge(_ckeditor_theme_css($info['base theme'], $css)); + } + } + return $css; +} diff --git a/core/modules/ckeditor/css/ckeditor-iframe.css b/core/modules/ckeditor/css/ckeditor-iframe.css new file mode 100644 index 0000000..54f4b3f --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor-iframe.css @@ -0,0 +1,18 @@ +/** + * CSS added to iframe-based instances only. + */ +body { + font-family: Arial, Verdana, sans-serif; + font-size: 12px; + color: #222; + background-color: #fff; + margin: 8px; +} + +ol, ul, dl { + /* IE7: reset rtl list margin. (CKEditor issue #7334) */ + *margin-right: 0px; + /* Preserved spaces for list items with text direction other than the list. + * (CKEditor issues #6249,#8049) */ + padding: 0 40px; +} diff --git a/core/modules/ckeditor/css/ckeditor-rtl.css b/core/modules/ckeditor/css/ckeditor-rtl.css new file mode 100644 index 0000000..59f97b8 --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor-rtl.css @@ -0,0 +1,18 @@ +/** + * RTL styles used by CKEditor. Added to front-end theme and iframe editors. + */ +.align-left { + text-align: right; +} +.align-right { + text-align: left; +} +.indent1 { + margin: 0 40px 0 0; +} +.indent2 { + margin: 0 80px 0 0; +} +.indent3 { + margin: 0 120px 0 0; +} diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css new file mode 100644 index 0000000..62f30b0 --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -0,0 +1,173 @@ +/** + * @file + * Styles for configuration of CKEditor module. + * + * Many of these styles are adapted directly from the default CKEditor theme + * "moono". + */ + +.ckeditor-toolbar-active { + border: 1px solid #b6b6b6; + padding: 6px 8px 2px; + box-shadow: 0 1px 0 white inset; + background: #cfd1cf; + background-image: -webkit-gradient(linear, left top, left bottom, from(whiteSmoke), to(#cfd1cf)); + background-image: -moz-linear-gradient(top, whiteSmoke, #cfd1cf); + background-image: -o-linear-gradient(top, whiteSmoke, #cfd1cf); + background-image: -ms-linear-gradient(top, whiteSmoke, #cfd1cf); + background-image: linear-gradient(top, whiteSmoke, #cfd1cf); + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#fff5f5f5', endColorstr='#ffcfd1cf'); + margin: 5px 0; + overflow: nowrap; +} +.ckeditor-toolbar-disabled ul.ckeditor-buttons { + border: 0; +} + +.ckeditor-toolbar-disabled ul.ckeditor-buttons li { + margin: 5px; +} + +.ckeditor-toolbar-disabled ul.ckeditor-buttons li, +ul.ckeditor-buttons { + border: 1px solid #a6a6a6; + border-bottom-color: #979797; + border-radius: 3px; + box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 0 2px rgba(255, 255, 255, .15) inset, 0 1px 0 rgba(255, 255, 255, .15) inset; +} + +ul.ckeditor-buttons { + min-height: 26px; + min-width: 26px; + list-style: none; + + float: left; + clear: left; + padding: 0; + margin: 0 6px 5px 0; + border: 1px solid #a6a6a6; + border-bottom-color: #979797; + border-radius: 3px; + box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 0 2px rgba(255, 255, 255, .15) inset, 0 1px 0 rgba(255, 255, 255, .15) inset; +} +ul.ckeditor-buttons li { + display: inline-block; + height: 18px; + padding: 4px 6px; + outline: none; + cursor: move; + float: left; + border: 0; + white-space: nowrap; + + background: #e4e4e4; + background-image: -webkit-gradient(linear,left top,left bottom,from(white),to(#e4e4e4)); + background-image: -moz-linear-gradient(top,white,#e4e4e4); + background-image: -webkit-linear-gradient(top,white,#e4e4e4); + background-image: -o-linear-gradient(top,white,#e4e4e4); + background-image: -ms-linear-gradient(top,white,#e4e4e4); + background-image: linear-gradient(top,white,#e4e4e4); + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffffff',endColorstr='#ffe4e4e4'); +} +ul.ckeditor-buttons li:first-child { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} +ul.ckeditor-buttons li:last-child { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} +ul.ckeditor-buttons li.ckeditor-button-placeholder { + background: #333; + opacity: 0.3; +} +ul.ckeditor-multiple-buttons { + padding: 1px 2px; + margin: 5px; + list-style: none; + float: left; +} +ul.ckeditor-multiple-buttons li { + padding: 2px 0; + margin: 0; + display: inline-block; + height: 18px; + cursor: move; + float: left; +} +.ckeditor-multiple-label { + float: left; + padding: 10px 4px; +} +ul.ckeditor-buttons li.ckeditor-group-button-separator, +ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAdCAMAAABG4xbVAAAAhFBMVEUAAACmpqampqampqb////l5eX////5+fmmpqatra2urq6vr6+1tbW2tra4uLi6urq8vLzb29ve3t7i4uLl5eXn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz+/v7qIQO+AAAACHRSTlMATVmAi8XM29MuWToAAABjSURBVBiVrc5BCoAwDETRMKhtRBduev9LKm1xjItWRBBE6Nt9QkIwOTcUzk0Imi8aoMssxbgoTHMtqsFMLta0vPh2N49HyfdelPg6k9uvX/a+Bmggt1qJRNzQFVgjEnkUZDoBmH57VSypjg4AAAAASUVORK5CYII=) no-repeat center center; + width: 13px; + padding: 0; + height: 29px; + margin: -1px -3px -2px; + position: relative; + z-index: 10; +} +ul.ckeditor-buttons li.ckeditor-button-separator { + width: 2px; + padding: 0 4px; + height: 26px; + margin: 0 -4px; + position: relative; + z-index: 10; + + background: #e4e4e4; + background-image: -webkit-gradient(linear, left top, left bottom, from(white), to(#e4e4e4)); + background-image: -moz-linear-gradient(top, white, #e4e4e4); + background-image: -webkit-linear-gradient(top, white, #e4e4e4); + background-image: -o-linear-gradient(top, white, #e4e4e4); + background-image: -ms-linear-gradient(top, white, #e4e4e4); + background-image: linear-gradient(top, white, #e4e4e4); + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#ffffffff', endColorstr='#ffe4e4e4'); +} +ul.ckeditor-multiple-buttons li.ckeditor-button-separator { + width: 2px; + padding: 0; + height: 26px; + margin: 0 10px; +} +.ckeditor-separator { + background-color: silver; + background-color: rgba(0, 0, 0, .2); + margin: 5px 0; + height: 18px; + width: 1px; + display: block; + -webkit-box-shadow: 1px 0 1px rgba(255, 255, 255, .5); + -moz-box-shadow: 1px 0 1px rgba(255,255,255,.5); + box-shadow: 1px 0 1px rgba(255, 255, 255, .5) +} +.ckeditor-button-arrow { + width: 0; + text-align: center; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 3px solid #333; + display: inline-block; + margin: 0 4px 2px; +} + +.ckeditor-row-controls { + float: right; + font-size: 18px; + width: 40px; + text-align: right; +} +.ckeditor-row-controls a { + display: inline-block; + padding: 6px 2px; + height: 16px; + width: 16px; + line-height: 0.9; + font-weight: bold; + color: #333; +} +.ckeditor-row-controls a:hover { + text-decoration: none; +} diff --git a/core/modules/ckeditor/css/ckeditor.css b/core/modules/ckeditor/css/ckeditor.css new file mode 100644 index 0000000..d5c6efe --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor.css @@ -0,0 +1,56 @@ +/** + * Common styles used by CKEditor. Added to front-end theme and iframe editors. + */ +.align-left { + text-align: left; /* RTL */ +} +.align-right { + text-align: right; /* RTL */ +} +.align-center { + text-align: center; +} +.align-justify { + text-align: justify; +} +img.align-left { + float: left; /* RTL */ +} +img.align-right { + float: right; /* RTL */ +} +img.align-center { + margin-left: auto; + margin-right: auto; + display: block; +} +img.full-width { + width: 100%; + height: auto; +} +.underline { + text-decoration: underline; +} +.indent1 { + margin: 0 0 0 40px; /* RTL */ +} +.indent2 { + margin: 0 0 0 80px; /* RTL */ +} +.indent3 { + margin: 0 0 0 120px; /* RTL */ +} +.caption-left { + float: left; /* RTL */ +} +.caption-right { + float: right; /* RTL */ +} +.caption-center { + text-align: center; + margin-left: auto; + margin-right: auto; +} +.align-justify { + text-align: justify; +} diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js new file mode 100644 index 0000000..d22ac6e --- /dev/null +++ b/core/modules/ckeditor/js/ckeditor.admin.js @@ -0,0 +1,101 @@ +(function ($, Drupal) { + +"use strict"; + +Drupal.ckeditor = Drupal.ckeditor || {}; + +Drupal.behaviors.ckeditorAdmin = { + attach: function (context, settings) { + var $context = $(context); + $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-toolbar', function() { + var $wrapper = $(this); + var $textareaWrapper = $(this).find('.form-item-editor-settings-toolbar-buttons').hide(); + var $textarea = $textareaWrapper.find('textarea'); + var $toolbarAdmin = $(settings.ckeditor.toolbarAdmin); + var sortableSettings = { + connectWith: '.ckeditor-buttons', + placeholder: 'ckeditor-button-placeholder', + forcePlaceholderSize: true, + tolerance: 'pointer', + cursor: 'move', + stop: adminToolbarValue + }; + $toolbarAdmin.insertAfter($textareaWrapper).find('.ckeditor-buttons').sortable(sortableSettings); + $toolbarAdmin.find('.ckeditor-multiple-buttons li').draggable({ + connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons', + helper: 'clone' + }); + $toolbarAdmin.on('click.ckeditorAddRow', 'a.ckeditor-row-add', adminToolbarAddRow); + $toolbarAdmin.on('click.ckeditorAddRow', 'a.ckeditor-row-remove', adminToolbarRemoveRow); + if ($toolbarAdmin.find('.ckeditor-toolbar-active ul').length > 1) { + $toolbarAdmin.find('a.ckeditor-row-remove').hide(); + } + + /** + * Add a new row of buttons. + */ + function adminToolbarAddRow(event) { + var $this = $(this); + var $rows = $this.closest('.ckeditor-toolbar-active').find('.ckeditor-buttons'); + $rows.last().clone().empty().insertAfter($rows.last()).sortable(sortableSettings); + $this.siblings('a').show(); + redrawToolbarGradient(); + event.preventDefault(); + } + + /** + * Remove a row of buttons. + */ + function adminToolbarRemoveRow(event) { + var $this = $(this); + var $rows = $this.closest('.ckeditor-toolbar-active').find('.ckeditor-buttons'); + if ($rows.length === 2) { + $this.hide(); + } + if ($rows.length > 1) { + var $lastRow = $rows.last(); + var $disabledButtons = $wrapper.find('.ckeditor-toolbar-disabled .ckeditor-buttons'); + $lastRow.children(':not(.ckeditor-multiple-button)').prependTo($disabledButtons); + $lastRow.sortable('destroy').remove(); + redrawToolbarGradient(); + } + event.preventDefault(); + } + + /** + * Browser quirk work-around to redraw CSS3 gradients. + */ + function redrawToolbarGradient() { + $wrapper.find('.ckeditor-toolbar-active').css('position', 'relative'); + window.setTimeout(function() { + $wrapper.find('.ckeditor-toolbar-active').css('position', ''); + }, 10); + } + + /** + * jQuery Sortable stop event. Save updated toolbar positions to the textarea. + */ + function adminToolbarValue(event, ui) { + // Update the toolbar config after updating a sortable. + var toolbarConfig = []; + $wrapper.find('.ckeditor-toolbar-active ul').each(function() { + var $rowButtons = $(this).find('li'); + var rowConfig = []; + if ($rowButtons.length) { + $rowButtons.each(function() { + rowConfig.push(this.getAttribute('data-button-name')); + }); + toolbarConfig.push(rowConfig); + } + }); + $textarea.val(JSON.stringify(toolbarConfig, null, ' ')); + } + + }); + }, + detach: function (context, settings) { + // @todo + } +}; + +})(jQuery, Drupal); diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js new file mode 100644 index 0000000..7cfed59 --- /dev/null +++ b/core/modules/ckeditor/js/ckeditor.js @@ -0,0 +1,32 @@ +(function ($, Drupal, drupalSettings, CKEDITOR) { + +"use strict"; + +Drupal.editors.ckeditor = { + attach: function (element, format) { + // Register and load additional CKEditor plugins as necessary. + if (format.editorSettings.externalPlugins) { + for (var pluginName in format.editorSettings.drupalExternalPlugins) { + if (format.editorSettings.drupalExternalPlugins.hasOwnProperty(pluginName)) { + CKEDITOR.plugins.addExternal(pluginName, format.editorSettings.drupalExternalPlugins[pluginName], ''); + } + } + delete format.editorSettings.drupalExternalPlugins; + } + return !!CKEDITOR.replace(element, format.editorSettings); + }, + detach: function (element, format, trigger) { + var editor = CKEDITOR.dom.element.get(element).getEditor(); + if (editor) { + if (trigger === 'serialize') { + editor.updateElement(); + } + else { + editor.destroy(); + } + } + return !!editor; + } +}; + +})(jQuery, Drupal, drupalSettings, CKEDITOR); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php new file mode 100644 index 0000000..4050be1 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php @@ -0,0 +1,42 @@ +settings. + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * + * @return bool + */ + public function isEnabled(Editor $editor); + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php new file mode 100644 index 0000000..7ce1f99 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php @@ -0,0 +1,69 @@ +settings, but be aware that + * it may not yet contain plugin-specific settings, because the user may not + * yet have configured the form. + * If there are plugin-specific settings (verify with isset()), they can be + * found at $editor->settings['plugins'][$plugin_id]. + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * A keyed array, whose keys will end up as keys under CKEDITOR.config. + */ + public function getConfig(Editor $editor); +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php new file mode 100644 index 0000000..57a3a4b --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php @@ -0,0 +1,158 @@ +discovery = new AnnotatedClassDiscovery('ckeditor', 'plugin'); + $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition')); + $this->discovery = new AlterDecorator($this->discovery, 'ckeditor_plugin_info'); + $this->discovery = new CacheDecorator($this->discovery, 'ckeditor_plugin'); + $this->factory = new DefaultFactory($this->discovery); + } + + /** + * Determines which plug-ins are enabled. + * + * For CKEditor plugins that implement: + * - CKEditorPluginButtonsInterface, not CKEditorPluginContextualInterface, + * a plugin is enabled if at least one of its buttons is in the toolbar; + * - CKEditorPluginContextualInterface, not CKEditorPluginButtonsInterface, + * a plugin is enabled if its isEnabled() method returns TRUE + * - both of these interfaces, a plugin is enabled if either is the case. + * + * Internal plugins (those that are part of the bundled build of CKEditor) are + * excluded by default, since they are loaded implicitly. If you need to know + * even implicitly loaded (i.e. internal) plugins, then set the optional + * second parameter. + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @param bool $include_internal_plugins + * Defaults to FALSE. When set to TRUE, plugins whose isInternal() method + * returns TRUE will also be included. + * @return array + * A list of the enabled CKEditor plugins, with the plugin IDs as keys and + * the Drupal root-relative plugin files as values. + * For internal plugins, the value is NULL. + */ + public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FALSE) { + $plugins = array_keys($this->getDefinitions()); + $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); + $enabled_plugins = array(); + + foreach ($plugins as $plugin_id) { + $plugin = $this->createInstance($plugin_id); + + if (!$include_internal_plugins && $plugin->isInternal()) { + continue; + } + + $enabled = FALSE; + if ($plugin instanceof CKEditorPluginButtonsInterface) { + $plugin_buttons = array_keys($plugin->getButtons()); + $enabled = (count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0); + } + if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) { + $enabled = $plugin->isEnabled($editor); + } + + if ($enabled) { + $enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile(); + } + } + + // Always return plugins in the same order. + asort($enabled_plugins); + + return $enabled_plugins; + } + + /** + * Retrieves all plugins that implement CKEditorPluginButtonsInterface. + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * A list of the CKEditor plugins that implement buttons, with the plugin + * IDs as keys and lists of button metadata (as implemented by getButtons()) + * as values. + * + * @see CKEditorPluginButtonsInterface::getButtons() + */ + public function getButtonsPlugins(Editor $editor) { + $plugins = array_keys($this->getDefinitions()); + $buttons_plugins = array(); + + foreach ($plugins as $plugin_id) { + $plugin = $this->createInstance($plugin_id); + if ($plugin instanceof CKEditorPluginButtonsInterface) { + $buttons_plugins[$plugin_id] = $plugin->getButtons(); + } + } + + return $buttons_plugins; + } + + /** + * Injects the CKEditor plugins settings forms as a vertical tabs subform. + * + * @param array &$form + * A reference to an associative array containing the structure of the form. + * @param array &$form_state + * A reference to a keyed array containing the current state of the form. + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + */ + public function injectPluginSettingsForm(array &$form, array &$form_state, Editor $editor) { + $definitions = $this->getDefinitions(); + + foreach (array_keys($definitions) as $plugin_id) { + $plugin = $this->createInstance($plugin_id); + if ($plugin instanceof CKEditorPluginConfigurableInterface) { + $plugin_settings_form = array(); + $form['plugins'][$plugin_id] = array( + '#type' => 'details', + '#title' => $definitions[$plugin_id]['label'], + '#group' => 'editor][settings][plugin_settings', + ); + $form['plugins'][$plugin_id] += $plugin->settingsForm($plugin_settings_form, $form_state, $editor); + } + } + } + + /** + * Overrides Drupal\Component\Plugin\PluginManagerBase::processDefinition(). + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + // @todo Remove this check once http://drupal.org/node/1780396 is resolved. + if (!module_exists($definition['module'])) { + $definition = NULL; + return; + } + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CkeditorBundle.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CkeditorBundle.php new file mode 100644 index 0000000..eb8af12 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CkeditorBundle.php @@ -0,0 +1,26 @@ +register('plugin.manager.ckeditor.plugin', 'Drupal\ckeditor\CKEditorPluginManager'); + } + +} 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 new file mode 100644 index 0000000..fffed1d --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php @@ -0,0 +1,321 @@ + TRUE, + 'indentClasses' => array('indent1', 'indent2', 'indent3'), + 'justifyClasses' => array('align-left', 'align-center', 'align-right', 'align-justify'), + 'coreStyles_underline' => array('element' => 'span', 'attributes' => array('class' => 'underline')), + 'removeDialogTabs' => 'image:Link;image:advanced;link:advanced', + 'resize_dir' => 'vertical', + ); + + // Next, add the format_tags setting, if its button is enabled. + $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); + if (in_array('Format', $toolbar_buttons)) { + $config['format_tags'] = $this->generateFormatTagsSetting($editor); + } + + // Finally, set the configurable settings. + if (isset($editor->settings['plugins']['internal'])) { + if ($editor->settings['plugins']['internal']['link_shortcut'] === 'CTRL+K') { + $config['keystrokes'] = array( + // 0x11000 is CKEDITOR.CTRL, see http://docs.ckeditor.com/#!/api/CKEDITOR-property-CTRL. + array(0x110000 + 75, 'link'), + array(0x110000 + 76, NULL), + ); + } + } + + return $config; + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons(). + */ + function getButtons() { + $button = function($name, $direction = 'ltr') { + return ' '; + }; + + return array( + // "basicstyles" plugin. + 'Bold' => array( + 'label' => t('Bold'), + 'image_alternative' => $button('bold'), + ), + 'Italic' => array( + 'label' => t('Italic'), + 'image_alternative' => $button('italic'), + ), + 'Underline' => array( + 'label' => t('Underline'), + 'image_alternative' => $button('underline'), + ), + 'Strike' => array( + 'label' => t('Strike-through'), + 'image_alternative' => $button('strike'), + ), + 'Superscript' => array( + 'label' => t('Superscript'), + 'image_alternative' => $button('superscript'), + ), + 'Subscript' => array( + 'label' => t('Subscript'), + 'image_alternative' => $button('subscript'), + ), + // "removeformat" plugin. + 'RemoveFormat' => array( + 'label' => t('Remove format'), + 'image_alternative' => $button('removeformat'), + ), + // "justify" plugin. + 'JustifyLeft' => array( + 'label' => t('Align left'), + 'image_alternative' => $button('justifyleft'), + ), + 'JustifyCenter' => array( + 'label' => t('Align center'), + 'image_alternative' => $button('justifycenter'), + ), + 'JustifyRight' => array( + 'label' => t('Align right'), + 'image_alternative' => $button('justifyright'), + ), + 'JustifyBlock' => array( + 'label' => t('Justify'), + 'image_alternative' => $button('justifyblock'), + ), + // "list" plugin. + 'BulletedList' => array( + 'label' => t('Bullet list'), + 'image_alternative' => $button('bulletedlist'), + 'image_alternative_rtl' => $button('bulletedlist', 'rtl'), + ), + 'NumberedList' => array( + 'label' => t('Numbered list'), + 'image_alternative' => $button('numberedlist'), + 'image_alternative_rtl' => $button('numberedlist', 'rtl'), + ), + // "indent" plugin. + 'Outdent' => array( + 'label' => t('Outdent'), + 'image_alternative' => $button('outdent'), + 'image_alternative_rtl' => $button('outdent', 'rtl'), + ), + 'Indent' => array( + 'label' => t('Indent'), + 'image_alternative' => $button('indent'), + 'image_alternative_rtl' => $button('indent', 'rtl'), + ), + // "undo" plugin. + 'Undo' => array( + 'label' => t('Undo'), + 'image_alternative' => $button('undo'), + 'image_alternative_rtl' => $button('undo', 'rtl'), + ), + 'Redo' => array( + 'label' => t('Redo'), + 'image_alternative' => $button('redo'), + 'image_alternative_rtl' => $button('redo', 'rtl'), + ), + // "link" plugin. + 'Link' => array( + 'label' => t('Link'), + 'image_alternative' => $button('link'), + ), + 'Unlink' => array( + 'label' => t('Unlink'), + 'image_alternative' => $button('unlink'), + ), + 'Anchor' => array( + 'label' => t('Anchor'), + 'image_alternative' => $button('anchor'), + 'image_alternative_rtl' => $button('anchor', 'rtl'), + ), + // "blockquote" plugin. + 'Blockquote' => array( + 'label' => t('Blockquote'), + 'image_alternative' => $button('blockquote'), + ), + // "horizontalrule" plugin + 'HorizontalRule' => array( + 'label' => t('Horizontal rule'), + 'image_alternative' => $button('horizontalrule'), + ), + // "clipboard" plugin. + 'Cut' => array( + 'label' => t('Cut'), + 'image_alternative' => $button('cut'), + 'image_alternative_rtl' => $button('cut', 'rtl'), + ), + 'Copy' => array( + 'label' => t('Copy'), + 'image_alternative' => $button('copy'), + 'image_alternative_rtl' => $button('copy', 'rtl'), + ), + 'Paste' => array( + 'label' => t('Paste'), + 'image_alternative' => $button('paste'), + 'image_alternative_rtl' => $button('paste', 'rtl'), + ), + // "pastetext" plugin. + 'PasteText' => array( + 'label' => t('Paste Text'), + 'image_alternative' => $button('pastetext'), + 'image_alternative_rtl' => $button('pastetext', 'rtl'), + ), + // "pastefromword" plugin. + 'PasteFromWord' => array( + 'label' => t('Paste from Word'), + 'image_alternative' => $button('pastefromword'), + 'image_alternative_rtl' => $button('pastefromword', 'rtl'), + ), + // "specialchar" plugin. + 'SpecialChar' => array( + 'label' => t('Character map'), + 'image_alternative' => $button('specialchar'), + ), + '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( + 'label' => t('Table'), + 'image_alternative' => $button('table'), + ), + // "showblocks" plugin. + 'ShowBlocks' => array( + 'label' => t('Show blocks'), + 'image_alternative' => $button('showblocks'), + 'image_alternative_rtl' => $button('showblocks', 'rtl'), + ), + // "sourcearea" plugin. + 'Source' => array( + 'label' => t('Source code'), + 'image_alternative' => $button('source'), + ), + // "maximize" plugin. + 'Maximize' => array( + 'label' => t('Maximize'), + 'image_alternative' => $button('maximize'), + ), + // No plugin, separator "buttons" for toolbar builder UI use only. + '|' => array( + 'label' => t('Group separator'), + 'image_alternative' => ' ', + 'attributes' => array('class' => array('ckeditor-group-button-separator')), + 'multiple' => TRUE, + ), + '-' => array( + 'label' => t('Separator'), + 'image_alternative' => ' ', + 'attributes' => array('class' => array('ckeditor-button-separator')), + 'multiple' => TRUE, + ), + ); + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginConfigurableInterface::settingsForm(). + */ + function settingsForm(array $form, array &$form_state, Editor $editor) { + // Defaults. + $config = array('link_shortcut' => 'CTRL+L'); + if (isset($editor->settings['plugins']['internal'])) { + $config = $editor->settings['plugins']['internal']; + } + + $form['link_shortcut'] = array( + '#title' => t('"Create link" keyboard shortcut'), + '#type' => 'radios', + '#options' => array( + 'CTRL+L' => t('CTRL+L (default)'), + 'CTRL+K' => t('CTRL+K'), + ), + '#default_value' => $config['link_shortcut'], + '#description' => t('In most browsers, CTRL+L will take you to the URL bar. Many online writing tools hence use CTRL+K.') + ); + + return $form; + } + + /** + * Builds the "format_tags" configuration part of the CKEditor JS settings. + * + * @see getConfig() + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * An array containing the "format_tags" configuration. + */ + protected function generateFormatTagsSetting(Editor $editor) { + // The

tag is always allowed — HTML without

tags is nonsensical. + $format_tags = array('p'); + + // Given the list of possible format tags, automatically determine whether + // the current text format allows this tag, and thus whether it should show + // up in the "Format" dropdown. + $possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'); + foreach ($possible_format_tags as $tag) { + $input = '<' . $tag . '>TEST'; + $output = trim(check_markup($input, $editor->format)); + if ($input == $output) { + $format_tags[] = $tag; + } + } + + return implode(';', $format_tags); + } +} 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 new file mode 100644 index 0000000..ee7cc32 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php @@ -0,0 +1,200 @@ + array( + 'buttons' => array( + array( + 'Source', '|', 'Bold', 'Italic', '|', + 'NumberedList', 'BulletedList', 'Blockquote', '|', + 'JustifyLeft', 'JustifyCenter', 'JustifyRight', '|', + 'Link', 'Unlink', '|', 'Image', 'Maximize', + ), + ), + ), + 'plugins' => array(), + ); + } + + /** + * Implements \Drupal\editor\Plugin\EditorInterface::settingsForm(). + */ + public function settingsForm(array $form, array &$form_state, Editor $editor) { + $module_path = drupal_get_path('module', 'ckeditor'); + $manager = drupal_container()->get('plugin.manager.ckeditor.plugin'); + + $form['toolbar'] = array( + '#type' => 'container', + '#attached' => array( + 'library' => array(array('ckeditor', 'drupal.ckeditor.admin')), + 'js' => array( + array( + 'type' => 'setting', + 'data' => array('ckeditor' => array( + 'toolbarAdmin' => theme('ckeditor_settings_toolbar', array('editor' => $editor, 'plugins' => $manager->getButtonsPlugins($editor))), + )), + ) + ), + ), + '#attributes' => array('class' => array('ckeditor-toolbar-configuration')), + ); + $form['toolbar']['buttons'] = array( + '#type' => 'textarea', + '#title' => t('Toolbar buttons'), + '#default_value' => json_encode($editor->settings['toolbar']['buttons']), + '#attributes' => array('class' => array('ckeditor-toolbar-textarea')), + ); + + // CKEditor plugin settings, if any. + $form['plugin_settings'] = array( + '#type' => 'vertical_tabs', + '#title' => t('CKEditor plugin settings'), + ); + $manager->injectPluginSettingsForm($form, $form_state, $editor); + if (count(element_children($form['plugins'])) === 0) { + unset($form['plugins']); + unset($form['plugin_settings']); + } + + return $form; + } + + /** + * Implements \Drupal\editor\Plugin\EditorInterface::settingsFormSubmit(). + */ + public function settingsFormSubmit(array $form, array &$form_state) { + // Modify the toolbar settings by reference. The values in + // $form_state['values']['editor']['settings'] will be saved directly by + // editor_form_filter_admin_format_submit(). + $toolbar_settings = &$form_state['values']['editor']['settings']['toolbar']; + + $toolbar_settings['buttons'] = json_decode($toolbar_settings['buttons'], FALSE); + + // Remove the plugin settings' vertical tabs state; no need to save that. + if (isset($form_state['values']['editor']['settings']['plugins'])) { + unset($form_state['values']['editor']['settings']['plugin_settings']); + } + } + + /** + * Implements \Drupal\editor\Plugin\EditorInterface::getJSSettings(). + */ + public function getJSSettings(Editor $editor) { + $language_interface = language(LANGUAGE_TYPE_INTERFACE); + + $settings = array(); + $manager = drupal_container()->get('plugin.manager.ckeditor.plugin'); + + // Get the settings for all enabled plugins, even the internal ones. + $enabled_plugins = array_keys($manager->getEnabledPlugins($editor, TRUE)); + foreach ($enabled_plugins as $plugin_id) { + $plugin = $manager->createInstance($plugin_id); + $settings += $plugin->getConfig($editor); + } + + // Next, set the most fundamental CKEditor settings. + $external_plugins = $manager->getEnabledPlugins($editor); + $settings += array( + 'toolbar' => $this->buildToolbarJSSetting($editor), + 'contentsCss' => $this->buildContentsCssJSSetting($editor), + 'extraPlugins' => implode(',', array_keys($external_plugins)), + 'language' => $language_interface->langcode, + ); + + // Finally, set Drupal-specific CKEditor settings. + $settings += array( + 'drupalExternalPlugins' => array_map('file_create_url', $external_plugins), + ); + + return $settings; + } + + /** + * Implements \Drupal\editor\Plugin\EditorInterface::getLibraries(). + */ + public function getLibraries(Editor $editor) { + return array( + array('ckeditor', 'drupal.ckeditor'), + ); + } + + /** + * Builds the "toolbar" configuration part of the CKEditor JS settings. + * + * @see getJSSettings() + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * An array containing the "toolbar" configuration. + */ + public function buildToolbarJSSetting(Editor $editor) { + $toolbar = array(); + foreach ($editor->settings['toolbar']['buttons'] as $row_number => $row) { + $button_group = array(); + foreach ($row as $button_name) { + // Change the toolbar separators into groups. + if ($button_name === '|') { + $toolbar[] = $button_group; + $button_group = array(); + } + else { + $button_group['items'][] = $button_name; + } + } + $toolbar[] = $button_group; + $toolbar[] = '/'; + } + + return $toolbar; + } + + /** + * Builds the "contentsCss" configuration part of the CKEditor JS settings. + * + * @see getJSSettings() + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * An array containing the "contentsCss" configuration. + */ + public function buildContentsCssJSSetting(Editor $editor) { + $css = array( + drupal_get_path('module', 'ckeditor') . '/css/ckeditor.css', + drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css', + ); + $css = array_merge($css, _ckeditor_theme_css()); + drupal_alter('ckeditor_css', $css, $editor); + $css = array_map('file_create_url', $css); + + return array_values($css); + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php new file mode 100644 index 0000000..72811e9 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php @@ -0,0 +1,147 @@ + 'CKEditor administration', + 'description' => 'Tests administration of CKEditor.', + 'group' => 'CKEditor', + ); + } + + function setUp() { + parent::setUp(); + + // Create text format. + $filtered_html_format = entity_create('filter_format', array( + 'format' => 'filtered_html', + 'name' => 'Filtered HTML', + 'weight' => 0, + 'filters' => array(), + )); + $filtered_html_format->save(); + + // Create admin user. + $this->admin_user = $this->drupalCreateUser(array('administer filters')); + } + + function testAdmin() { + $manager = drupal_container()->get('plugin.manager.editor'); + $ckeditor = $manager->createInstance('ckeditor'); + + $this->drupalLogin($this->admin_user); + $this->drupalGet('admin/config/content/formats/filtered_html'); + + // Ensure no Editor config entity exists yet. + $editor = entity_load('editor', 'filtered_html'); + $this->assertFalse($editor, 'No Editor config entity exists yet.'); + + // Verify the "Text Editor" . - $select = $this->xpath('//select[@name="editor"]'); - $select_is_disabled = $this->xpath('//select[@name="editor" and @disabled="disabled"]'); - $options = $this->xpath('//select[@name="editor"]/option'); + $select = $this->xpath('//select[@name="editor[editor]"]'); + $select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]'); + $options = $this->xpath('//select[@name="editor[editor]"]/option'); $this->assertTrue(count($select) === 1, 'The Text Editor select exists.'); $this->assertTrue(count($select_is_disabled) === 1, 'The Text Editor select is disabled.'); $this->assertTrue(count($options) === 1, 'The Text Editor select has only one option.'); @@ -72,9 +72,9 @@ function testWithoutEditorAvailable() { $this->drupalGet('admin/config/content/formats/filtered_html'); // Verify the