diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc new file mode 100644 index 0000000..c77dda9 --- /dev/null +++ b/core/modules/ckeditor/ckeditor.admin.inc @@ -0,0 +1,154 @@ +direction ? 'rtl' : 'ltr'; + + // Create lists of active and disabled buttons. + $editor = $variables['editor']; + $plugins = $variables['plugins']; + $buttons = array(); + $variables['multiple_buttons'] = array(); + foreach ($plugins as $plugin => $plugin_buttons) { + foreach ($plugin_buttons as $button_name => $button) { + $button['name'] = $button_name; + if (!empty($button['multiple'])) { + $variables['multiple_buttons'][$button_name] = $button; + } + $buttons[$button_name] = $button; + } + } + $variables['active_buttons'] = array(); + foreach ($editor->settings['toolbar']['buttons'] as $row_number => $row) { + foreach ($row as $button_name) { + if (isset($buttons[$button_name])) { + $variables['active_buttons'][$row_number][] = $buttons[$button_name]; + if (empty($buttons[$button_name]['multiple'])) { + unset($buttons[$button_name]); + } + } + } + } + $variables['disabled_buttons'] = array_diff_key($buttons, $variables['multiple_buttons']); +} + +/** + * Displays the toolbar configuration for CKEditor. + */ +function theme_ckeditor_settings_toolbar($variables) { + $editor = $variables['editor']; + $plugins = $variables['plugins']; + $rtl = $variables['language_direction'] === 'rtl' ? '_rtl' : ''; + + $build_button_item = function($button, $rtl) { + // Value of the button item. + if (isset($button['image_alternative'])) { + $value = $button['image_alternative' . $rtl]; + } + elseif (isset($button['image'])) { + $value = theme('image', array('uri' => $button['image' . $rtl], 'title' => $button['label'])); + } + else { + $value = '?'; + } + + // Set additional attribute on the button if it can occur multiple times. + if (!empty($button['multiple'])) { + $button['attributes']['class'][] = 'ckeditor-multiple-button'; + } + + // Build the button item. + $button_item = array( + 'value' => $value, + 'data-button-name' => $button['name'], + ); + if (!empty($button['attributes'])) { + $button_item = array_merge($button_item, $button['attributes']); + } + + return $button_item; + }; + + // Assemble items to be added to active button rows. + $active_buttons = array(); + foreach ($variables['active_buttons'] as $row_number => $row_buttons) { + foreach ($row_buttons as $button) { + $active_buttons[$row_number][] = $build_button_item($button, $rtl); + } + } + // Assemble list of disabled buttons (which are always a single row). + $disabled_buttons = array(); + foreach ($variables['disabled_buttons'] as $button) { + $disabled_buttons[] = $build_button_item($button, $rtl); + } + // Assemble list of multiple buttons that may be added multiple times. + $multiple_buttons = array(); + foreach ($variables['multiple_buttons'] as $button_name => $button) { + $multiple_buttons[] = $build_button_item($button, $rtl); + } + + $print_buttons = function($buttons) { + $output = ''; + foreach ($buttons as $button) { + $value = $button['value']; + unset($button['value']); + $attributes = (string) new Attribute($button); + $output .= '' . $value . ''; + } + return $output; + }; + + // We don't use theme_item_list() below in case there are no buttons in the + // active or disabled list, as theme_item_list() will not print an empty UL. + $output = ''; + $output .= '
'; + $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 .= $print_buttons($button_row); + $output .= '
'; + } + if (empty($active_buttons)) { + $output .= '
    '; + $output .= '
'; + } + + $output .= '
'; + $output .= '-'; + $output .= '+'; + $output .= '
'; + + $output .= '
'; + + $output .= '
'; + $output .= ''; + $output .= '
    '; + $output .= $print_buttons($disabled_buttons); + $output .= '
'; + $output .= ''; + $output .= '
    '; + $output .= $print_buttons($multiple_buttons); + $output .= '
