core/modules/ckeditor/ckeditor.admin.inc | 91 +- core/modules/ckeditor/ckeditor.module | 3 +- .../ckeditor/config/schema/ckeditor.schema.yml | 18 +- core/modules/ckeditor/css/ckeditor.admin.css | 204 ++- core/modules/ckeditor/js/ckeditor.admin.js | 1491 ++++++++++++++++---- .../ckeditor/js/ckeditor.drupalimage.admin.js | 2 +- .../ckeditor/js/ckeditor.stylescombo.admin.js | 2 +- .../lib/Drupal/ckeditor/CKEditorPluginManager.php | 9 +- .../Plugin/CKEditorPlugin/DrupalImageCaption.php | 15 +- .../ckeditor/Plugin/CKEditorPlugin/Internal.php | 21 +- .../lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php | 67 +- .../Drupal/ckeditor/Tests/CKEditorAdminTest.php | 41 +- .../ckeditor/Tests/CKEditorPluginManagerTest.php | 10 +- .../lib/Drupal/ckeditor/Tests/CKEditorTest.php | 51 +- .../Plugin/CKEditorPlugin/LlamaContextual.php | 8 +- .../CKEditorPlugin/LlamaContextualAndButton.php | 8 +- .../standard/config/editor.editor.basic_html.yml | 42 +- .../standard/config/editor.editor.full_html.yml | 64 +- 18 files changed, 1623 insertions(+), 524 deletions(-) diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc index e99518a..b50a53d 100644 --- a/core/modules/ckeditor/ckeditor.admin.inc +++ b/core/modules/ckeditor/ckeditor.admin.inc @@ -30,18 +30,29 @@ function template_preprocess_ckeditor_settings_toolbar(&$variables) { $buttons[$button_name] = $button; } } + $button_groups = array(); $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]); + foreach ($editor->settings['toolbar']['rows'] as $row_number => $row) { + $button_groups[$row_number] = array(); + foreach ($row as $group) { + foreach ($group['items'] as $button_name) { + if (isset($buttons[$button_name])) { + // Save a reference to the button's configured toolbar group. + $buttons[$button_name]['group'] = $group['name']; + $variables['active_buttons'][$row_number][] = $buttons[$button_name]; + if (empty($buttons[$button_name]['multiple'])) { + unset($buttons[$button_name]); + } + // Create a list of all the toolbar button groups. + if (!in_array($group['name'], $button_groups[$row_number])) { + array_push($button_groups[$row_number], $group['name']); + } } } } } $variables['disabled_buttons'] = array_diff_key($buttons, $variables['multiple_buttons']); + $variables['button_groups'] = $button_groups; } /** @@ -66,7 +77,7 @@ function theme_ckeditor_settings_toolbar($variables) { '#uri' => $button['image' . $rtl], '#title' => $button['label'], ); - $value = '' . drupal_render($image) . ''; + $value = '' . drupal_render($image) . ''; } else { $value = '?'; @@ -80,8 +91,13 @@ function theme_ckeditor_settings_toolbar($variables) { // Build the button item. $button_item = array( 'value' => $value, - 'data-button-name' => $button['name'], + 'data-drupal-ckeditor-button-name' => $button['name'], + 'class' => array('ckeditor-button'), ); + // If this button has group information, add it to the attributes. + if (!empty($button['group'])) { + $button_item['group'] = $button['group']; + } if (!empty($button['attributes'])) { $button_item = array_merge($button_item, $button['attributes']); } @@ -110,6 +126,7 @@ function theme_ckeditor_settings_toolbar($variables) { $print_buttons = function($buttons) { $output = ''; foreach ($buttons as $button) { + unset($button['group']); $value = $button['value']; unset($button['value']); $attributes = (string) new Attribute($button); @@ -118,6 +135,18 @@ function theme_ckeditor_settings_toolbar($variables) { return $output; }; + $print_button_group = function($buttons, $group_name, $print_buttons) { + $group = drupal_html_class($group_name); + + $output = ''; + $output .= "
  • {$group_name}"; + $output .= "
  • "; + + 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 = ''; @@ -125,43 +154,45 @@ function theme_ckeditor_settings_toolbar($variables) { $output .= '' . t('Toolbar configuration') . ''; $output .= '
    '; - // aria-live region for outputing aural information about the state of the - // configuration. - $output .= '
    '; - - $output .= '
    ' . t('Move a button into the Active toolbar to enable it, or into the list of Available buttons to disable it. Use dividers to create button groups. Buttons may be moved with the mouse or keyboard arrow keys.') . '
    '; + $output .= '
    ' . t('Move a button into the Active toolbar to enable it, or into the list of Available buttons to disable it. Buttons may be moved with the mouse or keyboard arrow keys. Create a new toolbar group by placing a button in the placeholder group at the end of a row.') . '
    '; $output .= '
    '; + // Available buttons. + $output .= '
    '; + $output .= ''; + $output .= '
      '; + $output .= $print_buttons($disabled_buttons); + $output .= '
    '; + $output .= '
    '; + // Dividers. $output .= '
    '; - $output .= ''; - $output .= '
      '; + $output .= ''; + $output .= '
        '; $output .= $print_buttons($multiple_buttons); $output .= '
      '; $output .= '
    '; - $output .= ''; - $output .= ''; $output .= '
    '; - + // Active toolbar. $output .= ''; - - $output .= '
    '; - foreach ($active_buttons as $button_row) { - $output .= '
    '; diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module index 39c0707..3170a2d 100644 --- a/core/modules/ckeditor/ckeditor.module +++ b/core/modules/ckeditor/ckeditor.module @@ -76,9 +76,10 @@ function ckeditor_library_info() { array('system', 'jquery.ui.sortable'), array('system', 'jquery.ui.draggable'), array('system', 'jquery.ui.touch-punch'), + array('system', 'backbone'), + array('system', 'drupal.dialog'), array('ckeditor', 'ckeditor'), array('editor', 'drupal.editor.admin'), - array('system', 'underscore') ), ); $libraries['drupal.ckeditor.drupalimage.admin'] = array( diff --git a/core/modules/ckeditor/config/schema/ckeditor.schema.yml b/core/modules/ckeditor/config/schema/ckeditor.schema.yml index b201870..0ac8c41 100644 --- a/core/modules/ckeditor/config/schema/ckeditor.schema.yml +++ b/core/modules/ckeditor/config/schema/ckeditor.schema.yml @@ -8,15 +8,25 @@ editor.settings.ckeditor: type: mapping label: 'Toolbar configuration' mapping: - buttons: + rows: type: sequence label: 'Rows' sequence: - type: sequence - label: 'Buttons' + label: 'Button groups' sequence: - - type: string - label: 'Button' + - type: mapping + label: 'Button group' + mapping: + name: + type: string + label: 'Button group name' + items: + type: sequence + label: 'Buttons' + sequence: + - type: string + label: 'Button' plugins: type: sequence label: 'Plugins' diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css index 5e1b2ab..7d4e608 100644 --- a/core/modules/ckeditor/css/ckeditor.admin.css +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -6,72 +6,137 @@ * "moono". */ -.ckeditor-toolbar-active { +.ckeditor-row { + border: 1px solid whitesmoke; + padding: 2px 0px 4px; + border-radius: 3px; +} +.ckeditor-row + .ckeditor-row { + margin-top: 0.25em; +} +.ckeditor-toolbar-groups { + min-height: 2em; +} +.ckeditor-toolbar-group { + margin: 0 0.3333em; + cursor: move; +} +.ckeditor-group-names-are-visible .ckeditor-toolbar-group { + padding: 0.2em 0.4em; + border: 1px dotted #a6a6a6; + border-radius: 3px; +} +.ckeditor-toolbar-group.placeholder { + cursor: not-allowed; +} +.ckeditor-toolbar-group.placeholder .ckeditor-toolbar-group-name { + font-style: italic; +} +.ckeditor-toolbar-group-name { + display: none; +} +.ckeditor-group-names-are-visible .ckeditor-toolbar-group-name { + display: block; + cursor: pointer; +} +.ckeditor-toolbar-group-buttons { + float: left; +} + +.ckeditor-toolbar { border: 1px solid #b6b6b6; - padding: 6px 8px 2px; + padding: 0.1667em 0.1667em 0.08em; box-shadow: 0 1px 0 white inset; background: #cfd1cf; - background-image: -webkit-gradient(linear, left top, left bottom, from(whiteSmoke), to(#cfd1cf)); + background-image: -webkit-linear-gradient(top, whiteSmoke, #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; + /* Disallow any user selections in the drag-and-drop toolbar config UI. */ + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.ckeditor-toolbar-disabled { + margin-bottom: 0.5em; +} +.ckeditor-toolbar ul, +.ckeditor-toolbar-disabled ul { + list-style: none; + margin: 0; + padding: 0; } -.ckeditor-toolbar-active > ul { - clear: left; /* LTR */ +.ckeditor-toolbar-group, +.ckeditor-toolbar-group-placeholder { + display: inline-block; float: left; /* LTR */ } -[dir="rtl"] .ckeditor-toolbar-active > ul { - clear: right; - float: right; +[dir="rtl"].ckeditor-toolbar-group, +[dir="rtl"].ckeditor-toolbar-group-placeholder { + display: inline-block; + float: left; /* LTR */ +} +.ckeditor-groupnames-toggle { + float: right; /* LTR */ +} +[dir="rtl"] .ckeditor-groupnames-toggle { + float: left; +} +.ckeditor-toolbar .ckeditor-toolbar-group > li { + border: 1px solid white; + border-radius: 5px; + background-image: -webkit-linear-gradient(transparent 60%, rgba(0, 0, 0, 0.1)); + background-image: -moz-linear-gradient(transparent 60%, rgba(0, 0, 0, 0.1)); + background-image: linear-gradient(transparent 60%, rgba(0, 0, 0, 0.1)); + margin: 3px 6px; + padding: 3px; } #ckeditor-button-description { margin-bottom: 1em; } -.ckeditor-toolbar-dividers { - float: right; /* LTR */ +.ckeditor-toolbar-disabled .ckeditor-toolbar-available, +.ckeditor-toolbar-disabled .ckeditor-toolbar-dividers { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } -[dir="rtl"] .ckeditor-toolbar-dividers { +.ckeditor-toolbar-disabled .ckeditor-toolbar-available { float: left; + width: 80%; } -.ckeditor-toolbar-disabled ul.ckeditor-buttons { - border: 0; -} -.ckeditor-toolbar-disabled ul.ckeditor-buttons li { - margin: 2px; +.ckeditor-toolbar-disabled .ckeditor-toolbar-dividers { + float: right; + width: 20%; } -.ckeditor-toolbar-disabled ul.ckeditor-buttons li a, -ul.ckeditor-buttons { +.ckeditor-toolbar-disabled .ckeditor-buttons li a, +.ckeditor-toolbar .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; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5), 0 0 2px rgba(255, 255, 255, 0.15) inset, 0 1px 0 rgba(255, 255, 255, 0.15) inset; +} +.ckeditor-toolbar-disabled .ckeditor-buttons { + border: 0; +} +.ckeditor-toolbar-disabled .ckeditor-buttons li { + margin: 2px; } -ul.ckeditor-buttons { +.ckeditor-buttons { min-height: 26px; min-width: 26px; - list-style: none; - 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 { +.ckeditor-buttons li { display: inline-block; padding: 0; margin: 0; float: left; /* LTR */ } -[dir="rtl"] ul.ckeditor-buttons li { +[dir="rtl"] .ckeditor-buttons li { float: right; } -ul.ckeditor-buttons li a { +.ckeditor-buttons li a { position: relative; display: block; height: 18px; @@ -83,74 +148,82 @@ ul.ckeditor-buttons li a { 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 { + background-image: -moz-linear-gradient(top, white, #e4e4e4); + background-image: -webkit-linear-gradient(top, white, #e4e4e4); + background-image: linear-gradient(top, white, #e4e4e4); +} +.ckeditor-toolbar-dividers { + float: right; /* LTR */ +} +[dir="rtl"] .ckeditor-toolbar-dividers { + float: left; +} +.ckeditor-buttons li .cke-icon-only { text-indent: -9999px; width: 16px; + /* Firefox includes the offscreen text in the focus indicator, resulting in a + far too wide focus indicator. This fixes that. */ + overflow: hidden; } -ul.ckeditor-buttons li a:focus, -ul.ckeditor-multiple-buttons li a:focus { +.ckeditor-buttons li a:focus, +.ckeditor-buttons li a:active, +.ckeditor-multiple-buttons li a:focus { z-index: 11; /* Ensure focused buttons show their outline on all sides. */ - outline: 1px dotted #333; } -ul.ckeditor-buttons li:first-child a { +.ckeditor-buttons li:first-child a { border-top-left-radius: 2px; /* LTR */ border-bottom-left-radius: 2px; /* LTR */ } -[dir="rtl"] ul.ckeditor-buttons li:first-child a { +[dir="rtl"] .ckeditor-buttons li:first-child a { border-top-right-radius: 2px; border-bottom-right-radius: 2px; } -ul.ckeditor-buttons li:last-child a { +.ckeditor-buttons li:last-child a { border-top-right-radius: 2px; /* LTR */ border-bottom-right-radius: 2px; /* LTR */ } -[dir="rtl"] ul.ckeditor-buttons li:last-child a { +[dir="rtl"] .ckeditor-buttons li:last-child a { border-top-left-radius: 2px; border-bottom-left-radius: 2px; } -ul.ckeditor-buttons li.ckeditor-button-placeholder a { - background: #333; - opacity: 0.3; +.ckeditor-button-placeholder, +.ckeditor-toolbar-group-placeholder { + background: #9dcae7; +} +.ckeditor-toolbar-group-placeholder { + border-radius: 4px; } -ul.ckeditor-multiple-buttons { +.ckeditor-multiple-buttons { padding: 1px 2px; margin: 5px; list-style: none; float: left; /* LTR */ } -[dir="rtl"] ul.ckeditor-multiple-buttons { +[dir="rtl"] .ckeditor-multiple-buttons { float: right; } -ul.ckeditor-multiple-buttons li { +.ckeditor-multiple-buttons li { display: inline-block; float: left; /* LTR */ margin: 0; padding: 0; } -[dir="rtl"] ul.ckeditor-multiple-buttons li { +[dir="rtl"] .ckeditor-multiple-buttons li { float: right; } -ul.ckeditor-multiple-buttons li a { +.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 { +.ckeditor-buttons .ckeditor-group-button-separator, +.ckeditor-multiple-buttons .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 { +.ckeditor-buttons .ckeditor-group-button-separator a, +.ckeditor-multiple-buttons .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; @@ -161,6 +234,7 @@ ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator a { ul.ckeditor-buttons li.ckeditor-button-separator a { background: #e4e4e4; background-image: -webkit-linear-gradient(#e4e4e4, #b4b4b4); + background-image: -moz-linear-gradient(#e4e4e4, #b4b4b4); background-image: linear-gradient(#e4e4e4, #b4b4b4); height: 24px; margin: 1px 0 0; @@ -169,7 +243,7 @@ ul.ckeditor-buttons li.ckeditor-button-separator a { width: 1px; z-index: 10; } -ul.ckeditor-multiple-buttons li.ckeditor-button-separator a { +.ckeditor-multiple-buttons .ckeditor-button-separator a { width: 2px; padding: 0; height: 26px; @@ -177,12 +251,12 @@ ul.ckeditor-multiple-buttons li.ckeditor-button-separator a { } .ckeditor-separator { background-color: silver; - background-color: rgba(0, 0, 0, .2); + background-color: rgba(0, 0, 0, 0.2); margin: 5px 0; height: 18px; width: 1px; display: block; - box-shadow: 1px 0 1px rgba(255, 255, 255, .5) + box-shadow: 1px 0 1px rgba(255, 255, 255, 0.5) } .ckeditor-button-arrow { width: 0; diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js index 338ae6e..3aa9539 100644 --- a/core/modules/ckeditor/js/ckeditor.admin.js +++ b/core/modules/ckeditor/js/ckeditor.admin.js @@ -1,207 +1,194 @@ -(function ($, Drupal, drupalSettings, CKEDITOR, _) { +/** + * @file + * CKEditor button and group configuration user interface. + */ +(function ($, Drupal, _, CKEDITOR) { "use strict"; Drupal.ckeditor = Drupal.ckeditor || {}; -// Aria-live element for speaking application state. -var $messages; - Drupal.behaviors.ckeditorAdmin = { attach: function (context) { - var $context = $(context); - var $ckeditorToolbar = $context.find('.ckeditor-toolbar-configuration').once('ckeditor-toolbar'); - var featuresMetadata = {}; - var hiddenCKEditorConfig = drupalSettings.ckeditor.hiddenCKEditorConfig; + // Process the CKEditor configuration fragment once. + var $configurationForm = $(context).find('.ckeditor-toolbar-configuration'); + if ($configurationForm.once('ckeditor-configuration').length) { + var $textarea = $configurationForm + // Hide the textarea that contains the serialized representation of the + // CKEditor configuration. + .find('.form-item-editor-settings-toolbar-button-groups') + .hide() + // Return the textarea child node from this expression. + .find('textarea'); - /** - * Event callback for keypress. Move buttons based on arrow keys. - */ - function adminToolbarMoveButton (event) { - var $target = $(event.currentTarget); - var $button = $target.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 -= 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 += rtl; - break; - - case 40: // Down arrow. - case 63233: // Safari down arrow. - $destinationRow = $($toolbarRows[$toolbarRows.index($currentRow) + 1]); - } + // The HTML for the CKEditor configuration is assembled on the server and + // and sent to the client as a serialized DOM fragment. + $configurationForm.append(drupalSettings.ckeditor.toolbarAdmin); - 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(); + // Create a configuration model. + var model = Drupal.ckeditor.models.configurationModel = new Drupal.ckeditor.ConfigurationModel({ + $textarea: $textarea, + activeEditorConfig: JSON.parse($textarea.val()), + hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig + }); + + // Create the configuration Views. + var viewDefaults = { + model: model, + el: $('.ckeditor-toolbar-configuration') + }; + Drupal.ckeditor.views = { + controller: new Drupal.ckeditor.ConfigurationController(viewDefaults), + visualView: new Drupal.ckeditor.ConfigurationVisualView(viewDefaults), + keyboardView: new Drupal.ckeditor.ConfigurationKeyboardView(viewDefaults), + auralView: new Drupal.ckeditor.ConfigurationAuralView(viewDefaults) + }; + } + }, + detach: function (context, settings, trigger) { + // Early-return if the trigger for detachment is something else than unload. + if (trigger !== 'unload') { + return; } - /** - * 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 = $(event.currentTarget).parent().clone().appendTo($toolbarRows.eq(-2)); - adminToolbarValue(event, { item: $button }); - event.preventDefault(); + // We're detaching because CKEditor as text editor has been disabled; this + // really means that all CKEditor toolbar buttons have been removed. Hence, + // all editor features will be removed, so any reactions from filters will + // be undone. + var $configurationForm = $(context).find('.ckeditor-toolbar-configuration.ckeditor-configuration-processed'); + if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.configurationModel) { + var config = Drupal.ckeditor.models.configurationModel.toJSON().activeEditorConfig; + var buttons = Drupal.ckeditor.views.controller.getButtonList(config); + var $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active'); + for (var i = 0; i < buttons.length; i++) { + $activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]); } } + } +}; - /** - * Provide help when a button is clicked on. - */ - function adminToolbarButtonHelp (event) { - var $link = $(event.currentTarget); - 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; - var message; +/** + * CKEditor configuration UI methods of Backbone objects. + */ +Drupal.ckeditor = { - if (enabled) { - if (type === 'separator') { - message = 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 { - message = 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') { - message = 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 { - message = 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.'); - } - } - $messages.text(message); - $link.focus(); - event.preventDefault(); - } + // A hash of View instances. + views: {}, - /** - * Add a new row of buttons. - */ - function adminToolbarAddRow (event) { - var $this = $(event.currentTarget); - var $rows = $this.closest('.ckeditor-toolbar-active').find('.ckeditor-buttons'); - var $rowNew = $rows.last().clone().empty().sortable(sortableSettings); - $rows.last().after($rowNew); - $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(); - } + // A hash of Model instances. + models: {}, - /** - * Remove a row of buttons. - */ - function adminToolbarRemoveRow (event) { - var $this = $(event.currentTarget); - 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 = $ckeditorToolbar.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.formatPlural($rows.length - 1, 'row removed. 1 row remaining.', 'row removed. @count rows remaining.')); - event.preventDefault(); + /** + * Backbone model for the CKEditor toolbar configuration state. + */ + ConfigurationModel: Backbone.Model.extend({ + defaults: { + // The CKEditor configuration that is being manipulated through the UI. + activeEditorConfig: null, + // The textarea that contains the serialized representation of the active + // CKEditor configuration. + $textarea: null, + // Tracks whether the active toolbar DOM structure has been changed. When + // true, activeEditorConfig needs to be updated, and when that is updated, + // $textarea will also be updated. + isDirty: false, + // The configuration for the hidden CKEditor instance that is used to build + // the features metadata. + hiddenEditorConfig: null, + // + featuresMetadata: null, + // Whether the button group names are currently visible. + groupNamesVisible: false, + }, + sync: function () { + // Push the settings into the textarea. + this.get('$textarea').val(JSON.stringify(this.get('activeEditorConfig'))); } + }), + + /** + * Backbone View acting as a controller for CKEditor toolbar configuration. + */ + ConfigurationController: Backbone.View.extend({ + + events: {}, /** - * Browser quirk work-around to redraw CSS3 gradients. + * {@inheritdoc} */ - function redrawToolbarGradient () { - $ckeditorToolbar.find('.ckeditor-toolbar-active').css('position', 'relative'); - window.setTimeout(function () { - $ckeditorToolbar.find('.ckeditor-toolbar-active').css('position', ''); - }, 10); - } + initialize: function () { + this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this)); + + // Push the active editor configuration to the textarea. + this.model.on('change:activeEditorConfig', this.model.sync, this.model); + this.model.on('change:isDirty', this.parseEditorDOM, this); + }, /** - * jQuery Sortable stop event. Save updated toolbar positions to the - * textarea. + * Converts the active toolbar DOM structure to an object representation. + * + * @param Drupal.ckeditor.ConfigurationModel model + * The state model for the CKEditor configuration. + * @param Boolean isDirty + * Tracks whether the active toolbar DOM structure has been changed. + * isDirty is toggled back to false in this method. + * @param Object options + * An object that includes: + * - Boolean broadcast: (optional) A flag that controls whether a + * CKEditorToolbarChanged event should be fired for configuration + * changes. */ - function adminToolbarValue (event, ui) { - var oldToolbarConfig = JSON.parse($textarea.val()); - - // Update the toolbar config after updating a sortable. - var toolbarConfig = []; - var $button = ui.item; - $button.find('a').focus(); - $ckeditorToolbar.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')); + parseEditorDOM: function (model, isDirty, options) { + if (isDirty) { + var currentConfig = this.model.get('activeEditorConfig'); + + // Process the rows + var rows = []; + this.$el + .find('.ckeditor-active-toolbar-configuration') + .children('.ckeditor-row').each(function () { + var groups = []; + // Process the button groups. + $(this).find('.ckeditor-toolbar-group').each(function () { + var $group = $(this); + var $buttons = $group.find('.ckeditor-button'); + if ($buttons.length) { + var group = { + name: $group.attr('data-drupal-ckeditor-toolbar-group-name'), + items: [] + }; + $group.find('.ckeditor-button, .ckeditor-multiple-button').each(function () { + group.items.push($(this).attr('data-drupal-ckeditor-button-name')); + }); + groups.push(group); + } + }); + if (groups.length) { + rows.push(groups); + } }); - toolbarConfig.push(rowConfig); - } - }); - $textarea.val(JSON.stringify(toolbarConfig, null, ' ')); + this.model.set('activeEditorConfig', rows); + // Mark the model as clean. Whether or not the sync to the textfield + // occurs depends on the activeEditorConfig attribute firing a change + // event. The DOM has at least been processed and posted, so as far as + // the model is concerned, it is clean. + this.model.set('isDirty', false); - if (!ui.silent) { // Determine whether we should trigger an event. - var prev = _.flatten(oldToolbarConfig); - var next = _.flatten(toolbarConfig); - if (prev.length !== next.length) { - $ckeditorToolbar - .find('.ckeditor-toolbar-active') - .trigger('CKEditorToolbarChanged', [ - (prev.length < next.length) ? 'added' : 'removed', - _.difference(_.union(prev, next), _.intersection(prev, next))[0] - ]); + if (options.broadcast !== false) { + var prev = this.getButtonList(currentConfig); + var next = this.getButtonList(rows); + if (prev.length !== next.length) { + this.$el + .find('.ckeditor-toolbar-active') + .trigger('CKEditorToolbarChanged', [ + (prev.length < next.length) ? 'added' : 'removed', + _.difference(_.union(prev, next), _.intersection(prev, next))[0] + ]); + } } } - } + }, /** * Asynchronously retrieve the metadata for all available CKEditor features. @@ -211,8 +198,15 @@ Drupal.behaviors.ckeditorAdmin = { * filter settings. Because creating an instance is expensive, a callback * must be provided that will receive a hash of Drupal.EditorFeature * features keyed by feature (button) name. + * + * @param Object CKEditorConfig + * An object that represents the configuration settings for a CKEditor + * editor component. + * @param Function callback + * A function to invoke when the instanceReady event is fired by the + * CKEditor object. */ - function getCKEditorFeatures(CKEditorConfig, callback) { + getCKEditorFeatures: function (CKEditorConfig, callback) { var getProperties = function (CKEPropertiesList) { return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : []; }; @@ -243,14 +237,15 @@ Drupal.behaviors.ckeditorAdmin = { }; // Create hidden CKEditor with all features enabled, retrieve metadata. - // @see \Drupal\ckeditor\Plugin\editor\editor\CKEditor::settingsForm. + // @see \Drupal\ckeditor\Plugin\Editor\CKEditor::settingsForm. var hiddenCKEditorID = 'ckeditor-hidden'; if (CKEDITOR.instances[hiddenCKEditorID]) { CKEDITOR.instances[hiddenCKEditorID].destroy(true); } // Load external plugins, if any. - if (hiddenCKEditorConfig.drupalExternalPlugins) { - var externalPlugins = hiddenCKEditorConfig.drupalExternalPlugins; + var hiddenEditorConfig = this.model.get('hiddenEditorConfig'); + if (hiddenEditorConfig.drupalExternalPlugins) { + var externalPlugins = hiddenEditorConfig.drupalExternalPlugins; for (var pluginName in externalPlugins) { if (externalPlugins.hasOwnProperty(pluginName)) { CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); @@ -289,15 +284,20 @@ Drupal.behaviors.ckeditorAdmin = { callback(features); } }); - } + }, /** * Retrieves the feature for a given button from featuresMetadata. Returns * false if the given button is in fact a divider. + * + * @param String button + * The name of a CKEditor button. + * @return Object + * The feature metadata object for a button. */ - function getFeatureForButton (button) { + getFeatureForButton: function (button) { // Return false if the button being added is a divider. - if (button === '|' || button === '-') { + if (button === '-') { return false; } @@ -305,21 +305,83 @@ Drupal.behaviors.ckeditorAdmin = { // the feature that was just added or removed. Not every feature has // such metadata. var featureName = button.toLowerCase(); + var featuresMetadata = this.model.get('featuresMetadata'); if (!featuresMetadata[featureName]) { featuresMetadata[featureName] = new Drupal.EditorFeature(featureName); + this.model.set('featuresMetadata', featuresMetadata); } return featuresMetadata[featureName]; - } + }, + + /** + * Checks buttons against filter settings; disables disallowed buttons. + * + * @param Object features + * A map of Drupal.EditorFeature objects. + */ + disableFeaturesDisallowedByFilters: function (features) { + this.model.set('featuresMetadata', features); + + // Ensure that toolbar configuration changes are broadcast. + this.broadcastConfigurationChanges(this.$el); + + // Initialization: not all of the default toolbar buttons may be allowed + // by the current filter settings. Remove any of the default toolbar + // buttons that require more permissive filter settings. The remaining + // default toolbar buttons are marked as "added". + var existingButtons = []; + // Loop through each button group after flattening the groups from the + // toolbar row arrays. + for (var i = 0, buttonGroups = _.flatten(this.model.get('activeEditorConfig')); i < buttonGroups.length; i++) { + // Pull the button names from each toolbar button group. + for (var k = 0, buttons = buttonGroups[i].items; k < buttons.length; k++) { + existingButtons.push(buttons[k]); + } + } + // Remove duplicate buttons. + existingButtons = _.unique(existingButtons); + // Prepare the active toolbar and available-button toolbars. + for (i = 0; i < existingButtons.length; i++) { + var button = existingButtons[i]; + var feature = this.getFeatureForButton(button); + // Skip dividers. + if (feature === false) { + continue; + } + + if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) { + // Default toolbar buttons are in fact "added features". + this.$el.find('.ckeditor-toolbar-active').trigger('CKEditorToolbarChanged', ['added', existingButtons[i]]); + } + else { + // Move the button element from the active the active toolbar to the + // list of available buttons. + $('.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="' + button + '"]') + .detach() + .appendTo('.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul'); + // Update the toolbar value field. + this.model.set({'isDirty': true}, {broadcast: false}); + } + } + }, /** * Sets up broadcasting of CKEditor toolbar configuration changes. + * + * @param jQuery $ckeditorToolbar + * The active toolbar DOM element wrapped in jQuery. */ - function broadcastConfigurationChanges ($ckeditorToolbar) { + broadcastConfigurationChanges: function ($ckeditorToolbar) { + var view = this; + var hiddenEditorConfig = this.model.get('hiddenEditorConfig'); + var featuresMetadata = this.model.get('featuresMetadata'); + var getFeatureForButton = this.getFeatureForButton.bind(this); + var getCKEditorFeatures = this.getCKEditorFeatures.bind(this); $ckeditorToolbar .find('.ckeditor-toolbar-active') // Listen for CKEditor toolbar configuration changes. When a button is // added/removed, call an appropriate Drupal.editorConfiguration method. - .on('CKEditorToolbarChanged.ckeditorAdmin', function (e, action, button) { + .on('CKEditorToolbarChanged.ckeditorAdmin', function (event, action, button) { var feature = getFeatureForButton(button); // Early-return if the button being added is a divider. @@ -329,21 +391,22 @@ Drupal.behaviors.ckeditorAdmin = { // Trigger a standardized text editor configuration event to indicate // whether a feature was added or removed, so that filters can react. - var event = (action === 'added') ? 'addedFeature' : 'removedFeature'; - Drupal.editorConfiguration[event](feature); + var configEvent = (action === 'added') ? 'addedFeature' : 'removedFeature'; + Drupal.editorConfiguration[configEvent](feature); }) // Listen for CKEditor plugin settings changes. When a plugin setting is // changed, rebuild the CKEditor features metadata. - .on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (e, settingsChanges) { + // @todo I think this is a dead code path. + .on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (event, settingsChanges) { // Update hidden CKEditor configuration. for (var key in settingsChanges) { if (settingsChanges.hasOwnProperty(key)) { - hiddenCKEditorConfig[key] = settingsChanges[key]; + hiddenEditorConfig[key] = settingsChanges[key]; } } // Retrieve features for the updated hidden CKEditor configuration. - getCKEditorFeatures(hiddenCKEditorConfig, function (features) { + getCKEditorFeatures(hiddenEditorConfig, function (features) { // Trigger a standardized text editor configuration event for each // feature that was modified by the configuration changes. for (var name in features) { @@ -355,165 +418,983 @@ Drupal.behaviors.ckeditorAdmin = { } } // Update the CKEditor features metadata. - featuresMetadata = features; + view.model.set('featuresMetadata', features); }); }); + }, + + /** + * Returns the list of buttons from an editor configuration. + * + * @param Object config + * A CKEditor configuration object. + * @return Array + * A list of buttons in the CKEditor configuration. + */ + getButtonList: function (config) { + var buttons = []; + // Remove the rows + config = _.flatten(config); + + // Loop through the button groups and pull out the buttons. + config.forEach(function (group) { + group.items.forEach(function (button) { + buttons.push(button); + }); + }); + + // Remove the dividing elements if any. + return _.without(buttons, '-'); } + }), + + /** + * Backbone View for CKEditor toolbar configuration; visual UX. + */ + ConfigurationVisualView: Backbone.View.extend({ + + events: { + 'click .ckeditor-toolbar-group-name': 'onGroupNameClick', + 'click .ckeditor-groupnames-toggle': 'onGroupNamesToggleClick' + }, + + /** + * {@inheritdoc} + */ + initialize: function () { + this.model.on('change:isDirty change:groupNamesVisible', this.render, this); + + // Add a toggle for the button group names. + $(Drupal.theme('ckeditorButtonGroupNamesToggle')) + .insertBefore(this.$el.find('.ckeditor-toolbar-active').prev('label')); + + this.render(); + }, + + /** + * {@inheritdoc} + */ + render: function (model, value, changedAttributes) { + this.insertPlaceholders(); + this.applySorting(); + + // Toggle button group names. + var groupNamesVisible = this.model.get('groupNamesVisible'); + // If a button was just placed in the active toolbar, ensure that the + // button group names are visible. + if (changedAttributes && changedAttributes.changes && changedAttributes.changes.isDirty) { + this.model.set({groupNamesVisible: true}, {silent: true}); + groupNamesVisible = true; + } + this.$el.find('[data-toolbar="active"]').toggleClass('ckeditor-group-names-are-visible', groupNamesVisible); + this.$el.find('.ckeditor-groupnames-toggle') + .text((groupNamesVisible) ? Drupal.t('Hide group names') : Drupal.t('Show group names')) + .attr('aria-pressed', groupNamesVisible); + + return this; + }, + + /** + * Handles clicks to a button group name. + * + * @param jQuery.Event event + */ + onGroupNameClick: function (event) { + var $group = $(event.currentTarget).closest('.ckeditor-toolbar-group'); + openGroupNameDialog(this, $group); + + event.stopPropagation(); + event.preventDefault(); + }, + + /** + * Handles clicks on the button group names toggle button. + */ + onGroupNamesToggleClick: function (event) { + event.preventDefault(); + this.model.set('groupNamesVisible', !this.model.get('groupNamesVisible')); + }, + + /** + * Handles jQuery Sortable stop sort of a button group. + * + * @param jQuery.Event event + * @param Object ui + * A jQuery.ui.sortable argument that contains information about the + * elements involved in the sort action. + */ + endGroupDrag: function (event, ui) { + var view = this; + registerGroupMove(this, ui.item, function (success) { + if (!success) { + // Cancel any sorting in the configuration area. + view.$el.find('.ckeditor-toolbar-configuration').find('.ui-sortable').sortable('cancel'); + } + }); + }, + + /** + * Handles jQuery Sortable start sort of a button. + * + * @param jQuery.Event event + * @param Object ui + * A jQuery.ui.sortable argument that contains information about the + * elements involved in the sort action. + */ + startButtonDrag: function (event, ui) { + this.$el.find('a:focus').blur(); + + // Show the button group names as soon as the user starts dragging. + this.model.set('groupNamesVisible', true); + }, + + /** + * Handles jQuery Sortable stop sort of a button. + * + * @param jQuery.Event event + * @param Object ui + * A jQuery.ui.sortable argument that contains information about the + * elements involved in the sort action. + */ + endButtonDrag: function (event, ui) { + var view = this; + registerButtonMove(this, ui.item, function (success) { + if (!success) { + // Cancel any sorting in the configuration area. + view.$el.find('.ui-sortable').sortable('cancel'); + } + // Refocus the target button so that the user can continue from a known + // place. + ui.item.find('a').focus(); + }); + }, - if ($ckeditorToolbar.length) { - var $textareaWrapper = $ckeditorToolbar.find('.form-item-editor-settings-toolbar-buttons').hide(); - var $textarea = $textareaWrapper.find('textarea'); - var $toolbarAdmin = $(drupalSettings.ckeditor.toolbarAdmin); - var sortableSettings = { + /** + * Invokes jQuery.sortable() on new buttons and groups in a CKEditor config. + */ + applySorting: function () { + // Make the buttons sortable. + this.$el.find('.ckeditor-buttons').not('.ui-sortable').sortable({ + // Change this to .ckeditor-toolbar-group-buttons. connectWith: '.ckeditor-buttons', placeholder: 'ckeditor-button-placeholder', forcePlaceholderSize: true, tolerance: 'pointer', cursor: 'move', - stop: adminToolbarValue - }; - // Add the toolbar to the page. - $toolbarAdmin.insertAfter($textareaWrapper); + start: this.startButtonDrag.bind(this), + // Sorting within a sortable. + stop: this.endButtonDrag.bind(this) + }).disableSelection(); - // 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 to button groups. + this.$el.find('.ckeditor-toolbar-groups').not('.ui-sortable').sortable({ + connectWith: '.ckeditor-toolbar-groups', + cancel: '.ckeditor-toolbar-group.placeholder', + placeholder: 'ckeditor-toolbar-group-placeholder', + forcePlaceholderSize: true, + cursor: 'move', + stop: this.endGroupDrag.bind(this) + }); - // Add the drag and drop functionality. - $toolbarRows.sortable(sortableSettings); - $toolbarAdmin.find('.ckeditor-multiple-buttons li').draggable({ + // Add the drag and drop functionality to buttons. + this.$el.find('.ckeditor-multiple-buttons li').draggable({ connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons', helper: 'clone' }); + }, + + /** + * Wraps the invocation of methods to insert blank groups and rows. + */ + insertPlaceholders: function () { + this.insertPlaceholderRow(); + this.insertPlaceholderGroup(); + }, + + /** + * Inserts a blank row at the bottom of the CKEditor configuration. + */ + insertPlaceholderRow: function () { + var $rows = this.$el.find('.ckeditor-row'); + // Add a placeholder row. to the end of the list if one does not exist. + if (!$rows.eq(-1).hasClass('placeholder')) { + this.$el + .find('.ckeditor-toolbar-active') + .children('.ckeditor-active-toolbar-configuration') + .append(Drupal.theme('ckeditorRow')); + } + // Update the $rows variable to include the new row. + $rows = this.$el.find('.ckeditor-row'); + // Remove blank rows except the last one. + var len = $rows.length; + $rows.filter(function (index, row) { + // Do not remove the last row. + if (index + 1 === len) { + return false; + } + return $(row).find('.ckeditor-toolbar-group').not('.placeholder').length === 0; + }) + // Then get all rows that are placeholders and remove them. + .remove(); + }, + + /** + * Inserts a blank group at the end of a row CKEditor configuration. + */ + insertPlaceholderGroup: function () { + // Add placeholder groups. + this.$el.find('.ckeditor-row').each(function () { + var $row = $(this); + var $groups = $row.find('.ckeditor-toolbar-group'); + var $placeholder = $groups.filter('.placeholder'); + if ($placeholder.length === 0) { + $row.children('.ckeditor-toolbar-groups').append(Drupal.theme('ckeditorToolbarGroup')); + } + // If a placeholder group exists, make sure it's at the end of the row. + else if (!$groups.eq(-1).hasClass('placeholder')) { + $placeholder.appendTo($row.children('.ckeditor-toolbar-groups')); + } + }); + } + }), + + /** + * Backbone View for CKEditor toolbar configuration; keyboard UX. + */ + ConfigurationKeyboardView: Backbone.View.extend({ + /** + * {@inheritdoc} + */ + initialize: function () { // 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(); + this.$el.on('keyup.ckeditor', '.ckeditor-buttons a, .ckeditor-multiple-buttons a', this.onPressButton.bind(this)); + this.$el.on('keyup.ckeditor', '[data-drupal-ckeditor-type="group"]', this.onPressGroup.bind(this)); + }, + + /** + * {@inheritdoc} + */ + render: function () {}, + + /** + * Handles keypresses on a CKEditor configuration button. + * + * @param jQuery.Event event + */ + onPressButton: function (event) { + var upDownKeys = [ + 38, // Up arrow. + 63232, // Safari up arrow. + 40, // Down arrow. + 63233 // Safari down arrow. + ]; + var leftRightKeys = [ + 37, // Left arrow. + 63234, // Safari left arrow. + 39, // Right arrow. + 63235 // Safari right arrow. + ]; + + // Respond to an enter key press. Prevent the bubbling of the enter key + // press to the button group parent element. + if (event.keyCode === 13) { + event.stopPropagation(); } - // 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'); - - getCKEditorFeatures(hiddenCKEditorConfig, function (features) { - featuresMetadata = features; - - // Ensure that toolbar configuration changes are broadcast. - broadcastConfigurationChanges($ckeditorToolbar); - - // Initialization: not all of the default toolbar buttons may be allowed - // by the current filter settings. Remove any of the default toolbar - // buttons that require more permissive filter settings. The remaining - // default toolbar buttons are marked as "added". - var $activeToolbar = $ckeditorToolbar.find('.ckeditor-toolbar-active'); - var existingButtons = _.unique(_.flatten(JSON.parse($textarea.val()))); - for (var i = 0; i < existingButtons.length; i++) { - var button = existingButtons[i]; - var feature = getFeatureForButton(button); + // Only take action when a direction key is pressed. + if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) { + var view = this; + var $target = $(event.currentTarget); + var $button = $target.parent(); + var $container = $button.parent(); + var $group = $button.closest('.ckeditor-toolbar-group'); + var $row = $button.closest('.ckeditor-row'); + var containerType = $container.data('drupal-ckeditor-button-sorting'); + var $availableButtons = this.$el.find('[data-drupal-ckeditor-button-sorting="source"]'); + var $activeButtons = this.$el.find('.ckeditor-toolbar-active'); + // The current location of the button, just in case it needs to be put + // back. + var $originalGroup = $group; + var dir; - // Skip dividers. - if (feature === false) { - continue; + // Move available buttons between their container and the active toolbar. + if (containerType === 'source') { + // Move the button to the active toolbar configuration when the down or + // up keys are pressed. + if (_.indexOf([40, 63233], event.keyCode) > -1) { + // Move the button to the first row, first button group index + // position. + $activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button); + } + } + else if (containerType === 'target') { + // Move buttons between sibling buttons in a group and between groups. + if (_.indexOf(leftRightKeys, event.keyCode) > -1) { + // Move left. + var $siblings = $container.children(); + var index = $siblings.index($button); + if (_.indexOf([37, 63234], event.keyCode) > -1) { + // Move between sibling buttons. + if (index > 0) { + $button.insertBefore($container.children().eq(index - 1)); + } + // Move between button groups and rows. + else { + // Move between button groups. + $group = $container.parent().prev(); + if ($group.length > 0) { + $group.find('.ckeditor-toolbar-group-buttons').append($button); + } + // Wrap between rows. + else { + $container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-group').not('.placeholder').find('.ckeditor-toolbar-group-buttons').eq(-1).append($button); + } + } + } + // Move right. + else if (_.indexOf([39, 63235], event.keyCode) > -1) { + // Move between sibling buttons. + if (index < ($siblings.length - 1)) { + $button.insertAfter($container.children().eq(index + 1)); + } + // Move between button groups. Moving right at the end of a row + // will create a new group. + else { + $container.parent().next().find('.ckeditor-toolbar-group-buttons').prepend($button); + } + } + } + // Move buttons between rows and the available button set. + else if (_.indexOf(upDownKeys, event.keyCode) > -1) { + dir = (_.indexOf([38, 63232], event.keyCode) > -1) ? 'prev' : 'next'; + $row = $container.closest('.ckeditor-row')[dir](); + // Move the button back into the available button set. + if (dir === 'prev' && $row.length === 0) { + // If this is a divider, just destroy it. + if ($button.data('drupal-ckeditor-type') === 'separator') { + $button + .off() + .remove(); + // Focus on the first button in the active toolbar. + $activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).children().eq(0).children().focus(); + } + // Otherwise, move it. + else { + $availableButtons.prepend($button); + } + } + else { + $row.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button); + } + } + } + // Move dividers between their container and the active toolbar. + else if (containerType === 'dividers') { + // Move the button to the active toolbar configuration when the down or + // up keys are pressed. + if (_.indexOf([40, 63233], event.keyCode) > -1) { + // Move the button to the first row, first button group index + // position. + $button = $button.clone(true); + $activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button); + $target = $button.children(); } + } + + view = this; + // Attempt to move the button to the new toolbar position. + registerButtonMove(this, $button, function (result) { - if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) { - // Default toolbar buttons are in fact "added features". - $activeToolbar.trigger('CKEditorToolbarChanged', ['added', existingButtons[i]]); + // Put the button back if the registration failed. + // If the button was in a row, then it was in the active toolbar + // configuration. The button was probably placed in a new group, but + // that action was canceled. + if (!result && $originalGroup) { + $originalGroup.find('.ckeditor-buttons').append($button); } + // Otherwise refresh the sortables to acknowledge the new button + // positions. else { - // Move the button element from the active the active toolbar to the - // list of available buttons. - var $button = $('.ckeditor-toolbar-active > ul > li[data-button-name="' + button + '"]') - .detach() - .appendTo('.ckeditor-toolbar-disabled > ul'); - // Update the toolbar value field. - adminToolbarValue({}, { silent: true, item: $button}); + view.$el.find('.ui-sortable').sortable('refresh'); + } + // Refocus the target button so that the user can continue from a known + // place. + $target.focus(); + }); + + event.preventDefault(); + event.stopPropagation(); + } + }, + + /** + * Handles keypresses on a CKEditor configuration group. + * + * @param jQuery.Event event + */ + onPressGroup: function (event) { + var upDownKeys = [ + 38, // Up arrow. + 63232, // Safari up arrow. + 40, // Down arrow. + 63233 // Safari down arrow. + ]; + var leftRightKeys = [ + 37, // Left arrow. + 63234, // Safari left arrow. + 39, // Right arrow. + 63235 // Safari right arrow. + ]; + + // Respond to an enter key press. + if (event.keyCode === 13) { + openGroupNameDialog(this, $(event.currentTarget)); + event.preventDefault(); + event.stopPropagation(); + } + + // Respond to direction key presses. + if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) { + var $group = $(event.currentTarget); + var $container = $group.parent(); + var $siblings = $container.children(); + var index, dir; + // Move groups between sibling groups. + if (_.indexOf(leftRightKeys, event.keyCode) > -1) { + index = $siblings.index($group); + // Move left between sibling groups. + if ((_.indexOf([37, 63234], event.keyCode) > -1)) { + if (index > 0) { + $group.insertBefore($siblings.eq(index - 1)); + } + // Wrap between rows. Insert the group before the placeholder group + // at the end of the previous row. + else { + $group.insertBefore($container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-groups').children().eq(-1)); + } } + // Move right between sibling groups. + else if (_.indexOf([39, 63235], event.keyCode) > -1) { + // Move to the right if the next group is not a placeholder. + if (!$siblings.eq(index + 1).hasClass('placeholder')) { + $group.insertAfter($container.children().eq(index + 1)); + } + // Wrap group between rows. + else { + $container.closest('.ckeditor-row').next().find('.ckeditor-toolbar-groups').prepend($group); + } + } + } - }); - } - }, - detach: function (context, settings, trigger) { - // Early-return if the trigger for detachment is something else than unload. - if (trigger !== 'unload') { - return; + // Move groups between rows. + else if (_.indexOf(upDownKeys, event.keyCode) > -1) { + dir = (_.indexOf([38, 63232], event.keyCode) > -1) ? 'prev' : 'next'; + $group.closest('.ckeditor-row')[dir]().find('.ckeditor-toolbar-groups').eq(0).prepend($group); + } + + registerGroupMove(this, $group); + $group.focus(); + event.preventDefault(); + event.stopPropagation(); + } } + }), - // We're detaching because CKEditor as text editor has been disabled; this - // really means that all CKEditor toolbar buttons have been removed. Hence, - // all editor features will be removed, so any reactions from filters will - // be undone. - var $ckeditorToolbar = $(context).find('.ckeditor-toolbar-configuration.ckeditor-toolbar-processed'); - if ($ckeditorToolbar.length) { - var value = $ckeditorToolbar - .find('.form-item-editor-settings-toolbar-buttons') - .find('textarea') - .val(); - var $activeToolbar = $ckeditorToolbar.find('.ckeditor-toolbar-active'); - var buttons = _.unique(_.flatten(JSON.parse(value))); - for (var i = 0; i < buttons.length; i++) { - $activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]); + /** + * Backbone View for CKEditor toolbar configuration; aural UX (output only). + */ + ConfigurationAuralView: Backbone.View.extend({ + + events: { + 'click .ckeditor-buttons a': 'announceButtonHelp', + 'click .ckeditor-multiple-buttons a': 'announceSeparatorHelp', + 'focus .ckeditor-button a': 'onFocus', + 'focus .ckeditor-button-separator a': 'onFocus', + 'focus .ckeditor-toolbar-group': 'onFocus' + }, + + /** + * {@inheritdoc} + */ + initialize: function () { + // Announce the button and group positions when the model is no longer + // dirty. + this.model.on('change:isDirty', this.announceMove, this); + }, + + /** + * Calls announce on buttons and groups when their position is changed. + * + * @param Drupal.ckeditor.ConfigurationModel model + * @param Boolean isDirty + * A model attribute that indicates if the changed toolbar configuration + * has been stored or not. + */ + announceMove: function (model, isDirty) { + // Announce the position of a button or group after the model has been + // updated. + if (!isDirty) { + var item = document.activeElement || null; + if (item) { + var $item = $(item); + if ($item.hasClass('ckeditor-toolbar-group')) { + this.announceButtonGroupPosition($item); + } + else if ($item.parent().hasClass('ckeditor-button')) { + this.announceButtonPosition($item.parent()); + } + } + } + }, + + /** + * Handles the focus event of elements in the active and available toolbars. + * + * @param jQuery.Event event + */ + onFocus: function (event) { + event.stopPropagation(); + + var $originalTarget = $(event.target); + var $currentTarget = $(event.currentTarget); + var $parent = $currentTarget.parent(); + if ($parent.hasClass('ckeditor-button') || $parent.hasClass('ckeditor-button-separator')) { + this.announceButtonPosition($currentTarget.parent()); + } + else if ($originalTarget.attr('role') !== 'button' && $currentTarget.hasClass('ckeditor-toolbar-group')) { + this.announceButtonGroupPosition($currentTarget); + } + }, + + /** + * Announces the current position of a button group. + * + * @param jQuery $group + * A jQuery set that contains an li element that wraps a group of buttons. + */ + announceButtonGroupPosition: function ($group) { + var $groups = $group.parent().children(); + var $row = $group.closest('.ckeditor-row'); + var $rows = $row.parent().children(); + var text = Drupal.t('"@groupName" button group in position @position of @positionCount in row @row of @rowCount', { + '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'), + '@position': $groups.index($group) + 1, + '@positionCount': $groups.not('.placeholder').length, + '@row': $rows.index($row) + 1, + '@rowCount': $rows.not('.placeholder').length + }); + Drupal.announce(text, 'assertive'); + }, + + + /** + * Announces current button position. + * + * @param jQuery $button + * A jQuery set that contains an li element that wraps a button. + */ + announceButtonPosition: function ($button) { + var $row = $button.closest('.ckeditor-row'); + var $rows = $row.parent().children(); + var $buttons = $button.closest('.ckeditor-buttons').children(); + var $group = $button.closest('.ckeditor-toolbar-group'); + // The name of the button separator is 'button separator' and its type + // is 'separator', so we do not want to print the type of this item, + // otherwise the UA will speak 'button separator separator'. + var type = ($button.attr('data-drupal-ckeditor-type') === 'separator') ? '' : Drupal.t('button'); + var text; + // The button is located in the available button set. + if ($button.closest('.ckeditor-toolbar-disabled').length > 0) { + text = Drupal.t('@name @type.', { + '@name': $button.children().attr('aria-label'), + '@type': type + }); + text += "\n" + Drupal.t('Press the down arrow key to activate.'); + + Drupal.announce(text, 'assertive'); + } + // The button is in the active toolbar. + else if ($group.not('.placeholder').length === 1) { + text = Drupal.t('@name @type in position @position of @positionCount in "@groupName" button group in row @row of @rowCount.', { + '@name': $button.children().attr('aria-label'), + '@type': type, + '@position': $buttons.index($button) + 1, + '@positionCount': $buttons.length, + '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'), + '@row': $rows.index($row) + 1, + '@rowCount': $rows.not('.placeholder').length + }); + Drupal.announce(text, 'assertive'); } + }, + + /** + * Provides help information when a button is clicked. + * + * @param jQuery.Event event + */ + announceButtonHelp: function (event) { + var $link = $(event.currentTarget); + var $button = $link.parent(); + var enabled = $button.closest('.ckeditor-toolbar-active').length > 0; + var message; + + if (enabled) { + message = Drupal.t('The "@name" button is currently enabled.', { + '@name': $link.attr('aria-label') + }); + message += "\n" + Drupal.t('Use the keyboard arrow keys to change the position of this button.'); + message += "\n" + Drupal.t('Press the up arrow key on the top row to disable the button.'); + } + else { + message = Drupal.t('The "@name" button is currently disabled.', { + '@name': $link.attr('aria-label') + }); + message += "\n" + Drupal.t('Use the down arrow key to move this button into the active toolbar.'); + } + Drupal.announce(message); + event.preventDefault(); + }, + + /** + * Provides help information when a separator is clicked. + * + * @param jQuery.Event event + */ + announceSeparatorHelp: function (event) { + var $link = $(event.currentTarget); + var $button = $link.parent(); + var enabled = $button.closest('.ckeditor-toolbar-active').length > 0; + var message; + + if (enabled) { + message = Drupal.t('This @name is currently enabled.', { + '@name': $link.attr('aria-label') + }); + message += "\n" + Drupal.t('Use the keyboard arrow keys to change the position of this separator.'); + } + else { + message = Drupal.t('Separators are used to visually split individual buttons.'); + message += "\n" + Drupal.t('This @name is currently disabled.', { + '@name': $link.attr('aria-label') + }); + message += "\n" + Drupal.t('Use the down arrow key to move this separator into the active toolbar.'); + message += "\n" + Drupal.t('You may add multiple separators to each button group.'); + } + Drupal.announce(message); + event.preventDefault(); } - } + }) }; /** - * Returns a string describing the type and index of a toolbar row. + * Translates a change in CKEditor config DOM structure into the config model. * - * @param {jQuery} $row - * A jQuery object containing a .ckeditor-button row. + * If the button is moved within an existing group, the DOM structure is simply + * translated to a configuration model. If the button is moved into a new group + * placeholder, then a process is launched to name that group before the button + * move is translated into configuration. * - * @return {String} - * A string describing the type and index of a toolbar row. + * @param Backbone.View view + * The Backbone View that invoked this function. + * @param jQuery $button + * A jQuery set that contains an li element that wraps a button element. + * @param function callback + * A callback to invoke after the button group naming modal dialog has been + * closed. */ -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}); +function registerButtonMove (view, $button, callback) { + var $group = $button.closest('.ckeditor-toolbar-group'); + + // If dropped in a placeholder button group, the user must name it. + if ($group.hasClass('placeholder')) { + + if (view.isProcessing) { + event.stopPropagation(); + return; + } + view.isProcessing = true; + + openGroupNameDialog(view, $group, callback); } else { - row = $('.ckeditor-toolbar-active').find('.ckeditor-buttons').index($row) + 1; - output += Drupal.t('active button row @row', {'@row': row}); + view.model.set('isDirty', true); + callback(true); } - return output; } /** - * Applies or removes the focused class to a toolbar row. + * Translates a change in CKEditor config DOM structure into the config model. + * + * Each row has a placeholder group at the end of the row. A user may not move + * an existing button group past the placeholder group at the end of a 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 Backbone.View view + * The Backbone View that invoked this function. + * @param jQuery $group + * A jQuery set that contains an li element that wraps a group of buttons. + */ +function registerGroupMove (view, $group) { + // Remove placeholder classes if necessary. + var $row = $group.closest('.ckeditor-row'); + if ($row.hasClass('placeholder')) { + $row.removeClass('placeholder'); + } + // If there are any rows with just a placeholder group, mark the row as a + // placeholder. + $row.parent().children().each(function () { + var $row = $(this); + if ($row.find('.ckeditor-toolbar-group').not('.placeholder').length === 0) { + $row.addClass('placeholder'); + } + }); + view.model.set('isDirty', true); +} + +/** + * Opens a Drupal dialog with a form for changing the title of a button group. * - * @param {jQuery} event - * A jQuery event. + * @param Backbone.View view + * The Backbone View that invoked this function. + * @param jQuery $group + * A jQuery set that contains an li element that wraps a group of buttons. + * @param function callback + * A callback to invoke after the button group naming modal dialog has been + * closed. */ -function grantRowFocus (event) { - var $row = $(event.currentTarget); - // 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)})); +function openGroupNameDialog (view, $group, callback) { + callback = callback || function () {}; + + /** + * Validates the string provided as a button group title. + * + * @param DOM form + * The form DOM element that contains the input with the new button group + * title string. + * @return Boolean + * Returns true when an error exists, otherwise returns false. + */ + function validateForm (form) { + if (form.elements[0].value.length === 0) { + var $form = $(form); + if (!$form.hasClass('errors')) { + $form + .addClass('errors') + .find('input') + .addClass('error') + .attr('aria-invalid', 'true'); + $('
    ' + Drupal.t('Please provide a name for the button group.') + '
    ').insertAfter(form.elements[0]); + } + return true; + } + return false; + } + + /** + * Attempts to close the dialog; Validates user input. + * + * @param String action + * The dialog action chosen by the user: 'apply' or 'cancel'. + * @param DOM form + * The form DOM element that contains the input with the new button group + * title string. + */ + function closeDialog (action, form) { + + /** + * Closes the dialog when the user cancels or supplies valid data. + */ + function shutdown () { + dialog.close(action); + + // The processing marker can be deleted since the dialog has been closed. + delete view.isProcessing; + } + + /** + * Applies a string as the name of a CKEditor button group. + * + * @param jQuery $group + * A jQuery set that contains an li element that wraps a group of buttons. + * @param String name + * The new name of the CKEditor button group. + */ + function namePlaceholderGroup ($group, name) { + // If it's currently still a placeholder, then that means we're creating + // a new group, and we must do some extra work. + if ($group.hasClass('placeholder')) { + // Remove all whitespace from the name, lowercase it and ensure + // HTML-safe encoding, then use this as the group ID for CKEditor + // configuration UI accessibility purposes only. + var groupID = 'ckeditor-toolbar-group-aria-label-for-' + Drupal.checkPlain(name.toLowerCase().replace(/ /g,'-')); + $group + // Update the group container. + .removeAttr('aria-label') + .attr('data-drupal-ckeditor-type', 'group') + .attr('tabindex', 0) + // Update the group heading. + .children('.ckeditor-toolbar-group-name') + .attr('id', groupID) + .end() + // Update the group items. + .children('.ckeditor-toolbar-group-buttons') + .attr('aria-labelledby', groupID); + } + + $group + .attr('data-drupal-ckeditor-toolbar-group-name', name) + .children('.ckeditor-toolbar-group-name') + .text(name); + } + + // Invoke a user-provided callback and indicate failure. + if (action === 'cancel') { + shutdown(); + callback(false); + return; + } + + // Validate that a group name was provided. + if (form && validateForm(form)) { + return; + } + + // React to application of a valid group name. + if (action === 'apply') { + shutdown(); + // Apply the provided name to the button group label. + namePlaceholderGroup($group, Drupal.checkPlain(form.elements[0].value)); + // Remove placeholder classes so that new placeholders will be + // inserted. + $group.closest('.ckeditor-row.placeholder').addBack().removeClass('placeholder'); + + // Invoke a user-provided callback and indicate success. + callback(true); + + // Signal that the active toolbar DOM structure has changed. + view.model.set('isDirty', true); + } } + + // Create a Drupal dialog that will get a button group name from the user. + var dialog = Drupal.dialog(Drupal.theme('ckeditorButtonGroupNameForm'), { + title: Drupal.t('Button group name'), + dialogClass: 'ckeditor-name-toolbar-group', + resizable: false, + buttons: [ + { + text: Drupal.t('Apply'), + click: function () { + closeDialog('apply', this); + } + }, + { + text: Drupal.t('Cancel'), + click: function () { + closeDialog('cancel'); + } + } + ], + // Disable jQuery UI Dialog's escape handling so we can handle this + // ourselves, we want the 'cancel' button to be pressed. + closeOnEscape: false, + open: function () { + var form = this; + var $form = $(this); + var $widget = $form.parent(); + $widget.find('.ui-dialog-titlebar-close').remove(); + // Set a click handler on the input and button in the form. + $widget.on('keyup.ckeditor', 'input, button', function (event) { + // React to enter key press. + if (event.keyCode === 13) { + var $target = $(event.currentTarget); + var data = $target.data('ui-button'); + var action = 'apply'; + // Assume 'apply', but take into account that the user might have + // pressed the enter key on the dialog buttons. + if (data && data.options && data.options.label) { + action = data.options.label.toLowerCase(); + } + closeDialog(action, form); + event.stopPropagation(); + event.stopImmediatePropagation(); + event.preventDefault(); + } + // React to ESC key press. + else if (event.keyCode === 27) { + closeDialog('cancel', form); + event.stopPropagation(); + event.stopImmediatePropagation(); + event.preventDefault(); + } + }); + // Prevent the form from submitting. + $widget.on('keydown keypress', function () { + if (event.keyCode === 13) { + return false; + } + }); + // Announce to the user that a modal dialog is open. + var text = Drupal.t('Editing the name of the new button group in a dialog.'); + if ($group.attr('data-drupal-ckeditor-toolbar-group-name') !== undefined) { + text = Drupal.t('Editing the name of the "@groupName" button group in a dialog.', { + '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name') + }); + } + Drupal.announce(text); + }, + beforeClose: false, + close: function (event) { + // Automatically destroy the DOM element that was used for the dialog. + $(event.target).remove(); + } + }); + // A modal dialog is used because the user must provide a button group name + // or cancel the button placement before taking any other action. + dialog.showModal(); + + $(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input')) + // When editing, set the "group name" input in the form to the current value. + .attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name')) + // Focus on the "group name" input in the form. + .focus(); } -})(jQuery, Drupal, drupalSettings, CKEDITOR, _); +/** + * Themes a blank CKEditor row. + * + * @return String + */ +Drupal.theme.ckeditorRow = function () { + return '
  • '; +}; + +/** + * Themes a blank CKEditor button group. + * + * @return String + */ +Drupal.theme.ckeditorToolbarGroup = function () { + return ''; +}; + +/** + * Themes a form for changing the title of a CKEditor button group. + * + * @return String + */ +Drupal.theme.ckeditorButtonGroupNameForm = function () { + return '
    '; +}; + +/** + * Themes a button that will toggle the button group names in active config. + * + * @return String + */ +Drupal.theme.ckeditorButtonGroupNamesToggle = function () { + return ''; +}; + +})(jQuery, Drupal, _, CKEDITOR); diff --git a/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js index ec093ab..c65cacc 100644 --- a/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js +++ b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js @@ -19,7 +19,7 @@ Drupal.behaviors.ckeditorDrupalImageSettings = { $context .find('.ckeditor-toolbar-active') .on('CKEditorToolbarChanged.ckeditorDrupalImageSettings', function (e, action, button) { - if (button === 'DrupalImage') { + if (button === 'DrupalImage' && $drupalImageVerticalTab) { if (action === 'added') { $drupalImageVerticalTab.tabShow(); } diff --git a/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js index 84e6e0a..441ae03 100644 --- a/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js +++ b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js @@ -19,7 +19,7 @@ Drupal.behaviors.ckeditorStylesComboSettings = { $context .find('.ckeditor-toolbar-active') .on('CKEditorToolbarChanged.ckeditorStylesComboSettings', function (e, action, button) { - if (button === 'Styles') { + if (button === 'Styles' && $stylesComboVerticalTab) { if (action === 'added') { $stylesComboVerticalTab.tabShow(); } diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php index 636d82b..fb97d9b 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php @@ -65,7 +65,14 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac */ public function getEnabledPluginFiles(Editor $editor, $include_internal_plugins = FALSE) { $plugins = array_keys($this->getDefinitions()); - $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); + // Flatten each row. + $toolbar_rows = array(); + foreach ($editor->settings['toolbar']['rows'] as $row_number => $row) { + $toolbar_rows[] = array_reduce($editor->settings['toolbar']['rows'][$row_number], function (&$result, $button_group) { + return array_merge($result, $button_group['items']); + }, array()); + } + $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($toolbar_rows)); $enabled_plugins = array(); $additional_plugins = array(); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php index 8fb20fe..3236690 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImageCaption.php @@ -76,12 +76,21 @@ function isEnabled(Editor $editor) { // Automatically enable this plugin if the text format associated with this // text editor uses the filter_caption filter and the DrupalImage button is // enabled. + // @todo, This parsing of the settings structure should be provided by a + // method on the CKEditor, but $editor here is not a CKEditor, it's a + // generic editor. I'm not sure how to get a reference to the CKEditor. if (isset($filters['filter_caption']) && $filters['filter_caption']->status) { - foreach ($editor->settings['toolbar']['buttons'] as $row) { - if (in_array('DrupalImage', $row)) { - return TRUE; + $enabled = FALSE; + foreach ($editor->settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + foreach ($group['items'] as $button) { + if ($button === 'DrupalImage') { + $enabled = TRUE; + } + } } } + return $enabled; } return FALSE; diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php index 75a97bf..3f86178 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php @@ -55,7 +55,13 @@ public function getConfig(Editor $editor) { $config['allowedContent'] = $this->generateAllowedContentSetting($editor); // Add the format_tags setting, if its button is enabled. - $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); + $toolbar_rows = array(); + foreach ($editor->settings['toolbar']['rows'] as $row_number => $row) { + $toolbar_rows[] = array_reduce($editor->settings['toolbar']['rows'][$row_number], function (&$result, $button_group) { + return array_merge($result, $button_group['items']); + }, array()); + } + $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($toolbar_rows)); if (in_array('Format', $toolbar_buttons)) { $config['format_tags'] = $this->generateFormatTagsSetting($editor); } @@ -220,17 +226,14 @@ public function getButtons() { '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, - ), + // No plugin, separator "button" for toolbar builder UI use only. '-' => array( 'label' => t('Separator'), 'image_alternative' => '', - 'attributes' => array('class' => array('ckeditor-button-separator')), + 'attributes' => array( + 'class' => array('ckeditor-button-separator'), + 'data-drupal-ckeditor-type' => 'separator', + ), 'multiple' => TRUE, ), ); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php index 0465cdf..2f69df6 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php @@ -94,13 +94,29 @@ public static function create(ContainerInterface $container, array $configuratio public function getDefaultSettings() { return array( 'toolbar' => array( - 'buttons' => array( + 'rows' => array( + // Button groups array( - 'Bold', 'Italic', - '|', 'DrupalLink', 'DrupalUnlink', - '|', 'BulletedList', 'NumberedList', - '|', 'Blockquote', 'DrupalImage', - '|', 'Source', + array( + 'name' => t('Formatting'), + 'items' => array('Bold', 'Italic',), + ), + array( + 'name' => t('Links'), + 'items' => array('DrupalLink', 'DrupalUnlink',), + ), + array( + 'name' => t('Lists'), + 'items' => array('BulletedList', 'NumberedList',), + ), + array( + 'name' => t('Media'), + 'items' => array('Blockquote', 'DrupalImage',), + ), + array( + 'name' => t('Tools'), + 'items' => array('Source',), + ), ), ), ), @@ -132,10 +148,11 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit ), '#attributes' => array('class' => array('ckeditor-toolbar-configuration')), ); - $form['toolbar']['buttons'] = array( + + $form['toolbar']['button_groups'] = array( '#type' => 'textarea', '#title' => t('Toolbar buttons'), - '#default_value' => json_encode($editor->settings['toolbar']['buttons']), + '#default_value' => json_encode($editor->settings['toolbar']['rows']), '#attributes' => array('class' => array('ckeditor-toolbar-textarea')), ); @@ -174,8 +191,17 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit 'format' => '', 'editor' => 'ckeditor', 'settings' => array( - // Single toolbar row that contains all existing buttons. - 'toolbar' => array('buttons' => array(0 => $all_buttons)), + // Single toolbar row, single button group, all existing buttons. + 'toolbar' => array( + 'rows' => array( + 0 => array( + 0 => array( + 'name' => 'All existing buttons', + 'items' => $all_buttons, + ) + ) + ), + ), 'plugins' => $editor->settings['plugins'], ), )); @@ -209,7 +235,10 @@ public function settingsFormSubmit(array $form, array &$form_state) { // editor_form_filter_admin_format_submit(). $toolbar_settings = &$form_state['values']['editor']['settings']['toolbar']; - $toolbar_settings['buttons'] = json_decode($toolbar_settings['buttons'], FALSE); + // The rows key is not built into the form structure, so decode the button + // groups data into this new key and remove the button_groups key. + $toolbar_settings['rows'] = json_decode($toolbar_settings['button_groups'], TRUE); + unset($toolbar_settings['button_groups']); // Remove the plugin settings' vertical tabs state; no need to save that. if (isset($form_state['values']['editor']['settings']['plugins'])) { @@ -349,22 +378,12 @@ public function getLibraries(EditorEntity $editor) { */ public function buildToolbarJSSetting(EditorEntity $editor) { $toolbar = array(); - foreach ($editor->settings['toolbar']['buttons'] as $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; - } + foreach ($editor->settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + $toolbar[] = $group; } - $toolbar[] = $button_group; $toolbar[] = '/'; } - return $toolbar; } diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php index ce2e0e9..f984160 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php @@ -77,13 +77,29 @@ function testAdmin() { // Ensure the CKEditor editor returns the expected default settings. $expected_default_settings = array( 'toolbar' => array( - 'buttons' => array( + 'rows' => array( + // Button groups array( - 'Bold', 'Italic', - '|', 'DrupalLink', 'DrupalUnlink', - '|', 'BulletedList', 'NumberedList', - '|', 'Blockquote', 'DrupalImage', - '|', 'Source', + array( + 'name' => t('Formatting'), + 'items' => array('Bold', 'Italic',), + ), + array( + 'name' => t('Links'), + 'items' => array('DrupalLink', 'DrupalUnlink',), + ), + array( + 'name' => t('Lists'), + 'items' => array('BulletedList', 'NumberedList',), + ), + array( + 'name' => t('Media'), + 'items' => array('Blockquote', 'DrupalImage',), + ), + array( + 'name' => t('Tools'), + 'items' => array('Source',), + ), ), ), ), @@ -98,8 +114,8 @@ function testAdmin() { // Ensure the toolbar buttons configuration value is initialized to the // expected default value. - $expected_buttons_value = json_encode($expected_default_settings['toolbar']['buttons']); - $this->assertFieldByName('editor[settings][toolbar][buttons]', $expected_buttons_value); + $expected_buttons_value = json_encode($expected_default_settings['toolbar']['rows']); + $this->assertFieldByName('editor[settings][toolbar][button_groups]', $expected_buttons_value); // Ensure the styles textarea exists and is initialized empty. $styles_textarea = $this->xpath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]'); @@ -131,12 +147,13 @@ function testAdmin() { // done via drag and drop, but here we can only emulate the end result of // that interaction). Test multiple toolbar rows and a divider within a row. $this->drupalGet('admin/config/content/formats/manage/filtered_html'); - $expected_settings['toolbar']['buttons'] = array( - array('Undo', '|', 'Redo'), - array('JustifyCenter'), + $expected_settings['toolbar']['rows'][0][] = array( + 'name' => 'Action history', + 'items' => array('Undo', '|', 'Redo'), + array('JustifyCenter') ); $edit = array( - 'editor[settings][toolbar][buttons]' => json_encode($expected_settings['toolbar']['buttons']), + 'editor[settings][toolbar][button_groups]' => json_encode($expected_settings['toolbar']['rows']), ); $this->drupalPostForm(NULL, $edit, t('Save configuration')); $editor = entity_load('editor', 'filtered_html'); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php index e376df8..db1c6a4 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php @@ -103,8 +103,8 @@ function testEnabledPlugins() { // cause the LlamaContextual and LlamaContextualAndButton plugins to be // enabled. Finally, we will add the "Strike" button back again, which would // cause all three plugins to be enabled. - $original_toolbar = $editor->settings['toolbar']['buttons'][0]; - $editor->settings['toolbar']['buttons'][0][] = 'Llama'; + $original_toolbar = $editor->settings['toolbar']; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Llama'; $editor->save(); $file = array(); $file['b'] = 'core/modules/ckeditor/tests/modules/js/llama_button.js'; @@ -113,13 +113,13 @@ function testEnabledPlugins() { $expected = $enabled_plugins + array('llama_button' => $file['b'], 'llama_contextual_and_button' => $file['cb']); $this->assertIdentical($expected, $this->manager->getEnabledPluginFiles($editor), 'The LlamaButton and LlamaContextualAndButton plugins are enabled.'); $this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPluginFiles($editor, TRUE), 'The LlamaButton and LlamaContextualAndButton plugins are enabled.'); - $editor->settings['toolbar']['buttons'][0] = $original_toolbar; - $editor->settings['toolbar']['buttons'][0][] = 'Strike'; + $editor->settings['toolbar'] = $original_toolbar; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Strike'; $editor->save(); $expected = $enabled_plugins + array('llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb']); $this->assertIdentical($expected, $this->manager->getEnabledPluginFiles($editor), 'The LLamaContextual and LlamaContextualAndButton plugins are enabled.'); $this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPluginFiles($editor, TRUE), 'The LlamaContextual and LlamaContextualAndButton plugins are enabled.'); - $editor->settings['toolbar']['buttons'][0][] = 'Llama'; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Llama'; $editor->save(); $expected = $enabled_plugins + array('llama_button' => $file['b'], 'llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb']); $this->assertIdentical($expected, $this->manager->getEnabledPluginFiles($editor), 'The LlamaButton, LlamaContextual and LlamaContextualAndButton plugins are enabled.'); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php index 782d590..e4e821f 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php @@ -109,12 +109,11 @@ function testGetJSSettings() { $this->container->get('plugin.manager.editor')->clearCachedDefinitions(); $this->ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor'); $this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions(); - $editor->settings['toolbar']['buttons'][0][] = 'Strike'; - $editor->settings['toolbar']['buttons'][1][] = 'Format'; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Strike'; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Format'; $editor->save(); - $expected_config['toolbar'][count($expected_config['toolbar'])-2]['items'][] = 'Strike'; - $expected_config['toolbar'][]['items'][] = 'Format'; - $expected_config['toolbar'][] = '/'; + $expected_config['toolbar'][0]['items'][] = 'Strike'; + $expected_config['toolbar'][0]['items'][] = 'Format'; $expected_config['format_tags'] = 'p;h4;h5;h6'; $expected_config['extraPlugins'] .= ',llama_contextual,llama_contextual_and_button'; $expected_config['drupalExternalPlugins']['llama_contextual'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js'); @@ -208,17 +207,20 @@ function testBuildToolbarJSSetting() { $this->assertIdentical($expected, $this->ckeditor->buildToolbarJSSetting($editor), '"toolbar" configuration part of JS settings built correctly for default toolbar.'); // Customize the configuration. - $editor->settings['toolbar']['buttons'][0][] = 'Strike'; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Strike'; $editor->save(); - $expected[count($expected)-2]['items'][] = 'Strike'; + $expected[0]['items'][] = 'Strike'; $this->assertIdentical($expected, $this->ckeditor->buildToolbarJSSetting($editor), '"toolbar" configuration part of JS settings built correctly for customized toolbar.'); // Enable the editor_test module, customize further. $this->enableModules(array('ckeditor_test')); $this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions(); - $editor->settings['toolbar']['buttons'][0][] = 'Llama'; + // Override the label of a toolbar component. + $editor->settings['toolbar']['rows'][0][0]['name'] = 'JunkScience'; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Llama'; $editor->save(); - $expected[count($expected)-2]['items'][] = 'Llama'; + $expected[0]['name'] = 'JunkScience'; + $expected[0]['items'][] = 'Llama'; $this->assertIdentical($expected, $this->ckeditor->buildToolbarJSSetting($editor), '"toolbar" configuration part of JS settings built correctly for customized toolbar with contrib module-provided CKEditor plugin.'); } @@ -253,7 +255,7 @@ function testInternalGetConfig() { $this->assertIdentical($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for default toolbar.'); // Format dropdown/button enabled: new setting should be present. - $editor->settings['toolbar']['buttons'][0][] = 'Format'; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Format'; $expected['format_tags'] = 'p;h4;h5;h6'; $this->assertIdentical($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for customized toolbar.'); } @@ -266,7 +268,7 @@ function testStylesComboGetConfig() { $stylescombo_plugin = $this->container->get('plugin.manager.ckeditor.plugin')->createInstance('stylescombo'); // Styles dropdown/button enabled: new setting should be present. - $editor->settings['toolbar']['buttons'][0][] = 'Styles'; + $editor->settings['toolbar']['rows'][0][0]['items'][] = 'Styles'; $editor->settings['plugins']['stylescombo']['styles'] = ''; $editor->save(); $expected['stylesSet'] = array(); @@ -367,12 +369,27 @@ protected function getDefaultAllowedContentConfig() { protected function getDefaultToolbarConfig() { return array( - 0 => array('items' => array('Bold', 'Italic')), - 1 => array('items' => array('DrupalLink', 'DrupalUnlink')), - 2 => array('items' => array('BulletedList', 'NumberedList')), - 3 => array('items' => array('Blockquote', 'DrupalImage')), - 4 => array('items' => array('Source')), - 5 => '/' + array( + 'name' => t('Formatting'), + 'items' => array('Bold', 'Italic',), + ), + array( + 'name' => t('Links'), + 'items' => array('DrupalLink', 'DrupalUnlink',), + ), + array( + 'name' => t('Lists'), + 'items' => array('BulletedList', 'NumberedList',), + ), + array( + 'name' => t('Media'), + 'items' => array('Blockquote', 'DrupalImage',), + ), + array( + 'name' => t('Tools'), + 'items' => array('Source',), + ), + '/', ); } diff --git a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextual.php b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextual.php index e2fb4b9..d73f910 100644 --- a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextual.php +++ b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextual.php @@ -28,9 +28,11 @@ class LlamaContextual extends Llama implements CKEditorPluginContextualInterface */ function isEnabled(Editor $editor) { // Automatically enable this plugin if the Underline button is enabled. - foreach ($editor->settings['toolbar']['buttons'] as $row) { - if (in_array('Strike', $row)) { - return TRUE; + foreach ($editor->settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + if (in_array('Strike', $group['items'])) { + return TRUE; + } } } return FALSE; diff --git a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextualAndButton.php b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextualAndButton.php index 87ec5a7..db6229e 100644 --- a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextualAndButton.php +++ b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/LlamaContextualAndButton.php @@ -31,9 +31,11 @@ class LlamaContextualAndButton extends Llama implements CKEditorPluginContextual */ function isEnabled(Editor $editor) { // Automatically enable this plugin if the Strike button is enabled. - foreach ($editor->settings['toolbar']['buttons'] as $row) { - if (in_array('Strike', $row)) { - return TRUE; + foreach ($editor->settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + if (in_array('Strike', $group['items'])) { + return TRUE; + } } } return FALSE; diff --git a/core/profiles/standard/config/editor.editor.basic_html.yml b/core/profiles/standard/config/editor.editor.basic_html.yml index e340685..e5e7d8f 100644 --- a/core/profiles/standard/config/editor.editor.basic_html.yml +++ b/core/profiles/standard/config/editor.editor.basic_html.yml @@ -2,21 +2,33 @@ format: basic_html editor: ckeditor settings: toolbar: - buttons: + rows: - - - Bold - - Italic - - '|' - - DrupalLink - - DrupalUnlink - - '|' - - BulletedList - - NumberedList - - '|' - - Blockquote - - DrupalImage - - '|' - - Source + - + name: Formatting + items: + - Bold + - Italic + - + name: Linking + items: + - DrupalLink + - DrupalUnlink + - + name: Lists + items: + - BulletedList + - NumberedList + - + - + name: Media + items: + - Blockquote + - DrupalImage + - + name: Tools + items: + - Source plugins: stylescombo: styles: '' @@ -29,4 +41,4 @@ image_upload: width: '' height: '' status: '1' -langcode: und +langcode: en diff --git a/core/profiles/standard/config/editor.editor.full_html.yml b/core/profiles/standard/config/editor.editor.full_html.yml index eb01c1c..3a61cf8 100644 --- a/core/profiles/standard/config/editor.editor.full_html.yml +++ b/core/profiles/standard/config/editor.editor.full_html.yml @@ -2,31 +2,45 @@ format: full_html editor: ckeditor settings: toolbar: - buttons: + rows: - - - Bold - - Italic - - Strike - - Superscript - - Subscript - - - - - RemoveFormat - - '|' - - DrupalLink - - DrupalUnlink - - '|' - - BulletedList - - NumberedList - - '|' - - Blockquote - - DrupalImage - - Table - - HorizontalRule - - '|' - - Format - - '|' - - ShowBlocks - - Source + - + name: Formatting + items: + - Bold + - Italic + - Strike + - Superscript + - Subscript + - - + - RemoveFormat + - + name: Linking + items: + - DrupalLink + - DrupalUnlink + - + name: Lists + items: + - BulletedList + - NumberedList + - + name: Media + items: + - Blockquote + - DrupalImage + - Table + - HorizontalRule + - + name: Block Formatting + items: + - Format + - + name: Tools + items: + - ShowBlocks + - Source + plugins: stylescombo: styles: '' @@ -39,4 +53,4 @@ image_upload: width: '' height: '' status: '1' -langcode: und +langcode: en