diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc index e99518a..cac6949 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; } /** @@ -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['data-drupal-ckeditor-toolbar-group'] = $button['group']; + } if (!empty($button['attributes'])) { $button_item = array_merge($button_item, $button['attributes']); } @@ -118,6 +134,19 @@ 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,44 @@ 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. Use dividers to create button groups. Buttons may be moved with the mouse or keyboard arrow keys. Create a new toolbar palette by placing a button in the placeholder group at the end of a row.') . '
    '; $output .= '
    '; $output .= '
    '; + // Dividers. $output .= ''; - $output .= '
      '; + $output .= '
        '; $output .= $print_buttons($multiple_buttons); $output .= '
      '; $output .= '
    '; + // Available buttons. $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 aceb3a8..6e2796d 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..dc22281 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: '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..f93103f 100644 --- a/core/modules/ckeditor/css/ckeditor.admin.css +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -6,61 +6,81 @@ * "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; +} +.ckeditor-toolbar-group.placeholder .ckeditor-toolbar-group-name { + font-style: italic; +} +.ckeditor-toolbar-group-name { + display: block; +} +.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: -moz-linear-gradient(top, whiteSmoke, #cfd1cf); background-image: -o-linear-gradient(top, whiteSmoke, #cfd1cf); - background-image: -ms-linear-gradient(top, whiteSmoke, #cfd1cf); background-image: linear-gradient(top, whiteSmoke, #cfd1cf); - filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#fff5f5f5', endColorstr='#ffcfd1cf'); margin: 5px 0; overflow: nowrap; } -.ckeditor-toolbar-active > ul { - clear: left; /* LTR */ +.ckeditor-toolbar ul, +.ckeditor-toolbar-disabled ul { + list-style: none; + margin: 0; + padding: 0; +} +.ckeditor-toolbar .ckeditor-toolbar-group { float: left; /* LTR */ } -[dir="rtl"] .ckeditor-toolbar-active > ul { - clear: right; +[dir="rtl"] .ckeditor-toolbar .ckeditor-toolbar-group { float: right; } +.ckeditor-toolbar .ckeditor-toolbar-group > li { + border: 1px solid white; + border-radius: 5px; + 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 */ -} -[dir="rtl"] .ckeditor-toolbar-dividers { - float: left; -} -.ckeditor-toolbar-disabled ul.ckeditor-buttons { - border: 0; -} -.ckeditor-toolbar-disabled ul.ckeditor-buttons li { - margin: 2px; -} -.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; } +.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 { display: inline-block; @@ -91,14 +111,21 @@ ul.ckeditor-buttons li a { background-image: linear-gradient(top,white,#e4e4e4); filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffffff',endColorstr='#ffe4e4e4'); } +.ckeditor-toolbar-dividers { + float: right; /* LTR */ +} +[dir="rtl"] .ckeditor-toolbar-dividers { + float: left; +} ul.ckeditor-buttons li .cke-icon-only { text-indent: -9999px; width: 16px; } ul.ckeditor-buttons li a:focus, +ul.ckeditor-buttons li a:active, ul.ckeditor-multiple-buttons li a:focus { + outline: thick dotted blue; z-index: 11; /* Ensure focused buttons show their outline on all sides. */ - outline: 1px dotted #333; } ul.ckeditor-buttons li:first-child a { border-top-left-radius: 2px; /* LTR */ diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js index 338ae6e..70f009d 100644 --- a/core/modules/ckeditor/js/ckeditor.admin.js +++ b/core/modules/ckeditor/js/ckeditor.admin.js @@ -1,207 +1,170 @@ +/** + * @file + * + * CKEditor button and group configuration user interface. + */ (function ($, Drupal, drupalSettings, 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; - /** - * 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]); - } + // 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'); - 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 }); + // 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); + + // 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 confiugration Views. + var viewDefaults = { + model: model, + el: $('.ckeditor-toolbar-configuration') } - event.preventDefault(); + Drupal.ckeditor.views.controller = new Drupal.ckeditor.ConfigurationController(viewDefaults); + Drupal.ckeditor.views.visualView = new Drupal.ckeditor.ConfigurationVisualView(viewDefaults); + Drupal.ckeditor.views.keyboardView = new Drupal.ckeditor.ConfigurationKeyboardView(viewDefaults); + Drupal.ckeditor.views.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; +/** + * Toolbar 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. + */ + ConfigurationModel: Backbone.Model.extend({ + isDirty: false, + $textarea: null, + activeEditorConfig: null, + hiddenEditorConfig: null, + featuresMetadata: null, + 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.sortAvailableFeatures.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. + * */ - 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. @@ -212,7 +175,7 @@ Drupal.behaviors.ckeditorAdmin = { * must be provided that will receive a hash of Drupal.EditorFeature * features keyed by feature (button) name. */ - function getCKEditorFeatures(CKEditorConfig, callback) { + getCKEditorFeatures: function (CKEditorConfig, callback) { var getProperties = function (CKEPropertiesList) { return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : []; }; @@ -249,8 +212,9 @@ Drupal.behaviors.ckeditorAdmin = { 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,13 +253,13 @@ 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. */ - function getFeatureForButton (button) { + getFeatureForButton: function (button) { // Return false if the button being added is a divider. if (button === '|' || button === '-') { return false; @@ -305,16 +269,70 @@ 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); } return featuresMetadata[featureName]; - } + }, + + /** + * + */ + sortAvailableFeatures: 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 (var 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". + // @todo + 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. + var $button = $('.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="' + button + '"]') + .detach() + .appendTo('.ckeditor-toolbar-disabled > ul'); + // Update the toolbar value field. + this.model.set({'isDirty': true}, {broadcast: false}); + } + } + }, /** * Sets up broadcasting of CKEditor toolbar configuration changes. */ - function broadcastConfigurationChanges ($ckeditorToolbar) { + broadcastConfigurationChanges: function ($ckeditorToolbar) { + var hiddenEditorConfig = this.model.get('hiddenEditorConfig'); + var featuresMetadata = this.model.get('featuresMetadata'); + var getFeatureForButton = this.getFeatureForButton.bind(this); $ckeditorToolbar .find('.ckeditor-toolbar-active') // Listen for CKEditor toolbar configuration changes. When a button is @@ -334,16 +352,17 @@ Drupal.behaviors.ckeditorAdmin = { }) // Listen for CKEditor plugin settings changes. When a plugin setting is // changed, rebuild the CKEditor features metadata. + // @todo I think this is a dead code path. .on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (e, 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 +374,781 @@ Drupal.behaviors.ckeditorAdmin = { } } // Update the CKEditor features metadata. - featuresMetadata = features; + this.model.set('featuresMetadata', features); }); }); + }, + + /** + * + */ + getButtonList: function (config) { + var buttons = []; + // Remove the rows + config = _.flatten(config); + + // Loop through the toolbar 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': 'onTitleClick' + }, + + /** + * {@inheritdoc} + */ + initialize: function () { + + this.model.on('change:isDirty', this.render, this); + + this.render(); + }, + + /** + * {@inheritdoc} + */ + render: function () { + + this.insertPlaceholders(); + this.applySorting(); + + // Browser quirk work-around to redraw CSS3 gradients. + var that = this; + this.$el.find('.ckeditor-toolbar-active').css('position', 'relative'); + window.setTimeout(function () { + that.$el.find('.ckeditor-toolbar-active').css('position', ''); + }, 10); + + return this; + }, - 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 = { + onTitleClick: function (event) { + var $group = $(event.currentTarget).closest('.ckeditor-toolbar-group'); + openGroupNameDialog(this, $group); + + event.stopPropagation(); + event.preventDefault(); + }, + + /** + * + */ + startGroupDrag: function (event, ui) {}, + + /** + * + */ + endGroupDrag: function (event, ui) { + // Do nothing if a placeholder group was moved. The settings are not + // considered changed. + if (ui.item.hasClass('placeholder')) { + ui.item.closest('.ckeditor-toolbar-groups').sortable('cancel'); + return; + } + + 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'); + } + }); + }, + + /** + * jQuery Sortable start event. Remove focus from any other buttons. + */ + startButtonDrag: function (event, ui) { + this.$el.find('a:focus').blur(); + }, + + /** + * + */ + 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(); + }); + }, + + /** + * + */ + 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', + forcePlaceholderSize: true, + cursor: 'move', + start: this.startGroupDrag.bind(this), + 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' }); + }, - // Add keyboard arrow support. - $toolbarAdmin.on('keyup.ckeditorMoveButton', '.ckeditor-buttons a', adminToolbarMoveButton); - $toolbarAdmin.on('keyup.ckeditorMoveSeparator', '.ckeditor-multiple-buttons a', adminToolbarMoveSeparator); + /** + * + */ + insertPlaceholders: function () { + this.insertPlaceholderRow(); + this.insertPlaceholderGroup(); + }, - // 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(); + /** + * + */ + insertPlaceholderRow: function () { + // Add a placeholder row. + var $rows = this.$el.find('.ckeditor-row'); + if (!$rows.eq(-1).hasClass('placeholder')) { + this.$el + .find('.ckeditor-toolbar-active') + .children('.ckeditor-active-toolbar-configuration') + .append(Drupal.theme('ckeditorRow')); } + }, - // 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); + /** + * + */ + 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')); + } + }); + } + }), - // Skip dividers. - if (feature === false) { - continue; + /** + * Backbone View for CKEditor toolbar configuration; keyboard UX. + */ + ConfigurationKeyboardView: Backbone.View.extend({ + + events: { + 'click [data-drupal-ckeditor-type="group"]': 'onTitleClick' + }, + + /** + * {@inheritdoc} + */ + initialize: function () { + // Add keyboard arrow support. + this.$el.on('keyup.ckeditor', '.ckeditor-buttons a, .ckeditor-multiple-buttons a', this.moveButton.bind(this)); + this.$el.on('keyup.ckeditor', '[data-drupal-ckeditor-type="group"]', this.moveGroup.bind(this)); + }, + + /** + * {@inheritdoc} + */ + render: function () {}, + + /** + * Event callback for keypress. Move buttons based on arrow keys. + */ + moveButton: 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. + ]; + + // 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 $availableDividers = this.$el.find('[data-drupal-ckeditor-button-sorting="dividers"]'); + 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 $row, $group, dir, view; + + // 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(); + } + } - if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) { - // Default toolbar buttons are in fact "added features". - $activeToolbar.trigger('CKEditorToolbarChanged', ['added', existingButtons[i]]); + view = this; + // Attempt to move the button to the new toolbar position. + registerButtonMove(this, $button, function (result) { + + // 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(); + } + }, + + /** + * + */ + moveGroup: 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, $anchor; + // 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(); + } + }, + + /** + * + */ + onTitleClick: function (event) { + var $group = $(event.currentTarget); + openGroupNameDialog(this, $group); } + }), - // 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. + */ + ConfigurationAuralView: Backbone.View.extend({ + + events: { + 'focus .ckeditor-buttons a': 'announceButtonPosition', + 'focus .ckeditor-toolbar-group': 'announceGroupPosition' + }, + + /** + * {@inheritdoc} + */ + initialize: function () { + // Add click for help. + this.$el.on('click.ckeditorClickButton', '.ckeditor-buttons a', { type: 'button' }, this.adminToolbarButtonHelp.bind(this)); + this.$el.on('click.ckeditorClickSeparator', '.ckeditor-multiple-buttons a', { type: 'separator' }, this.adminToolbarButtonHelp.bind(this)); + }, + + /** + * {@inheritdoc} + */ + announceMove: function () { + // Update the toolbar value field. + //adminToolbarValue(event, { item: $button }); + // Post the update to the aria-live message element. + Drupal.announce(Drupal.t('Moved to @row, position @position of @totalPositions.', { + '@row': getRowName($destinationRow), + '@position': (destinationPosition + 1), + '@totalPositions': $destinationRow.children().length + })); + }, + + /** + * + */ + announceGroupPosition: function (event) { + // The focus event must be allowed to bubble, so triggering focus on a + // button will also trigger focus on its group. Therefore, check the event + // target. If it is a button, ignore this focus event. + if ($(event.target).parent('.ckeditor-button').length === 0) { + var $group = $(event.currentTarget); + var $groups = $group.parent().children(); + var $row = $group.closest('.ckeditor-row'); + var $rows = $row.parent().children(); + Drupal.announce(Drupal.t('Toolbar palette in position @position of @positionCount in row @row of @rowCount', { + '@position': $groups.index($group) + 1, + // Subtract one so that the placeholder group is not counted. + '@positionCount': $groups.not('.placeholder').length, + '@row': $rows.index($row) + 1, + '@rowCount': $rows.not('.placeholder').length + })); } + }, + + + /** + * Announces current button position when a button receives focus. + * + * @param {jQuery} event + * A jQuery event. + */ + announceButtonPosition: function (event) { + var $button = $(event.currentTarget).parent(); + 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 button is located in the available button set. + if ($button.closest('.ckeditor-toolbar-disabled').length > 0) { + Drupal.announce(Drupal.t('Available button')); + } + // The button is in the active toolbar. + else if ($group.not('.placeholder').length === 1) { + Drupal.announce(Drupal.t('Position @position of @positionCount in @palette toolbar palette in row @row of @rowCount.', { + '@position': $buttons.index($button) + 1, + '@positionCount': $buttons.length, + '@palette': $group.attr('data-drupal-ckeditor-toolbar-group-name'), + '@row': $rows.index($row) + 1, + '@rowCount': $rows.not('.placeholder').length + })); + } + + }, + + /** + * Provide help when a button is clicked on. + */ + adminToolbarButtonHelp: function (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 type = event.data.type; + var message; + + if (enabled) { + if (type === 'separator') { + message = Drupal.t('Separators are used to visually split individual buttons. This @name is currently enabled.', { '@name': $link.attr('aria-label') }) + "\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.', { '@name': $link.attr('aria-label') }) + "\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 down arrow key to move this button into the active toolbar.'); + } + } + alert(message); + $link.focus(); + 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'); + + // Name the placeholder group. + 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); } - return output; } /** - * Applies or removes the focused class to a toolbar row. + * Translates a change in CKEditor config DOM structure into the config model. * - * 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. + * 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. + * + * @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); +} + +/** * - * @param {jQuery} event - * A jQuery event. */ -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) { + var $dialog; + callback = callback || function () {}; + + /** + * + */ + function validateSubmit (form) { + var $form = $(form); + if (form.elements[0].value.length === 0) { + 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; + } + + /** + * + */ + function closeDialog (action, form) { + + function shutdown () { + // Call the jQuery UI dialog close method. + dialog.close(action); + + // Remove the listener on the form. + $(form).closest('.ui-dialog').off(); + + // The processing marker can be deleted since the dialog has been closed. + delete view.isProcessing; + } + + // Invoke a user-provided callback and indicate failure. + if (action === 'cancel') { + shutdown(); + callback(false); + return; + } + + // Validate that a group name was provided. + if(form && validateSubmit(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); + + // Refresh the settings. + 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 (event) { + closeDialog('apply', this); + } + }, + { + text: Drupal.t('Cancel'), + click: function(event) { + closeDialog('cancel'); + } + } + ], + // Prevent this modal from being closed without the user making a choice + // as per http://stackoverflow.com/a/5438771. + 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) { + 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(); + } + }); + // Prevent the form from submitting. + $widget.on('keydown keypress', function () { + if (event.keyCode === 13) { + return false; + } + }); + // Annouce to the user that a modal dialog is open. + }, + beforeClose: false + }); + // 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(); + // Focus on the first input in the form. + document.querySelector('.ckeditor-name-toolbar-group').querySelector('input').focus(); } +/** + * + */ +function namePlaceholderGroup ($group, name) { + $group.attr({ + 'data-drupal-ckeditor-toolbar-group-name': name, + }) + .children('.ckeditor-toolbar-group-name') + .text(name); + +} + +Drupal.theme.ckeditorRow = function () { + return '
  • '; +}; + +Drupal.theme.ckeditorToolbarGroup = function () { + return ''; +}; + +Drupal.theme.ckeditorButtonGroupNameForm = function () { + return '
    '; +}; + })(jQuery, Drupal, drupalSettings, 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 f32a6a8..08bf122 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..80c4133 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); } @@ -224,13 +230,19 @@ public function getButtons() { '|' => array( 'label' => t('Group separator'), 'image_alternative' => '', - 'attributes' => array('class' => array('ckeditor-group-button-separator')), + 'attributes' => array( + 'class' => array('ckeditor-group-button-separator'), + 'data-drupal-ckeditor-type' => 'separator', + ), 'multiple' => TRUE, ), '-' => 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..43f50ea 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')), ); @@ -175,7 +192,7 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit 'editor' => 'ckeditor', 'settings' => array( // Single toolbar row that contains all existing buttons. - 'toolbar' => array('buttons' => array(0 => $all_buttons)), + 'toolbar' => $editor->settings['toolbar'], 'plugins' => $editor->settings['plugins'], ), )); @@ -209,7 +226,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'], FALSE); + 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 +369,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 d2e42f9..df22bd5 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..98f1c24 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 @@ -30,10 +30,12 @@ class LlamaContextualAndButton extends Llama implements CKEditorPluginContextual * Implements \Drupal\ckeditor\Plugin\CKEditorPluginContextualInterface::isEnabled(). */ 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; + // Automatically enable this plugin if the Underline button is enabled. + 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