core/modules/ckeditor/ckeditor.admin.inc | 26 ++- core/modules/ckeditor/css/ckeditor.admin.css | 91 +++++----- core/modules/ckeditor/js/ckeditor.admin.js | 179 +++++++++++++++++++- .../ckeditor/Plugin/ckeditor/plugin/Internal.php | 46 ++--- .../Plugin/ckeditor/plugin/StylesCombo.php | 2 +- 5 files changed, 271 insertions(+), 73 deletions(-) diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc index c5b459b..c77dda9 100644 --- a/core/modules/ckeditor/ckeditor.admin.inc +++ b/core/modules/ckeditor/ckeditor.admin.inc @@ -112,10 +112,17 @@ function theme_ckeditor_settings_toolbar($variables) { // We don't use theme_item_list() below in case there are no buttons in the // active or disabled list, as theme_item_list() will not print an empty UL. $output = ''; - $output .= '' . t('Active toolbar') . ''; + $output .= '
'; + $output .= '' . t('Button configuration') . ''; + // aria-live region for outputing aural information about the state of the + // configuration. + $output .= '
'; + $output .= '

' . t('Toolbar buttons may be moved by drag and drop or with the keyboard arrow keys. Move a button up into the active toolbar to enable it, or down into the available buttons list to disable it. Dividers are available to create logical button groups.') . '

'; + $output .= ''; + $output .= '
'; foreach ($active_buttons as $button_row) { - $output .= '
'; - $output .= '' . t('Available buttons') . ''; - $output .= '
'; - $output .= '
'; return $output; } diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css index 62f30b0..cf57de9 100644 --- a/core/modules/ckeditor/css/ckeditor.admin.css +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -28,7 +28,7 @@ margin: 5px; } -.ckeditor-toolbar-disabled ul.ckeditor-buttons li, +.ckeditor-toolbar-disabled ul.ckeditor-buttons li a, ul.ckeditor-buttons { border: 1px solid #a6a6a6; border-bottom-color: #979797; @@ -41,8 +41,8 @@ ul.ckeditor-buttons { min-width: 26px; list-style: none; - float: left; - clear: left; + float: left; /* LTR */ + clear: left; /* LTR */ padding: 0; margin: 0 6px 5px 0; border: 1px solid #a6a6a6; @@ -52,13 +52,21 @@ ul.ckeditor-buttons { } ul.ckeditor-buttons li { display: inline-block; + padding: 0; + margin: 0; + float: left; /* LTR */ +} +ul.ckeditor-buttons li a { + position: relative; + display: block; height: 18px; padding: 4px 6px; - outline: none; cursor: move; - float: left; border: 0; white-space: nowrap; + text-decoration: none; + text-shadow: 0 1px 0 rgba(255,255,255,.5); + color: #474747; background: #e4e4e4; background-image: -webkit-gradient(linear,left top,left bottom,from(white),to(#e4e4e4)); @@ -69,15 +77,22 @@ ul.ckeditor-buttons li { background-image: linear-gradient(top,white,#e4e4e4); filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffffff',endColorstr='#ffe4e4e4'); } -ul.ckeditor-buttons li:first-child { - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; +ul.ckeditor-buttons li .cke-icon-only { + text-indent: -9999px; + width: 1.333em; +} +ul.ckeditor-buttons li a:focus { + z-index: 11; /* Ensure focused buttons show their outline on all sides. */ } -ul.ckeditor-buttons li:last-child { - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; +ul.ckeditor-buttons li:first-child a { + border-top-left-radius: 2px; /* LTR */ + border-bottom-left-radius: 2px; /* LTR */ } -ul.ckeditor-buttons li.ckeditor-button-placeholder { +ul.ckeditor-buttons li:last-child a { + border-top-right-radius: 2px; /* LTR */ + border-bottom-right-radius: 2px; /* LTR */ +} +ul.ckeditor-buttons li.ckeditor-button-placeholder a { background: #333; opacity: 0.3; } @@ -85,48 +100,46 @@ ul.ckeditor-multiple-buttons { padding: 1px 2px; margin: 5px; list-style: none; - float: left; + float: left; /* LTR */ } ul.ckeditor-multiple-buttons li { - padding: 2px 0; + display: inline-block; + float: left; /* LTR */ margin: 0; + padding: 0; +} +ul.ckeditor-multiple-buttons li a { + cursor: move; display: inline-block; height: 18px; - cursor: move; - float: left; -} -.ckeditor-multiple-label { - float: left; - padding: 10px 4px; + margin: 0; + padding: 2px 0; } ul.ckeditor-buttons li.ckeditor-group-button-separator, ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator { + margin: -1px -3px -2px; +} +ul.ckeditor-buttons li.ckeditor-group-button-separator a, +ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator a { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAdCAMAAABG4xbVAAAAhFBMVEUAAACmpqampqampqb////l5eX////5+fmmpqatra2urq6vr6+1tbW2tra4uLi6urq8vLzb29ve3t7i4uLl5eXn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz+/v7qIQO+AAAACHRSTlMATVmAi8XM29MuWToAAABjSURBVBiVrc5BCoAwDETRMKhtRBduev9LKm1xjItWRBBE6Nt9QkIwOTcUzk0Imi8aoMssxbgoTHMtqsFMLta0vPh2N49HyfdelPg6k9uvX/a+Bmggt1qJRNzQFVgjEnkUZDoBmH57VSypjg4AAAAASUVORK5CYII=) no-repeat center center; width: 13px; padding: 0; height: 29px; - margin: -1px -3px -2px; position: relative; z-index: 10; } -ul.ckeditor-buttons li.ckeditor-button-separator { - width: 2px; - padding: 0 4px; - height: 26px; - margin: 0 -4px; +ul.ckeditor-buttons li.ckeditor-button-separator a { + background: #e4e4e4; + background-image: -webkit-linear-gradient(#e4e4e4, #b4b4b4); + background-image: linear-gradient(#e4e4e4, #b4b4b4); + height: 24px; + margin: 1px 0 0; + padding: 0; position: relative; + width: 1px; z-index: 10; - - background: #e4e4e4; - background-image: -webkit-gradient(linear, left top, left bottom, from(white), to(#e4e4e4)); - background-image: -moz-linear-gradient(top, white, #e4e4e4); - background-image: -webkit-linear-gradient(top, white, #e4e4e4); - background-image: -o-linear-gradient(top, white, #e4e4e4); - background-image: -ms-linear-gradient(top, white, #e4e4e4); - background-image: linear-gradient(top, white, #e4e4e4); - filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#ffffffff', endColorstr='#ffe4e4e4'); -} -ul.ckeditor-multiple-buttons li.ckeditor-button-separator { +} +ul.ckeditor-multiple-buttons li.ckeditor-button-separator a { width: 2px; padding: 0; height: 26px; @@ -154,10 +167,10 @@ ul.ckeditor-multiple-buttons li.ckeditor-button-separator { } .ckeditor-row-controls { - float: right; + float: right; /* LTR */ font-size: 18px; width: 40px; - text-align: right; + text-align: right; /* LTR */ } .ckeditor-row-controls a { display: inline-block; diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js index 18c070f..8c22b2a 100644 --- a/core/modules/ckeditor/js/ckeditor.admin.js +++ b/core/modules/ckeditor/js/ckeditor.admin.js @@ -4,6 +4,8 @@ Drupal.ckeditor = Drupal.ckeditor || {}; +var $messages; // Aria-live element for speaking application state. + Drupal.behaviors.ckeditorAdmin = { attach: function (context, settings) { var $context = $(context); @@ -20,17 +22,138 @@ Drupal.behaviors.ckeditorAdmin = { cursor: 'move', stop: adminToolbarValue }; - $toolbarAdmin.insertAfter($textareaWrapper).find('.ckeditor-buttons').sortable(sortableSettings); + // Add the toolbar to the page. + $toolbarAdmin.insertAfter($textareaWrapper); + + // Then determine if this is RTL or not. + var rtl = $toolbarAdmin.css('direction') === 'rtl' ? -1 : 1; + var $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons'); + + // Add the drag and drop functionality. + $toolbarRows.sortable(sortableSettings); $toolbarAdmin.find('.ckeditor-multiple-buttons li').draggable({ connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons', helper: 'clone' }); + + // Add keyboard arrow support. + $toolbarAdmin.on('keyup.ckeditorMoveButton', '.ckeditor-buttons a', adminToolbarMoveButton); + $toolbarAdmin.on('keyup.ckeditorMoveSeparator', '.ckeditor-multiple-buttons a', adminToolbarMoveSeparator); + + // Add click for help. + $toolbarAdmin.on('click.ckeditorClickButton', '.ckeditor-buttons a', { type: 'button' }, adminToolbarButtonHelp); + $toolbarAdmin.on('click.ckeditorClickSeparator', '.ckeditor-multiple-buttons a', { type: 'separator' }, adminToolbarButtonHelp); + + // Add/remove row button functionality. $toolbarAdmin.on('click.ckeditorAddRow', 'a.ckeditor-row-add', adminToolbarAddRow); $toolbarAdmin.on('click.ckeditorAddRow', 'a.ckeditor-row-remove', adminToolbarRemoveRow); if ($toolbarAdmin.find('.ckeditor-toolbar-active ul').length > 1) { $toolbarAdmin.find('a.ckeditor-row-remove').hide(); } + // Add aural UI focus updates when for individual toolbars. + $toolbarAdmin.on('focus.ckeditor', '.ckeditor-buttons', grantRowFocus); + // Identify the aria-live element for interaction updates for screen + // readers. + $messages = $('#ckeditor-button-configuration-aria-live'); + + /** + * Event callback for keypress. Move buttons based on arrow keys. + */ + function adminToolbarMoveButton(event) { + var label = Drupal.t('@label button', {'@label': $(this).attr('aria-label')}); + var $button = $(this).parent(); + var $currentRow = $button.closest('.ckeditor-buttons'); + var $destinationRow = null; + var destinationPosition = $button.index(); + + switch (event.keyCode) { + case 37: // Left arrow. + case 63234: // Safari left arrow. + $destinationRow = $currentRow; + destinationPosition = destinationPosition - (1 * rtl); + break; + case 38: // Up arrow. + case 63232: // Safari up arrow. + $destinationRow = $($toolbarRows[$toolbarRows.index($currentRow) - 1]); + break; + case 39: // Right arrow. + case 63235: // Safari right arrow. + $destinationRow = $currentRow; + destinationPosition = destinationPosition + (1 * rtl); + break; + case 40: // Down arrow. + case 63233: // Safari down arrow. + $destinationRow = $($toolbarRows[$toolbarRows.index($currentRow) + 1]); + } + + if ($destinationRow && $destinationRow.length) { + // Detach the button from the DOM so its position doesn't interfere. + $button.detach(); + // Move the button before the button whose position it should occupy. + var $targetButton = $destinationRow.children(':eq(' + destinationPosition + ')'); + if ($targetButton.length) { + $targetButton.before($button); + } + else { + $destinationRow.append($button); + } + // Post the update to the aria-live message element. + $messages.text(Drupal.t('moved to @row, position @position of @totalPositions', { + '@row': getRowInfo($destinationRow), + '@position': (destinationPosition + 1), + '@totalPositions': $destinationRow.children().length + })); + // Update the toolbar value field. + adminToolbarValue(event, { item: $button }); + } + event.preventDefault(); + } + + /** + * Event callback for keyup. Move a separator into the active toolbar. + */ + function adminToolbarMoveSeparator(event) { + switch (event.keyCode) { + case 38: // Up arrow. + case 63232: // Safari up arrow. + var $button = $(this).parent().clone().appendTo($toolbarRows.eq(-2)); + adminToolbarValue(event, { item: $button }); + event.preventDefault(); + } + } + + /** + * Provide help when a button is clicked on. + */ + function adminToolbarButtonHelp(event) { + var $link = $(this); + var $button = $link.parent(); + var $currentRow = $button.closest('.ckeditor-buttons'); + var enabled = $button.closest('.ckeditor-toolbar-active').length > 0; + var position = $button.index() + 1; // 1-based index for humans. + var rowNumber = $toolbarRows.index($currentRow) + 1; + var type = event.data.type; + if (enabled) { + if (type === 'separator') { + $messages.text(Drupal.t('Separators are used to visually split individual buttons. This @name is currently enabled, in row @row and position @position.', { '@name': $link.attr('aria-label'), '@row': rowNumber, '@position': position }) + "\n\n" + Drupal.t('Drag and drop the separator or use the keyboard arrow keys to change the position of this separator.')); + } + else { + $messages.text(Drupal.t('The @name button is currently enabled, in row @row and position @position.', { '@name': $link.attr('aria-label'), '@row': rowNumber, '@position': position }) + "\n\n" + Drupal.t('Drag and drop the buttons or use the keyboard arrow keys to change the position of this button.')); + } + } + else { + if (type === 'separator') { + $messages.text(Drupal.t('Separators are used to visually split individual buttons. This @name is currently disabled.', { '@name': $link.attr('aria-label') }) + "\n\n" + Drupal.t('Drag the button or use the up arrow key to move this separator into the active toolbar. You may add multiple separators to each row.')); + } + else { + $messages.text(Drupal.t('The @name button is currently disabled.', { '@name': $link.attr('aria-label') }) + "\n\n" + Drupal.t('Drag the button or use the up arrow key to move this button into the active toolbar.')); + } + } + $link.focus(); + event.preventDefault(); + } + /** * Add a new row of buttons. */ @@ -38,8 +161,11 @@ Drupal.behaviors.ckeditorAdmin = { var $this = $(this); var $rows = $this.closest('.ckeditor-toolbar-active').find('.ckeditor-buttons'); $rows.last().clone().empty().insertAfter($rows.last()).sortable(sortableSettings); + $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons'); $this.siblings('a').show(); redrawToolbarGradient(); + // Post the update to the aria-live message element. + $messages.text(Drupal.t('row number @count added.', {'@count': ($rows.length + 1)})); event.preventDefault(); } @@ -57,8 +183,11 @@ Drupal.behaviors.ckeditorAdmin = { var $disabledButtons = $wrapper.find('.ckeditor-toolbar-disabled .ckeditor-buttons'); $lastRow.children(':not(.ckeditor-multiple-button)').prependTo($disabledButtons); $lastRow.sortable('destroy').remove(); + $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons'); redrawToolbarGradient(); } + // Post the update to the aria-live message element. + $messages.text(Drupal.t('row removed. @count row@plural remaining.', {'@count': ($rows.length - 1), '@plural': ((($rows.length - 1) === 1 ) ? '' : 's')})); event.preventDefault(); } @@ -79,6 +208,8 @@ Drupal.behaviors.ckeditorAdmin = { function adminToolbarValue(event, ui) { // Update the toolbar config after updating a sortable. var toolbarConfig = []; + var $button = ui.item; + $button.find('a').focus(); $wrapper.find('.ckeditor-toolbar-active ul').each(function() { var $rowButtons = $(this).find('li'); var rowConfig = []; @@ -108,4 +239,50 @@ Drupal.behaviors.ckeditorAdmin = { } }; +/** + * Returns a string describing the type and index of a toolbar row. + * + * @param {jQuery} $row + * A jQuery object containing a .ckeditor-button row. + * + * @return {String} + * A string describing the type and index of a toolbar row. + */ +function getRowInfo ($row) { + var output = ''; + var row; + // Determine if this is an active row or an available row. + if ($row.closest('.ckeditor-toolbar-disabled').length > 0) { + row = $('.ckeditor-toolbar-disabled').find('.ckeditor-buttons').index($row) + 1; + output += Drupal.t('available button row @row', {'@row': row}); + } + else { + row = $('.ckeditor-toolbar-active').find('.ckeditor-buttons').index($row) + 1; + output += Drupal.t('active button row @row', {'@row': row}); + } + return output; +} + +/** + * Applies or removes the focused class to a toolbar row. + * + * When a button in a toolbar is focused, focus is triggered on the containing + * toolbar row. When a row is focused, the state change is announced through + * the aria-live message area. + * + * @param {jQuery} event + * A jQuery event. + */ +function grantRowFocus (event) { + var $row = $(this); + // Remove the focused class from all other toolbars. + $('.ckeditor-buttons.focused').not($row).removeClass('focused'); + // Post the update to the aria-live message element. + if (!$row.hasClass('focused')) { + // Indicate that the current row has focus. + $row.addClass('focused'); + $messages.text(Drupal.t('@row', {'@row': getRowInfo($row)})); + } +} + })(jQuery, Drupal); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php index 0260abf..51fa9c5 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php @@ -72,7 +72,7 @@ public function getConfig(Editor $editor) { */ public function getButtons() { $button = function($name, $direction = 'ltr') { - return ' '; + return '' . $name . ''; }; return array( @@ -95,44 +95,44 @@ public function getButtons() { ), 'Superscript' => array( 'label' => t('Superscript'), - 'image_alternative' => $button('superscript'), + 'image_alternative' => $button('super script'), ), 'Subscript' => array( 'label' => t('Subscript'), - 'image_alternative' => $button('subscript'), + 'image_alternative' => $button('sub script'), ), // "removeformat" plugin. 'RemoveFormat' => array( 'label' => t('Remove format'), - 'image_alternative' => $button('removeformat'), + 'image_alternative' => $button('remove format'), ), // "justify" plugin. 'JustifyLeft' => array( 'label' => t('Align left'), - 'image_alternative' => $button('justifyleft'), + 'image_alternative' => $button('justify left'), ), 'JustifyCenter' => array( 'label' => t('Align center'), - 'image_alternative' => $button('justifycenter'), + 'image_alternative' => $button('justify center'), ), 'JustifyRight' => array( 'label' => t('Align right'), - 'image_alternative' => $button('justifyright'), + 'image_alternative' => $button('justify right'), ), 'JustifyBlock' => array( 'label' => t('Justify'), - 'image_alternative' => $button('justifyblock'), + 'image_alternative' => $button('justify block'), ), // "list" plugin. 'BulletedList' => array( 'label' => t('Bullet list'), - 'image_alternative' => $button('bulletedlist'), - 'image_alternative_rtl' => $button('bulletedlist', 'rtl'), + 'image_alternative' => $button('bulleted list'), + 'image_alternative_rtl' => $button('bulleted list', 'rtl'), ), 'NumberedList' => array( 'label' => t('Numbered list'), - 'image_alternative' => $button('numberedlist'), - 'image_alternative_rtl' => $button('numberedlist', 'rtl'), + 'image_alternative' => $button('numbered list'), + 'image_alternative_rtl' => $button('numbered list', 'rtl'), ), // "indent" plugin. 'Outdent' => array( @@ -178,7 +178,7 @@ public function getButtons() { // "horizontalrule" plugin 'HorizontalRule' => array( 'label' => t('Horizontal rule'), - 'image_alternative' => $button('horizontalrule'), + 'image_alternative' => $button('horizontal rule'), ), // "clipboard" plugin. 'Cut' => array( @@ -199,23 +199,23 @@ public function getButtons() { // "pastetext" plugin. 'PasteText' => array( 'label' => t('Paste Text'), - 'image_alternative' => $button('pastetext'), - 'image_alternative_rtl' => $button('pastetext', 'rtl'), + 'image_alternative' => $button('paste text'), + 'image_alternative_rtl' => $button('paste text', 'rtl'), ), // "pastefromword" plugin. 'PasteFromWord' => array( 'label' => t('Paste from Word'), - 'image_alternative' => $button('pastefromword'), - 'image_alternative_rtl' => $button('pastefromword', 'rtl'), + 'image_alternative' => $button('paste from word'), + 'image_alternative_rtl' => $button('paste from word', 'rtl'), ), // "specialchar" plugin. 'SpecialChar' => array( 'label' => t('Character map'), - 'image_alternative' => $button('specialchar'), + 'image_alternative' => $button('special char'), ), 'Format' => array( 'label' => t('HTML block format'), - 'image_alternative' => '' . t('Format') . '', + 'image_alternative' => '' . t('Format') . '', ), // "image" plugin. 'Image' => array( @@ -230,8 +230,8 @@ public function getButtons() { // "showblocks" plugin. 'ShowBlocks' => array( 'label' => t('Show blocks'), - 'image_alternative' => $button('showblocks'), - 'image_alternative_rtl' => $button('showblocks', 'rtl'), + 'image_alternative' => $button('show blocks'), + 'image_alternative_rtl' => $button('show blocks', 'rtl'), ), // "sourcearea" plugin. 'Source' => array( @@ -246,13 +246,13 @@ public function getButtons() { // No plugin, separator "buttons" for toolbar builder UI use only. '|' => array( 'label' => t('Group separator'), - 'image_alternative' => ' ', + 'image_alternative' => '', 'attributes' => array('class' => array('ckeditor-group-button-separator')), 'multiple' => TRUE, ), '-' => array( 'label' => t('Separator'), - 'image_alternative' => ' ', + 'image_alternative' => '', 'attributes' => array('class' => array('ckeditor-button-separator')), 'multiple' => TRUE, ), diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php index e40e5ca..f810cbb 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php @@ -63,7 +63,7 @@ public function getButtons() { return array( 'Styles' => array( 'label' => t('Font style'), - 'image_alternative' => '' . t('Styles') . '', + 'image_alternative' => '' . t('Styles') . '', ), ); }