diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc index e99518a..0043075 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 .= '
    '; $output .= '
    '; + // Dividers. $output .= ''; $output .= '
      '; $output .= $print_buttons($multiple_buttons); $output .= '
    '; $output .= '
    '; + // Available buttons. $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 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..536e93c 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: thin dotted; 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..22c4486 100644 --- a/core/modules/ckeditor/js/ckeditor.admin.js +++ b/core/modules/ckeditor/js/ckeditor.admin.js @@ -4,204 +4,165 @@ 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; + // 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'); + + // 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 + }); - case 40: // Down arrow. - case 63233: // Safari down arrow. - $destinationRow = $($toolbarRows[$toolbarRows.index($currentRow) + 1]); + // Create the confiugration Views. + var viewDefaults = { + model: model, + el: $('.ckeditor-toolbar-configuration') } - - 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(); + 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); } - - /** - * 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(); - } + }, + detach: function (context, settings, trigger) { + // Early-return if the trigger for detachment is something else than unload. + if (trigger !== 'unload') { + return; } - /** - * 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; - - 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.'); - } + // 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-toolbar-processed'); + if ($configurationForm.length) { + var value = $configurationForm + .find('.form-item-editor-settings-toolbar-buttons') + .find('textarea') + .val(); + var $activeToolbar = $('.ckeditor-toolbar-configuration').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]]); } - $messages.text(message); - $link.focus(); - event.preventDefault(); } + } +}; - /** - * 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(); +/** + * Toolbar methods of Backbone objects. + */ +Drupal.ckeditor = { + + // A hash of View instances. + views: {}, + + // A hash of Model instances. + models: {}, + + /** + * + */ + 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'))); } + }), - /** - * 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(); - } + /** + * + */ + 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.data('drupal-ckeditor-toolbar-group-name'), + items: [] + }; + $group.find('.ckeditor-button, .ckeditor-multiple-button').each(function () { + group.items.push($(this).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 +173,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 +210,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 +251,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 +267,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 +350,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 +372,459 @@ 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, '-', '|'); } + }), + + /** + * + */ + 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; + }, + + onTitleClick: function (event) { + var $group = $(event.currentTarget).closest('.ckeditor-toolbar-group'); + openGroupNameDialog(this, $group); + }, + + /** + * + */ + startGroupDrag: function (event, ui) {}, + + /** + * + */ + endGroupDrag: function (event, ui) { + debugger; + // 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; + } + this.model.set('isDirty', true); + }, + + /** + * 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 that = this; + var $group = ui.item.closest('.ckeditor-toolbar-group'); + + // Name the placeholder group. + if ($group.hasClass('placeholder')) { + + if (this.isProcessing) { + event.stopPropagation(); + return; + } + this.isProcessing = true; + + openGroupNameDialog(this, $group); - 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 = { + // Focus on the moved item. + ui.item.find('a').focus(); + } + else { + this.model.set('isDirty', true); + } + }, + + /** + * + */ + 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); - - // Then determine if this is RTL or not. - var rtl = $toolbarAdmin.css('direction') === 'rtl' ? -1 : 1; - var $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons'); + start: this.startButtonDrag.bind(this), + // Sorting within a sortable. + stop: this.endButtonDrag.bind(this) + }).disableSelection(); + + // 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); + return; + }, - // 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(); - } + insertPlaceholders: function () { + this.insertPlaceholderRow(); + this.insertPlaceholderGroup(); + }, - // 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); + namePlaceholderGroup: function ($group, name) { + $group.attr({ + 'data-drupal-ckeditor-toolbar-group-name': name, + }) + .children('.ckeditor-toolbar-group-name') + .text(name); - // Skip dividers. - if (feature === false) { - continue; - } + }, - if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) { - // Default toolbar buttons are in fact "added features". - $activeToolbar.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 > ul > li[data-button-name="' + button + '"]') - .detach() - .appendTo('.ckeditor-toolbar-disabled > ul'); - // Update the toolbar value field. - adminToolbarValue({}, { silent: true, item: $button}); - } + /** + * + */ + insertPlaceholderRow: function () { + // Add a placeholder row. + if (this.$el.find('.ckeditor-row').filter('.placeholder').length === 0) { + this.$el + .find('.ckeditor-toolbar-active') + .children('.ckeditor-active-toolbar-configuration') + .append(Drupal.theme('ckeditorRow')); + } + }, + + /** + * + */ + 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')) { + debugger; + $placeholder.appendTo($row.children('.ckeditor-toolbar-groups')); } }); } - }, - detach: function (context, settings, trigger) { - // Early-return if the trigger for detachment is something else than unload. - if (trigger !== 'unload') { - return; + }), + + /** + * + */ + ConfigurationKeyboardView: Backbone.View.extend({ + + events: {}, + + /** + * {@inheritdoc} + */ + initialize: function () { + // Add keyboard arrow support. + this.$el.on('keyup.ckeditorMoveButton', '.ckeditor-buttons a', this.adminToolbarMoveButton.bind(this)); + this.$el.on('keyup.ckeditorMoveSeparator', '.ckeditor-multiple-buttons a', this.adminToolbarMoveSeparator.bind(this)); + }, + + /** + * {@inheritdoc} + */ + render: function () {}, + + /** + * Event callback for keypress. Move buttons based on arrow keys. + */ + adminToolbarMoveButton: function (event) { + var $target = $(event.currentTarget); + var $button = $target.parent(); + var $currentRow = $button.closest('.ckeditor-buttons'); + var $destinationRow = null; + var destinationPosition = $button.index(); + var rtl = this.$el.css('direction') === 'rtl' ? -1 : 1; + + 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 = $($buttonLists[$buttonLists.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 = $($buttonLists[$buttonLists.index($currentRow) + 1]); + } + + if ($destinationRow && $destinationRow.length) { + // Detach the button from the DOM so its position doesn't interfere. + $button.detach(); + // Move the button before the button whose position it should occupy. + var $targetButton = $destinationRow.children(':eq(' + destinationPosition + ')'); + if ($targetButton.length) { + $targetButton.before($button); + } + else { + $destinationRow.append($button); + } + // 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 + })); + } + event.preventDefault(); + }, + + /** + * Event callback for keyup. Move a separator into the active toolbar. + */ + adminToolbarMoveSeparator: function (event) { + switch (event.keyCode) { + case 38: // Up arrow. + case 63232: // Safari up arrow. + var $button = $(event.currentTarget).parent().clone().appendTo($buttonLists.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 $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]]); + /** + * + */ + ConfigurationAuralView: Backbone.View.extend({ + + events: { + 'focus .ckeditor-buttons a': 'announceButtonPosition' + }, + + /** + * {@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} + */ + render: function () {}, + + /** + * Announces current button position when a button receives focus. + * + * @param {jQuery} event + * A jQuery event. + */ + announceButtonPosition: function (event) { + var $button = $(event.currentTarget); + var $row = $button.closest('.ckeditor-buttons'); + Drupal.announce(Drupal.t('Position @position of @totalPositions in @row.', { + '@position': $row.find('li a').index($button) + 1, + '@totalPositions': $row.children().length, + '@row': this.getRowName($row) + })); + }, + + /** + * Returns a string describing the type and index of a toolbar row. + * + * @param {jQuery} $row + * A jQuery object containing a .ckeditor-button row. + * + * @return {String} + * A string describing a toolbar row. + */ + getRowName: function ($row) { + var output = ''; + var row; + // Determine if this is an active row or an available row. + if ($row.closest('.ckeditor-toolbar-disabled').length > 0) { + output += Drupal.t('available buttons row'); + } + else { + row = $('.ckeditor-toolbar-active').find('.ckeditor-buttons').index($row) + 1; + output += Drupal.t('active buttons row @row', {'@row': row}); + } + return output; + }, + + /** + * 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. * - * @param {jQuery} $row - * A jQuery object containing a .ckeditor-button row. - * - * @return {String} - * A string describing the type and index of a toolbar row. */ -function getRowInfo ($row) { - var output = ''; - var row; - // Determine if this is an active row or an available row. - if ($row.closest('.ckeditor-toolbar-disabled').length > 0) { - row = $('.ckeditor-toolbar-disabled').find('.ckeditor-buttons').index($row) + 1; - output += Drupal.t('available button row @row', {'@row': row}); - } - else { - row = $('.ckeditor-toolbar-active').find('.ckeditor-buttons').index($row) + 1; - output += Drupal.t('active button row @row', {'@row': row}); +function openGroupNameDialog (view, $group) { + var $dialog; + + function closeDialog (action, form) { + $dialog.close(action); + delete view.isProcessing; + + if (action === 'apply') { + view.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'); + // Refresh the settings. + view.model.set('isDirty', true); + } + else { + // Cancel the sort. + } } - return output; -} -/** - * Applies or removes the focused class to a toolbar row. - * - * When a button in a toolbar is focused, focus is triggered on the containing - * toolbar row. When a row is focused, the state change is announced through - * the aria-live message area. - * - * @param {jQuery} event - * A jQuery event. - */ -function grantRowFocus (event) { - var $row = $(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)})); - } + $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) { + var form = this; + closeDialog('apply', form); + } + }, + { + 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, + create: function () { + $(this).parent().find('.ui-dialog-titlebar-close').remove(); + }, + beforeClose: false + }); + $dialog.showModal(); } +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..5590ae6 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); } 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