'; + $output .= '
'; + $output .= '
'; + + return $output; +} diff --git a/core/modules/ckeditor/ckeditor.api.php b/core/modules/ckeditor/ckeditor.api.php new file mode 100644 index 0000000..0b4f343 --- /dev/null +++ b/core/modules/ckeditor/ckeditor.api.php @@ -0,0 +1,60 @@ + array( + 'modulePath' => drupal_get_path('module', 'ckeditor'), + ), + ); + $libraries['drupal.ckeditor'] = array( + 'title' => 'Drupal behavior to enable CKEditor on textareas.', + 'version' => VERSION, + 'js' => array( + $module_path . '/js/ckeditor.js' => array(), + array('data' => $settings, 'type' => 'setting'), + ), + 'dependencies' => array( + array('system', 'drupal'), + array('ckeditor', 'ckeditor'), + array('editor', 'drupal.editor'), + ), + ); + $libraries['drupal.ckeditor.css'] = array( + 'title' => 'Formatting CSS for common classes used in CKEditor.', + 'version' => VERSION, + 'css' => array( + $module_path . '/css/ckeditor.css' => array(), + ), + ); + $libraries['drupal.ckeditor.admin'] = array( + 'title' => 'Drupal behavior for drag-and-drop CKEditor toolbar builder UI.', + 'version' => VERSION, + 'js' => array( + $module_path . '/js/ckeditor.admin.js' => array(), + ), + 'css' => array( + $module_path . '/css/ckeditor.admin.css' => array(), + 'core/misc/ckeditor/skins/moono/editor.css' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'drupalSettings'), + array('system', 'jquery.once'), + array('system', 'jquery.ui.sortable'), + array('system', 'jquery.ui.draggable'), + ), + ); + $libraries['drupal.ckeditor.stylescombo.admin'] = array( + 'title' => 'Only show the "stylescombo" plugin settings when its button is enabled.', + 'version' => VERSION, + 'js' => array( + $module_path . '/js/ckeditor.stylescombo.admin.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'jquery.once'), + array('system', 'drupal.vertical-tabs'), + ), + ); + $libraries['ckeditor'] = array( + 'title' => 'Loads the main CKEditor library.', + 'version' => '4.0.1', + 'js' => array( + 'core/misc/ckeditor/ckeditor.js' => array(), + ), + ); + + return $libraries; +} + +/** + * Implements hook_theme(). + */ +function ckeditor_theme() { + return array( + 'ckeditor_settings_toolbar' => array( + 'file' => 'ckeditor.admin.inc', + 'variables' => array('editor' => NULL, 'plugins' => NULL), + ), + ); +} + +/** + * Implements hook_page_build(). + */ +function ckeditor_page_build(&$page) { + // Add our CSS file that adds common needed classes, such as align-left, + // align-right, underline, indent, etc. + $page['#attached']['library'][] = array('ckeditor', 'drupal.ckeditor.css'); +} + +/** + * Retrieves the default theme's CKEditor stylesheets defined in the .info file. + * + * Themes may specify iframe-specific CSS files for use with CKEditor by + * including a "ckeditor_stylesheets" key in the theme .info file. + * + * @code + * ckeditor_stylesheets[] = css/ckeditor-iframe.css + * @endcode + */ +function _ckeditor_theme_css($theme = NULL) { + $css = array(); + if (!isset($theme)) { + $theme = variable_get('theme_default'); + } + if ($theme_path = drupal_get_path('theme', $theme)) { + $info = system_get_info('theme', $theme); + if (isset($info['ckeditor_stylesheets'])) { + $css = $info['ckeditor_stylesheets']; + foreach ($css as $key => $path) { + $css[$key] = $theme_path . '/' . $path; + } + } + if (isset($info['base theme'])) { + $css = array_merge(_ckeditor_theme_css($info['base theme'], $css)); + } + } + return $css; +} diff --git a/core/modules/ckeditor/css/ckeditor-iframe.css b/core/modules/ckeditor/css/ckeditor-iframe.css new file mode 100644 index 0000000..54f4b3f --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor-iframe.css @@ -0,0 +1,18 @@ +/** + * CSS added to iframe-based instances only. + */ +body { + font-family: Arial, Verdana, sans-serif; + font-size: 12px; + color: #222; + background-color: #fff; + margin: 8px; +} + +ol, ul, dl { + /* IE7: reset rtl list margin. (CKEditor issue #7334) */ + *margin-right: 0px; + /* Preserved spaces for list items with text direction other than the list. + * (CKEditor issues #6249,#8049) */ + padding: 0 40px; +} diff --git a/core/modules/ckeditor/css/ckeditor-rtl.css b/core/modules/ckeditor/css/ckeditor-rtl.css new file mode 100644 index 0000000..59f97b8 --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor-rtl.css @@ -0,0 +1,18 @@ +/** + * RTL styles used by CKEditor. Added to front-end theme and iframe editors. + */ +.align-left { + text-align: right; +} +.align-right { + text-align: left; +} +.indent1 { + margin: 0 40px 0 0; +} +.indent2 { + margin: 0 80px 0 0; +} +.indent3 { + margin: 0 120px 0 0; +} diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css new file mode 100644 index 0000000..cf57de9 --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -0,0 +1,186 @@ +/** + * @file + * Styles for configuration of CKEditor module. + * + * Many of these styles are adapted directly from the default CKEditor theme + * "moono". + */ + +.ckeditor-toolbar-active { + border: 1px solid #b6b6b6; + padding: 6px 8px 2px; + box-shadow: 0 1px 0 white inset; + background: #cfd1cf; + background-image: -webkit-gradient(linear, left top, left bottom, from(whiteSmoke), to(#cfd1cf)); + background-image: -moz-linear-gradient(top, whiteSmoke, #cfd1cf); + background-image: -o-linear-gradient(top, whiteSmoke, #cfd1cf); + background-image: -ms-linear-gradient(top, whiteSmoke, #cfd1cf); + background-image: linear-gradient(top, whiteSmoke, #cfd1cf); + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#fff5f5f5', endColorstr='#ffcfd1cf'); + margin: 5px 0; + overflow: nowrap; +} +.ckeditor-toolbar-disabled ul.ckeditor-buttons { + border: 0; +} + +.ckeditor-toolbar-disabled ul.ckeditor-buttons li { + margin: 5px; +} + +.ckeditor-toolbar-disabled ul.ckeditor-buttons li a, +ul.ckeditor-buttons { + border: 1px solid #a6a6a6; + border-bottom-color: #979797; + border-radius: 3px; + box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 0 2px rgba(255, 255, 255, .15) inset, 0 1px 0 rgba(255, 255, 255, .15) inset; +} + +ul.ckeditor-buttons { + min-height: 26px; + min-width: 26px; + list-style: none; + + float: left; /* LTR */ + clear: left; /* LTR */ + padding: 0; + margin: 0 6px 5px 0; + border: 1px solid #a6a6a6; + border-bottom-color: #979797; + border-radius: 3px; + box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 0 2px rgba(255, 255, 255, .15) inset, 0 1px 0 rgba(255, 255, 255, .15) inset; +} +ul.ckeditor-buttons li { + display: inline-block; + padding: 0; + margin: 0; + float: left; /* LTR */ +} +ul.ckeditor-buttons li a { + position: relative; + display: block; + height: 18px; + padding: 4px 6px; + cursor: move; + border: 0; + white-space: nowrap; + text-decoration: none; + text-shadow: 0 1px 0 rgba(255,255,255,.5); + color: #474747; + + background: #e4e4e4; + background-image: -webkit-gradient(linear,left top,left bottom,from(white),to(#e4e4e4)); + background-image: -moz-linear-gradient(top,white,#e4e4e4); + background-image: -webkit-linear-gradient(top,white,#e4e4e4); + background-image: -o-linear-gradient(top,white,#e4e4e4); + background-image: -ms-linear-gradient(top,white,#e4e4e4); + background-image: linear-gradient(top,white,#e4e4e4); + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffffff',endColorstr='#ffe4e4e4'); +} +ul.ckeditor-buttons li .cke-icon-only { + text-indent: -9999px; + width: 1.333em; +} +ul.ckeditor-buttons li a:focus { + z-index: 11; /* Ensure focused buttons show their outline on all sides. */ +} +ul.ckeditor-buttons li:first-child a { + border-top-left-radius: 2px; /* LTR */ + border-bottom-left-radius: 2px; /* LTR */ +} +ul.ckeditor-buttons li:last-child a { + border-top-right-radius: 2px; /* LTR */ + border-bottom-right-radius: 2px; /* LTR */ +} +ul.ckeditor-buttons li.ckeditor-button-placeholder a { + background: #333; + opacity: 0.3; +} +ul.ckeditor-multiple-buttons { + padding: 1px 2px; + margin: 5px; + list-style: none; + float: left; /* LTR */ +} +ul.ckeditor-multiple-buttons li { + display: inline-block; + float: left; /* LTR */ + margin: 0; + padding: 0; +} +ul.ckeditor-multiple-buttons li a { + cursor: move; + display: inline-block; + height: 18px; + margin: 0; + padding: 2px 0; +} +ul.ckeditor-buttons li.ckeditor-group-button-separator, +ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator { + margin: -1px -3px -2px; +} +ul.ckeditor-buttons li.ckeditor-group-button-separator a, +ul.ckeditor-multiple-buttons li.ckeditor-group-button-separator a { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAdCAMAAABG4xbVAAAAhFBMVEUAAACmpqampqampqb////l5eX////5+fmmpqatra2urq6vr6+1tbW2tra4uLi6urq8vLzb29ve3t7i4uLl5eXn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz+/v7qIQO+AAAACHRSTlMATVmAi8XM29MuWToAAABjSURBVBiVrc5BCoAwDETRMKhtRBduev9LKm1xjItWRBBE6Nt9QkIwOTcUzk0Imi8aoMssxbgoTHMtqsFMLta0vPh2N49HyfdelPg6k9uvX/a+Bmggt1qJRNzQFVgjEnkUZDoBmH57VSypjg4AAAAASUVORK5CYII=) no-repeat center center; + width: 13px; + padding: 0; + height: 29px; + position: relative; + z-index: 10; +} +ul.ckeditor-buttons li.ckeditor-button-separator a { + background: #e4e4e4; + background-image: -webkit-linear-gradient(#e4e4e4, #b4b4b4); + background-image: linear-gradient(#e4e4e4, #b4b4b4); + height: 24px; + margin: 1px 0 0; + padding: 0; + position: relative; + width: 1px; + z-index: 10; +} +ul.ckeditor-multiple-buttons li.ckeditor-button-separator a { + width: 2px; + padding: 0; + height: 26px; + margin: 0 10px; +} +.ckeditor-separator { + background-color: silver; + background-color: rgba(0, 0, 0, .2); + margin: 5px 0; + height: 18px; + width: 1px; + display: block; + -webkit-box-shadow: 1px 0 1px rgba(255, 255, 255, .5); + -moz-box-shadow: 1px 0 1px rgba(255,255,255,.5); + box-shadow: 1px 0 1px rgba(255, 255, 255, .5) +} +.ckeditor-button-arrow { + width: 0; + text-align: center; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 3px solid #333; + display: inline-block; + margin: 0 4px 2px; +} + +.ckeditor-row-controls { + float: right; /* LTR */ + font-size: 18px; + width: 40px; + text-align: right; /* LTR */ +} +.ckeditor-row-controls a { + display: inline-block; + padding: 6px 2px; + height: 16px; + width: 16px; + line-height: 0.9; + font-weight: bold; + color: #333; +} +.ckeditor-row-controls a:hover { + text-decoration: none; +} diff --git a/core/modules/ckeditor/css/ckeditor.css b/core/modules/ckeditor/css/ckeditor.css new file mode 100644 index 0000000..d5c6efe --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor.css @@ -0,0 +1,56 @@ +/** + * Common styles used by CKEditor. Added to front-end theme and iframe editors. + */ +.align-left { + text-align: left; /* RTL */ +} +.align-right { + text-align: right; /* RTL */ +} +.align-center { + text-align: center; +} +.align-justify { + text-align: justify; +} +img.align-left { + float: left; /* RTL */ +} +img.align-right { + float: right; /* RTL */ +} +img.align-center { + margin-left: auto; + margin-right: auto; + display: block; +} +img.full-width { + width: 100%; + height: auto; +} +.underline { + text-decoration: underline; +} +.indent1 { + margin: 0 0 0 40px; /* RTL */ +} +.indent2 { + margin: 0 0 0 80px; /* RTL */ +} +.indent3 { + margin: 0 0 0 120px; /* RTL */ +} +.caption-left { + float: left; /* RTL */ +} +.caption-right { + float: right; /* RTL */ +} +.caption-center { + text-align: center; + margin-left: auto; + margin-right: auto; +} +.align-justify { + text-align: justify; +} diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js new file mode 100644 index 0000000..633a155 --- /dev/null +++ b/core/modules/ckeditor/js/ckeditor.admin.js @@ -0,0 +1,294 @@ +(function ($, Drupal, drupalSettings) { + +"use strict"; + +Drupal.ckeditor = Drupal.ckeditor || {}; + +var $messages; // Aria-live element for speaking application state. + +Drupal.behaviors.ckeditorAdmin = { + attach: function (context) { + var $context = $(context); + var $ckeditorToolbar = $context.find('.ckeditor-toolbar-configuration').once('ckeditor-toolbar'); + + + /** + * Event callback for keypress. Move buttons based on arrow keys. + */ + function adminToolbarMoveButton (event) { + var $target = $(event.target); + var label = Drupal.t('@label button', {'@label': $target.attr('aria-label')}); + 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]); + } + + 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 = $(event.target).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 = $(event.target); + 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.'); + } + } + $messages.text(message); + $link.focus(); + event.preventDefault(); + } + + /** + * Add a new row of buttons. + */ + function adminToolbarAddRow (event) { + var $this = $(event.target); + 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(); + } + + /** + * Remove a row of buttons. + */ + function adminToolbarRemoveRow (event) { + var $this = $(event.target); + 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.t('row removed. @count row@plural remaining.', {'@count': ($rows.length - 1), '@plural': ((($rows.length - 1) === 1 ) ? '' : 's')})); + event.preventDefault(); + } + + /** + * Browser quirk work-around to redraw CSS3 gradients. + */ + function redrawToolbarGradient () { + $ckeditorToolbar.find('.ckeditor-toolbar-active').css('position', 'relative'); + window.setTimeout(function () { + $ckeditorToolbar.find('.ckeditor-toolbar-active').css('position', ''); + }, 10); + } + + /** + * jQuery Sortable stop event. Save updated toolbar positions to the + * textarea. + */ + function adminToolbarValue (event, ui) { + // Update the toolbar config after updating a sortable. + var toolbarConfig = []; + var $button = ui.item; + $button.find('a').focus(); + $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')); + }); + toolbarConfig.push(rowConfig); + } + }); + $textarea.val(JSON.stringify(toolbarConfig, null, ' ')); + + // Determine whether we should trigger an event. + var from = $(event.target).parents('div[data-toolbar]').attr('data-toolbar'); + var to = $(event.toElement).parents('div[data-toolbar]').attr('data-toolbar'); + if (from !== to) { + $ckeditorToolbar.find('.ckeditor-toolbar-active') + .trigger('CKEditorToolbarChanged', [ + (to === 'active') ? 'added' : 'removed', + ui.item.get(0).getAttribute('data-button-name') + ]); + } + } + + + 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 = { + connectWith: '.ckeditor-buttons', + placeholder: 'ckeditor-button-placeholder', + forcePlaceholderSize: true, + tolerance: 'pointer', + cursor: 'move', + stop: adminToolbarValue + }; + // Add the toolbar to the page. + $toolbarAdmin.insertAfter($textareaWrapper); + + // Then determine if this is RTL or not. + var rtl = $toolbarAdmin.css('direction') === 'rtl' ? -1 : 1; + var $toolbarRows = $toolbarAdmin.find('.ckeditor-buttons'); + + // Add the drag and drop functionality. + $toolbarRows.sortable(sortableSettings); + $toolbarAdmin.find('.ckeditor-multiple-buttons li').draggable({ + connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons', + helper: 'clone' + }); + + // Add keyboard arrow support. + $toolbarAdmin.on('keyup.ckeditorMoveButton', '.ckeditor-buttons a', adminToolbarMoveButton); + $toolbarAdmin.on('keyup.ckeditorMoveSeparator', '.ckeditor-multiple-buttons a', adminToolbarMoveSeparator); + + // Add click for help. + $toolbarAdmin.on('click.ckeditorClickButton', '.ckeditor-buttons a', { type: 'button' }, adminToolbarButtonHelp); + $toolbarAdmin.on('click.ckeditorClickSeparator', '.ckeditor-multiple-buttons a', { type: 'separator' }, adminToolbarButtonHelp); + + // Add/remove row button functionality. + $toolbarAdmin.on('click.ckeditorAddRow', 'a.ckeditor-row-add', adminToolbarAddRow); + $toolbarAdmin.on('click.ckeditorAddRow', 'a.ckeditor-row-remove', adminToolbarRemoveRow); + if ($toolbarAdmin.find('.ckeditor-toolbar-active ul').length > 1) { + $toolbarAdmin.find('a.ckeditor-row-remove').hide(); + } + + // Add aural UI focus updates when for individual toolbars. + $toolbarAdmin.on('focus.ckeditor', '.ckeditor-buttons', grantRowFocus); + // Identify the aria-live element for interaction updates for screen + // readers. + $messages = $('#ckeditor-button-configuration-aria-live'); + } + } +}; + +/** + * 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 = $(event.target); + // 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, drupalSettings); diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js new file mode 100644 index 0000000..727f9e4 --- /dev/null +++ b/core/modules/ckeditor/js/ckeditor.js @@ -0,0 +1,33 @@ +(function (Drupal, CKEDITOR) { + +"use strict"; + +Drupal.editors.ckeditor = { + attach: function (element, format) { + var externalPlugins = format.editorSettings.externalPlugins; + // Register and load additional CKEditor plugins as necessary. + if (externalPlugins) { + for (var pluginName in externalPlugins) { + if (externalPlugins.hasOwnProperty(pluginName)) { + CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); + } + } + delete format.editorSettings.drupalExternalPlugins; + } + return !!CKEDITOR.replace(element, format.editorSettings); + }, + detach: function (element, format, trigger) { + var editor = CKEDITOR.dom.element.get(element).getEditor(); + if (editor) { + if (trigger === 'serialize') { + editor.updateElement(); + } + else { + editor.destroy(); + } + } + return !!editor; + } +}; + +})(Drupal, CKEDITOR); diff --git a/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js new file mode 100644 index 0000000..5a71a77 --- /dev/null +++ b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js @@ -0,0 +1,51 @@ +(function ($, Drupal) { + +"use strict"; + +/** + * Shows the "stylescombo" plugin settings only when the button is enabled. + */ +Drupal.behaviors.ckeditorStylesComboSettingsVisibility = { + attach: function (context) { + var $stylesComboVerticalTab = $('#edit-editor-settings-plugins-stylescombo').data('verticalTab'); + + // Hide if the "Styles" button is disabled. + if ($('.ckeditor-toolbar-disabled li[data-button-name="Styles"]').length === 1) { + $stylesComboVerticalTab.tabHide(); + } + + // React to added/removed toolbar buttons. + $(context) + .find('.ckeditor-toolbar-active') + .on('CKEditorToolbarChanged', function (e, action, button) { + if (button === 'Styles') { + if (action === 'added') { + $stylesComboVerticalTab.tabShow(); + } + else { + $stylesComboVerticalTab.tabHide(); + } + } + }); + } +}; + +/** + * Provides the summary for the "stylescombo" plugin settings vertical tab. + */ +Drupal.behaviors.ckeditorStylesComboSettingsSummary = { + attach: function () { + $('#edit-editor-settings-plugins-stylescombo').drupalSetSummary(function (context) { + var styles = $.trim($('#edit-editor-settings-plugins-stylescombo-styles').val()); + if (styles.length === 0) { + return Drupal.t('No styles configured'); + } + else { + var count = $.trim(styles).split("\n").length; + return Drupal.t('@count styles configured', { '@count': count}); + } + }); + } +}; + +})(jQuery, Drupal); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php new file mode 100644 index 0000000..4050be1 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php @@ -0,0 +1,42 @@ +settings. + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * + * @return bool + */ + public function isEnabled(Editor $editor); + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php new file mode 100644 index 0000000..601cafb --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php @@ -0,0 +1,69 @@ +settings, but be aware that + * it may not yet contain plugin-specific settings, because the user may not + * yet have configured the form. + * If there are plugin-specific settings (verify with isset()), they can be + * found at $editor->settings['plugins'][$plugin_id]. + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * A keyed array, whose keys will end up as keys under CKEDITOR.config. + */ + public function getConfig(Editor $editor); +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php new file mode 100644 index 0000000..57a3a4b --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php @@ -0,0 +1,158 @@ +discovery = new AnnotatedClassDiscovery('ckeditor', 'plugin'); + $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition')); + $this->discovery = new AlterDecorator($this->discovery, 'ckeditor_plugin_info'); + $this->discovery = new CacheDecorator($this->discovery, 'ckeditor_plugin'); + $this->factory = new DefaultFactory($this->discovery); + } + + /** + * Determines which plug-ins are enabled. + * + * For CKEditor plugins that implement: + * - CKEditorPluginButtonsInterface, not CKEditorPluginContextualInterface, + * a plugin is enabled if at least one of its buttons is in the toolbar; + * - CKEditorPluginContextualInterface, not CKEditorPluginButtonsInterface, + * a plugin is enabled if its isEnabled() method returns TRUE + * - both of these interfaces, a plugin is enabled if either is the case. + * + * Internal plugins (those that are part of the bundled build of CKEditor) are + * excluded by default, since they are loaded implicitly. If you need to know + * even implicitly loaded (i.e. internal) plugins, then set the optional + * second parameter. + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @param bool $include_internal_plugins + * Defaults to FALSE. When set to TRUE, plugins whose isInternal() method + * returns TRUE will also be included. + * @return array + * A list of the enabled CKEditor plugins, with the plugin IDs as keys and + * the Drupal root-relative plugin files as values. + * For internal plugins, the value is NULL. + */ + public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FALSE) { + $plugins = array_keys($this->getDefinitions()); + $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); + $enabled_plugins = array(); + + foreach ($plugins as $plugin_id) { + $plugin = $this->createInstance($plugin_id); + + if (!$include_internal_plugins && $plugin->isInternal()) { + continue; + } + + $enabled = FALSE; + if ($plugin instanceof CKEditorPluginButtonsInterface) { + $plugin_buttons = array_keys($plugin->getButtons()); + $enabled = (count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0); + } + if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) { + $enabled = $plugin->isEnabled($editor); + } + + if ($enabled) { + $enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile(); + } + } + + // Always return plugins in the same order. + asort($enabled_plugins); + + return $enabled_plugins; + } + + /** + * Retrieves all plugins that implement CKEditorPluginButtonsInterface. + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * A list of the CKEditor plugins that implement buttons, with the plugin + * IDs as keys and lists of button metadata (as implemented by getButtons()) + * as values. + * + * @see CKEditorPluginButtonsInterface::getButtons() + */ + public function getButtonsPlugins(Editor $editor) { + $plugins = array_keys($this->getDefinitions()); + $buttons_plugins = array(); + + foreach ($plugins as $plugin_id) { + $plugin = $this->createInstance($plugin_id); + if ($plugin instanceof CKEditorPluginButtonsInterface) { + $buttons_plugins[$plugin_id] = $plugin->getButtons(); + } + } + + return $buttons_plugins; + } + + /** + * Injects the CKEditor plugins settings forms as a vertical tabs subform. + * + * @param array &$form + * A reference to an associative array containing the structure of the form. + * @param array &$form_state + * A reference to a keyed array containing the current state of the form. + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + */ + public function injectPluginSettingsForm(array &$form, array &$form_state, Editor $editor) { + $definitions = $this->getDefinitions(); + + foreach (array_keys($definitions) as $plugin_id) { + $plugin = $this->createInstance($plugin_id); + if ($plugin instanceof CKEditorPluginConfigurableInterface) { + $plugin_settings_form = array(); + $form['plugins'][$plugin_id] = array( + '#type' => 'details', + '#title' => $definitions[$plugin_id]['label'], + '#group' => 'editor][settings][plugin_settings', + ); + $form['plugins'][$plugin_id] += $plugin->settingsForm($plugin_settings_form, $form_state, $editor); + } + } + } + + /** + * Overrides Drupal\Component\Plugin\PluginManagerBase::processDefinition(). + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + // @todo Remove this check once http://drupal.org/node/1780396 is resolved. + if (!module_exists($definition['module'])) { + $definition = NULL; + return; + } + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CkeditorBundle.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CkeditorBundle.php new file mode 100644 index 0000000..eb8af12 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CkeditorBundle.php @@ -0,0 +1,26 @@ +register('plugin.manager.ckeditor.plugin', 'Drupal\ckeditor\CKEditorPluginManager'); + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php new file mode 100644 index 0000000..51fa9c5 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/Internal.php @@ -0,0 +1,291 @@ + TRUE, + 'indentClasses' => array('indent1', 'indent2', 'indent3'), + 'justifyClasses' => array('align-left', 'align-center', 'align-right', 'align-justify'), + 'coreStyles_underline' => array('element' => 'span', 'attributes' => array('class' => 'underline')), + 'removeDialogTabs' => 'image:Link;image:advanced;link:advanced', + 'resize_dir' => 'vertical', + 'keystrokes' => array( + // 0x11000 is CKEDITOR.CTRL, see http://docs.ckeditor.com/#!/api/CKEDITOR-property-CTRL. + array(0x110000 + 75, 'link'), + array(0x110000 + 76, NULL), + ), + ); + + // Next, add the format_tags setting, if its button is enabled. + $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); + if (in_array('Format', $toolbar_buttons)) { + $config['format_tags'] = $this->generateFormatTagsSetting($editor); + } + + return $config; + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons(). + */ + public function getButtons() { + $button = function($name, $direction = 'ltr') { + return '' . $name . ''; + }; + + return array( + // "basicstyles" plugin. + 'Bold' => array( + 'label' => t('Bold'), + 'image_alternative' => $button('bold'), + ), + 'Italic' => array( + 'label' => t('Italic'), + 'image_alternative' => $button('italic'), + ), + 'Underline' => array( + 'label' => t('Underline'), + 'image_alternative' => $button('underline'), + ), + 'Strike' => array( + 'label' => t('Strike-through'), + 'image_alternative' => $button('strike'), + ), + 'Superscript' => array( + 'label' => t('Superscript'), + 'image_alternative' => $button('super script'), + ), + 'Subscript' => array( + 'label' => t('Subscript'), + 'image_alternative' => $button('sub script'), + ), + // "removeformat" plugin. + 'RemoveFormat' => array( + 'label' => t('Remove format'), + 'image_alternative' => $button('remove format'), + ), + // "justify" plugin. + 'JustifyLeft' => array( + 'label' => t('Align left'), + 'image_alternative' => $button('justify left'), + ), + 'JustifyCenter' => array( + 'label' => t('Align center'), + 'image_alternative' => $button('justify center'), + ), + 'JustifyRight' => array( + 'label' => t('Align right'), + 'image_alternative' => $button('justify right'), + ), + 'JustifyBlock' => array( + 'label' => t('Justify'), + 'image_alternative' => $button('justify block'), + ), + // "list" plugin. + 'BulletedList' => array( + 'label' => t('Bullet list'), + 'image_alternative' => $button('bulleted list'), + 'image_alternative_rtl' => $button('bulleted list', 'rtl'), + ), + 'NumberedList' => array( + 'label' => t('Numbered list'), + 'image_alternative' => $button('numbered list'), + 'image_alternative_rtl' => $button('numbered list', 'rtl'), + ), + // "indent" plugin. + 'Outdent' => array( + 'label' => t('Outdent'), + 'image_alternative' => $button('outdent'), + 'image_alternative_rtl' => $button('outdent', 'rtl'), + ), + 'Indent' => array( + 'label' => t('Indent'), + 'image_alternative' => $button('indent'), + 'image_alternative_rtl' => $button('indent', 'rtl'), + ), + // "undo" plugin. + 'Undo' => array( + 'label' => t('Undo'), + 'image_alternative' => $button('undo'), + 'image_alternative_rtl' => $button('undo', 'rtl'), + ), + 'Redo' => array( + 'label' => t('Redo'), + 'image_alternative' => $button('redo'), + 'image_alternative_rtl' => $button('redo', 'rtl'), + ), + // "link" plugin. + 'Link' => array( + 'label' => t('Link'), + 'image_alternative' => $button('link'), + ), + 'Unlink' => array( + 'label' => t('Unlink'), + 'image_alternative' => $button('unlink'), + ), + 'Anchor' => array( + 'label' => t('Anchor'), + 'image_alternative' => $button('anchor'), + 'image_alternative_rtl' => $button('anchor', 'rtl'), + ), + // "blockquote" plugin. + 'Blockquote' => array( + 'label' => t('Blockquote'), + 'image_alternative' => $button('blockquote'), + ), + // "horizontalrule" plugin + 'HorizontalRule' => array( + 'label' => t('Horizontal rule'), + 'image_alternative' => $button('horizontal rule'), + ), + // "clipboard" plugin. + 'Cut' => array( + 'label' => t('Cut'), + 'image_alternative' => $button('cut'), + 'image_alternative_rtl' => $button('cut', 'rtl'), + ), + 'Copy' => array( + 'label' => t('Copy'), + 'image_alternative' => $button('copy'), + 'image_alternative_rtl' => $button('copy', 'rtl'), + ), + 'Paste' => array( + 'label' => t('Paste'), + 'image_alternative' => $button('paste'), + 'image_alternative_rtl' => $button('paste', 'rtl'), + ), + // "pastetext" plugin. + 'PasteText' => array( + 'label' => t('Paste Text'), + 'image_alternative' => $button('paste text'), + 'image_alternative_rtl' => $button('paste text', 'rtl'), + ), + // "pastefromword" plugin. + 'PasteFromWord' => array( + 'label' => t('Paste from Word'), + 'image_alternative' => $button('paste from word'), + 'image_alternative_rtl' => $button('paste from word', 'rtl'), + ), + // "specialchar" plugin. + 'SpecialChar' => array( + 'label' => t('Character map'), + 'image_alternative' => $button('special char'), + ), + 'Format' => array( + 'label' => t('HTML block format'), + 'image_alternative' => '' . t('Format') . '', + ), + // "image" plugin. + 'Image' => array( + 'label' => t('Image'), + 'image_alternative' => $button('image'), + ), + // "table" plugin. + 'Table' => array( + 'label' => t('Table'), + 'image_alternative' => $button('table'), + ), + // "showblocks" plugin. + 'ShowBlocks' => array( + 'label' => t('Show blocks'), + 'image_alternative' => $button('show blocks'), + 'image_alternative_rtl' => $button('show blocks', 'rtl'), + ), + // "sourcearea" plugin. + 'Source' => array( + 'label' => t('Source code'), + 'image_alternative' => $button('source'), + ), + // "maximize" plugin. + 'Maximize' => array( + 'label' => t('Maximize'), + 'image_alternative' => $button('maximize'), + ), + // No plugin, separator "buttons" for toolbar builder UI use only. + '|' => array( + 'label' => t('Group separator'), + 'image_alternative' => '', + 'attributes' => array('class' => array('ckeditor-group-button-separator')), + 'multiple' => TRUE, + ), + '-' => array( + 'label' => t('Separator'), + 'image_alternative' => '', + 'attributes' => array('class' => array('ckeditor-button-separator')), + 'multiple' => TRUE, + ), + ); + } + + /** + * Builds the "format_tags" configuration part of the CKEditor JS settings. + * + * @see getConfig() + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * An array containing the "format_tags" configuration. + */ + protected function generateFormatTagsSetting(Editor $editor) { + // The

tag is always allowed — HTML without

tags is nonsensical. + $format_tags = array('p'); + + // Given the list of possible format tags, automatically determine whether + // the current text format allows this tag, and thus whether it should show + // up in the "Format" dropdown. + $possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'); + foreach ($possible_format_tags as $tag) { + $input = '<' . $tag . '>TEST'; + $output = trim(check_markup($input, $editor->format)); + if ($input == $output) { + $format_tags[] = $tag; + } + } + + return implode(';', $format_tags); + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php new file mode 100644 index 0000000..f810cbb --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/ckeditor/plugin/StylesCombo.php @@ -0,0 +1,159 @@ +settings['toolbar']['buttons'])); + if (in_array('Styles', $toolbar_buttons)) { + $styles = $editor->settings['plugins']['stylescombo']['styles']; + $config['stylesSet'] = $this->generateStylesSetSetting($styles); + } + + return $config; + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons(). + */ + public function getButtons() { + return array( + 'Styles' => array( + 'label' => t('Font style'), + 'image_alternative' => '' . t('Styles') . '', + ), + ); + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginConfigurableInterface::settingsForm(). + */ + public function settingsForm(array $form, array &$form_state, Editor $editor) { + // Defaults. + $config = array('styles' => ''); + if (isset($editor->settings['plugins']['stylescombo'])) { + $config = $editor->settings['plugins']['stylescombo']; + } + + $form['styles'] = array( + '#title' => t('Styles'), + '#title_display' => 'invisible', + '#type' => 'textarea', + '#default_value' => $config['styles'], + '#description' => t('A list of classes that will be provided in the "Styles" dropdown. Enter one class on each line in the format: label|element.class. Example: title|h1.title.
These styles should be available in your theme\'s CKEditor stylesheets as well as in your theme\'s main CSS file.'), + '#attached' => array( + 'library' => array(array('ckeditor', 'drupal.ckeditor.stylescombo.admin')), + ), + '#element_validate' => array( + array($this, 'validateStylesValue'), + ), + ); + + return $form; + } + + /** + * #element_validate handler for the "styles" element in settingsForm(). + */ + public function validateStylesValue(array $element, array &$form_state) { + if ($this->generateStylesSetSetting($element['#value']) === FALSE) { + form_error($element, t('The provided list of styles is syntactically incorrect.')); + } + } + + /** + * Builds the "stylesSet" configuration part of the CKEditor JS settings. + * + * @see getConfig() + * + * @param string $styles + * The "styles" setting. + * @return array|FALSE + * An array containing the "stylesSet" configuration, or FALSE when the + * syntax is invalid. + */ + protected function generateStylesSetSetting($styles) { + $styles_set = array(); + + // Early-return when empty. + $styles = trim($styles); + if (empty($styles)) { + return $styles_set; + } + + $styles = str_replace(array("\r\n", "\r"), "\n", $styles); + foreach (explode("\n", $styles) as $style) { + $style = trim($style); + + // Ignore empty lines in between non-empty lines. + if (empty($style)) { + continue; + } + + // Validate syntax: label|element.class[.class...] pattern expected. + if (!preg_match('@^.+\\| *[a-zA-Z0-9]+ *(\\.[a-zA-Z0-9_-]+ *)+$@', $style)) { + return FALSE; + } + + // Parse. + list($label, $selector) = explode('|', $style, 2); + $classes = explode('.', $selector); + $element = array_shift($classes); + + // Build the data structure CKEditor's stylescombo plugin expects. + // @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles + $styles_set[] = array( + 'name' => trim($label), + 'element' => trim($element), + 'attributes' => array( + 'class' => implode(' ', array_map('trim', $classes)) + ), + ); + } + return $styles_set; + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php new file mode 100644 index 0000000..379d00e --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php @@ -0,0 +1,200 @@ + array( + 'buttons' => array( + array( + 'Source', '|', 'Bold', 'Italic', '|', + 'NumberedList', 'BulletedList', 'Blockquote', '|', + 'JustifyLeft', 'JustifyCenter', 'JustifyRight', '|', + 'Link', 'Unlink', '|', 'Image', 'Maximize', + ), + ), + ), + 'plugins' => array(), + ); + } + + /** + * Implements \Drupal\editor\Plugin\EditorInterface::settingsForm(). + */ + public function settingsForm(array $form, array &$form_state, Editor $editor) { + $module_path = drupal_get_path('module', 'ckeditor'); + $manager = drupal_container()->get('plugin.manager.ckeditor.plugin'); + + $form['toolbar'] = array( + '#type' => 'container', + '#attached' => array( + 'library' => array(array('ckeditor', 'drupal.ckeditor.admin')), + 'js' => array( + array( + 'type' => 'setting', + 'data' => array('ckeditor' => array( + 'toolbarAdmin' => theme('ckeditor_settings_toolbar', array('editor' => $editor, 'plugins' => $manager->getButtonsPlugins($editor))), + )), + ) + ), + ), + '#attributes' => array('class' => array('ckeditor-toolbar-configuration')), + ); + $form['toolbar']['buttons'] = array( + '#type' => 'textarea', + '#title' => t('Toolbar buttons'), + '#default_value' => json_encode($editor->settings['toolbar']['buttons']), + '#attributes' => array('class' => array('ckeditor-toolbar-textarea')), + ); + + // CKEditor plugin settings, if any. + $form['plugin_settings'] = array( + '#type' => 'vertical_tabs', + '#title' => t('Optional settings'), + ); + $manager->injectPluginSettingsForm($form, $form_state, $editor); + if (count(element_children($form['plugins'])) === 0) { + unset($form['plugins']); + unset($form['plugin_settings']); + } + + return $form; + } + + /** + * Implements \Drupal\editor\Plugin\EditorInterface::settingsFormSubmit(). + */ + public function settingsFormSubmit(array $form, array &$form_state) { + // Modify the toolbar settings by reference. The values in + // $form_state['values']['editor']['settings'] will be saved directly by + // editor_form_filter_admin_format_submit(). + $toolbar_settings = &$form_state['values']['editor']['settings']['toolbar']; + + $toolbar_settings['buttons'] = json_decode($toolbar_settings['buttons'], FALSE); + + // Remove the plugin settings' vertical tabs state; no need to save that. + if (isset($form_state['values']['editor']['settings']['plugins'])) { + unset($form_state['values']['editor']['settings']['plugin_settings']); + } + } + + /** + * Implements \Drupal\editor\Plugin\EditorInterface::getJSSettings(). + */ + public function getJSSettings(Editor $editor) { + $language_interface = language(LANGUAGE_TYPE_INTERFACE); + + $settings = array(); + $manager = drupal_container()->get('plugin.manager.ckeditor.plugin'); + + // Get the settings for all enabled plugins, even the internal ones. + $enabled_plugins = array_keys($manager->getEnabledPlugins($editor, TRUE)); + foreach ($enabled_plugins as $plugin_id) { + $plugin = $manager->createInstance($plugin_id); + $settings += $plugin->getConfig($editor); + } + + // Next, set the most fundamental CKEditor settings. + $external_plugins = $manager->getEnabledPlugins($editor); + $settings += array( + 'toolbar' => $this->buildToolbarJSSetting($editor), + 'contentsCss' => $this->buildContentsCssJSSetting($editor), + 'extraPlugins' => implode(',', array_keys($external_plugins)), + 'language' => $language_interface->langcode, + ); + + // Finally, set Drupal-specific CKEditor settings. + $settings += array( + 'drupalExternalPlugins' => array_map('file_create_url', $external_plugins), + ); + + return $settings; + } + + /** + * Implements \Drupal\editor\Plugin\EditorInterface::getLibraries(). + */ + public function getLibraries(Editor $editor) { + return array( + array('ckeditor', 'drupal.ckeditor'), + ); + } + + /** + * Builds the "toolbar" configuration part of the CKEditor JS settings. + * + * @see getJSSettings() + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * An array containing the "toolbar" configuration. + */ + public function buildToolbarJSSetting(Editor $editor) { + $toolbar = array(); + foreach ($editor->settings['toolbar']['buttons'] as $row_number => $row) { + $button_group = array(); + foreach ($row as $button_name) { + // Change the toolbar separators into groups. + if ($button_name === '|') { + $toolbar[] = $button_group; + $button_group = array(); + } + else { + $button_group['items'][] = $button_name; + } + } + $toolbar[] = $button_group; + $toolbar[] = '/'; + } + + return $toolbar; + } + + /** + * Builds the "contentsCss" configuration part of the CKEditor JS settings. + * + * @see getJSSettings() + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * @return array + * An array containing the "contentsCss" configuration. + */ + public function buildContentsCssJSSetting(Editor $editor) { + $css = array( + drupal_get_path('module', 'ckeditor') . '/css/ckeditor.css', + drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css', + ); + $css = array_merge($css, _ckeditor_theme_css()); + drupal_alter('ckeditor_css', $css, $editor); + $css = array_map('file_create_url', $css); + + return array_values($css); + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php new file mode 100644 index 0000000..fa0adc9 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php @@ -0,0 +1,142 @@ + 'CKEditor administration', + 'description' => 'Tests administration of CKEditor.', + 'group' => 'CKEditor', + ); + } + + function setUp() { + parent::setUp(); + + // Create text format. + $filtered_html_format = entity_create('filter_format', array( + 'format' => 'filtered_html', + 'name' => 'Filtered HTML', + 'weight' => 0, + 'filters' => array(), + )); + $filtered_html_format->save(); + + // Create admin user. + $this->admin_user = $this->drupalCreateUser(array('administer filters')); + } + + function testAdmin() { + $manager = drupal_container()->get('plugin.manager.editor'); + $ckeditor = $manager->createInstance('ckeditor'); + + $this->drupalLogin($this->admin_user); + $this->drupalGet('admin/config/content/formats/filtered_html'); + + // Ensure no Editor config entity exists yet. + $editor = entity_load('editor', 'filtered_html'); + $this->assertFalse($editor, 'No Editor config entity exists yet.'); + + // Verify the "Text Editor" . - $select = $this->xpath('//select[@name="editor"]'); - $select_is_disabled = $this->xpath('//select[@name="editor" and @disabled="disabled"]'); - $options = $this->xpath('//select[@name="editor"]/option'); + $select = $this->xpath('//select[@name="editor[editor]"]'); + $select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]'); + $options = $this->xpath('//select[@name="editor[editor]"]/option'); $this->assertTrue(count($select) === 1, 'The Text Editor select exists.'); $this->assertTrue(count($select_is_disabled) === 1, 'The Text Editor select is disabled.'); $this->assertTrue(count($options) === 1, 'The Text Editor select has only one option.'); @@ -72,9 +72,9 @@ function testWithoutEditorAvailable() { $this->drupalGet('admin/config/content/formats/filtered_html'); // Verify the