core/modules/ckeditor/ckeditor.admin.inc | 154 +++++++++++
core/modules/ckeditor/ckeditor.api.php | 60 ++++
core/modules/ckeditor/ckeditor.info | 6 +
core/modules/ckeditor/ckeditor.module | 125 +++++++++
core/modules/ckeditor/css/ckeditor-iframe.css | 18 ++
core/modules/ckeditor/css/ckeditor-rtl.css | 18 ++
core/modules/ckeditor/css/ckeditor.admin.css | 186 +++++++++++++
core/modules/ckeditor/css/ckeditor.css | 56 ++++
core/modules/ckeditor/js/ckeditor.admin.js | 288 +++++++++++++++++++
core/modules/ckeditor/js/ckeditor.js | 32 +++
.../ckeditor/js/ckeditor.stylescombo.admin.js | 52 ++++
.../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 | 291 ++++++++++++++++++++
.../Plugin/ckeditor/plugin/StylesCombo.php | 159 +++++++++++
.../ckeditor/Plugin/editor/editor/CKEditor.php | 200 ++++++++++++++
.../Drupal/ckeditor/Tests/CKEditorAdminTest.php | 142 ++++++++++
.../Drupal/ckeditor/Tests/CKEditorLoadingTest.php | 149 ++++++++++
.../ckeditor/Tests/CKEditorPluginManagerTest.php | 123 +++++++++
.../lib/Drupal/ckeditor/Tests/CKEditorTest.php | 261 ++++++++++++++++++
.../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 +-
34 files changed, 3042 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..c77dda9
--- /dev/null
+++ b/core/modules/ckeditor/ckeditor.admin.inc
@@ -0,0 +1,154 @@
+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 => $plugin_buttons) {
+ foreach ($plugin_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 .= '';
+
+ 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 for drag-and-drop CKEditor toolbar builder UI.',
+ '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['drupal.ckeditor.stylescombo.admin'] = array(
+ 'title' => 'Only show the "stylescombo" plugin settings when its button is enabled.',
+ 'version' => VERSION,
+ 'js' => array(
+ $module_path . '/js/ckeditor.stylescombo.admin.js' => array(),
+ ),
+ 'dependencies' => array(
+ array('system', 'jquery.once'),
+ array('system', 'drupal.vertical-tabs'),
+ ),
+ );
+ $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..cf57de9
--- /dev/null
+++ b/core/modules/ckeditor/css/ckeditor.admin.css
@@ -0,0 +1,186 @@
+/**
+ * @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 a,
+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; /* LTR */
+ clear: left; /* LTR */
+ 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;
+ padding: 0;
+ margin: 0;
+ float: left; /* LTR */
+}
+ul.ckeditor-buttons li a {
+ position: relative;
+ display: block;
+ height: 18px;
+ padding: 4px 6px;
+ cursor: move;
+ border: 0;
+ white-space: nowrap;
+ text-decoration: none;
+ text-shadow: 0 1px 0 rgba(255,255,255,.5);
+ color: #474747;
+
+ 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 .cke-icon-only {
+ text-indent: -9999px;
+ width: 1.333em;
+}
+ul.ckeditor-buttons li a:focus {
+ z-index: 11; /* Ensure focused buttons show their outline on all sides. */
+}
+ul.ckeditor-buttons li:first-child a {
+ border-top-left-radius: 2px; /* LTR */
+ border-bottom-left-radius: 2px; /* LTR */
+}
+ul.ckeditor-buttons li:last-child a {
+ border-top-right-radius: 2px; /* LTR */
+ border-bottom-right-radius: 2px; /* LTR */
+}
+ul.ckeditor-buttons li.ckeditor-button-placeholder a {
+ background: #333;
+ opacity: 0.3;
+}
+ul.ckeditor-multiple-buttons {
+ padding: 1px 2px;
+ margin: 5px;
+ list-style: none;
+ float: left; /* LTR */
+}
+ul.ckeditor-multiple-buttons li {
+ display: inline-block;
+ float: left; /* LTR */
+ margin: 0;
+ padding: 0;
+}
+ul.ckeditor-multiple-buttons li a {
+ cursor: move;
+ display: inline-block;
+ height: 18px;
+ margin: 0;
+ padding: 2px 0;
+}
+ul.ckeditor-buttons li.ckeditor-group-button-separator,
+ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator {
+ margin: -1px -3px -2px;
+}
+ul.ckeditor-buttons li.ckeditor-group-button-separator a,
+ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator a {
+ 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;
+ position: relative;
+ z-index: 10;
+}
+ul.ckeditor-buttons li.ckeditor-button-separator a {
+ background: #e4e4e4;
+ background-image: -webkit-linear-gradient(#e4e4e4, #b4b4b4);
+ background-image: linear-gradient(#e4e4e4, #b4b4b4);
+ height: 24px;
+ margin: 1px 0 0;
+ padding: 0;
+ position: relative;
+ width: 1px;
+ z-index: 10;
+}
+ul.ckeditor-multiple-buttons li.ckeditor-button-separator a {
+ 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; /* LTR */
+ font-size: 18px;
+ width: 40px;
+ text-align: right; /* LTR */
+}
+.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..8c22b2a
--- /dev/null
+++ b/core/modules/ckeditor/js/ckeditor.admin.js
@@ -0,0 +1,288 @@
+(function ($, Drupal) {
+
+"use strict";
+
+Drupal.ckeditor = Drupal.ckeditor || {};
+
+var $messages; // Aria-live element for speaking application state.
+
+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
+ };
+ // Add the toolbar to the page.
+ $toolbarAdmin.insertAfter($textareaWrapper);
+
+ // Then determine if this is RTL or not.
+ var rtl = $toolbarAdmin.css('direction') === 'rtl' ? -1 : 1;
+ var $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons');
+
+ // Add the drag and drop functionality.
+ $toolbarRows.sortable(sortableSettings);
+ $toolbarAdmin.find('.ckeditor-multiple-buttons li').draggable({
+ connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons',
+ helper: 'clone'
+ });
+
+ // Add keyboard arrow support.
+ $toolbarAdmin.on('keyup.ckeditorMoveButton', '.ckeditor-buttons a', adminToolbarMoveButton);
+ $toolbarAdmin.on('keyup.ckeditorMoveSeparator', '.ckeditor-multiple-buttons a', adminToolbarMoveSeparator);
+
+ // Add click for help.
+ $toolbarAdmin.on('click.ckeditorClickButton', '.ckeditor-buttons a', { type: 'button' }, adminToolbarButtonHelp);
+ $toolbarAdmin.on('click.ckeditorClickSeparator', '.ckeditor-multiple-buttons a', { type: 'separator' }, adminToolbarButtonHelp);
+
+ // Add/remove row button functionality.
+ $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 aural UI focus updates when for individual toolbars.
+ $toolbarAdmin.on('focus.ckeditor', '.ckeditor-buttons', grantRowFocus);
+ // Identify the aria-live element for interaction updates for screen
+ // readers.
+ $messages = $('#ckeditor-button-configuration-aria-live');
+
+ /**
+ * Event callback for keypress. Move buttons based on arrow keys.
+ */
+ function adminToolbarMoveButton(event) {
+ var label = Drupal.t('@label button', {'@label': $(this).attr('aria-label')});
+ var $button = $(this).parent();
+ var $currentRow = $button.closest('.ckeditor-buttons');
+ var $destinationRow = null;
+ var destinationPosition = $button.index();
+
+ switch (event.keyCode) {
+ case 37: // Left arrow.
+ case 63234: // Safari left arrow.
+ $destinationRow = $currentRow;
+ destinationPosition = destinationPosition - (1 * rtl);
+ break;
+ case 38: // Up arrow.
+ case 63232: // Safari up arrow.
+ $destinationRow = $($toolbarRows[$toolbarRows.index($currentRow) - 1]);
+ break;
+ case 39: // Right arrow.
+ case 63235: // Safari right arrow.
+ $destinationRow = $currentRow;
+ destinationPosition = destinationPosition + (1 * rtl);
+ break;
+ case 40: // Down arrow.
+ case 63233: // Safari down arrow.
+ $destinationRow = $($toolbarRows[$toolbarRows.index($currentRow) + 1]);
+ }
+
+ if ($destinationRow && $destinationRow.length) {
+ // Detach the button from the DOM so its position doesn't interfere.
+ $button.detach();
+ // Move the button before the button whose position it should occupy.
+ var $targetButton = $destinationRow.children(':eq(' + destinationPosition + ')');
+ if ($targetButton.length) {
+ $targetButton.before($button);
+ }
+ else {
+ $destinationRow.append($button);
+ }
+ // Post the update to the aria-live message element.
+ $messages.text(Drupal.t('moved to @row, position @position of @totalPositions', {
+ '@row': getRowInfo($destinationRow),
+ '@position': (destinationPosition + 1),
+ '@totalPositions': $destinationRow.children().length
+ }));
+ // Update the toolbar value field.
+ adminToolbarValue(event, { item: $button });
+ }
+ event.preventDefault();
+ }
+
+ /**
+ * Event callback for keyup. Move a separator into the active toolbar.
+ */
+ function adminToolbarMoveSeparator(event) {
+ switch (event.keyCode) {
+ case 38: // Up arrow.
+ case 63232: // Safari up arrow.
+ var $button = $(this).parent().clone().appendTo($toolbarRows.eq(-2));
+ adminToolbarValue(event, { item: $button });
+ event.preventDefault();
+ }
+ }
+
+ /**
+ * Provide help when a button is clicked on.
+ */
+ function adminToolbarButtonHelp(event) {
+ var $link = $(this);
+ var $button = $link.parent();
+ var $currentRow = $button.closest('.ckeditor-buttons');
+ var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
+ var position = $button.index() + 1; // 1-based index for humans.
+ var rowNumber = $toolbarRows.index($currentRow) + 1;
+ var type = event.data.type;
+ if (enabled) {
+ if (type === 'separator') {
+ $messages.text(Drupal.t('Separators are used to visually split individual buttons. This @name is currently enabled, in row @row and position @position.', { '@name': $link.attr('aria-label'), '@row': rowNumber, '@position': position }) + "\n\n" + Drupal.t('Drag and drop the separator or use the keyboard arrow keys to change the position of this separator.'));
+ }
+ else {
+ $messages.text(Drupal.t('The @name button is currently enabled, in row @row and position @position.', { '@name': $link.attr('aria-label'), '@row': rowNumber, '@position': position }) + "\n\n" + Drupal.t('Drag and drop the buttons or use the keyboard arrow keys to change the position of this button.'));
+ }
+ }
+ else {
+ if (type === 'separator') {
+ $messages.text(Drupal.t('Separators are used to visually split individual buttons. This @name is currently disabled.', { '@name': $link.attr('aria-label') }) + "\n\n" + Drupal.t('Drag the button or use the up arrow key to move this separator into the active toolbar. You may add multiple separators to each row.'));
+ }
+ else {
+ $messages.text(Drupal.t('The @name button is currently disabled.', { '@name': $link.attr('aria-label') }) + "\n\n" + Drupal.t('Drag the button or use the up arrow key to move this button into the active toolbar.'));
+ }
+ }
+ $link.focus();
+ event.preventDefault();
+ }
+
+ /**
+ * 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);
+ $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons');
+ $this.siblings('a').show();
+ redrawToolbarGradient();
+ // Post the update to the aria-live message element.
+ $messages.text(Drupal.t('row number @count added.', {'@count': ($rows.length + 1)}));
+ 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();
+ $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons');
+ redrawToolbarGradient();
+ }
+ // Post the update to the aria-live message element.
+ $messages.text(Drupal.t('row removed. @count row@plural remaining.', {'@count': ($rows.length - 1), '@plural': ((($rows.length - 1) === 1 ) ? '' : 's')}));
+ 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 = [];
+ var $button = ui.item;
+ $button.find('a').focus();
+ $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, ' '));
+
+ // Determine whether we should trigger an event.
+ var from = $(event.target).parents('div[data-toolbar]').attr('data-toolbar');
+ var to = $(event.toElement).parents('div[data-toolbar]').attr('data-toolbar');
+ if (from !== to) {
+ $wrapper
+ .find('.ckeditor-toolbar-active')
+ .trigger('CKEditorToolbarChanged', [
+ (to === 'active') ? 'added' : 'removed',
+ $(ui.item).get(0).getAttribute('data-button-name')
+ ]);
+ }
+ }
+
+ });
+ }
+};
+
+/**
+ * Returns a string describing the type and index of a toolbar row.
+ *
+ * @param {jQuery} $row
+ * A jQuery object containing a .ckeditor-button row.
+ *
+ * @return {String}
+ * A string describing the type and index of a toolbar row.
+ */
+function getRowInfo ($row) {
+ var output = '';
+ var row;
+ // Determine if this is an active row or an available row.
+ if ($row.closest('.ckeditor-toolbar-disabled').length > 0) {
+ row = $('.ckeditor-toolbar-disabled').find('.ckeditor-buttons').index($row) + 1;
+ output += Drupal.t('available button row @row', {'@row': row});
+ }
+ else {
+ row = $('.ckeditor-toolbar-active').find('.ckeditor-buttons').index($row) + 1;
+ output += Drupal.t('active button row @row', {'@row': row});
+ }
+ return output;
+}
+
+/**
+ * Applies or removes the focused class to a toolbar row.
+ *
+ * When a button in a toolbar is focused, focus is triggered on the containing
+ * toolbar row. When a row is focused, the state change is announced through
+ * the aria-live message area.
+ *
+ * @param {jQuery} event
+ * A jQuery event.
+ */
+function grantRowFocus (event) {
+ var $row = $(this);
+ // Remove the focused class from all other toolbars.
+ $('.ckeditor-buttons.focused').not($row).removeClass('focused');
+ // Post the update to the aria-live message element.
+ if (!$row.hasClass('focused')) {
+ // Indicate that the current row has focus.
+ $row.addClass('focused');
+ $messages.text(Drupal.t('@row', {'@row': getRowInfo($row)}));
+ }
+}
+
+})(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/js/ckeditor.stylescombo.admin.js b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
new file mode 100644
index 0000000..707bf4b
--- /dev/null
+++ b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
@@ -0,0 +1,52 @@
+(function ($, Drupal) {
+
+"use strict";
+
+/**
+ * Shows the "stylescombo" plugin settings only when the button is enabled.
+ */
+Drupal.behaviors.ckeditorStylesComboSettingsVisibility = {
+ attach: function (context, settings) {
+ var $stylesComboVerticalTab = $('#edit-editor-settings-plugins-stylescombo').data('verticalTab');
+
+ // Hide if the "Styles" button is disabled.
+ if ($('.ckeditor-toolbar-disabled li[data-button-name="Styles"]').length === 1) {
+ $stylesComboVerticalTab.tabHide();
+ }
+
+ // React to added/removed toolbar buttons.
+ $(context)
+ .find('.ckeditor-toolbar-active')
+ .on('CKEditorToolbarChanged', function (e, action, button) {
+ if (button === 'Styles') {
+ if (action === 'added') {
+ $stylesComboVerticalTab.tabShow();
+ }
+ else {
+ $stylesComboVerticalTab.tabHide();
+ }
+ }
+ });
+ }
+};
+
+/**
+ * Provides the summary for the "stylescombo" plugin settings vertical tab.
+ */
+Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
+ attach: function (context, settings) {
+ $('#edit-editor-settings-plugins-stylescombo')
+ .drupalSetSummary(function (context) {
+ var styles = $.trim($('#edit-editor-settings-plugins-stylescombo-styles').val());
+ if (styles.length === 0) {
+ return Drupal.t('No styles configured');
+ }
+ else {
+ var count = $.trim(styles).split("\n").length;
+ return Drupal.t('@count styles configured', { '@count': count});
+ }
+ });
+ }
+};
+
+})(jQuery, Drupal);
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..601cafb
--- /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..51fa9c5
--- /dev/null
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php
@@ -0,0 +1,291 @@
+ 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',
+ 'keystrokes' => array(
+ // 0x11000 is CKEDITOR.CTRL, see http://docs.ckeditor.com/#!/api/CKEDITOR-property-CTRL.
+ array(0x110000 + 75, 'link'),
+ array(0x110000 + 76, NULL),
+ ),
+ );
+
+ // 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);
+ }
+
+ return $config;
+ }
+
+ /**
+ * Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons().
+ */
+ public function getButtons() {
+ $button = function($name, $direction = 'ltr') {
+ return '' . $name . '';
+ };
+
+ 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('super script'),
+ ),
+ 'Subscript' => array(
+ 'label' => t('Subscript'),
+ 'image_alternative' => $button('sub script'),
+ ),
+ // "removeformat" plugin.
+ 'RemoveFormat' => array(
+ 'label' => t('Remove format'),
+ 'image_alternative' => $button('remove format'),
+ ),
+ // "justify" plugin.
+ 'JustifyLeft' => array(
+ 'label' => t('Align left'),
+ 'image_alternative' => $button('justify left'),
+ ),
+ 'JustifyCenter' => array(
+ 'label' => t('Align center'),
+ 'image_alternative' => $button('justify center'),
+ ),
+ 'JustifyRight' => array(
+ 'label' => t('Align right'),
+ 'image_alternative' => $button('justify right'),
+ ),
+ 'JustifyBlock' => array(
+ 'label' => t('Justify'),
+ 'image_alternative' => $button('justify block'),
+ ),
+ // "list" plugin.
+ 'BulletedList' => array(
+ 'label' => t('Bullet list'),
+ 'image_alternative' => $button('bulleted list'),
+ 'image_alternative_rtl' => $button('bulleted list', 'rtl'),
+ ),
+ 'NumberedList' => array(
+ 'label' => t('Numbered list'),
+ 'image_alternative' => $button('numbered list'),
+ 'image_alternative_rtl' => $button('numbered list', '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('horizontal rule'),
+ ),
+ // "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('paste text'),
+ 'image_alternative_rtl' => $button('paste text', 'rtl'),
+ ),
+ // "pastefromword" plugin.
+ 'PasteFromWord' => array(
+ 'label' => t('Paste from Word'),
+ 'image_alternative' => $button('paste from word'),
+ 'image_alternative_rtl' => $button('paste from word', 'rtl'),
+ ),
+ // "specialchar" plugin.
+ 'SpecialChar' => array(
+ 'label' => t('Character map'),
+ 'image_alternative' => $button('special char'),
+ ),
+ '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('show blocks'),
+ 'image_alternative_rtl' => $button('show blocks', '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,
+ ),
+ );
+ }
+
+ /**
+ * 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' . $tag . '>';
+ $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/ckeditor/plugin/StylesCombo.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php
new file mode 100644
index 0000000..f810cbb
--- /dev/null
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/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: label|element.class. Example: title|h1.title.
These styles should be available in your theme\'s CKEditor stylesheets as well as in your theme\'s main 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: label|element.class[.class...] pattern expected.
+ if (!preg_match('@^.+\\| *[a-zA-Z0-9]+ *(\\.[a-zA-Z0-9_-]+ *)+$@', $style)) {
+ return FALSE;
+ }
+
+ // Parse.
+ list($label, $selector) = explode('|', $style, 2);
+ $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;
+ }
+
+}
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..379d00e
--- /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('Optional 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..fa0adc9
--- /dev/null
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php
@@ -0,0 +1,142 @@
+ '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"