Move into nested docroot
This commit is contained in:
parent
83a0d3a149
commit
c8b70abde9
13405 changed files with 0 additions and 0 deletions
143
web/core/modules/ckeditor/ckeditor.admin.inc
Normal file
143
web/core/modules/ckeditor/ckeditor.admin.inc
Normal file
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Callbacks and theming for the CKEditor toolbar configuration UI.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
/**
|
||||
* Prepares variables for CKEditor settings toolbar templates.
|
||||
*
|
||||
* Default template: ckeditor-settings-toolbar.html.twig.
|
||||
*
|
||||
* @param array $variables
|
||||
* An associative array containing:
|
||||
* - editor: An editor object.
|
||||
* - plugins: A list of plugins.
|
||||
* - active_buttons: A list of disabled buttons.
|
||||
* - disabled_buttons: A list of disabled buttons.
|
||||
* - multiple_buttons: A list of multiple buttons that may be added multiple
|
||||
* times.
|
||||
*/
|
||||
function template_preprocess_ckeditor_settings_toolbar(&$variables) {
|
||||
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
|
||||
|
||||
// Create lists of active and disabled buttons.
|
||||
$editor = $variables['editor'];
|
||||
$plugins = $variables['plugins'];
|
||||
$buttons = array();
|
||||
$multiple_buttons = array();
|
||||
foreach ($plugins as $plugin_buttons) {
|
||||
foreach ($plugin_buttons as $button_name => $button) {
|
||||
$button['name'] = $button_name;
|
||||
if (!empty($button['multiple'])) {
|
||||
$multiple_buttons[$button_name] = $button;
|
||||
}
|
||||
$buttons[$button_name] = $button;
|
||||
}
|
||||
}
|
||||
$button_groups = array();
|
||||
$active_buttons = array();
|
||||
$settings = $editor->getSettings();
|
||||
foreach ($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'];
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$disabled_buttons = array_diff_key($buttons, $multiple_buttons);
|
||||
|
||||
$rtl = $language_interface->getDirection() === LanguageInterface::DIRECTION_RTL ? '_rtl' : '';
|
||||
|
||||
$build_button_item = function($button, $rtl) {
|
||||
// Value of the button item.
|
||||
if (isset($button['image_alternative' . $rtl])) {
|
||||
$value = $button['image_alternative' . $rtl];
|
||||
}
|
||||
elseif (isset($button['image_alternative'])) {
|
||||
$value = $button['image_alternative'];
|
||||
}
|
||||
elseif (isset($button['image']) || isset($button['image' . $rtl])) {
|
||||
$value = array(
|
||||
'#theme' => 'image',
|
||||
'#uri' => isset($button['image' . $rtl]) ? $button['image' . $rtl] : $button['image'],
|
||||
'#title' => $button['label'],
|
||||
'#prefix' => '<a href="#" role="button" title="' . $button['label'] . '" aria-label="' . $button['label'] . '"><span class="cke_button_icon">',
|
||||
'#suffix' => '</span></a>',
|
||||
);
|
||||
}
|
||||
else {
|
||||
$value = '?';
|
||||
}
|
||||
|
||||
// Build the button attributes.
|
||||
$attributes = array(
|
||||
'data-drupal-ckeditor-button-name' => $button['name'],
|
||||
);
|
||||
if (!empty($button['attributes'])) {
|
||||
$attributes = array_merge($attributes, $button['attributes']);
|
||||
}
|
||||
|
||||
// Build the button item.
|
||||
$button_item = array(
|
||||
'value' => $value,
|
||||
'attributes' => new Attribute($attributes),
|
||||
);
|
||||
// If this button has group information, add it to the attributes.
|
||||
if (!empty($button['group'])) {
|
||||
$button_item['group'] = $button['group'];
|
||||
}
|
||||
|
||||
// Set additional flag on the button if it can occur multiple times.
|
||||
if (!empty($button['multiple'])) {
|
||||
$button_item['multiple'] = TRUE;
|
||||
}
|
||||
|
||||
return $button_item;
|
||||
};
|
||||
|
||||
// Assemble list of disabled buttons (which are always a single row).
|
||||
$variables['active_buttons'] = array();
|
||||
foreach ($active_buttons as $row_number => $button_row) {
|
||||
foreach ($button_groups[$row_number] as $group_name) {
|
||||
$group_name = (string) $group_name;
|
||||
$variables['active_buttons'][$row_number][$group_name] = array(
|
||||
'group_name_class' => Html::getClass($group_name),
|
||||
'buttons' => array(),
|
||||
);
|
||||
$buttons = array_filter($button_row, function ($button) use ($group_name) {
|
||||
return (string) $button['group'] === $group_name;
|
||||
});
|
||||
foreach ($buttons as $button) {
|
||||
$variables['active_buttons'][$row_number][$group_name]['buttons'][] = $build_button_item($button, $rtl);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Assemble list of disabled buttons (which are always a single row).
|
||||
$variables['disabled_buttons'] = array();
|
||||
foreach ($disabled_buttons as $button) {
|
||||
$variables['disabled_buttons'][] = $build_button_item($button, $rtl);
|
||||
}
|
||||
// Assemble list of multiple buttons that may be added multiple times.
|
||||
$variables['multiple_buttons'] = array();
|
||||
foreach ($multiple_buttons as $button) {
|
||||
$variables['multiple_buttons'][] = $build_button_item($button, $rtl);
|
||||
}
|
||||
}
|
61
web/core/modules/ckeditor/ckeditor.api.php
Normal file
61
web/core/modules/ckeditor/ckeditor.api.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Documentation for CKEditor module APIs.
|
||||
*/
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Modify the list of available CKEditor plugins.
|
||||
*
|
||||
* This hook may be used to modify plugin properties after they have been
|
||||
* specified by other modules.
|
||||
*
|
||||
* @param $plugins
|
||||
* An array of all the existing plugin definitions, passed by reference.
|
||||
*
|
||||
* @see CKEditorPluginManager
|
||||
*/
|
||||
function hook_ckeditor_plugin_info_alter(array &$plugins) {
|
||||
$plugins['someplugin']['label'] = t('Better name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the list of CSS files that will be added to a CKEditor instance.
|
||||
*
|
||||
* Modules may use this hook to provide their own custom CSS file without
|
||||
* providing a CKEditor plugin. This list of CSS files is only used in the
|
||||
* iframe versions of CKEditor.
|
||||
*
|
||||
* Front-end themes (and base themes) can easily specify CSS files to be used in
|
||||
* iframe instances of CKEditor through an entry in their .info.yml file:
|
||||
*
|
||||
* @code
|
||||
* ckeditor_stylesheets:
|
||||
* - css/ckeditor-iframe.css
|
||||
* @endcode
|
||||
*
|
||||
* @param array &$css
|
||||
* An array of CSS files, passed by reference. This is a flat list of file
|
||||
* paths which can be either relative to the Drupal root or external URLs.
|
||||
* @param $editor
|
||||
* The text editor object as returned by editor_load(), for which these files
|
||||
* are being loaded. Based on this information, it is possible to load the
|
||||
* corresponding text format object.
|
||||
*
|
||||
* @see _ckeditor_theme_css()
|
||||
*/
|
||||
function hook_ckeditor_css_alter(array &$css, Editor $editor) {
|
||||
$css[] = drupal_get_path('module', 'mymodule') . '/css/mymodule-ckeditor.css';
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
8
web/core/modules/ckeditor/ckeditor.info.yml
Normal file
8
web/core/modules/ckeditor/ckeditor.info.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
name: CKEditor
|
||||
type: module
|
||||
description: "WYSIWYG editing for rich text fields using CKEditor."
|
||||
package: Core
|
||||
core: 8.x
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- editor
|
94
web/core/modules/ckeditor/ckeditor.libraries.yml
Normal file
94
web/core/modules/ckeditor/ckeditor.libraries.yml
Normal file
|
@ -0,0 +1,94 @@
|
|||
drupal.ckeditor:
|
||||
version: VERSION
|
||||
js:
|
||||
js/ckeditor.js: {}
|
||||
css:
|
||||
state:
|
||||
css/ckeditor.css: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/drupalSettings
|
||||
- core/drupal.debounce
|
||||
- core/ckeditor
|
||||
- editor/drupal.editor
|
||||
# Ensure to run after core/matchmedia.
|
||||
- core/matchmedia
|
||||
|
||||
drupal.ckeditor.plugins.drupalimagecaption:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css: {}
|
||||
dependencies:
|
||||
- filter/caption
|
||||
|
||||
drupal.ckeditor.plugins.language:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/plugins/language/ckeditor.language.css: {}
|
||||
|
||||
drupal.ckeditor.admin:
|
||||
version: VERSION
|
||||
js:
|
||||
# Core.
|
||||
js/ckeditor.admin.js: {}
|
||||
# Models.
|
||||
js/models/Model.js: {}
|
||||
# Views.
|
||||
js/views/AuralView.js: {}
|
||||
js/views/KeyboardView.js: {}
|
||||
js/views/ControllerView.js: {}
|
||||
js/views/VisualView.js: {}
|
||||
css:
|
||||
theme:
|
||||
css/ckeditor.admin.css: {}
|
||||
/core/assets/vendor/ckeditor/skins/moono/editor.css: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/drupalSettings
|
||||
- core/jquery.once
|
||||
- core/jquery.ui.sortable
|
||||
- core/jquery.ui.draggable
|
||||
- core/jquery.ui.touch-punch
|
||||
- core/backbone
|
||||
- core/drupal.dialog
|
||||
- core/drupal.announce
|
||||
- core/ckeditor
|
||||
- editor/drupal.editor.admin
|
||||
# Ensure to run after core/drupal.vertical-tabs.
|
||||
- core/drupal.vertical-tabs
|
||||
|
||||
drupal.ckeditor.drupalimage.admin:
|
||||
version: VERSION
|
||||
js:
|
||||
js/ckeditor.drupalimage.admin.js: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/jquery.once
|
||||
- core/drupal.vertical-tabs
|
||||
- core/drupalSettings
|
||||
|
||||
drupal.ckeditor.stylescombo.admin:
|
||||
version: VERSION
|
||||
js:
|
||||
js/ckeditor.stylescombo.admin.js: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/drupal.vertical-tabs
|
||||
- core/drupalSettings
|
||||
# Ensure to run after ckeditor/drupal.ckeditor.admin.
|
||||
- ckeditor/drupal.ckeditor.admin
|
||||
|
||||
drupal.ckeditor.language.admin:
|
||||
version: VERSION
|
||||
js:
|
||||
js/ckeditor.language.admin.js: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/drupal.vertical-tabs
|
116
web/core/modules/ckeditor/ckeditor.module
Normal file
116
web/core/modules/ckeditor/ckeditor.module
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provides integration with the CKEditor WYSIWYG editor.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function ckeditor_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
case 'help.page.ckeditor':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The CKEditor module provides a highly-accessible, highly-usable visual text editor and adds a toolbar to text fields. Users can use buttons to format content and to create semantically correct and valid HTML. The CKEditor module uses the framework provided by the <a href=":text_editor">Text Editor module</a>. It requires JavaScript to be enabled in the browser. For more information, see the <a href=":doc_url">online documentation for the CKEditor module</a> and the <a href=":cke_url">CKEditor website</a>.', array( ':doc_url' => 'https://www.drupal.org/documentation/modules/ckeditor', ':cke_url' => 'http://ckeditor.com', ':text_editor' => \Drupal::url('help.page', array('name' => 'editor')))) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Enabling CKEditor for individual text formats') . '</dt>';
|
||||
$output .= '<dd>' . t('CKEditor has to be enabled and configured separately for individual text formats from the <a href=":formats">Text formats and editors page</a> because the filter settings for each text format can be different. For more information, see the <a href=":text_editor">Text Editor help page</a> and <a href=":filter">Filter help page</a>.', array(':formats' => \Drupal::url('filter.admin_overview'), ':text_editor' => \Drupal::url('help.page', array('name' => 'editor')), ':filter' => \Drupal::url('help.page', array('name' => 'filter')))) . '</dd>';
|
||||
$output .= '<dt>' . t('Configuring the toolbar') . '</dt>';
|
||||
$output .= '<dd>' . t('When CKEditor is chosen from the <em>Text editor</em> drop-down menu, its toolbar configuration is displayed. You can add and remove buttons from the <em>Active toolbar</em> by dragging and dropping them, and additional rows can be added to organize the buttons.') . '</dd>';
|
||||
$output .= '<dt>' . t('Formatting content') . '</dt>';
|
||||
$output .= '<dd>' . t('CKEditor only allow users to format content in accordance with the filter configuration of the specific text format. If a text format excludes certain HTML tags, the corresponding toolbar buttons are not displayed to users when they edit a text field in this format. For more information see the <a href=":filter">Filter help page</a>.', array(':filter' => \Drupal::url('help.page', array('name' => 'filter')))) . '</dd>';
|
||||
$output .= '<dt>' . t('Toggling between formatted text and HTML source') . '</dt>';
|
||||
$output .= '<dd>' . t('If the <em>Source</em> button is available in the toolbar, users can click this button to disable the visual editor and edit the HTML source directly. After toggling back, the visual editor uses the allowed HTML tags to format the text — independent of whether buttons for these tags are available in the toolbar. If the text format is set to <em>limit the use of HTML tags</em>, then all excluded tags will be stripped out of the HTML source when the user toggles back to the text editor.') . '</dd>';
|
||||
$output .= '<dt>' . t('Check my spelling as I type') . '</dt>';
|
||||
$output .= '<dd>' . t('By default, CKEditor is configured to leverage your browser\'s spell check capability. Make sure your browser\'s spell checker is enabled in your browser\'s settings. To access suggested corrections for misspelled words, it may be necessary to hold the <em>Control</em> or <em>command</em> (Mac) key while right-clicking the misspelling.') . '</dd>';
|
||||
$output .= '<dt>' . t('Accessibility features') . '</dt>';
|
||||
$output .= '<dd>' . t('The built in WYSIWYG editor (CKEditor) comes with a number of <a href=":features">accessibility features</a>. CKEditor comes with built in <a href=":shortcuts">keyboard shortcuts</a>, which can be beneficial for both power users and keyboard only users.', array(':features' => 'http://docs.ckeditor.com/#!/guide/dev_a11y', ':shortcuts' => 'http://docs.ckeditor.com/#!/guide/dev_shortcuts')) . '</dd>';
|
||||
$output .= '<dt>' . t('Generating accessible content') . '</dt>';
|
||||
$output .= '<dd>' . t('HTML tables can be created with both table headers as well as caption/summary elements. Alt text is required by default on images added through CKEditor (note that this can be overridden). Semantic HTML5 figure/figcaption are available to add captions to images.') . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
function ckeditor_theme() {
|
||||
return array(
|
||||
'ckeditor_settings_toolbar' => array(
|
||||
'file' => 'ckeditor.admin.inc',
|
||||
'variables' => array('editor' => NULL, 'plugins' => NULL),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ckeditor_css_alter().
|
||||
*/
|
||||
function ckeditor_ckeditor_css_alter(array &$css, Editor $editor) {
|
||||
if (!$editor->hasAssociatedFilterFormat()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the filter caption CSS if the text format associated with this text
|
||||
// editor uses the filter_caption filter. This is used by the included
|
||||
// CKEditor DrupalImageCaption plugin.
|
||||
if ($editor->getFilterFormat()->filters('filter_caption')->status) {
|
||||
$css[] = drupal_get_path('module', 'filter') . '/css/filter.caption.css';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default theme's CKEditor stylesheets.
|
||||
*
|
||||
* Themes may specify iframe-specific CSS files for use with CKEditor by
|
||||
* including a "ckeditor_stylesheets" key in their .info.yml file.
|
||||
*
|
||||
* @code
|
||||
* ckeditor_stylesheets:
|
||||
* - css/ckeditor-iframe.css
|
||||
* @endcode
|
||||
*/
|
||||
function _ckeditor_theme_css($theme = NULL) {
|
||||
$css = array();
|
||||
if (!isset($theme)) {
|
||||
$theme = \Drupal::config('system.theme')->get('default');
|
||||
}
|
||||
if (isset($theme) && $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 => $url) {
|
||||
if (UrlHelper::isExternal($url)) {
|
||||
$css[$key] = $url;
|
||||
}
|
||||
else {
|
||||
$css[$key] = $theme_path . '/' . $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($info['base theme'])) {
|
||||
$css = array_merge(_ckeditor_theme_css($info['base theme']), $css);
|
||||
}
|
||||
}
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_library_info_alter().
|
||||
*/
|
||||
function ckeditor_library_info_alter(&$libraries, $extension) {
|
||||
// Pass Drupal's JS cache-busting string via settings along to CKEditor.
|
||||
// @see http://docs.ckeditor.com/#!/api/CKEDITOR-property-timestamp
|
||||
if ($extension === 'ckeditor' && isset($libraries['drupal.ckeditor'])) {
|
||||
$query_string = \Drupal::state()->get('system.css_js_query_string') ?: '0';
|
||||
$libraries['drupal.ckeditor']['drupalSettings']['ckeditor']['timestamp'] = $query_string;
|
||||
}
|
||||
}
|
4
web/core/modules/ckeditor/ckeditor.services.yml
Normal file
4
web/core/modules/ckeditor/ckeditor.services.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
services:
|
||||
plugin.manager.ckeditor.plugin:
|
||||
class: Drupal\ckeditor\CKEditorPluginManager
|
||||
parent: default_plugin_manager
|
52
web/core/modules/ckeditor/config/schema/ckeditor.schema.yml
Normal file
52
web/core/modules/ckeditor/config/schema/ckeditor.schema.yml
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Schema for the configuration files of the CKEditor module.
|
||||
|
||||
editor.settings.ckeditor:
|
||||
type: mapping
|
||||
label: 'CKEditor settings'
|
||||
mapping:
|
||||
toolbar:
|
||||
type: mapping
|
||||
label: 'Toolbar configuration'
|
||||
mapping:
|
||||
rows:
|
||||
type: sequence
|
||||
label: 'Rows'
|
||||
sequence:
|
||||
type: sequence
|
||||
label: 'Button groups'
|
||||
sequence:
|
||||
type: mapping
|
||||
label: 'Button group'
|
||||
mapping:
|
||||
name:
|
||||
type: string
|
||||
label: 'Button group name'
|
||||
items:
|
||||
type: sequence
|
||||
label: 'Buttons'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Button'
|
||||
plugins:
|
||||
type: sequence
|
||||
label: 'Plugins'
|
||||
sequence:
|
||||
type: ckeditor.plugin.[%key]
|
||||
|
||||
# Plugin \Drupal\ckeditor\Plugin\CKEditorPlugin\Language
|
||||
ckeditor.plugin.language:
|
||||
type: mapping
|
||||
label: 'Language'
|
||||
mapping:
|
||||
language_list:
|
||||
type: string
|
||||
label: 'Language list ID'
|
||||
|
||||
# Plugin \Drupal\ckeditor\Plugin\ckeditor\plugin\StylesCombo
|
||||
ckeditor.plugin.stylescombo:
|
||||
type: mapping
|
||||
label: 'Styles dropdown'
|
||||
mapping:
|
||||
styles:
|
||||
type: string
|
||||
label: 'List of styles'
|
23
web/core/modules/ckeditor/css/ckeditor-iframe.css
Normal file
23
web/core/modules/ckeditor/css/ckeditor-iframe.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* CSS added to iframe-based instances only.
|
||||
*/
|
||||
body {
|
||||
font-family: Arial, Verdana, sans-serif;
|
||||
font-size: 15px;
|
||||
color: #222;
|
||||
background-color: #fff;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
/* A font-size of 16px prevents iOS from zooming. */
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
ol, ul, dl {
|
||||
/* Preserved spaces for list items with text direction other than the list.
|
||||
* (CKEditor issues #6249,#8049) */
|
||||
padding: 0 40px;
|
||||
}
|
332
web/core/modules/ckeditor/css/ckeditor.admin.css
Normal file
332
web/core/modules/ckeditor/css/ckeditor.admin.css
Normal file
|
@ -0,0 +1,332 @@
|
|||
/**
|
||||
* @file
|
||||
* Styles for configuration of CKEditor module.
|
||||
*
|
||||
* Many of these styles are adapted directly from the default CKEditor theme
|
||||
* "moono".
|
||||
*/
|
||||
|
||||
|
||||
|
||||
.ckeditor-toolbar {
|
||||
border: 1px solid #b6b6b6;
|
||||
padding: 0.1667em 0.1667em 0.08em;
|
||||
box-shadow: 0 1px 0 white inset;
|
||||
background: #cfd1cf;
|
||||
background-image: -webkit-linear-gradient(top, whiteSmoke, #cfd1cf);
|
||||
background-image: linear-gradient(top, whiteSmoke, #cfd1cf);
|
||||
margin: 5px 0;
|
||||
/* Disallow any user selections in the drag-and-drop toolbar config UI. */
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.ckeditor-toolbar-active {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
.ckeditor-toolbar-disabled {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.ckeditor-toolbar ul,
|
||||
.ckeditor-toolbar-disabled ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* This is required to win over specificity of [dir="rtl"] ul */
|
||||
[dir="rtl"] .ckeditor-toolbar ul,
|
||||
[dir="rtl"] .ckeditor-toolbar-disabled ul {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ckeditor-row {
|
||||
padding: 2px 0 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.ckeditor-group-names-are-visible .ckeditor-row {
|
||||
border: 1px solid whitesmoke;
|
||||
}
|
||||
.ckeditor-row + .ckeditor-row {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
.ckeditor-toolbar-group,
|
||||
.ckeditor-toolbar-group-placeholder,
|
||||
.ckeditor-add-new-group {
|
||||
float: left; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-toolbar-group,
|
||||
[dir="rtl"] .ckeditor-toolbar-group-placeholder,
|
||||
[dir="rtl"] .ckeditor-add-new-group {
|
||||
float: right;
|
||||
}
|
||||
.ckeditor-toolbar-groups {
|
||||
min-height: 2em;
|
||||
}
|
||||
.ckeditor-toolbar-group {
|
||||
margin: 0 0.3333em;
|
||||
cursor: move;
|
||||
}
|
||||
.ckeditor-group-names-are-visible .ckeditor-toolbar-group,
|
||||
.ckeditor-add-new-group {
|
||||
border: 1px dotted #a6a6a6;
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
.ckeditor-toolbar-group.placeholder,
|
||||
.ckeditor-toolbar-group.placeholder .ckeditor-toolbar-group-name {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ckeditor-toolbar-group.placeholder .ckeditor-toolbar-group-name {
|
||||
font-style: italic;
|
||||
}
|
||||
.ckeditor-toolbar-group-name {
|
||||
display: none;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
.ckeditor-group-names-are-visible .ckeditor-toolbar-group-name {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ckeditor-toolbar-active .placeholder,
|
||||
.ckeditor-toolbar-active .ckeditor-add-new-group {
|
||||
display: none;
|
||||
}
|
||||
.ckeditor-group-names-are-visible .placeholder,
|
||||
.ckeditor-group-names-are-visible .ckeditor-add-new-group {
|
||||
display: block;
|
||||
}
|
||||
.ckeditor-toolbar-group-buttons {
|
||||
float: left; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-toolbar-group-buttons {
|
||||
float: right;
|
||||
}
|
||||
.ckeditor-groupnames-toggle {
|
||||
cursor: pointer;
|
||||
float: right; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-groupnames-toggle {
|
||||
float: left;
|
||||
}
|
||||
.ckeditor-toolbar .ckeditor-toolbar-group > li {
|
||||
border: 1px solid white;
|
||||
border-radius: 5px;
|
||||
background-image: -webkit-linear-gradient(transparent 60%, rgba(0, 0, 0, 0.1));
|
||||
background-image: linear-gradient(transparent 60%, rgba(0, 0, 0, 0.1));
|
||||
margin: 3px 6px;
|
||||
padding: 3px;
|
||||
}
|
||||
.ckeditor-toolbar-configuration .fieldset-description{
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.ckeditor-toolbar-disabled .ckeditor-toolbar-available,
|
||||
.ckeditor-toolbar-disabled .ckeditor-toolbar-dividers {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ckeditor-toolbar-disabled .ckeditor-toolbar-available {
|
||||
float: left; /* LTR */
|
||||
width: 80%;
|
||||
}
|
||||
[dir="rtl"] .ckeditor-toolbar-disabled .ckeditor-toolbar-available {
|
||||
float: right;
|
||||
}
|
||||
.ckeditor-toolbar-disabled .ckeditor-toolbar-dividers {
|
||||
float: right; /* LTR */
|
||||
width: 20%;
|
||||
}
|
||||
[dir="rtl"] .ckeditor-toolbar-disabled .ckeditor-toolbar-dividers {
|
||||
float: left;
|
||||
}
|
||||
.ckeditor-toolbar-disabled .ckeditor-buttons li a,
|
||||
.ckeditor-toolbar .ckeditor-buttons,
|
||||
.ckeditor-add-new-group button {
|
||||
border: 1px solid #a6a6a6;
|
||||
border-bottom-color: #979797;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5), 0 0 2px rgba(255, 255, 255, 0.15) inset, 0 1px 0 rgba(255, 255, 255, 0.15) inset;
|
||||
}
|
||||
.ckeditor-toolbar-disabled .ckeditor-buttons {
|
||||
border: 0;
|
||||
}
|
||||
.ckeditor-toolbar-disabled .ckeditor-buttons li {
|
||||
margin: 2px;
|
||||
}
|
||||
.ckeditor-buttons {
|
||||
min-height: 26px;
|
||||
min-width: 26px;
|
||||
}
|
||||
.ckeditor-buttons li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
float: left; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-buttons li {
|
||||
float: right;
|
||||
}
|
||||
.ckeditor-buttons li a,
|
||||
.ckeditor-add-new-group button {
|
||||
background: #e4e4e4;
|
||||
background-image: -webkit-linear-gradient(top, white, #e4e4e4);
|
||||
background-image: linear-gradient(top, white, #e4e4e4);
|
||||
color: #474747;
|
||||
}
|
||||
.ckeditor-buttons li a {
|
||||
border: 0;
|
||||
cursor: move;
|
||||
display: block;
|
||||
min-height: 18px;
|
||||
line-height: 1.4;
|
||||
padding: 4px 6px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,.5);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ckeditor-toolbar-dividers {
|
||||
float: right; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-toolbar-dividers {
|
||||
float: left;
|
||||
}
|
||||
.ckeditor-buttons li .cke-icon-only {
|
||||
text-indent: -9999px;
|
||||
width: 16px;
|
||||
/* Firefox includes the offscreen text in the focus indicator, resulting in a
|
||||
far too wide focus indicator. This fixes that. */
|
||||
overflow: hidden;
|
||||
}
|
||||
.ckeditor-buttons .cke_button_icon img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.ckeditor-buttons li .cke_ltr {
|
||||
direction: ltr;
|
||||
}
|
||||
.ckeditor-buttons li .cke_rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
.ckeditor-buttons li a:focus,
|
||||
.ckeditor-buttons li a:active,
|
||||
.ckeditor-multiple-buttons li a:focus {
|
||||
z-index: 11; /* Ensure focused buttons show their outline on all sides. */
|
||||
}
|
||||
.ckeditor-buttons li:first-child a {
|
||||
border-top-left-radius: 2px; /* LTR */
|
||||
border-bottom-left-radius: 2px; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-buttons li:first-child a {
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
.ckeditor-buttons li:last-child a {
|
||||
border-top-right-radius: 2px; /* LTR */
|
||||
border-bottom-right-radius: 2px; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-buttons li:last-child a {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
.ckeditor-button-placeholder,
|
||||
.ckeditor-toolbar-group-placeholder {
|
||||
background: #9dcae7;
|
||||
}
|
||||
.ckeditor-toolbar-group-placeholder {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ckeditor-multiple-buttons {
|
||||
padding: 1px 2px;
|
||||
margin: 5px;
|
||||
list-style: none;
|
||||
float: left; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-multiple-buttons {
|
||||
float: right;
|
||||
}
|
||||
.ckeditor-multiple-buttons li {
|
||||
float: left; /* LTR */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
[dir="rtl"] .ckeditor-multiple-buttons li {
|
||||
float: right;
|
||||
}
|
||||
.ckeditor-multiple-buttons li a {
|
||||
cursor: move;
|
||||
display: inline-block;
|
||||
min-height: 18px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.ckeditor-buttons .ckeditor-group-button-separator,
|
||||
.ckeditor-multiple-buttons .ckeditor-group-button-separator {
|
||||
margin: -1px -3px -2px;
|
||||
}
|
||||
.ckeditor-buttons .ckeditor-group-button-separator a,
|
||||
.ckeditor-multiple-buttons .ckeditor-group-button-separator a {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAdCAMAAABG4xbVAAAAhFBMVEUAAACmpqampqampqb////l5eX////5+fmmpqatra2urq6vr6+1tbW2tra4uLi6urq8vLzb29ve3t7i4uLl5eXn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz+/v7qIQO+AAAACHRSTlMATVmAi8XM29MuWToAAABjSURBVBiVrc5BCoAwDETRMKhtRBduev9LKm1xjItWRBBE6Nt9QkIwOTcUzk0Imi8aoMssxbgoTHMtqsFMLta0vPh2N49HyfdelPg6k9uvX/a+Bmggt1qJRNzQFVgjEnkUZDoBmH57VSypjg4AAAAASUVORK5CYII=) no-repeat center center;
|
||||
width: 13px;
|
||||
padding: 0;
|
||||
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;
|
||||
}
|
||||
.ckeditor-multiple-buttons .ckeditor-button-separator a {
|
||||
width: 2px;
|
||||
padding: 0;
|
||||
height: 26px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.ckeditor-separator {
|
||||
background-color: silver;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
margin: 5px 0;
|
||||
height: 18px;
|
||||
width: 1px;
|
||||
display: block;
|
||||
box-shadow: 1px 0 1px rgba(255, 255, 255, 0.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 */
|
||||
}
|
||||
[dir="rtl"] .ckeditor-row-controls {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
.ckeditor-row-controls a {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 2px;
|
||||
height: 28px;
|
||||
width: 20px;
|
||||
line-height: 0.9;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
39
web/core/modules/ckeditor/css/ckeditor.css
Normal file
39
web/core/modules/ckeditor/css/ckeditor.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
.ckeditor-dialog-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ckeditor-dialog-loading-link {
|
||||
border-radius: 0 0 5px 5px;
|
||||
border: 1px solid #B6B6B6;
|
||||
border-top: none;
|
||||
background: white;
|
||||
padding: 3px 10px;
|
||||
box-shadow: 0 0 10px -3px #000;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the style of in-place editing CKEditor instances.
|
||||
*/
|
||||
.quickedit-toolgroup.wysiwyg-main .cke_chrome,
|
||||
.quickedit-toolgroup.wysiwyg-main .cke_inner,
|
||||
.quickedit-toolgroup.wysiwyg-main .cke_top {
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
box-shadow: none;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @file
|
||||
* Image Caption: overrides to make centered alignment work inside CKEditor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Since .align-center is set on the non-captioned image's parent block element
|
||||
* in CKEditor, the image must be centered separately.
|
||||
*/
|
||||
p[data-widget="image"].align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since .align-center is set on captioned widget's wrapper element in CKEditor,
|
||||
* the alignment of internals must be set separately.
|
||||
*/
|
||||
div[data-cke-widget-wrapper].align-center > figure[data-widget="image"] {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* @file
|
||||
* Language: add styling for elements that have a language attribute.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Show the user that a 'lang' tag has been applied by adding a thin dotted
|
||||
* border. We also append the value of the tag between brackets, for example:
|
||||
* '(en)'. Since the html element has a 'lang' attribute too we only target
|
||||
* elements within the html scope.
|
||||
*/
|
||||
html [lang] {
|
||||
outline: 1px dotted gray;
|
||||
}
|
||||
html [lang]:after {
|
||||
content: " ("attr(lang)")";
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
499
web/core/modules/ckeditor/js/ckeditor.admin.js
Normal file
499
web/core/modules/ckeditor/js/ckeditor.admin.js
Normal file
|
@ -0,0 +1,499 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor button and group configuration user interface.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor = Drupal.ckeditor || {};
|
||||
|
||||
/**
|
||||
* Sets config behaviour and creates config views for the CKEditor toolbar.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches admin behaviour to the CKEditor buttons.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches admin behaviour from the CKEditor buttons on 'unload'.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorAdmin = {
|
||||
attach: function (context) {
|
||||
// Process the CKEditor configuration fragment once.
|
||||
var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-configuration');
|
||||
if ($configurationForm.length) {
|
||||
var $textarea = $configurationForm
|
||||
// Hide the textarea that contains the serialized representation of the
|
||||
// CKEditor configuration.
|
||||
.find('.js-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 sent to the client as a serialized DOM fragment.
|
||||
$configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
|
||||
|
||||
// Create a configuration model.
|
||||
var model = Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
|
||||
$textarea: $textarea,
|
||||
activeEditorConfig: JSON.parse($textarea.val()),
|
||||
hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig
|
||||
});
|
||||
|
||||
// Create the configuration Views.
|
||||
var viewDefaults = {
|
||||
model: model,
|
||||
el: $('.ckeditor-toolbar-configuration')
|
||||
};
|
||||
Drupal.ckeditor.views = {
|
||||
controller: new Drupal.ckeditor.ControllerView(viewDefaults),
|
||||
visualView: new Drupal.ckeditor.VisualView(viewDefaults),
|
||||
keyboardView: new Drupal.ckeditor.KeyboardView(viewDefaults),
|
||||
auralView: new Drupal.ckeditor.AuralView(viewDefaults)
|
||||
};
|
||||
}
|
||||
},
|
||||
detach: function (context, settings, trigger) {
|
||||
// Early-return if the trigger for detachment is something else than
|
||||
// unload.
|
||||
if (trigger !== 'unload') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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').findOnce('ckeditor-configuration');
|
||||
if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.Model) {
|
||||
var config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
|
||||
var buttons = Drupal.ckeditor.views.controller.getButtonList(config);
|
||||
var $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
$activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CKEditor configuration UI methods of Backbone objects.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.ckeditor = {
|
||||
|
||||
/**
|
||||
* A hash of View instances.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
views: {},
|
||||
|
||||
/**
|
||||
* A hash of Model instances.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
models: {},
|
||||
|
||||
/**
|
||||
* Translates changes in CKEditor config DOM structure to the config model.
|
||||
*
|
||||
* If the button is moved within an existing group, the DOM structure is
|
||||
* simply translated to a configuration model. If the button is moved into a
|
||||
* new group placeholder, then a process is launched to name that group
|
||||
* before the button move is translated into configuration.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $button
|
||||
* A jQuery set that contains an li element that wraps a button element.
|
||||
* @param {function} callback
|
||||
* A callback to invoke after the button group naming modal dialog has
|
||||
* been closed.
|
||||
*
|
||||
*/
|
||||
registerButtonMove: function (view, $button, callback) {
|
||||
var $group = $button.closest('.ckeditor-toolbar-group');
|
||||
|
||||
// If dropped in a placeholder button group, the user must name it.
|
||||
if ($group.hasClass('placeholder')) {
|
||||
if (view.isProcessing) {
|
||||
return;
|
||||
}
|
||||
view.isProcessing = true;
|
||||
|
||||
Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
|
||||
}
|
||||
else {
|
||||
view.model.set('isDirty', true);
|
||||
callback(true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Translates changes in CKEditor config DOM structure to the config model.
|
||||
*
|
||||
* Each row has a placeholder group at the end of the row. A user may not
|
||||
* move an existing button group past the placeholder group at the end of a
|
||||
* row.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
*/
|
||||
registerGroupMove: function (view, $group) {
|
||||
// Remove placeholder classes if necessary.
|
||||
var $row = $group.closest('.ckeditor-row');
|
||||
if ($row.hasClass('placeholder')) {
|
||||
$row.removeClass('placeholder');
|
||||
}
|
||||
// If there are any rows with just a placeholder group, mark the row as a
|
||||
// placeholder.
|
||||
$row.parent().children().each(function () {
|
||||
$row = $(this);
|
||||
if ($row.find('.ckeditor-toolbar-group').not('.placeholder').length === 0) {
|
||||
$row.addClass('placeholder');
|
||||
}
|
||||
});
|
||||
view.model.set('isDirty', true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a dialog with a form for changing the title of a button group.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
* @param {function} callback
|
||||
* A callback to invoke after the button group naming modal dialog has
|
||||
* been closed.
|
||||
*/
|
||||
openGroupNameDialog: function (view, $group, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
/**
|
||||
* Validates the string provided as a button group title.
|
||||
*
|
||||
* @param {HTMLElement} form
|
||||
* The form DOM element that contains the input with the new button
|
||||
* group title string.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true when an error exists, otherwise returns false.
|
||||
*/
|
||||
function validateForm(form) {
|
||||
if (form.elements[0].value.length === 0) {
|
||||
var $form = $(form);
|
||||
if (!$form.hasClass('errors')) {
|
||||
$form
|
||||
.addClass('errors')
|
||||
.find('input')
|
||||
.addClass('error')
|
||||
.attr('aria-invalid', 'true');
|
||||
$('<div class=\"description\" >' + Drupal.t('Please provide a name for the button group.') + '</div>').insertAfter(form.elements[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to close the dialog; Validates user input.
|
||||
*
|
||||
* @param {string} action
|
||||
* The dialog action chosen by the user: 'apply' or 'cancel'.
|
||||
* @param {HTMLElement} form
|
||||
* The form DOM element that contains the input with the new button
|
||||
* group title string.
|
||||
*/
|
||||
function closeDialog(action, form) {
|
||||
|
||||
/**
|
||||
* Closes the dialog when the user cancels or supplies valid data.
|
||||
*/
|
||||
function shutdown() {
|
||||
dialog.close(action);
|
||||
|
||||
// The processing marker can be deleted since the dialog has been
|
||||
// closed.
|
||||
delete view.isProcessing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a string as the name of a CKEditor button group.
|
||||
*
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of
|
||||
* buttons.
|
||||
* @param {string} name
|
||||
* The new name of the CKEditor button group.
|
||||
*/
|
||||
function namePlaceholderGroup($group, name) {
|
||||
// If it's currently still a placeholder, then that means we're
|
||||
// creating a new group, and we must do some extra work.
|
||||
if ($group.hasClass('placeholder')) {
|
||||
// Remove all whitespace from the name, lowercase it and ensure
|
||||
// HTML-safe encoding, then use this as the group ID for CKEditor
|
||||
// configuration UI accessibility purposes only.
|
||||
var groupID = 'ckeditor-toolbar-group-aria-label-for-' + Drupal.checkPlain(name.toLowerCase().replace(/\s/g, '-'));
|
||||
$group
|
||||
// Update the group container.
|
||||
.removeAttr('aria-label')
|
||||
.attr('data-drupal-ckeditor-type', 'group')
|
||||
.attr('tabindex', 0)
|
||||
// Update the group heading.
|
||||
.children('.ckeditor-toolbar-group-name')
|
||||
.attr('id', groupID)
|
||||
.end()
|
||||
// Update the group items.
|
||||
.children('.ckeditor-toolbar-group-buttons')
|
||||
.attr('aria-labelledby', groupID);
|
||||
}
|
||||
|
||||
$group
|
||||
.attr('data-drupal-ckeditor-toolbar-group-name', name)
|
||||
.children('.ckeditor-toolbar-group-name')
|
||||
.text(name);
|
||||
}
|
||||
|
||||
// Invoke a user-provided callback and indicate failure.
|
||||
if (action === 'cancel') {
|
||||
shutdown();
|
||||
callback(false, $group);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that a group name was provided.
|
||||
if (form && validateForm(form)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// React to application of a valid group name.
|
||||
if (action === 'apply') {
|
||||
shutdown();
|
||||
// Apply the provided name to the button group label.
|
||||
namePlaceholderGroup($group, Drupal.checkPlain(form.elements[0].value));
|
||||
// Remove placeholder classes so that new placeholders will be
|
||||
// inserted.
|
||||
$group.closest('.ckeditor-row.placeholder').addBack().removeClass('placeholder');
|
||||
|
||||
// Invoke a user-provided callback and indicate success.
|
||||
callback(true, $group);
|
||||
|
||||
// Signal that the active toolbar DOM structure has changed.
|
||||
view.model.set('isDirty', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a Drupal dialog that will get a button group name from the user.
|
||||
var $ckeditorButtonGroupNameForm = $(Drupal.theme('ckeditorButtonGroupNameForm'));
|
||||
var dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
|
||||
title: Drupal.t('Button group name'),
|
||||
dialogClass: 'ckeditor-name-toolbar-group',
|
||||
resizable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: Drupal.t('Apply'),
|
||||
click: function () {
|
||||
closeDialog('apply', this);
|
||||
},
|
||||
primary: true
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Cancel'),
|
||||
click: function () {
|
||||
closeDialog('cancel');
|
||||
}
|
||||
}
|
||||
],
|
||||
open: function () {
|
||||
var form = this;
|
||||
var $form = $(this);
|
||||
var $widget = $form.parent();
|
||||
$widget.find('.ui-dialog-titlebar-close').remove();
|
||||
// Set a click handler on the input and button in the form.
|
||||
$widget.on('keypress.ckeditor', 'input, button', function (event) {
|
||||
// React to enter key press.
|
||||
if (event.keyCode === 13) {
|
||||
var $target = $(event.currentTarget);
|
||||
var data = $target.data('ui-button');
|
||||
var action = 'apply';
|
||||
// Assume 'apply', but take into account that the user might have
|
||||
// pressed the enter key on the dialog buttons.
|
||||
if (data && data.options && data.options.label) {
|
||||
action = data.options.label.toLowerCase();
|
||||
}
|
||||
closeDialog(action, form);
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
// Announce to the user that a modal dialog is open.
|
||||
var text = Drupal.t('Editing the name of the new button group in a dialog.');
|
||||
if (typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !== 'undefined') {
|
||||
text = Drupal.t('Editing the name of the "@groupName" button group in a dialog.', {
|
||||
'@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name')
|
||||
});
|
||||
}
|
||||
Drupal.announce(text);
|
||||
},
|
||||
close: function (event) {
|
||||
// Automatically destroy the DOM element that was used for the dialog.
|
||||
$(event.target).remove();
|
||||
}
|
||||
});
|
||||
// A modal dialog is used because the user must provide a button group
|
||||
// name or cancel the button placement before taking any other action.
|
||||
dialog.showModal();
|
||||
|
||||
$(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input'))
|
||||
// When editing, set the "group name" input in the form to the current
|
||||
// value.
|
||||
.attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
|
||||
// Focus on the "group name" input in the form.
|
||||
.trigger('focus');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically shows/hides settings of buttons-only CKEditor plugins.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches show/hide behaviour to Plugin Settings buttons.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
|
||||
attach: function (context) {
|
||||
var $context = $(context);
|
||||
var $ckeditorPluginSettings = $context.find('#ckeditor-plugin-settings').once('ckeditor-plugin-settings');
|
||||
if ($ckeditorPluginSettings.length) {
|
||||
// Hide all button-dependent plugin settings initially.
|
||||
$ckeditorPluginSettings.find('[data-ckeditor-buttons]').each(function () {
|
||||
var $this = $(this);
|
||||
if ($this.data('verticalTab')) {
|
||||
$this.data('verticalTab').tabHide();
|
||||
}
|
||||
else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
$this.hide();
|
||||
}
|
||||
$this.data('ckeditorButtonPluginSettingsActiveButtons', []);
|
||||
});
|
||||
|
||||
// Whenever a button is added or removed, check if we should show or
|
||||
// hide the corresponding plugin settings. (Note that upon
|
||||
// initialization, each button that already is part of the toolbar still
|
||||
// is considered "added", hence it also works correctly for buttons that
|
||||
// were added previously.)
|
||||
$context
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
|
||||
.on('CKEditorToolbarChanged.ckeditorAdminPluginSettings', function (event, action, button) {
|
||||
var $pluginSettings = $ckeditorPluginSettings
|
||||
.find('[data-ckeditor-buttons~=' + button + ']');
|
||||
|
||||
// No settings for this button.
|
||||
if ($pluginSettings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var verticalTab = $pluginSettings.data('verticalTab');
|
||||
var activeButtons = $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons');
|
||||
if (action === 'added') {
|
||||
activeButtons.push(button);
|
||||
// Show this plugin's settings if >=1 of its buttons are active.
|
||||
if (verticalTab) {
|
||||
verticalTab.tabShow();
|
||||
}
|
||||
else {
|
||||
// On very narrow viewports, Vertical Tabs remain fieldsets.
|
||||
$pluginSettings.show();
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// Remove this button from the list of active buttons.
|
||||
activeButtons.splice(activeButtons.indexOf(button), 1);
|
||||
// Show this plugin's settings 0 of its buttons are active.
|
||||
if (activeButtons.length === 0) {
|
||||
if (verticalTab) {
|
||||
verticalTab.tabHide();
|
||||
}
|
||||
else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
$pluginSettings.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
$pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons', activeButtons);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a blank CKEditor row.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for a CKEditor row.
|
||||
*/
|
||||
Drupal.theme.ckeditorRow = function () {
|
||||
return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a blank CKEditor button group.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for a CKEditor button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorToolbarGroup = function () {
|
||||
var group = '';
|
||||
group += '<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="' + Drupal.t('Place a button to create a new button group.') + '">';
|
||||
group += '<h3 class="ckeditor-toolbar-group-name">' + Drupal.t('New group') + '</h3>';
|
||||
group += '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
|
||||
group += '</li>';
|
||||
return group;
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a form for changing the title of a CKEditor button group.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for the form for the title of a CKEditor button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNameForm = function () {
|
||||
return '<form><input name="group-name" required="required"></form>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a button that will toggle the button group names in active config.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for the button to toggle group names.
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
|
||||
return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a button that will prompt the user to name a new button group.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for the button to create a name for a new button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorNewButtonGroup = function () {
|
||||
return '<li class="ckeditor-add-new-group"><button aria-label="' + Drupal.t('Add a CKEditor button group to the end of this row.') + '">' + Drupal.t('Add group') + '</button></li>';
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
45
web/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
Normal file
45
web/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor 'drupalimage' plugin admin behavior.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides the summary for the "drupalimage" plugin settings vertical tab.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behaviour to the "drupalimage" settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorDrupalImageSettingsSummary = {
|
||||
attach: function () {
|
||||
$('[data-ckeditor-plugin-id="drupalimage"]').drupalSetSummary(function (context) {
|
||||
var root = 'input[name="editor[settings][plugins][drupalimage][image_upload]';
|
||||
var $status = $(root + '[status]"]');
|
||||
var $maxFileSize = $(root + '[max_size]"]');
|
||||
var $maxWidth = $(root + '[max_dimensions][width]"]');
|
||||
var $maxHeight = $(root + '[max_dimensions][height]"]');
|
||||
var $scheme = $(root + '[scheme]"]:checked');
|
||||
|
||||
var maxFileSize = $maxFileSize.val() ? $maxFileSize.val() : $maxFileSize.attr('placeholder');
|
||||
var maxDimensions = ($maxWidth.val() && $maxHeight.val()) ? '(' + $maxWidth.val() + 'x' + $maxHeight.val() + ')' : '';
|
||||
|
||||
if (!$status.is(':checked')) {
|
||||
return Drupal.t('Uploads disabled');
|
||||
}
|
||||
|
||||
var output = '';
|
||||
output += Drupal.t('Uploads enabled, max size: @size @dimensions', {'@size': maxFileSize, '@dimensions': maxDimensions});
|
||||
if ($scheme.length) {
|
||||
output += '<br />' + $scheme.attr('data-label');
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings);
|
293
web/core/modules/ckeditor/js/ckeditor.js
vendored
Normal file
293
web/core/modules/ckeditor/js/ckeditor.js
vendored
Normal file
|
@ -0,0 +1,293 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor implementation of {@link Drupal.editors} API.
|
||||
*/
|
||||
|
||||
(function (Drupal, debounce, CKEDITOR, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editors.ckeditor = {
|
||||
|
||||
/**
|
||||
* Editor attach callback.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to attach the editor to.
|
||||
* @param {string} format
|
||||
* The text format for the editor.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.replace()` created an editor or not.
|
||||
*/
|
||||
attach: function (element, format) {
|
||||
this._loadExternalPlugins(format);
|
||||
// Also pass settings that are Drupal-specific.
|
||||
format.editorSettings.drupal = {
|
||||
format: format.format
|
||||
};
|
||||
|
||||
// Set a title on the CKEditor instance that includes the text field's
|
||||
// label so that screen readers say something that is understandable
|
||||
// for end users.
|
||||
var label = $('label[for=' + element.getAttribute('id') + ']').html();
|
||||
format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', {'!label': label});
|
||||
|
||||
return !!CKEDITOR.replace(element, format.editorSettings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Editor detach callback.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to detach the editor from.
|
||||
* @param {string} format
|
||||
* The text format used for the editor.
|
||||
* @param {string} trigger
|
||||
* The event trigger for the detach.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
|
||||
* found an editor or not.
|
||||
*/
|
||||
detach: function (element, format, trigger) {
|
||||
var editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
if (trigger === 'serialize') {
|
||||
editor.updateElement();
|
||||
}
|
||||
else {
|
||||
editor.destroy();
|
||||
element.removeAttribute('contentEditable');
|
||||
}
|
||||
}
|
||||
return !!editor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reacts on a change in the editor element.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element where the change occured.
|
||||
* @param {function} callback
|
||||
* Callback called with the value of the editor.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
|
||||
* found an editor or not.
|
||||
*/
|
||||
onChange: function (element, callback) {
|
||||
var editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
editor.on('change', debounce(function () {
|
||||
callback(editor.getData());
|
||||
}, 400));
|
||||
}
|
||||
return !!editor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Attaches an inline editor to a DOM element.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to attach the editor to.
|
||||
* @param {object} format
|
||||
* The text format used in the editor.
|
||||
* @param {string} [mainToolbarId]
|
||||
* The id attribute for the main editor toolbar, if any.
|
||||
* @param {string} [floatedToolbarId]
|
||||
* The id attribute for the floated editor toolbar, if any.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.replace()` created an editor or not.
|
||||
*/
|
||||
attachInlineEditor: function (element, format, mainToolbarId, floatedToolbarId) {
|
||||
this._loadExternalPlugins(format);
|
||||
// Also pass settings that are Drupal-specific.
|
||||
format.editorSettings.drupal = {
|
||||
format: format.format
|
||||
};
|
||||
|
||||
var settings = $.extend(true, {}, format.editorSettings);
|
||||
|
||||
// If a toolbar is already provided for "true WYSIWYG" (in-place editing),
|
||||
// then use that toolbar instead: override the default settings to render
|
||||
// CKEditor UI's top toolbar into mainToolbar, and don't render the bottom
|
||||
// toolbar at all. (CKEditor doesn't need a floated toolbar.)
|
||||
if (mainToolbarId) {
|
||||
var settingsOverride = {
|
||||
extraPlugins: 'sharedspace',
|
||||
removePlugins: 'floatingspace,elementspath',
|
||||
sharedSpaces: {
|
||||
top: mainToolbarId
|
||||
}
|
||||
};
|
||||
|
||||
// Find the "Source" button, if any, and replace it with "Sourcedialog".
|
||||
// (The 'sourcearea' plugin only works in CKEditor's iframe mode.)
|
||||
var sourceButtonFound = false;
|
||||
for (var i = 0; !sourceButtonFound && i < settings.toolbar.length; i++) {
|
||||
if (settings.toolbar[i] !== '/') {
|
||||
for (var j = 0; !sourceButtonFound && j < settings.toolbar[i].items.length; j++) {
|
||||
if (settings.toolbar[i].items[j] === 'Source') {
|
||||
sourceButtonFound = true;
|
||||
// Swap sourcearea's "Source" button for sourcedialog's.
|
||||
settings.toolbar[i].items[j] = 'Sourcedialog';
|
||||
settingsOverride.extraPlugins += ',sourcedialog';
|
||||
settingsOverride.removePlugins += ',sourcearea';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings.extraPlugins += ',' + settingsOverride.extraPlugins;
|
||||
settings.removePlugins += ',' + settingsOverride.removePlugins;
|
||||
settings.sharedSpaces = settingsOverride.sharedSpaces;
|
||||
}
|
||||
|
||||
// CKEditor requires an element to already have the contentEditable
|
||||
// attribute set to "true", otherwise it won't attach an inline editor.
|
||||
element.setAttribute('contentEditable', 'true');
|
||||
|
||||
return !!CKEDITOR.inline(element, settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the required external plugins for the editor.
|
||||
*
|
||||
* @param {object} format
|
||||
* The text format used in the editor.
|
||||
*/
|
||||
_loadExternalPlugins: function (format) {
|
||||
var externalPlugins = format.editorSettings.drupalExternalPlugins;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Drupal.ckeditor = {
|
||||
|
||||
/**
|
||||
* Variable storing the current dialog's save callback.
|
||||
*
|
||||
* @type {?function}
|
||||
*/
|
||||
saveCallback: null,
|
||||
|
||||
/**
|
||||
* Open a dialog for a Drupal-based plugin.
|
||||
*
|
||||
* This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
|
||||
* framework, then opens a dialog at the specified Drupal path.
|
||||
*
|
||||
* @param {CKEditor} editor
|
||||
* The CKEditor instance that is opening the dialog.
|
||||
* @param {string} url
|
||||
* The URL that contains the contents of the dialog.
|
||||
* @param {object} existingValues
|
||||
* Existing values that will be sent via POST to the url for the dialog
|
||||
* contents.
|
||||
* @param {function} saveCallback
|
||||
* A function to be called upon saving the dialog.
|
||||
* @param {object} dialogSettings
|
||||
* An object containing settings to be passed to the jQuery UI.
|
||||
*/
|
||||
openDialog: function (editor, url, existingValues, saveCallback, dialogSettings) {
|
||||
// Locate a suitable place to display our loading indicator.
|
||||
var $target = $(editor.container.$);
|
||||
if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) {
|
||||
$target = $target.find('.cke_contents');
|
||||
}
|
||||
|
||||
// Remove any previous loading indicator.
|
||||
$target.css('position', 'relative').find('.ckeditor-dialog-loading').remove();
|
||||
|
||||
// Add a consistent dialog class.
|
||||
var classes = dialogSettings.dialogClass ? dialogSettings.dialogClass.split(' ') : [];
|
||||
classes.push('ui-dialog--narrow');
|
||||
dialogSettings.dialogClass = classes.join(' ');
|
||||
dialogSettings.autoResize = window.matchMedia('(min-width: 600px)').matches;
|
||||
dialogSettings.width = 'auto';
|
||||
|
||||
// Add a "Loading…" message, hide it underneath the CKEditor toolbar,
|
||||
// create a Drupal.Ajax instance to load the dialog and trigger it.
|
||||
var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">' + Drupal.t('Loading...') + '</span></div>');
|
||||
$content.appendTo($target);
|
||||
|
||||
var ckeditorAjaxDialog = Drupal.ajax({
|
||||
dialog: dialogSettings,
|
||||
dialogType: 'modal',
|
||||
selector: '.ckeditor-dialog-loading-link',
|
||||
url: url,
|
||||
progress: {type: 'throbber'},
|
||||
submit: {
|
||||
editor_object: existingValues
|
||||
}
|
||||
});
|
||||
ckeditorAjaxDialog.execute();
|
||||
|
||||
// After a short delay, show "Loading…" message.
|
||||
window.setTimeout(function () {
|
||||
$content.find('span').animate({top: '0px'});
|
||||
}, 1000);
|
||||
|
||||
// Store the save callback to be executed when this dialog is closed.
|
||||
Drupal.ckeditor.saveCallback = saveCallback;
|
||||
}
|
||||
};
|
||||
|
||||
// Moves the dialog to the top of the CKEDITOR stack.
|
||||
$(window).on('dialogcreate', function (e, dialog, $element, settings) {
|
||||
$('.ui-dialog--narrow').css('zIndex', CKEDITOR.config.baseFloatZIndex + 1);
|
||||
});
|
||||
|
||||
// Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
|
||||
$(window).on('dialog:beforecreate', function (e, dialog, $element, settings) {
|
||||
$('.ckeditor-dialog-loading').animate({top: '-40px'}, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Respond to dialogs that are saved, sending data back to CKEditor.
|
||||
$(window).on('editor:dialogsave', function (e, values) {
|
||||
if (Drupal.ckeditor.saveCallback) {
|
||||
Drupal.ckeditor.saveCallback(values);
|
||||
}
|
||||
});
|
||||
|
||||
// Respond to dialogs that are closed, removing the current save handler.
|
||||
$(window).on('dialog:afterclose', function (e, dialog, $element) {
|
||||
if (Drupal.ckeditor.saveCallback) {
|
||||
Drupal.ckeditor.saveCallback = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Redirect on hash change when the original hash has an associated CKEditor.
|
||||
function redirectTextareaFragmentToCKEditorInstance() {
|
||||
var hash = location.hash.substr(1);
|
||||
var element = document.getElementById(hash);
|
||||
if (element) {
|
||||
var editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
var id = editor.container.getAttribute('id');
|
||||
location.replace('#' + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
$(window).on('hashchange.ckeditor', redirectTextareaFragmentToCKEditorInstance);
|
||||
|
||||
// Set the CKEditor cache-busting string to the same value as Drupal.
|
||||
CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp;
|
||||
|
||||
})(Drupal, Drupal.debounce, CKEDITOR, jQuery);
|
16
web/core/modules/ckeditor/js/ckeditor.language.admin.js
Normal file
16
web/core/modules/ckeditor/js/ckeditor.language.admin.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides the summary for the "language" plugin settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorLanguageSettingsSummary = {
|
||||
attach: function () {
|
||||
$('#edit-editor-settings-plugins-language').drupalSetSummary(function (context) {
|
||||
return $('#edit-editor-settings-plugins-language-language-list-type option:selected').text();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
128
web/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
Normal file
128
web/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor StylesCombo admin behavior.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Ensures that the "stylescombo" button's metadata remains up-to-date.
|
||||
*
|
||||
* Triggers the CKEditorPluginSettingsChanged event whenever the "stylescombo"
|
||||
* plugin settings change, to ensure that the corresponding feature metadata
|
||||
* is immediately updated — i.e. ensure that HTML tags and classes entered
|
||||
* here are known to be "required", which may affect filter settings.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches admin behaviour to the "stylescombo" button.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorStylesComboSettings = {
|
||||
attach: function (context) {
|
||||
var $context = $(context);
|
||||
|
||||
// React to changes in the list of user-defined styles: calculate the new
|
||||
// stylesSet setting up to 2 times per second, and if it is different,
|
||||
// fire the CKEditorPluginSettingsChanged event with the updated parts of
|
||||
// the CKEditor configuration. (This will, in turn, cause the hidden
|
||||
// CKEditor instance to be updated and a drupalEditorFeatureModified event
|
||||
// to fire.)
|
||||
var $ckeditorActiveToolbar = $context
|
||||
.find('.ckeditor-toolbar-configuration')
|
||||
.find('.ckeditor-toolbar-active');
|
||||
var previousStylesSet = drupalSettings.ckeditor.hiddenCKEditorConfig.stylesSet;
|
||||
var that = this;
|
||||
$context.find('[name="editor[settings][plugins][stylescombo][styles]"]')
|
||||
.on('blur.ckeditorStylesComboSettings', function () {
|
||||
var styles = $.trim($(this).val());
|
||||
var stylesSet = that._generateStylesSetSetting(styles);
|
||||
if (!_.isEqual(previousStylesSet, stylesSet)) {
|
||||
previousStylesSet = stylesSet;
|
||||
$ckeditorActiveToolbar.trigger('CKEditorPluginSettingsChanged', [
|
||||
{stylesSet: stylesSet}
|
||||
]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the "stylesSet" configuration part of the CKEditor JS settings.
|
||||
*
|
||||
* @see \Drupal\ckeditor\Plugin\ckeditor\plugin\StylesCombo::generateStylesSetSetting()
|
||||
*
|
||||
* Note that this is a more forgiving implementation than the PHP version:
|
||||
* the parsing works identically, but instead of failing on invalid styles,
|
||||
* we just ignore those.
|
||||
*
|
||||
* @param {string} styles
|
||||
* The "styles" setting.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array containing the "stylesSet" configuration.
|
||||
*/
|
||||
_generateStylesSetSetting: function (styles) {
|
||||
var stylesSet = [];
|
||||
|
||||
styles = styles.replace(/\r/g, '\n');
|
||||
var lines = styles.split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var style = $.trim(lines[i]);
|
||||
|
||||
// Ignore empty lines in between non-empty lines.
|
||||
if (style.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate syntax: element[.class...]|label pattern expected.
|
||||
if (style.match(/^ *[a-zA-Z0-9]+ *(\.[a-zA-Z0-9_-]+ *)*\| *.+ *$/) === null) {
|
||||
// Instead of failing, we just ignore any invalid styles.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse.
|
||||
var parts = style.split('|');
|
||||
var selector = parts[0];
|
||||
var label = parts[1];
|
||||
var classes = selector.split('.');
|
||||
var element = classes.shift();
|
||||
|
||||
// Build the data structure CKEditor's stylescombo plugin expects.
|
||||
// @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles
|
||||
stylesSet.push({
|
||||
attributes: {class: classes.join(' ')},
|
||||
element: element,
|
||||
name: label
|
||||
});
|
||||
}
|
||||
|
||||
return stylesSet;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides the summary for the "stylescombo" plugin settings vertical tab.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behaviour to the plugin settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
|
||||
attach: function () {
|
||||
$('[data-ckeditor-plugin-id="stylescombo"]').drupalSetSummary(function (context) {
|
||||
var styles = $.trim($('[data-drupal-selector="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, drupalSettings, _);
|
75
web/core/modules/ckeditor/js/models/Model.js
Normal file
75
web/core/modules/ckeditor/js/models/Model.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone Model for the state of a CKEditor toolbar configuration .
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Backbone model for the CKEditor toolbar configuration state.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @augments Backbone.Model
|
||||
*/
|
||||
Drupal.ckeditor.Model = Backbone.Model.extend(/** @lends Drupal.ckeditor.Model# */{
|
||||
|
||||
/**
|
||||
* Default values.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
defaults: /** @lends Drupal.ckeditor.Model# */{
|
||||
|
||||
/**
|
||||
* The CKEditor configuration that is being manipulated through the UI.
|
||||
*/
|
||||
activeEditorConfig: null,
|
||||
|
||||
/**
|
||||
* The textarea that contains the serialized representation of the active
|
||||
* CKEditor configuration.
|
||||
*/
|
||||
$textarea: null,
|
||||
|
||||
/**
|
||||
* Tracks whether the active toolbar DOM structure has been changed. When
|
||||
* true, activeEditorConfig needs to be updated, and when that is updated,
|
||||
* $textarea will also be updated.
|
||||
*/
|
||||
isDirty: false,
|
||||
|
||||
/**
|
||||
* The configuration for the hidden CKEditor instance that is used to
|
||||
* build the features metadata.
|
||||
*/
|
||||
hiddenEditorConfig: null,
|
||||
|
||||
/**
|
||||
* A hash that maps buttons to features.
|
||||
*/
|
||||
buttonsToFeatures: null,
|
||||
|
||||
/**
|
||||
* A hash, keyed by a feature name, that details CKEditor plugin features.
|
||||
*/
|
||||
featuresMetadata: null,
|
||||
|
||||
/**
|
||||
* Whether the button group names are currently visible.
|
||||
*/
|
||||
groupNamesVisible: false
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*/
|
||||
sync: function () {
|
||||
// Push the settings into the textarea.
|
||||
this.get('$textarea').val(JSON.stringify(this.get('activeEditorConfig')));
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone);
|
Binary file not shown.
After Width: | Height: | Size: 470 B |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
371
web/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
Normal file
371
web/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
Normal file
|
@ -0,0 +1,371 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Image plugin.
|
||||
*
|
||||
* This alters the existing CKEditor image2 widget plugin to:
|
||||
* - require a data-entity-type and a data-entity-uuid attribute (which Drupal
|
||||
* uses to track where images are being used)
|
||||
* - use a Drupal-native dialog (that is in fact just an alterable Drupal form
|
||||
* like any other) instead of CKEditor's own dialogs.
|
||||
*
|
||||
* @see \Drupal\editor\Form\EditorImageDialog
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function ($, Drupal, CKEDITOR) {
|
||||
|
||||
'use strict';
|
||||
|
||||
CKEDITOR.plugins.add('drupalimage', {
|
||||
requires: 'image2',
|
||||
icons: 'drupalimage',
|
||||
hidpi: true,
|
||||
|
||||
beforeInit: function (editor) {
|
||||
// Override the image2 widget definition to require and handle the
|
||||
// additional data-entity-type and data-entity-uuid attributes.
|
||||
editor.on('widgetDefinition', function (event) {
|
||||
var widgetDefinition = event.data;
|
||||
if (widgetDefinition.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
// First, convert requiredContent & allowedContent from the string
|
||||
// format that image2 uses for both to formats that are better suited
|
||||
// for extending, so that both this basic drupalimage plugin and Drupal
|
||||
// modules can easily extend it.
|
||||
// @see http://docs.ckeditor.com/#!/api/CKEDITOR.filter.allowedContentRules
|
||||
// Mapped from image2's allowedContent. Unlike image2, we don't allow
|
||||
// <figure>, <figcaption>, <div> or <p> in our downcast, so we omit
|
||||
// those. For the <img> tag, we list all attributes it lists, but omit
|
||||
// the classes, because the listed classes are for alignment, and for
|
||||
// alignment we use the data-align attribute.
|
||||
widgetDefinition.allowedContent = {
|
||||
img: {
|
||||
attributes: {
|
||||
'!src': true,
|
||||
'!alt': true,
|
||||
'width': true,
|
||||
'height': true
|
||||
},
|
||||
classes: {}
|
||||
}
|
||||
};
|
||||
// Mapped from image2's requiredContent: "img[src,alt]". This does not
|
||||
// use the object format unlike above, but a CKEDITOR.style instance,
|
||||
// because requiredContent does not support the object format.
|
||||
// @see https://www.drupal.org/node/2585173#comment-10456981
|
||||
widgetDefinition.requiredContent = new CKEDITOR.style({
|
||||
element: 'img',
|
||||
attributes: {
|
||||
src: '',
|
||||
alt: ''
|
||||
}
|
||||
});
|
||||
|
||||
// Extend requiredContent & allowedContent.
|
||||
// CKEDITOR.style is an immutable object: we cannot modify its
|
||||
// definition to extend requiredContent. Hence we get the definition,
|
||||
// modify it, and pass it to a new CKEDITOR.style instance.
|
||||
var requiredContent = widgetDefinition.requiredContent.getDefinition();
|
||||
requiredContent.attributes['data-entity-type'] = '';
|
||||
requiredContent.attributes['data-entity-uuid'] = '';
|
||||
widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent);
|
||||
widgetDefinition.allowedContent.img.attributes['!data-entity-type'] = true;
|
||||
widgetDefinition.allowedContent.img.attributes['!data-entity-uuid'] = true;
|
||||
|
||||
// Override downcast(): since we only accept <img> in our upcast method,
|
||||
// the element is already correct. We only need to update the element's
|
||||
// data-entity-uuid attribute.
|
||||
widgetDefinition.downcast = function (element) {
|
||||
element.attributes['data-entity-type'] = this.data['data-entity-type'];
|
||||
element.attributes['data-entity-uuid'] = this.data['data-entity-uuid'];
|
||||
};
|
||||
|
||||
// We want to upcast <img> elements to a DOM structure required by the
|
||||
// image2 widget; we only accept an <img> tag, and that <img> tag MAY
|
||||
// have a data-entity-type and a data-entity-uuid attribute.
|
||||
widgetDefinition.upcast = function (element, data) {
|
||||
if (element.name !== 'img') {
|
||||
return;
|
||||
}
|
||||
// Don't initialize on pasted fake objects.
|
||||
else if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the data-entity-type attribute.
|
||||
data['data-entity-type'] = element.attributes['data-entity-type'];
|
||||
// Parse the data-entity-uuid attribute.
|
||||
data['data-entity-uuid'] = element.attributes['data-entity-uuid'];
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
// Overrides default implementation. Used to populate the "classes"
|
||||
// property of the widget's "data" property, which is used for the
|
||||
// "widget styles" functionality
|
||||
// (http://docs.ckeditor.com/#!/guide/dev_styles-section-widget-styles).
|
||||
// Is applied to whatever the main element of the widget is (<figure> or
|
||||
// <img>). The classes in image2_captionedClass are always added due to
|
||||
// a bug in CKEditor. In the case of drupalimage, we don't ever want to
|
||||
// add that class, because the widget template already contains it.
|
||||
// @see http://dev.ckeditor.com/ticket/13888
|
||||
// @see https://www.drupal.org/node/2268941
|
||||
var originalGetClasses = widgetDefinition.getClasses;
|
||||
widgetDefinition.getClasses = function () {
|
||||
var classes = originalGetClasses.call(this);
|
||||
var captionedClasses = (this.editor.config.image2_captionedClass || '').split(/\s+/);
|
||||
|
||||
if (captionedClasses.length && classes) {
|
||||
for (var i = 0; i < captionedClasses.length; i++) {
|
||||
if (captionedClasses[i] in classes) {
|
||||
delete classes[captionedClasses[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
// Protected; keys of the widget data to be sent to the Drupal dialog.
|
||||
// Keys in the hash are the keys for image2's data, values are the keys
|
||||
// that the Drupal dialog uses.
|
||||
widgetDefinition._mapDataToDialog = {
|
||||
'src': 'src',
|
||||
'alt': 'alt',
|
||||
'width': 'width',
|
||||
'height': 'height',
|
||||
'data-entity-type': 'data-entity-type',
|
||||
'data-entity-uuid': 'data-entity-uuid'
|
||||
};
|
||||
|
||||
// Protected; transforms widget's data object to the format used by the
|
||||
// \Drupal\editor\Form\EditorImageDialog dialog, keeping only the data
|
||||
// listed in widgetDefinition._dataForDialog.
|
||||
widgetDefinition._dataToDialogValues = function (data) {
|
||||
var dialogValues = {};
|
||||
var map = widgetDefinition._mapDataToDialog;
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach(function (key) {
|
||||
dialogValues[map[key]] = data[key];
|
||||
});
|
||||
return dialogValues;
|
||||
};
|
||||
|
||||
// Protected; the inverse of _dataToDialogValues.
|
||||
widgetDefinition._dialogValuesToData = function (dialogReturnValues) {
|
||||
var data = {};
|
||||
var map = widgetDefinition._mapDataToDialog;
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach(function (key) {
|
||||
if (dialogReturnValues.hasOwnProperty(map[key])) {
|
||||
data[key] = dialogReturnValues[map[key]];
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// Protected; creates Drupal dialog save callback.
|
||||
widgetDefinition._createDialogSaveCallback = function (editor, widget) {
|
||||
return function (dialogReturnValues) {
|
||||
var firstEdit = !widget.ready;
|
||||
|
||||
// Dialog may have blurred the widget. Re-focus it first.
|
||||
if (!firstEdit) {
|
||||
widget.focus();
|
||||
}
|
||||
|
||||
editor.fire('saveSnapshot');
|
||||
|
||||
// Pass `true` so DocumentFragment will also be returned.
|
||||
var container = widget.wrapper.getParent(true);
|
||||
var image = widget.parts.image;
|
||||
|
||||
// Set the updated widget data, after the necessary conversions from
|
||||
// the dialog's return values.
|
||||
// Note: on widget#setData this widget instance might be destroyed.
|
||||
var data = widgetDefinition._dialogValuesToData(dialogReturnValues.attributes);
|
||||
widget.setData(data);
|
||||
|
||||
// Retrieve the widget once again. It could've been destroyed
|
||||
// when shifting state, so might deal with a new instance.
|
||||
widget = editor.widgets.getByElement(image);
|
||||
|
||||
// It's first edit, just after widget instance creation, but before
|
||||
// it was inserted into DOM. So we need to retrieve the widget
|
||||
// wrapper from inside the DocumentFragment which we cached above
|
||||
// and finalize other things (like ready event and flag).
|
||||
if (firstEdit) {
|
||||
editor.widgets.finalizeCreation(container);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
// (Re-)focus the widget.
|
||||
widget.focus();
|
||||
// Save snapshot for undo support.
|
||||
editor.fire('saveSnapshot');
|
||||
});
|
||||
|
||||
return widget;
|
||||
};
|
||||
};
|
||||
|
||||
var originalInit = widgetDefinition.init;
|
||||
widgetDefinition.init = function () {
|
||||
originalInit.call(this);
|
||||
|
||||
// Update data.link object with attributes if the link has been
|
||||
// discovered.
|
||||
// @see plugins/image2/plugin.js/init() in CKEditor; this is similar.
|
||||
if (this.parts.link) {
|
||||
this.setData('link', CKEDITOR.plugins.image2.getLinkAttributesParser()(editor, this.parts.link));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Add a widget#edit listener to every instance of image2 widget in order
|
||||
// to handle its editing with a Drupal-native dialog.
|
||||
// This includes also a case just after the image was created
|
||||
// and dialog should be opened for it for the first time.
|
||||
editor.widgets.on('instanceCreated', function (event) {
|
||||
var widget = event.data;
|
||||
|
||||
if (widget.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.on('edit', function (event) {
|
||||
// Cancel edit event to break image2's dialog binding
|
||||
// (and also to prevent automatic insertion before opening dialog).
|
||||
event.cancel();
|
||||
|
||||
// Open drupalimage dialog.
|
||||
editor.execCommand('editdrupalimage', {
|
||||
existingValues: widget.definition._dataToDialogValues(widget.data),
|
||||
saveCallback: widget.definition._createDialogSaveCallback(editor, widget),
|
||||
// Drupal.t() will not work inside CKEditor plugins because CKEditor
|
||||
// loads the JavaScript file instead of Drupal. Pull translated
|
||||
// strings from the plugin settings that are translated server-side.
|
||||
dialogTitle: widget.data.src ? editor.config.drupalImage_dialogTitleEdit : editor.config.drupalImage_dialogTitleAdd
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Register the "editdrupalimage" command, which essentially just replaces
|
||||
// the "image" command's CKEditor dialog with a Drupal-native dialog.
|
||||
editor.addCommand('editdrupalimage', {
|
||||
allowedContent: 'img[alt,!src,width,height,!data-entity-type,!data-entity-uuid]',
|
||||
requiredContent: 'img[alt,src,data-entity-type,data-entity-uuid]',
|
||||
modes: {wysiwyg: 1},
|
||||
canUndo: true,
|
||||
exec: function (editor, data) {
|
||||
var dialogSettings = {
|
||||
title: data.dialogTitle,
|
||||
dialogClass: 'editor-image-dialog'
|
||||
};
|
||||
Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/image/' + editor.config.drupal.format), data.existingValues, data.saveCallback, dialogSettings);
|
||||
}
|
||||
});
|
||||
|
||||
// Register the toolbar button.
|
||||
if (editor.ui.addButton) {
|
||||
editor.ui.addButton('DrupalImage', {
|
||||
label: Drupal.t('Image'),
|
||||
// Note that we use the original image2 command!
|
||||
command: 'image'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
afterInit: function (editor) {
|
||||
linkCommandIntegrator(editor);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Override image2's integration with the official CKEditor link plugin:
|
||||
// integrate with the drupallink plugin instead.
|
||||
CKEDITOR.plugins.image2.getLinkAttributesParser = function () {
|
||||
return CKEDITOR.plugins.drupallink.parseLinkAttributes;
|
||||
};
|
||||
CKEDITOR.plugins.image2.getLinkAttributesGetter = function () {
|
||||
return CKEDITOR.plugins.drupallink.getLinkAttributes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Integrates the drupalimage widget with the drupallink plugin.
|
||||
*
|
||||
* Makes images linkable.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*/
|
||||
function linkCommandIntegrator(editor) {
|
||||
// Nothing to integrate with if the drupallink plugin is not loaded.
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Override default behaviour of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
// Override 'drupalunlink' only when link truly belongs to the widget. If
|
||||
// wrapped inline widget in a link, let default unlink work.
|
||||
// @see https://dev.ckeditor.com/ticket/11814
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
// Selection (which is fake) may not change if unlinked image in focused
|
||||
// widget, i.e. if captioned image. Let's refresh command state manually
|
||||
// here.
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
// Override default refresh of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note that widget may be wrapped in a link, which
|
||||
// does not belong to that widget (#11814).
|
||||
this.setState(widget.data.link || widget.wrapper.getAscendant('a') ?
|
||||
CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the focused widget, if of the type specific for this plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*
|
||||
* @return {?CKEDITOR.plugins.widget}
|
||||
* The focused image2 widget instance, or null.
|
||||
*/
|
||||
function getFocusedWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expose an API for other plugins to interact with drupalimage widgets.
|
||||
CKEDITOR.plugins.drupalimage = {
|
||||
getFocusedWidget: getFocusedWidget
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, CKEDITOR);
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Image Caption plugin.
|
||||
*
|
||||
* This alters the existing CKEditor image2 widget plugin, which is already
|
||||
* altered by the Drupal Image plugin, to:
|
||||
* - allow for the data-caption and data-align attributes to be set
|
||||
* - mimic the upcasting behavior of the caption_filter filter.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function (CKEDITOR) {
|
||||
|
||||
'use strict';
|
||||
|
||||
CKEDITOR.plugins.add('drupalimagecaption', {
|
||||
requires: 'drupalimage',
|
||||
|
||||
beforeInit: function (editor) {
|
||||
// Disable default placeholder text that comes with CKEditor's image2
|
||||
// plugin: it has an inferior UX (it requires the user to manually delete
|
||||
// the place holder text).
|
||||
editor.lang.image2.captionPlaceholder = '';
|
||||
|
||||
// Drupal.t() will not work inside CKEditor plugins because CKEditor loads
|
||||
// the JavaScript file instead of Drupal. Pull translated strings from the
|
||||
// plugin settings that are translated server-side.
|
||||
var placeholderText = editor.config.drupalImageCaption_captionPlaceholderText;
|
||||
|
||||
// Override the image2 widget definition to handle the additional
|
||||
// data-align and data-caption attributes.
|
||||
editor.on('widgetDefinition', function (event) {
|
||||
var widgetDefinition = event.data;
|
||||
if (widgetDefinition.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only perform the downcasting/upcasting for to the enabled filters.
|
||||
var captionFilterEnabled = editor.config.drupalImageCaption_captionFilterEnabled;
|
||||
var alignFilterEnabled = editor.config.drupalImageCaption_alignFilterEnabled;
|
||||
|
||||
// Override default features definitions for drupalimagecaption.
|
||||
CKEDITOR.tools.extend(widgetDefinition.features, {
|
||||
caption: {
|
||||
requiredContent: 'img[data-caption]'
|
||||
},
|
||||
align: {
|
||||
requiredContent: 'img[data-align]'
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Extend requiredContent & allowedContent.
|
||||
// CKEDITOR.style is an immutable object: we cannot modify its
|
||||
// definition to extend requiredContent. Hence we get the definition,
|
||||
// modify it, and pass it to a new CKEDITOR.style instance.
|
||||
var requiredContent = widgetDefinition.requiredContent.getDefinition();
|
||||
requiredContent.attributes['data-align'] = '';
|
||||
requiredContent.attributes['data-caption'] = '';
|
||||
widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent);
|
||||
widgetDefinition.allowedContent.img.attributes['!data-align'] = true;
|
||||
widgetDefinition.allowedContent.img.attributes['!data-caption'] = true;
|
||||
|
||||
// Override allowedContent setting for the 'caption' nested editable.
|
||||
// This must match what caption_filter enforces.
|
||||
// @see \Drupal\filter\Plugin\Filter\FilterCaption::process()
|
||||
// @see \Drupal\Component\Utility\Xss::filter()
|
||||
widgetDefinition.editables.caption.allowedContent = 'a[!href]; em strong cite code br';
|
||||
|
||||
// Override downcast(): ensure we *only* output <img>, but also ensure
|
||||
// we include the data-entity-type, data-entity-uuid, data-align and
|
||||
// data-caption attributes.
|
||||
var originalDowncast = widgetDefinition.downcast;
|
||||
widgetDefinition.downcast = function (element) {
|
||||
var img = findElementByName(element, 'img');
|
||||
originalDowncast.call(this, img);
|
||||
|
||||
var caption = this.editables.caption;
|
||||
var captionHtml = caption && caption.getData();
|
||||
var attrs = img.attributes;
|
||||
|
||||
if (captionFilterEnabled) {
|
||||
// If image contains a non-empty caption, serialize caption to the
|
||||
// data-caption attribute.
|
||||
if (captionHtml) {
|
||||
attrs['data-caption'] = captionHtml;
|
||||
}
|
||||
}
|
||||
if (alignFilterEnabled) {
|
||||
if (this.data.align !== 'none') {
|
||||
attrs['data-align'] = this.data.align;
|
||||
}
|
||||
}
|
||||
|
||||
// If img is wrapped with a link, we want to return that link.
|
||||
if (img.parent.name === 'a') {
|
||||
return img.parent;
|
||||
}
|
||||
else {
|
||||
return img;
|
||||
}
|
||||
};
|
||||
|
||||
// We want to upcast <img> elements to a DOM structure required by the
|
||||
// image2 widget. Depending on a case it may be:
|
||||
// - just an <img> tag (non-captioned, not-centered image),
|
||||
// - <img> tag in a paragraph (non-captioned, centered image),
|
||||
// - <figure> tag (captioned image).
|
||||
// We take the same attributes into account as downcast() does.
|
||||
var originalUpcast = widgetDefinition.upcast;
|
||||
widgetDefinition.upcast = function (element, data) {
|
||||
if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) {
|
||||
return;
|
||||
}
|
||||
// Don't initialize on pasted fake objects.
|
||||
else if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
element = originalUpcast.call(this, element, data);
|
||||
var attrs = element.attributes;
|
||||
|
||||
if (element.parent.name === 'a') {
|
||||
element = element.parent;
|
||||
}
|
||||
|
||||
var retElement = element;
|
||||
var caption;
|
||||
|
||||
// We won't need the attributes during editing: we'll use widget.data
|
||||
// to store them (except the caption, which is stored in the DOM).
|
||||
if (captionFilterEnabled) {
|
||||
caption = attrs['data-caption'];
|
||||
delete attrs['data-caption'];
|
||||
}
|
||||
if (alignFilterEnabled) {
|
||||
data.align = attrs['data-align'];
|
||||
delete attrs['data-align'];
|
||||
}
|
||||
data['data-entity-type'] = attrs['data-entity-type'];
|
||||
delete attrs['data-entity-type'];
|
||||
data['data-entity-uuid'] = attrs['data-entity-uuid'];
|
||||
delete attrs['data-entity-uuid'];
|
||||
|
||||
if (captionFilterEnabled) {
|
||||
// Unwrap from <p> wrapper created by HTML parser for a captioned
|
||||
// image. The captioned image will be transformed to <figure>, so we
|
||||
// don't want the <p> anymore.
|
||||
if (element.parent.name === 'p' && caption) {
|
||||
var index = element.getIndex();
|
||||
var splitBefore = index > 0;
|
||||
var splitAfter = index + 1 < element.parent.children.length;
|
||||
|
||||
if (splitBefore) {
|
||||
element.parent.split(index);
|
||||
}
|
||||
index = element.getIndex();
|
||||
if (splitAfter) {
|
||||
element.parent.split(index + 1);
|
||||
}
|
||||
|
||||
element.parent.replaceWith(element);
|
||||
retElement = element;
|
||||
}
|
||||
|
||||
// If this image has a caption, create a full <figure> structure.
|
||||
if (caption) {
|
||||
var figure = new CKEDITOR.htmlParser.element('figure');
|
||||
caption = new CKEDITOR.htmlParser.fragment.fromHtml(caption, 'figcaption');
|
||||
|
||||
// Use Drupal's data-placeholder attribute to insert a CSS-based,
|
||||
// translation-ready placeholder for empty captions. Note that it
|
||||
// also must to be done for new instances (see
|
||||
// widgetDefinition._createDialogSaveCallback).
|
||||
caption.attributes['data-placeholder'] = placeholderText;
|
||||
|
||||
element.replaceWith(figure);
|
||||
figure.add(element);
|
||||
figure.add(caption);
|
||||
figure.attributes['class'] = editor.config.image2_captionedClass;
|
||||
retElement = figure;
|
||||
}
|
||||
}
|
||||
|
||||
if (alignFilterEnabled) {
|
||||
// If this image doesn't have a caption (or the caption filter is
|
||||
// disabled), but it is centered, make sure that it's wrapped with
|
||||
// <p>, which will become a part of the widget.
|
||||
if (data.align === 'center' && (!captionFilterEnabled || !caption)) {
|
||||
var p = new CKEDITOR.htmlParser.element('p');
|
||||
element.replaceWith(p);
|
||||
p.add(element);
|
||||
// Apply the class for centered images.
|
||||
p.addClass(editor.config.image2_alignClasses[1]);
|
||||
retElement = p;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the upcasted element (<img>, <figure> or <p>).
|
||||
return retElement;
|
||||
};
|
||||
|
||||
// Protected; keys of the widget data to be sent to the Drupal dialog.
|
||||
// Append to the values defined by the drupalimage plugin.
|
||||
// @see core/modules/ckeditor/js/plugins/drupalimage/plugin.js
|
||||
CKEDITOR.tools.extend(widgetDefinition._mapDataToDialog, {
|
||||
'align': 'data-align',
|
||||
'data-caption': 'data-caption',
|
||||
'hasCaption': 'hasCaption'
|
||||
});
|
||||
|
||||
// Override Drupal dialog save callback.
|
||||
var originalCreateDialogSaveCallback = widgetDefinition._createDialogSaveCallback;
|
||||
widgetDefinition._createDialogSaveCallback = function (editor, widget) {
|
||||
var saveCallback = originalCreateDialogSaveCallback.call(this, editor, widget);
|
||||
|
||||
return function (dialogReturnValues) {
|
||||
// Ensure hasCaption is a boolean. image2 assumes it always works
|
||||
// with booleans; if this is not the case, then
|
||||
// CKEDITOR.plugins.image2.stateShifter() will incorrectly mark
|
||||
// widget.data.hasCaption as "changed" (e.g. when hasCaption === 0
|
||||
// instead of hasCaption === false). This causes image2's "state
|
||||
// shifter" to enter the wrong branch of the algorithm and blow up.
|
||||
dialogReturnValues.attributes.hasCaption = !!dialogReturnValues.attributes.hasCaption;
|
||||
|
||||
var actualWidget = saveCallback(dialogReturnValues);
|
||||
|
||||
// By default, the template of captioned widget has no
|
||||
// data-placeholder attribute. Note that it also must be done when
|
||||
// upcasting existing elements (see widgetDefinition.upcast).
|
||||
if (dialogReturnValues.attributes.hasCaption) {
|
||||
actualWidget.editables.caption.setAttribute('data-placeholder', placeholderText);
|
||||
|
||||
// Some browsers will add a <br> tag to a newly created DOM
|
||||
// element with no content. Remove this <br> if it is the only
|
||||
// thing in the caption. Our placeholder support requires the
|
||||
// element be entirely empty. See filter-caption.css.
|
||||
var captionElement = actualWidget.editables.caption.$;
|
||||
if (captionElement.childNodes.length === 1 && captionElement.childNodes.item(0).nodeName === 'BR') {
|
||||
captionElement.removeChild(captionElement.childNodes.item(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
// Low priority to ensure drupalimage's event handler runs first.
|
||||
}, null, null, 20);
|
||||
},
|
||||
|
||||
afterInit: function (editor) {
|
||||
var disableButtonIfOnWidget = function (evt) {
|
||||
var widget = editor.widgets.focused;
|
||||
if (widget && widget.name === 'image') {
|
||||
this.setState(CKEDITOR.TRISTATE_DISABLED);
|
||||
evt.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
// Disable alignment buttons if the align filter is not enabled.
|
||||
if (editor.plugins.justify && !editor.config.drupalImageCaption_alignFilterEnabled) {
|
||||
var cmd;
|
||||
var commands = ['justifyleft', 'justifycenter', 'justifyright', 'justifyblock'];
|
||||
for (var n = 0; n < commands.length; n++) {
|
||||
cmd = editor.getCommand(commands[n]);
|
||||
cmd.contextSensitive = 1;
|
||||
cmd.on('refresh', disableButtonIfOnWidget, null, null, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds an element by its name.
|
||||
*
|
||||
* Function will check first the passed element itself and then all its
|
||||
* children in DFS order.
|
||||
*
|
||||
* @param {CKEDITOR.htmlParser.element} element
|
||||
* The element to search.
|
||||
* @param {string} name
|
||||
* The element name to search for.
|
||||
*
|
||||
* @return {?CKEDITOR.htmlParser.element}
|
||||
* The found element, or null.
|
||||
*/
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
var found = null;
|
||||
element.forEach(function (el) {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
// Stop here.
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
|
||||
})(CKEDITOR);
|
Binary file not shown.
After Width: | Height: | Size: 328 B |
Binary file not shown.
After Width: | Height: | Size: 312 B |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
304
web/core/modules/ckeditor/js/plugins/drupallink/plugin.js
Normal file
304
web/core/modules/ckeditor/js/plugins/drupallink/plugin.js
Normal file
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Link plugin.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, CKEDITOR) {
|
||||
|
||||
'use strict';
|
||||
|
||||
function parseAttributes(editor, element) {
|
||||
var parsedAttributes = {};
|
||||
|
||||
var domElement = element.$;
|
||||
var attribute = null;
|
||||
var attributeName;
|
||||
for (var attrIndex = 0; attrIndex < domElement.attributes.length; attrIndex++) {
|
||||
attribute = domElement.attributes.item(attrIndex);
|
||||
attributeName = attribute.nodeName.toLowerCase();
|
||||
// Ignore data-cke-* attributes; they're CKEditor internals.
|
||||
if (attributeName.indexOf('data-cke-') === 0) {
|
||||
continue;
|
||||
}
|
||||
// Store the value for this attribute, unless there's a data-cke-saved-
|
||||
// alternative for it, which will contain the quirk-free, original value.
|
||||
parsedAttributes[attributeName] = element.data('cke-saved-' + attributeName) || attribute.nodeValue;
|
||||
}
|
||||
|
||||
// Remove any cke_* classes.
|
||||
if (parsedAttributes.class) {
|
||||
parsedAttributes.class = CKEDITOR.tools.trim(parsedAttributes.class.replace(/cke_\S+/, ''));
|
||||
}
|
||||
|
||||
return parsedAttributes;
|
||||
}
|
||||
|
||||
function getAttributes(editor, data) {
|
||||
var set = {};
|
||||
for (var attributeName in data) {
|
||||
if (data.hasOwnProperty(attributeName)) {
|
||||
set[attributeName] = data[attributeName];
|
||||
}
|
||||
}
|
||||
|
||||
// CKEditor tracks the *actual* saved href in a data-cke-saved-* attribute
|
||||
// to work around browser quirks. We need to update it.
|
||||
set['data-cke-saved-href'] = set.href;
|
||||
|
||||
// Remove all attributes which are not currently set.
|
||||
var removed = {};
|
||||
for (var s in set) {
|
||||
if (set.hasOwnProperty(s)) {
|
||||
delete removed[s];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
set: set,
|
||||
removed: CKEDITOR.tools.objectKeys(removed)
|
||||
};
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupallink', {
|
||||
icons: 'drupallink,drupalunlink',
|
||||
hidpi: true,
|
||||
|
||||
init: function (editor) {
|
||||
// Add the commands for link and unlink.
|
||||
editor.addCommand('drupallink', {
|
||||
allowedContent: {
|
||||
a: {
|
||||
attributes: {
|
||||
'!href': true
|
||||
},
|
||||
classes: {}
|
||||
}
|
||||
},
|
||||
requiredContent: new CKEDITOR.style({
|
||||
element: 'a',
|
||||
attributes: {
|
||||
href: ''
|
||||
}
|
||||
}),
|
||||
modes: {wysiwyg: 1},
|
||||
canUndo: true,
|
||||
exec: function (editor) {
|
||||
var drupalImageUtils = CKEDITOR.plugins.drupalimage;
|
||||
var focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
|
||||
var linkElement = getSelectedLink(editor);
|
||||
|
||||
// Set existing values based on selected element.
|
||||
var existingValues = {};
|
||||
if (linkElement && linkElement.$) {
|
||||
existingValues = parseAttributes(editor, linkElement);
|
||||
}
|
||||
// Or, if an image widget is focused, we're editing a link wrapping
|
||||
// an image widget.
|
||||
else if (focusedImageWidget && focusedImageWidget.data.link) {
|
||||
existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
|
||||
}
|
||||
|
||||
// Prepare a save callback to be used upon saving the dialog.
|
||||
var saveCallback = function (returnValues) {
|
||||
// If an image widget is focused, we're not editing an independent
|
||||
// link, but we're wrapping an image widget in a link.
|
||||
if (focusedImageWidget) {
|
||||
focusedImageWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedImageWidget.data.link));
|
||||
editor.fire('saveSnapshot');
|
||||
return;
|
||||
}
|
||||
|
||||
editor.fire('saveSnapshot');
|
||||
|
||||
// Create a new link element if needed.
|
||||
if (!linkElement && returnValues.attributes.href) {
|
||||
var selection = editor.getSelection();
|
||||
var range = selection.getRanges(1)[0];
|
||||
|
||||
// Use link URL as text with a collapsed cursor.
|
||||
if (range.collapsed) {
|
||||
// Shorten mailto URLs to just the email address.
|
||||
var text = new CKEDITOR.dom.text(returnValues.attributes.href.replace(/^mailto:/, ''), editor.document);
|
||||
range.insertNode(text);
|
||||
range.selectNodeContents(text);
|
||||
}
|
||||
|
||||
// Create the new link by applying a style to the new text.
|
||||
var style = new CKEDITOR.style({element: 'a', attributes: returnValues.attributes});
|
||||
style.type = CKEDITOR.STYLE_INLINE;
|
||||
style.applyToRange(range);
|
||||
range.select();
|
||||
|
||||
// Set the link so individual properties may be set below.
|
||||
linkElement = getSelectedLink(editor);
|
||||
}
|
||||
// Update the link properties.
|
||||
else if (linkElement) {
|
||||
for (var attrName in returnValues.attributes) {
|
||||
if (returnValues.attributes.hasOwnProperty(attrName)) {
|
||||
// Update the property if a value is specified.
|
||||
if (returnValues.attributes[attrName].length > 0) {
|
||||
var value = returnValues.attributes[attrName];
|
||||
linkElement.data('cke-saved-' + attrName, value);
|
||||
linkElement.setAttribute(attrName, value);
|
||||
}
|
||||
// Delete the property if set to an empty string.
|
||||
else {
|
||||
linkElement.removeAttribute(attrName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save snapshot for undo support.
|
||||
editor.fire('saveSnapshot');
|
||||
};
|
||||
// Drupal.t() will not work inside CKEditor plugins because CKEditor
|
||||
// loads the JavaScript file instead of Drupal. Pull translated
|
||||
// strings from the plugin settings that are translated server-side.
|
||||
var dialogSettings = {
|
||||
title: linkElement ? editor.config.drupalLink_dialogTitleEdit : editor.config.drupalLink_dialogTitleAdd,
|
||||
dialogClass: 'editor-link-dialog'
|
||||
};
|
||||
|
||||
// Open the dialog for the edit form.
|
||||
Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/link/' + editor.config.drupal.format), existingValues, saveCallback, dialogSettings);
|
||||
}
|
||||
});
|
||||
editor.addCommand('drupalunlink', {
|
||||
contextSensitive: 1,
|
||||
startDisabled: 1,
|
||||
requiredContent: new CKEDITOR.style({
|
||||
element: 'a',
|
||||
attributes: {
|
||||
href: ''
|
||||
}
|
||||
}),
|
||||
exec: function (editor) {
|
||||
var style = new CKEDITOR.style({element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1});
|
||||
editor.removeStyle(style);
|
||||
},
|
||||
refresh: function (editor, path) {
|
||||
var element = path.lastElement && path.lastElement.getAscendant('a', true);
|
||||
if (element && element.getName() === 'a' && element.getAttribute('href') && element.getChildCount()) {
|
||||
this.setState(CKEDITOR.TRISTATE_OFF);
|
||||
}
|
||||
else {
|
||||
this.setState(CKEDITOR.TRISTATE_DISABLED);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// CTRL + K.
|
||||
editor.setKeystroke(CKEDITOR.CTRL + 75, 'drupallink');
|
||||
|
||||
// Add buttons for link and unlink.
|
||||
if (editor.ui.addButton) {
|
||||
editor.ui.addButton('DrupalLink', {
|
||||
label: Drupal.t('Link'),
|
||||
command: 'drupallink'
|
||||
});
|
||||
editor.ui.addButton('DrupalUnlink', {
|
||||
label: Drupal.t('Unlink'),
|
||||
command: 'drupalunlink'
|
||||
});
|
||||
}
|
||||
|
||||
editor.on('doubleclick', function (evt) {
|
||||
var element = getSelectedLink(editor) || evt.data.element;
|
||||
|
||||
if (!element.isReadOnly()) {
|
||||
if (element.is('a')) {
|
||||
editor.getSelection().selectElement(element);
|
||||
editor.getCommand('drupallink').exec();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If the "menu" plugin is loaded, register the menu items.
|
||||
if (editor.addMenuItems) {
|
||||
editor.addMenuItems({
|
||||
link: {
|
||||
label: Drupal.t('Edit Link'),
|
||||
command: 'drupallink',
|
||||
group: 'link',
|
||||
order: 1
|
||||
},
|
||||
|
||||
unlink: {
|
||||
label: Drupal.t('Unlink'),
|
||||
command: 'drupalunlink',
|
||||
group: 'link',
|
||||
order: 5
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the "contextmenu" plugin is loaded, register the listeners.
|
||||
if (editor.contextMenu) {
|
||||
editor.contextMenu.addListener(function (element, selection) {
|
||||
if (!element || element.isReadOnly()) {
|
||||
return null;
|
||||
}
|
||||
var anchor = getSelectedLink(editor);
|
||||
if (!anchor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var menu = {};
|
||||
if (anchor.getAttribute('href') && anchor.getChildCount()) {
|
||||
menu = {link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF};
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the surrounding link element of current selection.
|
||||
*
|
||||
* The following selection will all return the link element.
|
||||
*
|
||||
* @example
|
||||
* <a href="#">li^nk</a>
|
||||
* <a href="#">[link]</a>
|
||||
* text[<a href="#">link]</a>
|
||||
* <a href="#">li[nk</a>]
|
||||
* [<b><a href="#">li]nk</a></b>]
|
||||
* [<a href="#"><b>li]nk</b></a>
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* The CKEditor editor object
|
||||
*
|
||||
* @return {?HTMLElement}
|
||||
* The selected link element, or null.
|
||||
*
|
||||
*/
|
||||
function getSelectedLink(editor) {
|
||||
var selection = editor.getSelection();
|
||||
var selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
var range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expose an API for other plugins to interact with drupallink widgets.
|
||||
// (Compatible with the official CKEditor link plugin's API:
|
||||
// http://dev.ckeditor.com/ticket/13885.)
|
||||
CKEDITOR.plugins.drupallink = {
|
||||
parseLinkAttributes: parseAttributes,
|
||||
getLinkAttributes: getAttributes
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, CKEDITOR);
|
233
web/core/modules/ckeditor/js/views/AuralView.js
Normal file
233
web/core/modules/ckeditor/js/views/AuralView.js
Normal file
|
@ -0,0 +1,233 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the aural view of CKEditor toolbar
|
||||
* configuration.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor.AuralView = Backbone.View.extend(/** @lends Drupal.ckeditor.AuralView# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {
|
||||
'click .ckeditor-buttons a': 'announceButtonHelp',
|
||||
'click .ckeditor-multiple-buttons a': 'announceSeparatorHelp',
|
||||
'focus .ckeditor-button a': 'onFocus',
|
||||
'focus .ckeditor-button-separator a': 'onFocus',
|
||||
'focus .ckeditor-toolbar-group': 'onFocus'
|
||||
},
|
||||
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; aural UX (output only).
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
// Announce the button and group positions when the model is no longer
|
||||
// dirty.
|
||||
this.listenTo(this.model, 'change:isDirty', this.announceMove);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls announce on buttons and groups when their position is changed.
|
||||
*
|
||||
* @param {Drupal.ckeditor.ConfigurationModel} model
|
||||
* The ckeditor configuration model.
|
||||
* @param {bool} isDirty
|
||||
* A model attribute that indicates if the changed toolbar configuration
|
||||
* has been stored or not.
|
||||
*/
|
||||
announceMove: function (model, isDirty) {
|
||||
// Announce the position of a button or group after the model has been
|
||||
// updated.
|
||||
if (!isDirty) {
|
||||
var item = document.activeElement || null;
|
||||
if (item) {
|
||||
var $item = $(item);
|
||||
if ($item.hasClass('ckeditor-toolbar-group')) {
|
||||
this.announceButtonGroupPosition($item);
|
||||
}
|
||||
else if ($item.parent().hasClass('ckeditor-button')) {
|
||||
this.announceButtonPosition($item.parent());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the focus event of elements in the active and available toolbars.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The focus event that was triggered.
|
||||
*/
|
||||
onFocus: function (event) {
|
||||
event.stopPropagation();
|
||||
|
||||
var $originalTarget = $(event.target);
|
||||
var $currentTarget = $(event.currentTarget);
|
||||
var $parent = $currentTarget.parent();
|
||||
if ($parent.hasClass('ckeditor-button') || $parent.hasClass('ckeditor-button-separator')) {
|
||||
this.announceButtonPosition($currentTarget.parent());
|
||||
}
|
||||
else if ($originalTarget.attr('role') !== 'button' && $currentTarget.hasClass('ckeditor-toolbar-group')) {
|
||||
this.announceButtonGroupPosition($currentTarget);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Announces the current position of a button group.
|
||||
*
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
*/
|
||||
announceButtonGroupPosition: function ($group) {
|
||||
var $groups = $group.parent().children();
|
||||
var $row = $group.closest('.ckeditor-row');
|
||||
var $rows = $row.parent().children();
|
||||
var position = $groups.index($group) + 1;
|
||||
var positionCount = $groups.not('.placeholder').length;
|
||||
var row = $rows.index($row) + 1;
|
||||
var rowCount = $rows.not('.placeholder').length;
|
||||
var text = Drupal.t('@groupName button group in position @position of @positionCount in row @row of @rowCount.', {
|
||||
'@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
|
||||
'@position': position,
|
||||
'@positionCount': positionCount,
|
||||
'@row': row,
|
||||
'@rowCount': rowCount
|
||||
});
|
||||
// If this position is the first in the last row then tell the user that
|
||||
// pressing the down arrow key will create a new row.
|
||||
if (position === 1 && row === rowCount) {
|
||||
text += '\n';
|
||||
text += Drupal.t('Press the down arrow key to create a new row.');
|
||||
}
|
||||
Drupal.announce(text, 'assertive');
|
||||
},
|
||||
|
||||
/**
|
||||
* Announces current button position.
|
||||
*
|
||||
* @param {jQuery} $button
|
||||
* A jQuery set that contains an li element that wraps a button.
|
||||
*/
|
||||
announceButtonPosition: function ($button) {
|
||||
var $row = $button.closest('.ckeditor-row');
|
||||
var $rows = $row.parent().children();
|
||||
var $buttons = $button.closest('.ckeditor-buttons').children();
|
||||
var $group = $button.closest('.ckeditor-toolbar-group');
|
||||
var $groups = $group.parent().children();
|
||||
var groupPosition = $groups.index($group) + 1;
|
||||
var groupPositionCount = $groups.not('.placeholder').length;
|
||||
var position = $buttons.index($button) + 1;
|
||||
var positionCount = $buttons.length;
|
||||
var row = $rows.index($row) + 1;
|
||||
var rowCount = $rows.not('.placeholder').length;
|
||||
// The name of the button separator is 'button separator' and its type
|
||||
// is 'separator', so we do not want to print the type of this item,
|
||||
// otherwise the UA will speak 'button separator separator'.
|
||||
var type = ($button.attr('data-drupal-ckeditor-type') === 'separator') ? '' : Drupal.t('button');
|
||||
var text;
|
||||
// The button is located in the available button set.
|
||||
if ($button.closest('.ckeditor-toolbar-disabled').length > 0) {
|
||||
text = Drupal.t('@name @type.', {
|
||||
'@name': $button.children().attr('aria-label'),
|
||||
'@type': type
|
||||
});
|
||||
text += '\n' + Drupal.t('Press the down arrow key to activate.');
|
||||
|
||||
Drupal.announce(text, 'assertive');
|
||||
}
|
||||
// The button is in the active toolbar.
|
||||
else if ($group.not('.placeholder').length === 1) {
|
||||
text = Drupal.t('@name @type in position @position of @positionCount in @groupName button group in row @row of @rowCount.', {
|
||||
'@name': $button.children().attr('aria-label'),
|
||||
'@type': type,
|
||||
'@position': position,
|
||||
'@positionCount': positionCount,
|
||||
'@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
|
||||
'@row': row,
|
||||
'@rowCount': rowCount
|
||||
});
|
||||
// If this position is the first in the last row then tell the user that
|
||||
// pressing the down arrow key will create a new row.
|
||||
if (groupPosition === 1 && position === 1 && row === rowCount) {
|
||||
text += '\n';
|
||||
text += Drupal.t('Press the down arrow key to create a new button group in a new row.');
|
||||
}
|
||||
// If this position is the last one in this row then tell the user that
|
||||
// moving the button to the next group will create a new group.
|
||||
if (groupPosition === groupPositionCount && position === positionCount) {
|
||||
text += '\n';
|
||||
text += Drupal.t('This is the last group. Move the button forward to create a new group.');
|
||||
}
|
||||
Drupal.announce(text, 'assertive');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides help information when a button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event for the button click.
|
||||
*/
|
||||
announceButtonHelp: function (event) {
|
||||
var $link = $(event.currentTarget);
|
||||
var $button = $link.parent();
|
||||
var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
var message;
|
||||
|
||||
if (enabled) {
|
||||
message = Drupal.t('The "@name" button is currently enabled.', {
|
||||
'@name': $link.attr('aria-label')
|
||||
});
|
||||
message += '\n' + Drupal.t('Use the keyboard arrow keys to change the position of this button.');
|
||||
message += '\n' + Drupal.t('Press the up arrow key on the top row to disable the button.');
|
||||
}
|
||||
else {
|
||||
message = Drupal.t('The "@name" button is currently disabled.', {
|
||||
'@name': $link.attr('aria-label')
|
||||
});
|
||||
message += '\n' + Drupal.t('Use the down arrow key to move this button into the active toolbar.');
|
||||
}
|
||||
Drupal.announce(message);
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides help information when a separator is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event for the separator click.
|
||||
*/
|
||||
announceSeparatorHelp: function (event) {
|
||||
var $link = $(event.currentTarget);
|
||||
var $button = $link.parent();
|
||||
var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
var message;
|
||||
|
||||
if (enabled) {
|
||||
message = Drupal.t('This @name is currently enabled.', {
|
||||
'@name': $link.attr('aria-label')
|
||||
});
|
||||
message += '\n' + Drupal.t('Use the keyboard arrow keys to change the position of this separator.');
|
||||
}
|
||||
else {
|
||||
message = Drupal.t('Separators are used to visually split individual buttons.');
|
||||
message += '\n' + Drupal.t('This @name is currently disabled.', {
|
||||
'@name': $link.attr('aria-label')
|
||||
});
|
||||
message += '\n' + Drupal.t('Use the down arrow key to move this separator into the active toolbar.');
|
||||
message += '\n' + Drupal.t('You may add multiple separators to each button group.');
|
||||
}
|
||||
Drupal.announce(message);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, jQuery);
|
383
web/core/modules/ckeditor/js/views/ControllerView.js
Normal file
383
web/core/modules/ckeditor/js/views/ControllerView.js
Normal file
|
@ -0,0 +1,383 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, Backbone, CKEDITOR, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor.ControllerView = Backbone.View.extend(/** @lends Drupal.ckeditor.ControllerView# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {},
|
||||
|
||||
/**
|
||||
* Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this));
|
||||
|
||||
// Push the active editor configuration to the textarea.
|
||||
this.model.listenTo(this.model, 'change:activeEditorConfig', this.model.sync);
|
||||
this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM);
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the active toolbar DOM structure to an object representation.
|
||||
*
|
||||
* @param {Drupal.ckeditor.ConfigurationModel} model
|
||||
* The state model for the CKEditor configuration.
|
||||
* @param {bool} isDirty
|
||||
* Tracks whether the active toolbar DOM structure has been changed.
|
||||
* isDirty is toggled back to false in this method.
|
||||
* @param {object} options
|
||||
* An object that includes:
|
||||
* @param {bool} [options.broadcast]
|
||||
* A flag that controls whether a CKEditorToolbarChanged event should be
|
||||
* fired for configuration changes.
|
||||
*
|
||||
* @fires event:CKEditorToolbarChanged
|
||||
*/
|
||||
parseEditorDOM: function (model, isDirty, options) {
|
||||
if (isDirty) {
|
||||
var currentConfig = this.model.get('activeEditorConfig');
|
||||
|
||||
// Process the rows.
|
||||
var rows = [];
|
||||
this.$el
|
||||
.find('.ckeditor-active-toolbar-configuration')
|
||||
.children('.ckeditor-row').each(function () {
|
||||
var groups = [];
|
||||
// Process the button groups.
|
||||
$(this).find('.ckeditor-toolbar-group').each(function () {
|
||||
var $group = $(this);
|
||||
var $buttons = $group.find('.ckeditor-button');
|
||||
if ($buttons.length) {
|
||||
var group = {
|
||||
name: $group.attr('data-drupal-ckeditor-toolbar-group-name'),
|
||||
items: []
|
||||
};
|
||||
$group.find('.ckeditor-button, .ckeditor-multiple-button').each(function () {
|
||||
group.items.push($(this).attr('data-drupal-ckeditor-button-name'));
|
||||
});
|
||||
groups.push(group);
|
||||
}
|
||||
});
|
||||
if (groups.length) {
|
||||
rows.push(groups);
|
||||
}
|
||||
});
|
||||
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);
|
||||
|
||||
// Determine whether we should trigger an event.
|
||||
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.
|
||||
*
|
||||
* In order to get a list of all features needed by CKEditor, we create a
|
||||
* hidden CKEditor instance, then check the CKEditor's "allowedContent"
|
||||
* filter settings. Because creating an instance is expensive, a callback
|
||||
* must be provided that will receive a hash of {@link Drupal.EditorFeature}
|
||||
* features keyed by feature (button) name.
|
||||
*
|
||||
* @param {object} CKEditorConfig
|
||||
* An object that represents the configuration settings for a CKEditor
|
||||
* editor component.
|
||||
* @param {function} callback
|
||||
* A function to invoke when the instanceReady event is fired by the
|
||||
* CKEditor object.
|
||||
*/
|
||||
getCKEditorFeatures: function (CKEditorConfig, callback) {
|
||||
var getProperties = function (CKEPropertiesList) {
|
||||
return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : [];
|
||||
};
|
||||
|
||||
var convertCKERulesToEditorFeature = function (feature, CKEFeatureRules) {
|
||||
for (var i = 0; i < CKEFeatureRules.length; i++) {
|
||||
var CKERule = CKEFeatureRules[i];
|
||||
var rule = new Drupal.EditorFeatureHTMLRule();
|
||||
|
||||
// Tags.
|
||||
var tags = getProperties(CKERule.elements);
|
||||
rule.required.tags = (CKERule.propertiesOnly) ? [] : tags;
|
||||
rule.allowed.tags = tags;
|
||||
// Attributes.
|
||||
rule.required.attributes = getProperties(CKERule.requiredAttributes);
|
||||
rule.allowed.attributes = getProperties(CKERule.attributes);
|
||||
// Styles.
|
||||
rule.required.styles = getProperties(CKERule.requiredStyles);
|
||||
rule.allowed.styles = getProperties(CKERule.styles);
|
||||
// Classes.
|
||||
rule.required.classes = getProperties(CKERule.requiredClasses);
|
||||
rule.allowed.classes = getProperties(CKERule.classes);
|
||||
// Raw.
|
||||
rule.raw = CKERule;
|
||||
|
||||
feature.addHTMLRule(rule);
|
||||
}
|
||||
};
|
||||
|
||||
// Create hidden CKEditor with all features enabled, retrieve metadata.
|
||||
// @see \Drupal\ckeditor\Plugin\Editor\CKEditor::settingsForm.
|
||||
var hiddenCKEditorID = 'ckeditor-hidden';
|
||||
if (CKEDITOR.instances[hiddenCKEditorID]) {
|
||||
CKEDITOR.instances[hiddenCKEditorID].destroy(true);
|
||||
}
|
||||
// Load external plugins, if any.
|
||||
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], '');
|
||||
}
|
||||
}
|
||||
}
|
||||
CKEDITOR.inline($('#' + hiddenCKEditorID).get(0), CKEditorConfig);
|
||||
|
||||
// Once the instance is ready, retrieve the allowedContent filter rules
|
||||
// and convert them to Drupal.EditorFeature objects.
|
||||
CKEDITOR.once('instanceReady', function (e) {
|
||||
if (e.editor.name === hiddenCKEditorID) {
|
||||
// First collect all CKEditor allowedContent rules.
|
||||
var CKEFeatureRulesMap = {};
|
||||
var rules = e.editor.filter.allowedContent;
|
||||
var rule;
|
||||
var name;
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
rule = rules[i];
|
||||
name = rule.featureName || ':(';
|
||||
if (!CKEFeatureRulesMap[name]) {
|
||||
CKEFeatureRulesMap[name] = [];
|
||||
}
|
||||
CKEFeatureRulesMap[name].push(rule);
|
||||
}
|
||||
|
||||
// Now convert these to Drupal.EditorFeature objects. And track which
|
||||
// buttons are mapped to which features.
|
||||
// @see getFeatureForButton()
|
||||
var features = {};
|
||||
var buttonsToFeatures = {};
|
||||
for (var featureName in CKEFeatureRulesMap) {
|
||||
if (CKEFeatureRulesMap.hasOwnProperty(featureName)) {
|
||||
var feature = new Drupal.EditorFeature(featureName);
|
||||
convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]);
|
||||
features[featureName] = feature;
|
||||
var command = e.editor.getCommand(featureName);
|
||||
if (command) {
|
||||
buttonsToFeatures[command.uiItems[0].name] = featureName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(features, buttonsToFeatures);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the feature for a given button from featuresMetadata. Returns
|
||||
* false if the given button is in fact a divider.
|
||||
*
|
||||
* @param {string} button
|
||||
* The name of a CKEditor button.
|
||||
*
|
||||
* @return {object}
|
||||
* The feature metadata object for a button.
|
||||
*/
|
||||
getFeatureForButton: function (button) {
|
||||
// Return false if the button being added is a divider.
|
||||
if (button === '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get a Drupal.editorFeature object that contains all metadata for
|
||||
// the feature that was just added or removed. Not every feature has
|
||||
// such metadata.
|
||||
var featureName = this.model.get('buttonsToFeatures')[button.toLowerCase()];
|
||||
// Features without an associated command do not have a 'feature name' by
|
||||
// default, so we use the lowercased button name instead.
|
||||
if (!featureName) {
|
||||
featureName = button.toLowerCase();
|
||||
}
|
||||
var featuresMetadata = this.model.get('featuresMetadata');
|
||||
if (!featuresMetadata[featureName]) {
|
||||
featuresMetadata[featureName] = new Drupal.EditorFeature(featureName);
|
||||
this.model.set('featuresMetadata', featuresMetadata);
|
||||
}
|
||||
return featuresMetadata[featureName];
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks buttons against filter settings; disables disallowed buttons.
|
||||
*
|
||||
* @param {object} features
|
||||
* A map of {@link Drupal.EditorFeature} objects.
|
||||
* @param {object} buttonsToFeatures
|
||||
* Object containing the button-to-feature mapping.
|
||||
*
|
||||
* @see Drupal.ckeditor.ControllerView#getFeatureForButton
|
||||
*/
|
||||
disableFeaturesDisallowedByFilters: function (features, buttonsToFeatures) {
|
||||
this.model.set('featuresMetadata', features);
|
||||
// Store the button-to-feature mapping. Needs to happen only once, because
|
||||
// the same buttons continue to have the same features; only the rules for
|
||||
// specific features may change.
|
||||
// @see getFeatureForButton()
|
||||
this.model.set('buttonsToFeatures', buttonsToFeatures);
|
||||
|
||||
// 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.
|
||||
var buttonGroups = _.flatten(this.model.get('activeEditorConfig'));
|
||||
for (var i = 0; i < buttonGroups.length; i++) {
|
||||
// Pull the button names from each toolbar button group.
|
||||
var buttons = buttonGroups[i].items;
|
||||
for (var k = 0; k < buttons.length; k++) {
|
||||
existingButtons.push(buttons[k]);
|
||||
}
|
||||
}
|
||||
// Remove duplicate buttons.
|
||||
existingButtons = _.unique(existingButtons);
|
||||
// Prepare the active toolbar and available-button toolbars.
|
||||
for (var n = 0; n < existingButtons.length; n++) {
|
||||
var button = existingButtons[n];
|
||||
var feature = this.getFeatureForButton(button);
|
||||
// Skip dividers.
|
||||
if (feature === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) {
|
||||
// Existing toolbar buttons are in fact "added features".
|
||||
this.$el.find('.ckeditor-toolbar-active').trigger('CKEditorToolbarChanged', ['added', existingButtons[n]]);
|
||||
}
|
||||
else {
|
||||
// Move the button element from the active the active toolbar to the
|
||||
// list of available buttons.
|
||||
$('.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="' + button + '"]')
|
||||
.detach()
|
||||
.appendTo('.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul');
|
||||
// Update the toolbar value field.
|
||||
this.model.set({isDirty: true}, {broadcast: false});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up broadcasting of CKEditor toolbar configuration changes.
|
||||
*
|
||||
* @param {jQuery} $ckeditorToolbar
|
||||
* The active toolbar DOM element wrapped in jQuery.
|
||||
*/
|
||||
broadcastConfigurationChanges: function ($ckeditorToolbar) {
|
||||
var view = this;
|
||||
var hiddenEditorConfig = this.model.get('hiddenEditorConfig');
|
||||
var getFeatureForButton = this.getFeatureForButton.bind(this);
|
||||
var getCKEditorFeatures = this.getCKEditorFeatures.bind(this);
|
||||
$ckeditorToolbar
|
||||
.find('.ckeditor-toolbar-active')
|
||||
// Listen for CKEditor toolbar configuration changes. When a button is
|
||||
// added/removed, call an appropriate Drupal.editorConfiguration method.
|
||||
.on('CKEditorToolbarChanged.ckeditorAdmin', function (event, action, button) {
|
||||
var feature = getFeatureForButton(button);
|
||||
|
||||
// Early-return if the button being added is a divider.
|
||||
if (feature === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger a standardized text editor configuration event to indicate
|
||||
// whether a feature was added or removed, so that filters can react.
|
||||
var configEvent = (action === 'added') ? 'addedFeature' : 'removedFeature';
|
||||
Drupal.editorConfiguration[configEvent](feature);
|
||||
})
|
||||
// Listen for CKEditor plugin settings changes. When a plugin setting is
|
||||
// changed, rebuild the CKEditor features metadata.
|
||||
.on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (event, settingsChanges) {
|
||||
// Update hidden CKEditor configuration.
|
||||
for (var key in settingsChanges) {
|
||||
if (settingsChanges.hasOwnProperty(key)) {
|
||||
hiddenEditorConfig[key] = settingsChanges[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve features for the updated hidden CKEditor configuration.
|
||||
getCKEditorFeatures(hiddenEditorConfig, function (features) {
|
||||
// Trigger a standardized text editor configuration event for each
|
||||
// feature that was modified by the configuration changes.
|
||||
var featuresMetadata = view.model.get('featuresMetadata');
|
||||
for (var name in features) {
|
||||
if (features.hasOwnProperty(name)) {
|
||||
var feature = features[name];
|
||||
if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) {
|
||||
Drupal.editorConfiguration.modifiedFeature(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the CKEditor features metadata.
|
||||
view.model.set('featuresMetadata', features);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the list of buttons from an editor configuration.
|
||||
*
|
||||
* @param {object} config
|
||||
* A CKEditor configuration object.
|
||||
*
|
||||
* @return {Array}
|
||||
* A list of buttons in the CKEditor configuration.
|
||||
*/
|
||||
getButtonList: function (config) {
|
||||
var buttons = [];
|
||||
// Remove the rows.
|
||||
config = _.flatten(config);
|
||||
|
||||
// Loop through the button groups and pull out the buttons.
|
||||
config.forEach(function (group) {
|
||||
group.items.forEach(function (button) {
|
||||
buttons.push(button);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove the dividing elements if any.
|
||||
return _.without(buttons, '-');
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, Backbone, CKEDITOR, _);
|
266
web/core/modules/ckeditor/js/views/KeyboardView.js
Normal file
266
web/core/modules/ckeditor/js/views/KeyboardView.js
Normal file
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* @file
|
||||
* Backbone View providing the aural view of CKEditor keyboard UX configuration.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, Backbone, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor.KeyboardView = Backbone.View.extend(/** @lends Drupal.ckeditor.KeyboardView# */{
|
||||
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; keyboard UX.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
// Add keyboard arrow support.
|
||||
this.$el.on('keydown.ckeditor', '.ckeditor-buttons a, .ckeditor-multiple-buttons a', this.onPressButton.bind(this));
|
||||
this.$el.on('keydown.ckeditor', '[data-drupal-ckeditor-type="group"]', this.onPressGroup.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
render: function () {
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keypresses on a CKEditor configuration button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The keypress event triggered.
|
||||
*/
|
||||
onPressButton: function (event) {
|
||||
var upDownKeys = [
|
||||
38, // Up arrow.
|
||||
63232, // Safari up arrow.
|
||||
40, // Down arrow.
|
||||
63233 // Safari down arrow.
|
||||
];
|
||||
var leftRightKeys = [
|
||||
37, // Left arrow.
|
||||
63234, // Safari left arrow.
|
||||
39, // Right arrow.
|
||||
63235 // Safari right arrow.
|
||||
];
|
||||
|
||||
// Respond to an enter key press. Prevent the bubbling of the enter key
|
||||
// press to the button group parent element.
|
||||
if (event.keyCode === 13) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Only take action when a direction key is pressed.
|
||||
if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
|
||||
var view = this;
|
||||
var $target = $(event.currentTarget);
|
||||
var $button = $target.parent();
|
||||
var $container = $button.parent();
|
||||
var $group = $button.closest('.ckeditor-toolbar-group');
|
||||
var $row = $button.closest('.ckeditor-row');
|
||||
var containerType = $container.data('drupal-ckeditor-button-sorting');
|
||||
var $availableButtons = this.$el.find('[data-drupal-ckeditor-button-sorting="source"]');
|
||||
var $activeButtons = this.$el.find('.ckeditor-toolbar-active');
|
||||
// The current location of the button, just in case it needs to be put
|
||||
// back.
|
||||
var $originalGroup = $group;
|
||||
var dir;
|
||||
|
||||
// Move available buttons between their container and the active
|
||||
// toolbar.
|
||||
if (containerType === 'source') {
|
||||
// Move the button to the active toolbar configuration when the down
|
||||
// or up keys are pressed.
|
||||
if (_.indexOf([40, 63233], event.keyCode) > -1) {
|
||||
// Move the button to the first row, first button group index
|
||||
// position.
|
||||
$activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
|
||||
}
|
||||
}
|
||||
else if (containerType === 'target') {
|
||||
// Move buttons between sibling buttons in a group and between groups.
|
||||
if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
|
||||
// Move left.
|
||||
var $siblings = $container.children();
|
||||
var index = $siblings.index($button);
|
||||
if (_.indexOf([37, 63234], event.keyCode) > -1) {
|
||||
// Move between sibling buttons.
|
||||
if (index > 0) {
|
||||
$button.insertBefore($container.children().eq(index - 1));
|
||||
}
|
||||
// Move between button groups and rows.
|
||||
else {
|
||||
// Move between button groups.
|
||||
$group = $container.parent().prev();
|
||||
if ($group.length > 0) {
|
||||
$group.find('.ckeditor-toolbar-group-buttons').append($button);
|
||||
}
|
||||
// Wrap between rows.
|
||||
else {
|
||||
$container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-group').not('.placeholder').find('.ckeditor-toolbar-group-buttons').eq(-1).append($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move right.
|
||||
else if (_.indexOf([39, 63235], event.keyCode) > -1) {
|
||||
// Move between sibling buttons.
|
||||
if (index < ($siblings.length - 1)) {
|
||||
$button.insertAfter($container.children().eq(index + 1));
|
||||
}
|
||||
// Move between button groups. Moving right at the end of a row
|
||||
// will create a new group.
|
||||
else {
|
||||
$container.parent().next().find('.ckeditor-toolbar-group-buttons').prepend($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move buttons between rows and the available button set.
|
||||
else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
|
||||
dir = (_.indexOf([38, 63232], event.keyCode) > -1) ? 'prev' : 'next';
|
||||
$row = $container.closest('.ckeditor-row')[dir]();
|
||||
// Move the button back into the available button set.
|
||||
if (dir === 'prev' && $row.length === 0) {
|
||||
// If this is a divider, just destroy it.
|
||||
if ($button.data('drupal-ckeditor-type') === 'separator') {
|
||||
$button
|
||||
.off()
|
||||
.remove();
|
||||
// Focus on the first button in the active toolbar.
|
||||
$activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).children().eq(0).children().trigger('focus');
|
||||
}
|
||||
// Otherwise, move it.
|
||||
else {
|
||||
$availableButtons.prepend($button);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$row.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move dividers between their container and the active toolbar.
|
||||
else if (containerType === 'dividers') {
|
||||
// Move the button to the active toolbar configuration when the down
|
||||
// or up keys are pressed.
|
||||
if (_.indexOf([40, 63233], event.keyCode) > -1) {
|
||||
// Move the button to the first row, first button group index
|
||||
// position.
|
||||
$button = $button.clone(true);
|
||||
$activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
|
||||
$target = $button.children();
|
||||
}
|
||||
}
|
||||
|
||||
view = this;
|
||||
// Attempt to move the button to the new toolbar position.
|
||||
Drupal.ckeditor.registerButtonMove(this, $button, function (result) {
|
||||
|
||||
// Put the button back if the registration failed.
|
||||
// If the button was in a row, then it was in the active toolbar
|
||||
// configuration. The button was probably placed in a new group, but
|
||||
// that action was canceled.
|
||||
if (!result && $originalGroup) {
|
||||
$originalGroup.find('.ckeditor-buttons').append($button);
|
||||
}
|
||||
// Otherwise refresh the sortables to acknowledge the new button
|
||||
// positions.
|
||||
else {
|
||||
view.$el.find('.ui-sortable').sortable('refresh');
|
||||
}
|
||||
// Refocus the target button so that the user can continue from a
|
||||
// known place.
|
||||
$target.trigger('focus');
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keypresses on a CKEditor configuration group.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The keypress event triggered.
|
||||
*/
|
||||
onPressGroup: function (event) {
|
||||
var upDownKeys = [
|
||||
38, // Up arrow.
|
||||
63232, // Safari up arrow.
|
||||
40, // Down arrow.
|
||||
63233 // Safari down arrow.
|
||||
];
|
||||
var leftRightKeys = [
|
||||
37, // Left arrow.
|
||||
63234, // Safari left arrow.
|
||||
39, // Right arrow.
|
||||
63235 // Safari right arrow.
|
||||
];
|
||||
|
||||
// Respond to an enter key press.
|
||||
if (event.keyCode === 13) {
|
||||
var view = this;
|
||||
// Open the group renaming dialog in the next evaluation cycle so that
|
||||
// this event can be cancelled and the bubbling wiped out. Otherwise,
|
||||
// Firefox has issues because the page focus is shifted to the dialog
|
||||
// along with the keydown event.
|
||||
window.setTimeout(function () {
|
||||
Drupal.ckeditor.openGroupNameDialog(view, $(event.currentTarget));
|
||||
}, 0);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Respond to direction key presses.
|
||||
if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
|
||||
var $group = $(event.currentTarget);
|
||||
var $container = $group.parent();
|
||||
var $siblings = $container.children();
|
||||
var index;
|
||||
var dir;
|
||||
// Move groups between sibling groups.
|
||||
if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
|
||||
index = $siblings.index($group);
|
||||
// Move left between sibling groups.
|
||||
if ((_.indexOf([37, 63234], event.keyCode) > -1)) {
|
||||
if (index > 0) {
|
||||
$group.insertBefore($siblings.eq(index - 1));
|
||||
}
|
||||
// Wrap between rows. Insert the group before the placeholder group
|
||||
// at the end of the previous row.
|
||||
else {
|
||||
$group.insertBefore($container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-groups').children().eq(-1));
|
||||
}
|
||||
}
|
||||
// Move right between sibling groups.
|
||||
else if (_.indexOf([39, 63235], event.keyCode) > -1) {
|
||||
// Move to the right if the next group is not a placeholder.
|
||||
if (!$siblings.eq(index + 1).hasClass('placeholder')) {
|
||||
$group.insertAfter($container.children().eq(index + 1));
|
||||
}
|
||||
// Wrap group between rows.
|
||||
else {
|
||||
$container.closest('.ckeditor-row').next().find('.ckeditor-toolbar-groups').prepend($group);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Move groups between rows.
|
||||
else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
|
||||
dir = (_.indexOf([38, 63232], event.keyCode) > -1) ? 'prev' : 'next';
|
||||
$group.closest('.ckeditor-row')[dir]().find('.ckeditor-toolbar-groups').eq(0).prepend($group);
|
||||
}
|
||||
|
||||
Drupal.ckeditor.registerGroupMove(this, $group);
|
||||
$group.trigger('focus');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, Backbone, _);
|
273
web/core/modules/ckeditor/js/views/VisualView.js
Normal file
273
web/core/modules/ckeditor/js/views/VisualView.js
Normal file
|
@ -0,0 +1,273 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the visual UX view of CKEditor toolbar
|
||||
* configuration.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor.VisualView = Backbone.View.extend(/** @lends Drupal.ckeditor.VisualView# */{
|
||||
|
||||
events: {
|
||||
'click .ckeditor-toolbar-group-name': 'onGroupNameClick',
|
||||
'click .ckeditor-groupnames-toggle': 'onGroupNamesToggleClick',
|
||||
'click .ckeditor-add-new-group button': 'onAddGroupButtonClick'
|
||||
},
|
||||
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; visual UX.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
this.listenTo(this.model, 'change:isDirty change:groupNamesVisible', this.render);
|
||||
|
||||
// Add a toggle for the button group names.
|
||||
$(Drupal.theme('ckeditorButtonGroupNamesToggle'))
|
||||
.prependTo(this.$el.find('#ckeditor-active-toolbar').parent());
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* Render function for rendering the toolbar configuration.
|
||||
*
|
||||
* @param {*} model
|
||||
* Model used for the view.
|
||||
* @param {string} [value]
|
||||
* The value that was changed.
|
||||
* @param {object} changedAttributes
|
||||
* The attributes that was changed.
|
||||
*
|
||||
* @return {Drupal.ckeditor.VisualView}
|
||||
* The {@link Drupal.ckeditor.VisualView} object.
|
||||
*/
|
||||
render: function (model, value, changedAttributes) {
|
||||
this.insertPlaceholders();
|
||||
this.applySorting();
|
||||
|
||||
// Toggle button group names.
|
||||
var groupNamesVisible = this.model.get('groupNamesVisible');
|
||||
// If a button was just placed in the active toolbar, ensure that the
|
||||
// button group names are visible.
|
||||
if (changedAttributes && changedAttributes.changes && changedAttributes.changes.isDirty) {
|
||||
this.model.set({groupNamesVisible: true}, {silent: true});
|
||||
groupNamesVisible = true;
|
||||
}
|
||||
this.$el.find('[data-toolbar="active"]').toggleClass('ckeditor-group-names-are-visible', groupNamesVisible);
|
||||
this.$el.find('.ckeditor-groupnames-toggle')
|
||||
.text((groupNamesVisible) ? Drupal.t('Hide group names') : Drupal.t('Show group names'))
|
||||
.attr('aria-pressed', groupNamesVisible);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles clicks to a button group name.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event on the button group.
|
||||
*/
|
||||
onGroupNameClick: function (event) {
|
||||
var $group = $(event.currentTarget).closest('.ckeditor-toolbar-group');
|
||||
Drupal.ckeditor.openGroupNameDialog(this, $group);
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles clicks on the button group names toggle button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event on the toggle button.
|
||||
*/
|
||||
onGroupNamesToggleClick: function (event) {
|
||||
this.model.set('groupNamesVisible', !this.model.get('groupNamesVisible'));
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the user to provide a name for a new button group; inserts it.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event of the button click.
|
||||
*/
|
||||
onAddGroupButtonClick: function (event) {
|
||||
|
||||
/**
|
||||
* Inserts a new button if the openGroupNameDialog function returns true.
|
||||
*
|
||||
* @param {bool} success
|
||||
* A flag that indicates if the user created a new group (true) or
|
||||
* canceled out of the dialog (false).
|
||||
* @param {jQuery} $group
|
||||
* A jQuery DOM fragment that represents the new button group. It has
|
||||
* not been added to the DOM yet.
|
||||
*/
|
||||
function insertNewGroup(success, $group) {
|
||||
if (success) {
|
||||
$group.appendTo($(event.currentTarget).closest('.ckeditor-row').children('.ckeditor-toolbar-groups'));
|
||||
// Focus on the new group.
|
||||
$group.trigger('focus');
|
||||
}
|
||||
}
|
||||
|
||||
// Pass in a DOM fragment of a placeholder group so that the new group
|
||||
// name can be applied to it.
|
||||
Drupal.ckeditor.openGroupNameDialog(this, $(Drupal.theme('ckeditorToolbarGroup')), insertNewGroup);
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable stop sort of a button group.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the group drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
endGroupDrag: function (event, ui) {
|
||||
var view = this;
|
||||
Drupal.ckeditor.registerGroupMove(this, ui.item, function (success) {
|
||||
if (!success) {
|
||||
// Cancel any sorting in the configuration area.
|
||||
view.$el.find('.ckeditor-toolbar-configuration').find('.ui-sortable').sortable('cancel');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable start sort of a button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the group drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
startButtonDrag: function (event, ui) {
|
||||
this.$el.find('a:focus').trigger('blur');
|
||||
|
||||
// Show the button group names as soon as the user starts dragging.
|
||||
this.model.set('groupNamesVisible', true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable stop sort of a button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the button drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
endButtonDrag: function (event, ui) {
|
||||
var view = this;
|
||||
Drupal.ckeditor.registerButtonMove(this, ui.item, function (success) {
|
||||
if (!success) {
|
||||
// Cancel any sorting in the configuration area.
|
||||
view.$el.find('.ui-sortable').sortable('cancel');
|
||||
}
|
||||
// Refocus the target button so that the user can continue from a known
|
||||
// place.
|
||||
ui.item.find('a').trigger('focus');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invokes jQuery.sortable() on new buttons and groups in a CKEditor config.
|
||||
*/
|
||||
applySorting: function () {
|
||||
// Make the buttons sortable.
|
||||
this.$el.find('.ckeditor-buttons').not('.ui-sortable').sortable({
|
||||
// Change this to .ckeditor-toolbar-group-buttons.
|
||||
connectWith: '.ckeditor-buttons',
|
||||
placeholder: 'ckeditor-button-placeholder',
|
||||
forcePlaceholderSize: true,
|
||||
tolerance: 'pointer',
|
||||
cursor: 'move',
|
||||
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',
|
||||
cancel: '.ckeditor-add-new-group',
|
||||
placeholder: 'ckeditor-toolbar-group-placeholder',
|
||||
forcePlaceholderSize: true,
|
||||
cursor: 'move',
|
||||
stop: this.endGroupDrag.bind(this)
|
||||
});
|
||||
|
||||
// Add the drag and drop functionality to buttons.
|
||||
this.$el.find('.ckeditor-multiple-buttons li').draggable({
|
||||
connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons',
|
||||
helper: 'clone'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps the invocation of methods to insert blank groups and rows.
|
||||
*/
|
||||
insertPlaceholders: function () {
|
||||
this.insertPlaceholderRow();
|
||||
this.insertNewGroupButtons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a blank row at the bottom of the CKEditor configuration.
|
||||
*/
|
||||
insertPlaceholderRow: function () {
|
||||
var $rows = this.$el.find('.ckeditor-row');
|
||||
// Add a placeholder row. to the end of the list if one does not exist.
|
||||
if (!$rows.eq(-1).hasClass('placeholder')) {
|
||||
this.$el
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.children('.ckeditor-active-toolbar-configuration')
|
||||
.append(Drupal.theme('ckeditorRow'));
|
||||
}
|
||||
// Update the $rows variable to include the new row.
|
||||
$rows = this.$el.find('.ckeditor-row');
|
||||
// Remove blank rows except the last one.
|
||||
var len = $rows.length;
|
||||
$rows.filter(function (index, row) {
|
||||
// Do not remove the last row.
|
||||
if (index + 1 === len) {
|
||||
return false;
|
||||
}
|
||||
return $(row).find('.ckeditor-toolbar-group').not('.placeholder').length === 0;
|
||||
})
|
||||
// Then get all rows that are placeholders and remove them.
|
||||
.remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a button in each row that will add a new CKEditor button group.
|
||||
*/
|
||||
insertNewGroupButtons: function () {
|
||||
// Insert an add group button to each row.
|
||||
this.$el.find('.ckeditor-row').each(function () {
|
||||
var $row = $(this);
|
||||
var $groups = $row.find('.ckeditor-toolbar-group');
|
||||
var $button = $row.find('.ckeditor-add-new-group');
|
||||
if ($button.length === 0) {
|
||||
$row.children('.ckeditor-toolbar-groups').append(Drupal.theme('ckeditorNewButtonGroup'));
|
||||
}
|
||||
// If a placeholder group exists, make sure it's at the end of the row.
|
||||
else if (!$groups.eq(-1).hasClass('ckeditor-add-new-group')) {
|
||||
$button.appendTo($row.children('.ckeditor-toolbar-groups'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, jQuery);
|
40
web/core/modules/ckeditor/src/Annotation/CKEditorPlugin.php
Normal file
40
web/core/modules/ckeditor/src/Annotation/CKEditorPlugin.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
|
||||
/**
|
||||
* Defines a CKEditorPlugin annotation object.
|
||||
*
|
||||
* Plugin Namespace: Plugin\CKEditorPlugin
|
||||
*
|
||||
* For a working example, see \Drupal\ckeditor\Plugin\CKEditorPlugin\DrupalImage
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginBase
|
||||
* @see \Drupal\ckeditor\CKEditorPluginManager
|
||||
* @see hook_ckeditor_plugin_info_alter()
|
||||
* @see plugin_api
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class CKEditorPlugin extends Plugin {
|
||||
|
||||
/**
|
||||
* The plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* The human-readable name of the CKEditor plugin.
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*/
|
||||
public $label;
|
||||
|
||||
}
|
54
web/core/modules/ckeditor/src/CKEditorPluginBase.php
Normal file
54
web/core/modules/ckeditor/src/CKEditorPluginBase.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor;
|
||||
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines a base CKEditor plugin implementation.
|
||||
*
|
||||
* No other CKEditor plugins can be internal, unless a different CKEditor build
|
||||
* than the one provided by Drupal core is used. Most CKEditor plugins don't
|
||||
* need to provide additional settings forms.
|
||||
*
|
||||
* This base class assumes that your plugin has buttons that you want to be
|
||||
* enabled through the toolbar builder UI. It is still possible to also
|
||||
* implement the CKEditorPluginContextualInterface (for contextual enabling) and
|
||||
* CKEditorPluginConfigurableInterface interfaces (for configuring plugin
|
||||
* settings).
|
||||
*
|
||||
* NOTE: the Drupal plugin ID should correspond to the CKEditor plugin name.
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginManager
|
||||
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
|
||||
* @see plugin_api
|
||||
*/
|
||||
abstract class CKEditorPluginBase extends PluginBase implements CKEditorPluginInterface, CKEditorPluginButtonsInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function isInternal() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getDependencies(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getLibraries(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor;
|
||||
|
||||
/**
|
||||
* Defines an interface for CKEditor plugins with buttons.
|
||||
*
|
||||
* This allows a CKEditor plugin to define which buttons it provides, so that
|
||||
* users can configure a CKEditor toolbar instance via the toolbar builder UI.
|
||||
* If at least one button that this plugin provides is added to the toolbar via
|
||||
* the toolbar builder UI, then this plugin will be enabled automatically.
|
||||
*
|
||||
* If a CKEditor plugin implements this interface, it can still also implement
|
||||
* CKEditorPluginContextualInterface if it wants a button to conditionally be
|
||||
* added as well. The downside of conditionally adding buttons is that the user
|
||||
* cannot see these buttons in the toolbar builder UI.
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginBase
|
||||
* @see \Drupal\ckeditor\CKEditorPluginManager
|
||||
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface CKEditorPluginButtonsInterface extends CKEditorPluginInterface {
|
||||
|
||||
/**
|
||||
* Returns the buttons that this plugin provides, along with metadata.
|
||||
*
|
||||
* The metadata is used by the CKEditor module to generate a visual CKEditor
|
||||
* toolbar builder UI.
|
||||
*
|
||||
* @return array
|
||||
* An array of buttons that are provided by this plugin. This will
|
||||
* only be used in the administrative section for assembling the toolbar.
|
||||
* Each button should by keyed by its CKEditor button name, and should
|
||||
* contain an array of button properties, including:
|
||||
* - label: A human-readable, translated button name.
|
||||
* - image: An image for the button to be used in the toolbar.
|
||||
* - image_rtl: If the image needs to have a right-to-left version, specify
|
||||
* an alternative file that will be used in RTL editors.
|
||||
* - image_alternative: If this button does not render as an image, specify
|
||||
* an HTML string representing the contents of this button.
|
||||
* - image_alternative_rtl: Similar to image_alternative, but a
|
||||
* right-to-left version.
|
||||
* - attributes: An array of HTML attributes which should be added to this
|
||||
* button when rendering the button in the administrative section for
|
||||
* assembling the toolbar.
|
||||
* - multiple: Boolean value indicating if this button may be added multiple
|
||||
* times to the toolbar. This typically is only applicable for dividers
|
||||
* and group indicators.
|
||||
*/
|
||||
public function getButtons();
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines an interface for configurable CKEditor plugins.
|
||||
*
|
||||
* This allows a CKEditor plugin to define a settings form. These settings can
|
||||
* then be automatically passed on to the corresponding CKEditor instance via
|
||||
* CKEditorPluginInterface::getConfig().
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginBase
|
||||
* @see \Drupal\ckeditor\CKEditorPluginManager
|
||||
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface CKEditorPluginConfigurableInterface extends CKEditorPluginInterface {
|
||||
|
||||
/**
|
||||
* Returns a settings form to configure this CKEditor plugin.
|
||||
*
|
||||
* If the plugin's behavior depends on extensive options and/or external data,
|
||||
* then the implementing module can choose to provide a separate, global
|
||||
* configuration page rather than per-text-editor settings. In that case, this
|
||||
* form should provide a link to the separate settings page.
|
||||
*
|
||||
* @param array $form
|
||||
* An empty form array to be populated with a configuration form, if any.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The state of the entire filter administration form.
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* A render array for the settings form.
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor);
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines an interface for contextually enabled CKEditor plugins.
|
||||
*
|
||||
* Contextually enabled CKEditor plugins can be enabled via an explicit setting,
|
||||
* or enable themselves based on the configuration of another setting, such as
|
||||
* enabling based on a particular button being present in the toolbar.
|
||||
*
|
||||
* If a contextually enabled CKEditor plugin must also be configurable (for
|
||||
* instance, in the case where it must be enabled based on an explicit setting),
|
||||
* then one must also implement the CKEditorPluginConfigurableInterface
|
||||
* interface.
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginBase
|
||||
* @see \Drupal\ckeditor\CKEditorPluginManager
|
||||
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface CKEditorPluginContextualInterface extends CKEditorPluginInterface {
|
||||
|
||||
/**
|
||||
* Checks if this plugin should be enabled based on the editor configuration.
|
||||
*
|
||||
* The editor's settings can be retrieved via $editor->getSettings().
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled(Editor $editor);
|
||||
|
||||
}
|
39
web/core/modules/ckeditor/src/CKEditorPluginCssInterface.php
Normal file
39
web/core/modules/ckeditor/src/CKEditorPluginCssInterface.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines an interface for CKEditor plugins with associated CSS.
|
||||
*
|
||||
* This allows a CKEditor plugin to add additional CSS in iframe CKEditor
|
||||
* instances without needing to implement hook_ckeditor_css_alter().
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginBase
|
||||
* @see \Drupal\ckeditor\CKEditorPluginManager
|
||||
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface CKEditorPluginCssInterface extends CKEditorPluginInterface {
|
||||
|
||||
/**
|
||||
* Retrieves enabled plugins' iframe instance CSS files.
|
||||
*
|
||||
* Note: this does not use a Drupal asset library because this CSS will be
|
||||
* loaded by CKEditor, not by Drupal.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of CSS files. This is a flat list of file paths relative to
|
||||
* the Drupal root.
|
||||
*/
|
||||
public function getCssFiles(Editor $editor);
|
||||
|
||||
}
|
99
web/core/modules/ckeditor/src/CKEditorPluginInterface.php
Normal file
99
web/core/modules/ckeditor/src/CKEditorPluginInterface.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines an interface for (loading of) CKEditor plugins.
|
||||
*
|
||||
* This is the most basic CKEditor plugin interface; it provides the bare
|
||||
* minimum information. Solely implementing this interface is not sufficient to
|
||||
* be able to enable the plugin though — a CKEditor plugin can either be enabled
|
||||
* automatically when a button it provides is present in the toolbar, or when
|
||||
* some programmatically defined condition is true. In the former case,
|
||||
* implement the CKEditorPluginButtonsInterface interface, in the latter case,
|
||||
* implement the CKEditorPluginContextualInterface interface. It is also
|
||||
* possible to implement both, for advanced use cases.
|
||||
*
|
||||
* Finally, if your plugin must be configurable, you can also implement the
|
||||
* CKEditorPluginConfigurableInterface interface.
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginBase
|
||||
* @see \Drupal\ckeditor\CKEditorPluginManager
|
||||
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface CKEditorPluginInterface extends PluginInspectionInterface {
|
||||
|
||||
/**
|
||||
* Indicates if this plugin is part of the optimized CKEditor build.
|
||||
*
|
||||
* Plugins marked as internal are implicitly loaded as part of CKEditor.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInternal();
|
||||
|
||||
/**
|
||||
* Returns a list of plugins this plugin requires.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
* @return array
|
||||
* An unindexed array of plugin names this plugin requires. Each plugin is
|
||||
* is identified by its annotated ID.
|
||||
*/
|
||||
public function getDependencies(Editor $editor);
|
||||
|
||||
/**
|
||||
* Returns a list of libraries this plugin requires.
|
||||
*
|
||||
* These libraries will be attached to the text_format element on which the
|
||||
* editor is being loaded.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
* @return array
|
||||
* An array of libraries suitable for usage in a render API #attached
|
||||
* property.
|
||||
*/
|
||||
public function getLibraries(Editor $editor);
|
||||
|
||||
/**
|
||||
* Returns the Drupal root-relative file path to the plugin JavaScript file.
|
||||
*
|
||||
* Note: this does not use a Drupal library because this uses CKEditor's API,
|
||||
* see http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.resourceManager.html#addExternal.
|
||||
*
|
||||
* @return string|false
|
||||
* The Drupal root-relative path to the file, FALSE if an internal plugin.
|
||||
*/
|
||||
public function getFile();
|
||||
|
||||
/**
|
||||
* Returns the additions to CKEDITOR.config for a specific CKEditor instance.
|
||||
*
|
||||
* The editor's settings can be retrieved via $editor->getSettings(), 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
|
||||
* @code
|
||||
* $settings = $editor->getSettings();
|
||||
* $plugin_specific_settings = $settings['plugins'][$plugin_id];
|
||||
* @endcode
|
||||
*
|
||||
* @param \Drupal\editor\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);
|
||||
|
||||
}
|
221
web/core/modules/ckeditor/src/CKEditorPluginManager.php
Normal file
221
web/core/modules/ckeditor/src/CKEditorPluginManager.php
Normal file
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Provides a CKEditor Plugin plugin manager.
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
|
||||
* @see \Drupal\ckeditor\CKEditorPluginBase
|
||||
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
|
||||
* @see plugin_api
|
||||
*/
|
||||
class CKEditorPluginManager extends DefaultPluginManager {
|
||||
|
||||
/**
|
||||
* Constructs a CKEditorPluginManager object.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke the alter hook with.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/CKEditorPlugin', $namespaces, $module_handler, 'Drupal\ckeditor\CKEditorPluginInterface', 'Drupal\ckeditor\Annotation\CKEditorPlugin');
|
||||
$this->alterInfo('ckeditor_plugin_info');
|
||||
$this->setCacheBackend($cache_backend, 'ckeditor_plugins');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves enabled plugins' files, keyed by plugin ID.
|
||||
*
|
||||
* 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\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 getEnabledPluginFiles(Editor $editor, $include_internal_plugins = FALSE) {
|
||||
$plugins = array_keys($this->getDefinitions());
|
||||
$toolbar_buttons = $this->getEnabledButtons($editor);
|
||||
$enabled_plugins = array();
|
||||
$additional_plugins = array();
|
||||
|
||||
foreach ($plugins as $plugin_id) {
|
||||
$plugin = $this->createInstance($plugin_id);
|
||||
|
||||
if (!$include_internal_plugins && $plugin->isInternal()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$enabled = FALSE;
|
||||
// Enable this plugin if it provides a button that has been enabled.
|
||||
if ($plugin instanceof CKEditorPluginButtonsInterface) {
|
||||
$plugin_buttons = array_keys($plugin->getButtons());
|
||||
$enabled = (count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0);
|
||||
}
|
||||
// Otherwise enable this plugin if it declares itself as enabled.
|
||||
if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) {
|
||||
$enabled = $plugin->isEnabled($editor);
|
||||
}
|
||||
|
||||
if ($enabled) {
|
||||
$enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile();
|
||||
// Check if this plugin has dependencies that also need to be enabled.
|
||||
$additional_plugins = array_merge($additional_plugins, array_diff($plugin->getDependencies($editor), $additional_plugins));
|
||||
}
|
||||
}
|
||||
|
||||
// Add the list of dependent plugins.
|
||||
foreach ($additional_plugins as $plugin_id) {
|
||||
$plugin = $this->createInstance($plugin_id);
|
||||
$enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile();
|
||||
}
|
||||
|
||||
// Always return plugins in the same order.
|
||||
asort($enabled_plugins);
|
||||
|
||||
return $enabled_plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the enabled toolbar buttons in the given text editor instance.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return string[]
|
||||
* A list of the toolbar buttons enabled in the given text editor instance.
|
||||
*/
|
||||
public static function getEnabledButtons(Editor $editor) {
|
||||
$toolbar_rows = [];
|
||||
$settings = $editor->getSettings();
|
||||
foreach ($settings['toolbar']['rows'] as $row_number => $row) {
|
||||
$toolbar_rows[] = array_reduce($settings['toolbar']['rows'][$row_number], function (&$result, $button_group) {
|
||||
return array_merge($result, $button_group['items']);
|
||||
}, []);
|
||||
}
|
||||
return array_unique(NestedArray::mergeDeepArray($toolbar_rows));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all available CKEditor buttons, keyed by plugin ID.
|
||||
*
|
||||
* @return array
|
||||
* All available CKEditor buttons, with plugin IDs as keys and button
|
||||
* metadata (as implemented by getButtons()) as values.
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface::getButtons()
|
||||
*/
|
||||
public function getButtons() {
|
||||
$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 \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*/
|
||||
public function injectPluginSettingsForm(array &$form, FormStateInterface $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'],
|
||||
'#open' => TRUE,
|
||||
'#group' => 'editor][settings][plugin_settings',
|
||||
'#attributes' => array(
|
||||
'data-ckeditor-plugin-id' => $plugin_id,
|
||||
),
|
||||
);
|
||||
// Provide enough metadata for the drupal.ckeditor.admin library to
|
||||
// allow it to automatically show/hide the vertical tab containing the
|
||||
// settings for this plugin. Only do this if it's a CKEditor plugin that
|
||||
// just provides buttons, don't do this if it's a contextually enabled
|
||||
// CKEditor plugin. After all, in the latter case, we can't know when
|
||||
// its settings should be shown!
|
||||
if ($plugin instanceof CKEditorPluginButtonsInterface && !$plugin instanceof CKEditorPluginContextualInterface) {
|
||||
$form['plugins'][$plugin_id]['#attributes']['data-ckeditor-buttons'] = implode(' ', array_keys($plugin->getButtons()));
|
||||
}
|
||||
$form['plugins'][$plugin_id] += $plugin->settingsForm($plugin_settings_form, $form_state, $editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves enabled plugins' iframe instance CSS files, keyed by plugin ID.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return string[]
|
||||
* Enabled plugins CKEditor CSS files, with plugin IDs as keys and CSS file
|
||||
* paths relative to the Drupal root (as implemented by getCssFiles()) as
|
||||
* values.
|
||||
*
|
||||
* @see \Drupal\ckeditor\CKEditorPluginCssInterface::getCssFiles()
|
||||
*/
|
||||
public function getCssFiles(Editor $editor) {
|
||||
$enabled_plugins = array_keys($this->getEnabledPluginFiles($editor, TRUE));
|
||||
$css_files = array();
|
||||
|
||||
foreach ($enabled_plugins as $plugin_id) {
|
||||
$plugin = $this->createInstance($plugin_id);
|
||||
if ($plugin instanceof CKEditorPluginCssInterface) {
|
||||
$css_files[$plugin_id] = $plugin->getCssFiles($editor);
|
||||
}
|
||||
}
|
||||
|
||||
return $css_files;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginBase;
|
||||
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines the "drupalimage" plugin.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "drupalimage",
|
||||
* label = @Translation("Image"),
|
||||
* module = "ckeditor"
|
||||
* )
|
||||
*/
|
||||
class DrupalImage extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/plugin.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(Editor $editor) {
|
||||
return array(
|
||||
'core/drupal.ajax',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
return array(
|
||||
'drupalImage_dialogTitleAdd' => t('Insert Image'),
|
||||
'drupalImage_dialogTitleEdit' => t('Edit Image'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getButtons() {
|
||||
return array(
|
||||
'DrupalImage' => array(
|
||||
'label' => t('Image'),
|
||||
'image' => drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/icons/drupalimage.png',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see \Drupal\editor\Form\EditorImageDialog
|
||||
* @see editor_image_upload_settings_form()
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
|
||||
$form_state->loadInclude('editor', 'admin.inc');
|
||||
$form['image_upload'] = editor_image_upload_settings_form($editor);
|
||||
$form['image_upload']['#attached']['library'][] = 'ckeditor/drupal.ckeditor.drupalimage.admin';
|
||||
$form['image_upload']['#element_validate'][] = array($this, 'validateImageUploadSettings');
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* #element_validate handler for the "image_upload" element in settingsForm().
|
||||
*
|
||||
* Moves the text editor's image upload settings from the DrupalImage plugin's
|
||||
* own settings into $editor->image_upload.
|
||||
*
|
||||
* @see \Drupal\editor\Form\EditorImageDialog
|
||||
* @see editor_image_upload_settings_form()
|
||||
*/
|
||||
function validateImageUploadSettings(array $element, FormStateInterface $form_state) {
|
||||
$settings = &$form_state->getValue(array('editor', 'settings', 'plugins', 'drupalimage', 'image_upload'));
|
||||
$form_state->get('editor')->setImageUploadSettings($settings);
|
||||
$form_state->unsetValue(array('editor', 'settings', 'plugins', 'drupalimage'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\ckeditor\CKEditorPluginInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginCssInterface;
|
||||
|
||||
/**
|
||||
* Defines the "drupalimagecaption" plugin.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "drupalimagecaption",
|
||||
* label = @Translation("Drupal image caption widget"),
|
||||
* module = "ckeditor"
|
||||
* )
|
||||
*/
|
||||
class DrupalImageCaption extends PluginBase implements CKEditorPluginInterface, CKEditorPluginContextualInterface, CKEditorPluginCssInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isInternal() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDependencies(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(Editor $editor) {
|
||||
return array(
|
||||
'ckeditor/drupal.ckeditor.plugins.drupalimagecaption',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimagecaption/plugin.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
$format = $editor->getFilterFormat();
|
||||
return array(
|
||||
'image2_captionedClass' => 'caption caption-img',
|
||||
'image2_alignClasses' => array('align-left', 'align-center', 'align-right'),
|
||||
'drupalImageCaption_captionPlaceholderText' => t('Enter caption here'),
|
||||
// Only enable those parts of DrupalImageCaption for which the
|
||||
// corresponding Drupal text filters are enabled.
|
||||
'drupalImageCaption_captionFilterEnabled' => $format->filters('filter_caption')->status,
|
||||
'drupalImageCaption_alignFilterEnabled' => $format->filters('filter_align')->status,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCssFiles(Editor $editor) {
|
||||
return array(
|
||||
drupal_get_path('module', 'ckeditor') . '/css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function isEnabled(Editor $editor) {
|
||||
if (!$editor->hasAssociatedFilterFormat()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Automatically enable this plugin if the text format associated with this
|
||||
// text editor uses the filter_align or filter_caption filter and the
|
||||
// DrupalImage button is enabled.
|
||||
$format = $editor->getFilterFormat();
|
||||
if ($format->filters('filter_align')->status || $format->filters('filter_caption')->status) {
|
||||
$enabled = FALSE;
|
||||
$settings = $editor->getSettings();
|
||||
foreach ($settings['toolbar']['rows'] as $row) {
|
||||
foreach ($row as $group) {
|
||||
foreach ($group['items'] as $button) {
|
||||
if ($button === 'DrupalImage') {
|
||||
$enabled = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines the "drupallink" plugin.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "drupallink",
|
||||
* label = @Translation("Drupal link"),
|
||||
* module = "ckeditor"
|
||||
* )
|
||||
*/
|
||||
class DrupalLink extends CKEditorPluginBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupallink/plugin.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(Editor $editor) {
|
||||
return array(
|
||||
'core/drupal.ajax',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
return array(
|
||||
'drupalLink_dialogTitleAdd' => t('Add Link'),
|
||||
'drupalLink_dialogTitleEdit' => t('Edit Link'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getButtons() {
|
||||
$path = drupal_get_path('module', 'ckeditor') . '/js/plugins/drupallink';
|
||||
return array(
|
||||
'DrupalLink' => array(
|
||||
'label' => t('Link'),
|
||||
'image' => $path . '/icons/drupallink.png',
|
||||
),
|
||||
'DrupalUnlink' => array(
|
||||
'label' => t('Unlink'),
|
||||
'image' => $path . '/icons/drupalunlink.png',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
607
web/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
Normal file
607
web/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
Normal file
|
@ -0,0 +1,607 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginBase;
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginManager;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines the "internal" plugin (i.e. core plugins part of our CKEditor build).
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "internal",
|
||||
* label = @Translation("CKEditor core")
|
||||
* )
|
||||
*/
|
||||
class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInterface, CKEditorPluginContextualInterface {
|
||||
|
||||
/**
|
||||
* The cache backend.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheBackendInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* Constructs a \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* The cache backend.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, CacheBackendInterface $cache_backend) {
|
||||
$this->cache = $cache_backend;
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of the plugin.
|
||||
*
|
||||
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
|
||||
* The container to pull out services used in the plugin.
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
*
|
||||
* @return static
|
||||
* Returns an instance of this plugin.
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('cache.default')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isInternal() {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEnabled(Editor $editor) {
|
||||
// This plugin represents the core CKEditor plugins. They're always enabled:
|
||||
// its configuration is always necessary.
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFile() {
|
||||
// This plugin is already part of Drupal core's CKEditor build.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
// Reasonable defaults that provide expected basic behavior.
|
||||
$config = array(
|
||||
'customConfig' => '', // Don't load CKEditor's config.js file.
|
||||
'pasteFromWordPromptCleanup' => TRUE,
|
||||
'resize_dir' => 'vertical',
|
||||
'justifyClasses' => array('text-align-left', 'text-align-center', 'text-align-right', 'text-align-justify'),
|
||||
'entities' => FALSE,
|
||||
'disableNativeSpellChecker' => FALSE,
|
||||
);
|
||||
|
||||
// Add the allowedContent setting, which ensures CKEditor only allows tags
|
||||
// and attributes that are allowed by the text format for this text editor.
|
||||
list($config['allowedContent'], $config['disallowedContent']) = $this->generateACFSettings($editor);
|
||||
|
||||
// Add the format_tags setting, if its button is enabled.
|
||||
$toolbar_buttons = CKEditorPluginManager::getEnabledButtons($editor);
|
||||
if (in_array('Format', $toolbar_buttons)) {
|
||||
$config['format_tags'] = $this->generateFormatTagsSetting($editor);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getButtons() {
|
||||
$button = function($name, $direction = 'ltr') {
|
||||
// In the markup below, we mostly use the name (which may include spaces),
|
||||
// but in one spot we use it as a CSS class, so strip spaces.
|
||||
// Note: this uses str_replace() instead of Html::cleanCssIdentifier()
|
||||
// because we must provide these class names exactly how CKEditor expects
|
||||
// them in its library, which cleanCssIdentifier() does not do.
|
||||
$class_name = str_replace(' ', '', $name);
|
||||
return [
|
||||
'#type' => 'inline_template',
|
||||
'#template' => '<a href="#" class="cke-icon-only cke_{{ direction }}" role="button" title="{{ name }}" aria-label="{{ name }}"><span class="cke_button_icon cke_button__{{ classname }}_icon">{{ name }}</span></a>',
|
||||
'#context' => [
|
||||
'direction' => $direction,
|
||||
'name' => $name,
|
||||
'classname' => $class_name,
|
||||
],
|
||||
];
|
||||
};
|
||||
|
||||
return array(
|
||||
// "basicstyles" plugin.
|
||||
'Bold' => array(
|
||||
'label' => t('Bold'),
|
||||
'image_alternative' => $button('bold'),
|
||||
'image_alternative_rtl' => $button('bold', 'rtl'),
|
||||
),
|
||||
'Italic' => array(
|
||||
'label' => t('Italic'),
|
||||
'image_alternative' => $button('italic'),
|
||||
'image_alternative_rtl' => $button('italic', 'rtl'),
|
||||
),
|
||||
'Underline' => array(
|
||||
'label' => t('Underline'),
|
||||
'image_alternative' => $button('underline'),
|
||||
'image_alternative_rtl' => $button('underline', 'rtl'),
|
||||
),
|
||||
'Strike' => array(
|
||||
'label' => t('Strike-through'),
|
||||
'image_alternative' => $button('strike'),
|
||||
'image_alternative_rtl' => $button('strike', 'rtl'),
|
||||
),
|
||||
'Superscript' => array(
|
||||
'label' => t('Superscript'),
|
||||
'image_alternative' => $button('super script'),
|
||||
'image_alternative_rtl' => $button('super script', 'rtl'),
|
||||
),
|
||||
'Subscript' => array(
|
||||
'label' => t('Subscript'),
|
||||
'image_alternative' => $button('sub script'),
|
||||
'image_alternative_rtl' => $button('sub script', 'rtl'),
|
||||
),
|
||||
// "removeformat" plugin.
|
||||
'RemoveFormat' => array(
|
||||
'label' => t('Remove format'),
|
||||
'image_alternative' => $button('remove format'),
|
||||
'image_alternative_rtl' => $button('remove format', 'rtl'),
|
||||
),
|
||||
// "justify" plugin.
|
||||
'JustifyLeft' => array(
|
||||
'label' => t('Align left'),
|
||||
'image_alternative' => $button('justify left'),
|
||||
'image_alternative_rtl' => $button('justify left', 'rtl'),
|
||||
),
|
||||
'JustifyCenter' => array(
|
||||
'label' => t('Align center'),
|
||||
'image_alternative' => $button('justify center'),
|
||||
'image_alternative_rtl' => $button('justify center', 'rtl'),
|
||||
),
|
||||
'JustifyRight' => array(
|
||||
'label' => t('Align right'),
|
||||
'image_alternative' => $button('justify right'),
|
||||
'image_alternative_rtl' => $button('justify right', 'rtl'),
|
||||
),
|
||||
'JustifyBlock' => array(
|
||||
'label' => t('Justify'),
|
||||
'image_alternative' => $button('justify block'),
|
||||
'image_alternative_rtl' => $button('justify block', 'rtl'),
|
||||
),
|
||||
// "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'),
|
||||
),
|
||||
// "blockquote" plugin.
|
||||
'Blockquote' => array(
|
||||
'label' => t('Blockquote'),
|
||||
'image_alternative' => $button('blockquote'),
|
||||
'image_alternative_rtl' => $button('blockquote', 'rtl'),
|
||||
),
|
||||
// "horizontalrule" plugin
|
||||
'HorizontalRule' => array(
|
||||
'label' => t('Horizontal rule'),
|
||||
'image_alternative' => $button('horizontal rule'),
|
||||
'image_alternative_rtl' => $button('horizontal rule', 'rtl'),
|
||||
),
|
||||
// "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'),
|
||||
'image_alternative_rtl' => $button('special char', 'rtl'),
|
||||
),
|
||||
'Format' => array(
|
||||
'label' => t('HTML block format'),
|
||||
'image_alternative' => [
|
||||
'#type' => 'inline_template',
|
||||
'#template' => '<a href="#" role="button" aria-label="{{ format_text }}"><span class="ckeditor-button-dropdown">{{ format_text }}<span class="ckeditor-button-arrow"></span></span></a>',
|
||||
'#context' => [
|
||||
'format_text' => t('Format'),
|
||||
],
|
||||
],
|
||||
),
|
||||
// "table" plugin.
|
||||
'Table' => array(
|
||||
'label' => t('Table'),
|
||||
'image_alternative' => $button('table'),
|
||||
'image_alternative_rtl' => $button('table', 'rtl'),
|
||||
),
|
||||
// "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'),
|
||||
'image_alternative_rtl' => $button('source', 'rtl'),
|
||||
),
|
||||
// "maximize" plugin.
|
||||
'Maximize' => array(
|
||||
'label' => t('Maximize'),
|
||||
'image_alternative' => $button('maximize'),
|
||||
'image_alternative_rtl' => $button('maximize', 'rtl'),
|
||||
),
|
||||
// No plugin, separator "button" for toolbar builder UI use only.
|
||||
'-' => array(
|
||||
'label' => t('Separator'),
|
||||
'image_alternative' => [
|
||||
'#type' => 'inline_template',
|
||||
'#template' => '<a href="#" role="button" aria-label="{{ button_separator_text }}" class="ckeditor-separator"></a>',
|
||||
'#context' => [
|
||||
'button_separator_text' => t('Button separator'),
|
||||
],
|
||||
],
|
||||
'attributes' => array(
|
||||
'class' => array('ckeditor-button-separator'),
|
||||
'data-drupal-ckeditor-type' => 'separator',
|
||||
),
|
||||
'multiple' => TRUE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the "format_tags" configuration part of the CKEditor JS settings.
|
||||
*
|
||||
* @see getConfig()
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* An array containing the "format_tags" configuration.
|
||||
*/
|
||||
protected function generateFormatTagsSetting(Editor $editor) {
|
||||
// When no text format is associated yet, assume no tag is allowed.
|
||||
// @see \Drupal\Editor\EditorInterface::hasAssociatedFilterFormat()
|
||||
if (!$editor->hasAssociatedFilterFormat()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$format = $editor->getFilterFormat();
|
||||
$cid = 'ckeditor_internal_format_tags:' . $format->id();
|
||||
|
||||
if ($cached = $this->cache->get($cid)) {
|
||||
$format_tags = $cached->data;
|
||||
}
|
||||
else {
|
||||
// The <p> tag is always allowed — HTML without <p> tags is nonsensical.
|
||||
$format_tags = ['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 = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'];
|
||||
foreach ($possible_format_tags as $tag) {
|
||||
$input = '<' . $tag . '>TEST</' . $tag . '>';
|
||||
$output = trim(check_markup($input, $editor->id()));
|
||||
if (Html::load($output)->getElementsByTagName($tag)->length !== 0) {
|
||||
$format_tags[] = $tag;
|
||||
}
|
||||
}
|
||||
$format_tags = implode(';', $format_tags);
|
||||
|
||||
// Cache the "format_tags" configuration. This cache item is infinitely
|
||||
// valid; it only changes whenever the text format is changed, hence it's
|
||||
// tagged with the text format's cache tag.
|
||||
$this->cache->set($cid, $format_tags, Cache::PERMANENT, $format->getCacheTags());
|
||||
}
|
||||
|
||||
return $format_tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the ACF part of the CKEditor JS settings.
|
||||
*
|
||||
* This ensures that CKEditor obeys the HTML restrictions defined by Drupal's
|
||||
* filter system, by enabling CKEditor's Advanced Content Filter (ACF)
|
||||
* functionality: http://ckeditor.com/blog/CKEditor-4.1-RC-Released.
|
||||
*
|
||||
* @see getConfig()
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* An array with two values:
|
||||
* - the first value is the "allowedContent" setting: a well-formatted array
|
||||
* or TRUE. The latter indicates that anything is allowed.
|
||||
* - the second value is the "disallowedContent" setting: a well-formatted
|
||||
* array or FALSE. The latter indicates that nothing is disallowed.
|
||||
*/
|
||||
protected function generateACFSettings(Editor $editor) {
|
||||
// When no text format is associated yet, assume nothing is disallowed, so
|
||||
// set allowedContent to true.
|
||||
if (!$editor->hasAssociatedFilterFormat()) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
$format = $editor->getFilterFormat();
|
||||
$filter_types = $format->getFilterTypes();
|
||||
|
||||
// When nothing is disallowed, set allowedContent to true.
|
||||
if (!in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $filter_types)) {
|
||||
return array(TRUE, FALSE);
|
||||
}
|
||||
// Generate setting that accurately reflects allowed tags and attributes.
|
||||
else {
|
||||
$get_attribute_values = function($attribute_values, $allowed_values) {
|
||||
$values = array_keys(array_filter($attribute_values, function($value) use ($allowed_values) {
|
||||
if ($allowed_values) {
|
||||
return $value !== FALSE;
|
||||
}
|
||||
else {
|
||||
return $value === FALSE;
|
||||
}
|
||||
}));
|
||||
if (count($values)) {
|
||||
return implode(',', $values);
|
||||
}
|
||||
else {
|
||||
return NULL;
|
||||
}
|
||||
};
|
||||
|
||||
$html_restrictions = $format->getHtmlRestrictions();
|
||||
// When all HTML is allowed, also set allowedContent to true and
|
||||
// disallowedContent to false.
|
||||
if ($html_restrictions === FALSE) {
|
||||
return array(TRUE, FALSE);
|
||||
}
|
||||
$allowed = array();
|
||||
$disallowed = array();
|
||||
if (isset($html_restrictions['forbidden_tags'])) {
|
||||
foreach ($html_restrictions['forbidden_tags'] as $tag) {
|
||||
$disallowed[$tag] = TRUE;
|
||||
}
|
||||
}
|
||||
foreach ($html_restrictions['allowed'] as $tag => $attributes) {
|
||||
// Tell CKEditor the tag is allowed, but no attributes.
|
||||
if ($attributes === FALSE) {
|
||||
$allowed[$tag] = array(
|
||||
'attributes' => FALSE,
|
||||
'styles' => FALSE,
|
||||
'classes' => FALSE,
|
||||
);
|
||||
}
|
||||
// Tell CKEditor the tag is allowed, as well as any attribute on it. The
|
||||
// "style" and "class" attributes are handled separately by CKEditor:
|
||||
// they are disallowed even if you specify it in the list of allowed
|
||||
// attributes, unless you state specific values for them that are
|
||||
// allowed. Or, in this case: any value for them is allowed.
|
||||
elseif ($attributes === TRUE) {
|
||||
$allowed[$tag] = array(
|
||||
'attributes' => TRUE,
|
||||
'styles' => TRUE,
|
||||
'classes' => TRUE,
|
||||
);
|
||||
// We've just marked that any value for the "style" and "class"
|
||||
// attributes is allowed. However, that may not be the case: the "*"
|
||||
// tag may still apply restrictions.
|
||||
// Since CKEditor's ACF follows the following principle:
|
||||
// Once validated, an element or its property cannot be
|
||||
// invalidated by another rule.
|
||||
// That means that the most permissive setting wins. Which means that
|
||||
// it will still be allowed by CKEditor, for instance, to define any
|
||||
// style, no matter what the "*" tag's restrictions may be. If there
|
||||
// is a setting for either the "style" or "class" attribute, it cannot
|
||||
// possibly be more permissive than what was set above. Hence, inherit
|
||||
// from the "*" tag where possible.
|
||||
if (isset($html_restrictions['allowed']['*'])) {
|
||||
$wildcard = $html_restrictions['allowed']['*'];
|
||||
if (isset($wildcard['style'])) {
|
||||
if (!is_array($wildcard['style'])) {
|
||||
$allowed[$tag]['styles'] = $wildcard['style'];
|
||||
}
|
||||
else {
|
||||
$allowed_styles = $get_attribute_values($wildcard['style'], TRUE);
|
||||
if (isset($allowed_styles)) {
|
||||
$allowed[$tag]['styles'] = $allowed_styles;
|
||||
}
|
||||
else {
|
||||
unset($allowed[$tag]['styles']);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($wildcard['class'])) {
|
||||
if (!is_array($wildcard['class'])) {
|
||||
$allowed[$tag]['classes'] = $wildcard['class'];
|
||||
}
|
||||
else {
|
||||
$allowed_classes = $get_attribute_values($wildcard['class'], TRUE);
|
||||
if (isset($allowed_classes)) {
|
||||
$allowed[$tag]['classes'] = $allowed_classes;
|
||||
}
|
||||
else {
|
||||
unset($allowed[$tag]['classes']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tell CKEditor the tag is allowed, along with some tags.
|
||||
elseif (is_array($attributes)) {
|
||||
// Set defaults (these will be overridden below if more specific
|
||||
// values are present).
|
||||
$allowed[$tag] = array(
|
||||
'attributes' => FALSE,
|
||||
'styles' => FALSE,
|
||||
'classes' => FALSE,
|
||||
);
|
||||
// Configure allowed attributes, allowed "style" attribute values and
|
||||
// allowed "class" attribute values.
|
||||
// CKEditor only allows specific values for the "class" and "style"
|
||||
// attributes; so ignore restrictions on other attributes, which
|
||||
// Drupal filters may provide.
|
||||
// NOTE: A Drupal contrib module can subclass this class, override the
|
||||
// getConfig() method, and override the JavaScript at
|
||||
// Drupal.editors.ckeditor to somehow make validation of values for
|
||||
// attributes other than "class" and "style" work.
|
||||
$allowed_attributes = array_filter($attributes, function($value) {
|
||||
return $value !== FALSE;
|
||||
});
|
||||
if (count($allowed_attributes)) {
|
||||
$allowed[$tag]['attributes'] = implode(',', array_keys($allowed_attributes));
|
||||
}
|
||||
if (isset($allowed_attributes['style'])) {
|
||||
if (is_bool($allowed_attributes['style'])) {
|
||||
$allowed[$tag]['styles'] = $allowed_attributes['style'];
|
||||
}
|
||||
elseif (is_array($allowed_attributes['style'])) {
|
||||
$allowed_classes = $get_attribute_values($allowed_attributes['style'], TRUE);
|
||||
if (isset($allowed_classes)) {
|
||||
$allowed[$tag]['styles'] = $allowed_classes;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($allowed_attributes['class'])) {
|
||||
if (is_bool($allowed_attributes['class'])) {
|
||||
$allowed[$tag]['classes'] = $allowed_attributes['class'];
|
||||
}
|
||||
elseif (is_array($allowed_attributes['class'])) {
|
||||
$allowed_classes = $get_attribute_values($allowed_attributes['class'], TRUE);
|
||||
if (isset($allowed_classes)) {
|
||||
$allowed[$tag]['classes'] = $allowed_classes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle disallowed attributes analogously. However, to handle *dis-
|
||||
// allowed* attribute values, we must look at *allowed* attributes'
|
||||
// disallowed attribute values! After all, a disallowed attribute
|
||||
// implies that all of its possible attribute values are disallowed,
|
||||
// thus we must look at the disallowed attribute values on allowed
|
||||
// attributes.
|
||||
$disallowed_attributes = array_filter($attributes, function($value) {
|
||||
return $value === FALSE;
|
||||
});
|
||||
if (count($disallowed_attributes)) {
|
||||
// No need to blacklist the 'class' or 'style' attributes; CKEditor
|
||||
// handles them separately (if no specific class or style attribute
|
||||
// values are allowed, then those attributes are disallowed).
|
||||
if (isset($disallowed_attributes['class'])) {
|
||||
unset($disallowed_attributes['class']);
|
||||
}
|
||||
if (isset($disallowed_attributes['style'])) {
|
||||
unset($disallowed_attributes['style']);
|
||||
}
|
||||
$disallowed[$tag]['attributes'] = implode(',', array_keys($disallowed_attributes));
|
||||
}
|
||||
if (isset($allowed_attributes['style']) && is_array($allowed_attributes['style'])) {
|
||||
$disallowed_styles = $get_attribute_values($allowed_attributes['style'], FALSE);
|
||||
if (isset($disallowed_styles)) {
|
||||
$disallowed[$tag]['styles'] = $disallowed_styles;
|
||||
}
|
||||
}
|
||||
if (isset($allowed_attributes['class']) && is_array($allowed_attributes['class'])) {
|
||||
$disallowed_classes = $get_attribute_values($allowed_attributes['class'], FALSE);
|
||||
if (isset($disallowed_classes)) {
|
||||
$disallowed[$tag]['classes'] = $disallowed_classes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksort($allowed);
|
||||
ksort($disallowed);
|
||||
|
||||
return array($allowed, $disallowed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
135
web/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Language.php
Normal file
135
web/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Language.php
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginBase;
|
||||
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginCssInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageManager;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines the "language" plugin.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "language",
|
||||
* label = @Translation("Language")
|
||||
* )
|
||||
*/
|
||||
class Language extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface, CKEditorPluginCssInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isInternal() {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFile() {
|
||||
// This plugin is already part of Drupal core's CKEditor build.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(Editor $editor) {
|
||||
return ['ckeditor/drupal.ckeditor.plugins.language'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
$language_list = [];
|
||||
$config = ['language_list' => 'un'];
|
||||
$settings = $editor->getSettings();
|
||||
if (isset($settings['plugins']['language'])) {
|
||||
$config = $settings['plugins']['language'];
|
||||
}
|
||||
|
||||
$predefined_languages = ($config['language_list'] === 'all') ?
|
||||
LanguageManager::getStandardLanguageList() :
|
||||
LanguageManager::getUnitedNationsLanguageList();
|
||||
|
||||
// Generate the language_list setting as expected by the CKEditor Language
|
||||
// plugin, but key the values by the full language name so that we can sort
|
||||
// them later on.
|
||||
foreach ($predefined_languages as $langcode => $language) {
|
||||
$english_name = $language[0];
|
||||
$direction = empty($language[2]) ? NULL : $language[2];
|
||||
if ($direction === LanguageInterface::DIRECTION_RTL) {
|
||||
$language_list[$english_name] = $langcode . ':' . $english_name . ':rtl';
|
||||
}
|
||||
else {
|
||||
$language_list[$english_name] = $langcode . ':' . $english_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort on full language name.
|
||||
ksort($language_list);
|
||||
$config = ['language_list' => array_values($language_list)];
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getButtons() {
|
||||
return [
|
||||
'Language' => [
|
||||
'label' => $this->t('Language'),
|
||||
'image_alternative' => [
|
||||
'#type' => 'inline_template',
|
||||
'#template' => '<a href="#" class="cke-icon-only" role="button" title="' . $this->t('Language') . '" aria-label="' . $this->t('Language') . '"><span class="cke_button_icon cke_button__language_icon">' . $this->t('Language') . '</span></a>',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
|
||||
// Defaults.
|
||||
$config = ['language_list' => 'un'];
|
||||
$settings = $editor->getSettings();
|
||||
if (isset($settings['plugins']['language'])) {
|
||||
$config = $settings['plugins']['language'];
|
||||
}
|
||||
|
||||
$predefined_languages = LanguageManager::getStandardLanguageList();
|
||||
$form['language_list'] = array(
|
||||
'#title' => $this->t('Language list'),
|
||||
'#title_display' => 'invisible',
|
||||
'#type' => 'select',
|
||||
'#options' => [
|
||||
'un' => $this->t("United Nations' official languages"),
|
||||
'all' => $this->t('All @count languages', ['@count' => count($predefined_languages)]),
|
||||
],
|
||||
'#default_value' => $config['language_list'],
|
||||
'#description' => $this->t('The list of languages to show in the language dropdown. The basic list will only show the <a href=":url">six official languages of the UN</a>. The extended list will show all @count languages that are available in Drupal.', [
|
||||
':url' => 'https://www.un.org/en/sections/about-un/official-languages',
|
||||
'@count' => count($predefined_languages),
|
||||
]),
|
||||
'#attached' => ['library' => ['ckeditor/drupal.ckeditor.language.admin']],
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getCssFiles(Editor $editor) {
|
||||
return array(
|
||||
drupal_get_path('module', 'ckeditor') . '/css/plugins/language/ckeditor.language.css'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginBase;
|
||||
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines the "stylescombo" plugin.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "stylescombo",
|
||||
* label = @Translation("Styles dropdown")
|
||||
* )
|
||||
*/
|
||||
class StylesCombo extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isInternal() {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFile() {
|
||||
// This plugin is already part of Drupal core's CKEditor build.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
$config = array();
|
||||
$settings = $editor->getSettings();
|
||||
if (!isset($settings['plugins']['stylescombo']['styles'])) {
|
||||
return $config;
|
||||
}
|
||||
$styles = $settings['plugins']['stylescombo']['styles'];
|
||||
$config['stylesSet'] = $this->generateStylesSetSetting($styles);
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getButtons() {
|
||||
return array(
|
||||
'Styles' => array(
|
||||
'label' => t('Font style'),
|
||||
'image_alternative' => [
|
||||
'#type' => 'inline_template',
|
||||
'#template' => '<a href="#" role="button" aria-label="{{ styles_text }}"><span class="ckeditor-button-dropdown">{{ styles_text }}<span class="ckeditor-button-arrow"></span></span></a>',
|
||||
'#context' => [
|
||||
'styles_text' => t('Styles'),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
|
||||
// Defaults.
|
||||
$config = array('styles' => '');
|
||||
$settings = $editor->getSettings();
|
||||
if (isset($settings['plugins']['stylescombo'])) {
|
||||
$config = $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 or more classes on each line in the format: element.classA.classB|Label. Example: h1.title|Title. Advanced example: h1.fancy.title|Fancy title.<br />These styles should be available in your theme\'s CSS file.'),
|
||||
'#attached' => array(
|
||||
'library' => 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, FormStateInterface $form_state) {
|
||||
$styles_setting = $this->generateStylesSetSetting($element['#value']);
|
||||
if ($styles_setting === FALSE) {
|
||||
$form_state->setError($element, t('The provided list of styles is syntactically incorrect.'));
|
||||
}
|
||||
else {
|
||||
$style_names = array_map(function ($style) { return $style['name']; }, $styles_setting);
|
||||
if (count($style_names) !== count(array_unique($style_names))) {
|
||||
$form_state->setError($element, t('Each style must have a unique label.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: element[.class...]|label pattern expected.
|
||||
if (!preg_match('@^ *[a-zA-Z0-9]+ *(\\.[a-zA-Z0-9_-]+ *)*\\| *.+ *$@', $style)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Parse.
|
||||
list($selector, $label) = explode('|', $style);
|
||||
$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
|
||||
$configured_style = array(
|
||||
'name' => trim($label),
|
||||
'element' => trim($element),
|
||||
);
|
||||
if (!empty($classes)) {
|
||||
$configured_style['attributes'] = array(
|
||||
'class' => implode(' ', array_map('trim', $classes))
|
||||
);
|
||||
}
|
||||
$styles_set[] = $configured_style;
|
||||
}
|
||||
return $styles_set;
|
||||
}
|
||||
|
||||
}
|
432
web/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
Normal file
432
web/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
Normal file
|
@ -0,0 +1,432 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\Editor;
|
||||
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginManager;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\editor\Plugin\EditorBase;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a CKEditor-based text editor for Drupal.
|
||||
*
|
||||
* @Editor(
|
||||
* id = "ckeditor",
|
||||
* label = @Translation("CKEditor"),
|
||||
* supports_content_filtering = TRUE,
|
||||
* supports_inline_editing = TRUE,
|
||||
* is_xss_safe = FALSE,
|
||||
* supported_element_types = {
|
||||
* "textarea"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The module handler to invoke hooks on.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*
|
||||
* @var \Drupal\Core\Language\LanguageManagerInterface
|
||||
*/
|
||||
protected $languageManager;
|
||||
|
||||
/**
|
||||
* The CKEditor plugin manager.
|
||||
*
|
||||
* @var \Drupal\ckeditor\CKEditorPluginManager
|
||||
*/
|
||||
protected $ckeditorPluginManager;
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs a Drupal\Component\Plugin\PluginBase object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\ckeditor\CKEditorPluginManager $ckeditor_plugin_manager
|
||||
* The CKEditor plugin manager.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke hooks on.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
* The language manager.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, RendererInterface $renderer) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->ckeditorPluginManager = $ckeditor_plugin_manager;
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->languageManager = $language_manager;
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('plugin.manager.ckeditor.plugin'),
|
||||
$container->get('module_handler'),
|
||||
$container->get('language_manager'),
|
||||
$container->get('renderer')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultSettings() {
|
||||
return array(
|
||||
'toolbar' => array(
|
||||
'rows' => array(
|
||||
// Button groups.
|
||||
array(
|
||||
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',),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'plugins' => ['language' => ['language_list' => 'un']],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
|
||||
$settings = $editor->getSettings();
|
||||
|
||||
$ckeditor_settings_toolbar = array(
|
||||
'#theme' => 'ckeditor_settings_toolbar',
|
||||
'#editor' => $editor,
|
||||
'#plugins' => $this->ckeditorPluginManager->getButtons(),
|
||||
);
|
||||
$form['toolbar'] = array(
|
||||
'#type' => 'container',
|
||||
'#attached' => array(
|
||||
'library' => array('ckeditor/drupal.ckeditor.admin'),
|
||||
'drupalSettings' => [
|
||||
'ckeditor' => [
|
||||
'toolbarAdmin' => (string) $this->renderer->renderPlain($ckeditor_settings_toolbar),
|
||||
],
|
||||
],
|
||||
),
|
||||
'#attributes' => array('class' => array('ckeditor-toolbar-configuration')),
|
||||
);
|
||||
|
||||
$form['toolbar']['button_groups'] = array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Toolbar buttons'),
|
||||
'#default_value' => json_encode($settings['toolbar']['rows']),
|
||||
'#attributes' => array('class' => array('ckeditor-toolbar-textarea')),
|
||||
);
|
||||
|
||||
// CKEditor plugin settings, if any.
|
||||
$form['plugin_settings'] = array(
|
||||
'#type' => 'vertical_tabs',
|
||||
'#title' => t('CKEditor plugin settings'),
|
||||
'#attributes' => array(
|
||||
'id' => 'ckeditor-plugin-settings',
|
||||
),
|
||||
);
|
||||
$this->ckeditorPluginManager->injectPluginSettingsForm($form, $form_state, $editor);
|
||||
if (count(Element::children($form['plugins'])) === 0) {
|
||||
unset($form['plugins']);
|
||||
unset($form['plugin_settings']);
|
||||
}
|
||||
|
||||
// Hidden CKEditor instance. We need a hidden CKEditor instance with all
|
||||
// plugins enabled, so we can retrieve CKEditor's per-feature metadata (on
|
||||
// which tags, attributes, styles and classes are enabled). This metadata is
|
||||
// necessary for certain filters' (for instance, the html_filter filter)
|
||||
// settings to be updated accordingly.
|
||||
// Get a list of all external plugins and their corresponding files.
|
||||
$plugins = array_keys($this->ckeditorPluginManager->getDefinitions());
|
||||
$all_external_plugins = array();
|
||||
foreach ($plugins as $plugin_id) {
|
||||
$plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
|
||||
if (!$plugin->isInternal()) {
|
||||
$all_external_plugins[$plugin_id] = $plugin->getFile();
|
||||
}
|
||||
}
|
||||
// Get a list of all buttons that are provided by all plugins.
|
||||
$all_buttons = array_reduce($this->ckeditorPluginManager->getButtons(), function($result, $item) {
|
||||
return array_merge($result, array_keys($item));
|
||||
}, array());
|
||||
// Build a fake Editor object, which we'll use to generate JavaScript
|
||||
// settings for this fake Editor instance.
|
||||
$fake_editor = Editor::create(array(
|
||||
'format' => $editor->id(),
|
||||
'editor' => 'ckeditor',
|
||||
'settings' => array(
|
||||
// Single toolbar row, single button group, all existing buttons.
|
||||
'toolbar' => array(
|
||||
'rows' => array(
|
||||
0 => array(
|
||||
0 => array(
|
||||
'name' => 'All existing buttons',
|
||||
'items' => $all_buttons,
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
'plugins' => $settings['plugins'],
|
||||
),
|
||||
));
|
||||
$config = $this->getJSSettings($fake_editor);
|
||||
// Remove the ACF configuration that is generated based on filter settings,
|
||||
// because otherwise we cannot retrieve per-feature metadata.
|
||||
unset($config['allowedContent']);
|
||||
$form['hidden_ckeditor'] = array(
|
||||
'#markup' => '<div id="ckeditor-hidden" class="hidden"></div>',
|
||||
'#attached' => array(
|
||||
'drupalSettings' => ['ckeditor' => ['hiddenCKEditorConfig' => $config]],
|
||||
),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormSubmit(array $form, FormStateInterface $form_state) {
|
||||
// Modify the toolbar settings by reference. The values in
|
||||
// $form_state->getValue(array('editor', 'settings')) will be saved directly
|
||||
// by editor_form_filter_admin_format_submit().
|
||||
$toolbar_settings = &$form_state->getValue(array('editor', 'settings', 'toolbar'));
|
||||
|
||||
// The rows key is not built into the form structure, so decode the button
|
||||
// groups data into this new key and remove the button_groups key.
|
||||
$toolbar_settings['rows'] = json_decode($toolbar_settings['button_groups'], TRUE);
|
||||
unset($toolbar_settings['button_groups']);
|
||||
|
||||
// Remove the plugin settings' vertical tabs state; no need to save that.
|
||||
if ($form_state->hasValue(array('editor', 'settings', 'plugins'))) {
|
||||
$form_state->unsetValue(array('editor', 'settings', 'plugin_settings'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getJSSettings(Editor $editor) {
|
||||
$settings = array();
|
||||
|
||||
// Get the settings for all enabled plugins, even the internal ones.
|
||||
$enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor, TRUE));
|
||||
foreach ($enabled_plugins as $plugin_id) {
|
||||
$plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
|
||||
$settings += $plugin->getConfig($editor);
|
||||
}
|
||||
|
||||
// Fall back on English if no matching language code was found.
|
||||
$display_langcode = 'en';
|
||||
|
||||
// Map the interface language code to a CKEditor translation if interface
|
||||
// translation is enabled.
|
||||
if ($this->moduleHandler->moduleExists('locale')) {
|
||||
$ckeditor_langcodes = $this->getLangcodes();
|
||||
$language_interface = $this->languageManager->getCurrentLanguage();
|
||||
if (isset($ckeditor_langcodes[$language_interface->getId()])) {
|
||||
$display_langcode = $ckeditor_langcodes[$language_interface->getId()];
|
||||
}
|
||||
}
|
||||
|
||||
// Next, set the most fundamental CKEditor settings.
|
||||
$external_plugin_files = $this->ckeditorPluginManager->getEnabledPluginFiles($editor);
|
||||
$settings += array(
|
||||
'toolbar' => $this->buildToolbarJSSetting($editor),
|
||||
'contentsCss' => $this->buildContentsCssJSSetting($editor),
|
||||
'extraPlugins' => implode(',', array_keys($external_plugin_files)),
|
||||
'language' => $display_langcode,
|
||||
// Configure CKEditor to not load styles.js. The StylesCombo plugin will
|
||||
// set stylesSet according to the user's settings, if the "Styles" button
|
||||
// is enabled. We cannot get rid of this until CKEditor will stop loading
|
||||
// styles.js by default.
|
||||
// See http://dev.ckeditor.com/ticket/9992#comment:9.
|
||||
'stylesSet' => FALSE,
|
||||
);
|
||||
|
||||
// Finally, set Drupal-specific CKEditor settings.
|
||||
$root_relative_file_url = function ($uri) {
|
||||
return file_url_transform_relative(file_create_url($uri));
|
||||
};
|
||||
$settings += array(
|
||||
'drupalExternalPlugins' => array_map($root_relative_file_url, $external_plugin_files),
|
||||
);
|
||||
|
||||
// Parse all CKEditor plugin JavaScript files for translations.
|
||||
if ($this->moduleHandler->moduleExists('locale')) {
|
||||
locale_js_translate(array_values($external_plugin_files));
|
||||
}
|
||||
|
||||
ksort($settings);
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of language codes supported by CKEditor.
|
||||
*
|
||||
* @return array
|
||||
* An associative array keyed by language codes.
|
||||
*/
|
||||
public function getLangcodes() {
|
||||
// Cache the file system based language list calculation because this would
|
||||
// be expensive to calculate all the time. The cache is cleared on core
|
||||
// upgrades which is the only situation the CKEditor file listing should
|
||||
// change.
|
||||
$langcode_cache = \Drupal::cache()->get('ckeditor.langcodes');
|
||||
if (!empty($langcode_cache)) {
|
||||
$langcodes = $langcode_cache->data;
|
||||
}
|
||||
if (empty($langcodes)) {
|
||||
$langcodes = array();
|
||||
// Collect languages included with CKEditor based on file listing.
|
||||
$files = scandir('core/assets/vendor/ckeditor/lang');
|
||||
foreach ($files as $file) {
|
||||
if ($file[0] !== '.' && preg_match('/\.js$/', $file)) {
|
||||
$langcode = basename($file, '.js');
|
||||
$langcodes[$langcode] = $langcode;
|
||||
}
|
||||
}
|
||||
\Drupal::cache()->set('ckeditor.langcodes', $langcodes);
|
||||
}
|
||||
|
||||
// Get language mapping if available to map to Drupal language codes.
|
||||
// This is configurable in the user interface and not expensive to get, so
|
||||
// we don't include it in the cached language list.
|
||||
$language_mappings = $this->moduleHandler->moduleExists('language') ? language_get_browser_drupal_langcode_mappings() : array();
|
||||
foreach ($langcodes as $langcode) {
|
||||
// If this language code is available in a Drupal mapping, use that to
|
||||
// compute a possibility for matching from the Drupal langcode to the
|
||||
// CKEditor langcode.
|
||||
// For instance, CKEditor uses the langcode 'no' for Norwegian, Drupal
|
||||
// uses 'nb'. This would then remove the 'no' => 'no' mapping and replace
|
||||
// it with 'nb' => 'no'. Now Drupal knows which CKEditor translation to
|
||||
// load.
|
||||
if (isset($language_mappings[$langcode]) && !isset($langcodes[$language_mappings[$langcode]])) {
|
||||
$langcodes[$language_mappings[$langcode]] = $langcode;
|
||||
unset($langcodes[$langcode]);
|
||||
}
|
||||
}
|
||||
|
||||
return $langcodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(Editor $editor) {
|
||||
$libraries = array(
|
||||
'ckeditor/drupal.ckeditor',
|
||||
);
|
||||
|
||||
// Get the required libraries for any enabled plugins.
|
||||
$enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor));
|
||||
foreach ($enabled_plugins as $plugin_id) {
|
||||
$plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
|
||||
$additional_libraries = array_diff($plugin->getLibraries($editor), $libraries);
|
||||
$libraries = array_merge($libraries, $additional_libraries);
|
||||
}
|
||||
|
||||
return $libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the "toolbar" configuration part of the CKEditor JS settings.
|
||||
*
|
||||
* @see getJSSettings()
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
* @return array
|
||||
* An array containing the "toolbar" configuration.
|
||||
*/
|
||||
public function buildToolbarJSSetting(Editor $editor) {
|
||||
$toolbar = array();
|
||||
|
||||
$settings = $editor->getSettings();
|
||||
foreach ($settings['toolbar']['rows'] as $row) {
|
||||
foreach ($row as $group) {
|
||||
$toolbar[] = $group;
|
||||
}
|
||||
$toolbar[] = '/';
|
||||
}
|
||||
return $toolbar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the "contentsCss" configuration part of the CKEditor JS settings.
|
||||
*
|
||||
* @see getJSSettings()
|
||||
*
|
||||
* @param \Drupal\editor\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-iframe.css',
|
||||
drupal_get_path('module', 'system') . '/css/components/align.module.css',
|
||||
);
|
||||
$this->moduleHandler->alter('ckeditor_css', $css, $editor);
|
||||
// Get a list of all enabled plugins' iframe instance CSS files.
|
||||
$plugins_css = array_reduce($this->ckeditorPluginManager->getCssFiles($editor), function($result, $item) {
|
||||
return array_merge($result, array_values($item));
|
||||
}, array());
|
||||
$css = array_merge($css, $plugins_css);
|
||||
$css = array_merge($css, _ckeditor_theme_css());
|
||||
$css = array_map('file_create_url', $css);
|
||||
$css = array_map('file_url_transform_relative', $css);
|
||||
|
||||
return array_values($css);
|
||||
}
|
||||
|
||||
}
|
284
web/core/modules/ckeditor/src/Tests/CKEditorAdminTest.php
Normal file
284
web/core/modules/ckeditor/src/Tests/CKEditorAdminTest.php
Normal file
|
@ -0,0 +1,284 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* Tests administration of CKEditor.
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorAdminTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'editor', 'ckeditor');
|
||||
|
||||
/**
|
||||
* A user with the 'administer filters' permission.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create text format.
|
||||
$filtered_html_format = FilterFormat::create(array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
|
||||
// Create admin user.
|
||||
$this->adminUser = $this->drupalCreateUser(array('administer filters'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests configuring a text editor for an existing text format.
|
||||
*/
|
||||
function testExistingFormat() {
|
||||
$ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
|
||||
// Ensure no Editor config entity exists yet.
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->assertFalse($editor, 'No Editor config entity exists yet.');
|
||||
|
||||
// Verify the "Text Editor" <select> when a text editor is available.
|
||||
$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) === 0, 'The Text Editor select is not disabled.');
|
||||
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');
|
||||
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
|
||||
$this->assertTrue(((string) $options[1]) === 'CKEditor', 'Option 2 in the Text Editor select is "CKEditor".');
|
||||
$this->assertTrue(((string) $options[0]['selected']) === 'selected', 'Option 1 ("None") is selected.');
|
||||
|
||||
// Select the "CKEditor" editor and click the "Save configuration" button.
|
||||
$edit = array(
|
||||
'editor[editor]' => 'ckeditor',
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$this->assertRaw(t('You must configure the selected text editor.'));
|
||||
|
||||
// Ensure the CKEditor editor returns the expected default settings.
|
||||
$expected_default_settings = array(
|
||||
'toolbar' => array(
|
||||
'rows' => array(
|
||||
// Button groups
|
||||
array(
|
||||
array(
|
||||
'name' => 'Formatting',
|
||||
'items' => array('Bold', 'Italic',),
|
||||
),
|
||||
array(
|
||||
'name' => 'Links',
|
||||
'items' => array('DrupalLink', 'DrupalUnlink',),
|
||||
),
|
||||
array(
|
||||
'name' => 'Lists',
|
||||
'items' => array('BulletedList', 'NumberedList',),
|
||||
),
|
||||
array(
|
||||
'name' => 'Media',
|
||||
'items' => array('Blockquote', 'DrupalImage',),
|
||||
),
|
||||
array(
|
||||
'name' => 'Tools',
|
||||
'items' => array('Source',),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'plugins' => ['language' => ['language_list' => 'un']],
|
||||
);
|
||||
$this->assertIdentical($this->castSafeStrings($ckeditor->getDefaultSettings()), $expected_default_settings);
|
||||
|
||||
// Keep the "CKEditor" editor selected and click the "Configure" button.
|
||||
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->assertFalse($editor, 'No Editor config entity exists yet.');
|
||||
|
||||
// Ensure that drupalSettings is correct.
|
||||
$ckeditor_settings_toolbar = array(
|
||||
'#theme' => 'ckeditor_settings_toolbar',
|
||||
'#editor' => Editor::create(['editor' => 'ckeditor']),
|
||||
'#plugins' => $this->container->get('plugin.manager.ckeditor.plugin')->getButtons(),
|
||||
);
|
||||
$this->assertEqual(
|
||||
$this->drupalSettings['ckeditor']['toolbarAdmin'],
|
||||
$this->container->get('renderer')->renderPlain($ckeditor_settings_toolbar),
|
||||
'CKEditor toolbar settings are rendered as part of drupalSettings.'
|
||||
);
|
||||
|
||||
// Ensure the toolbar buttons configuration value is initialized to the
|
||||
// expected default 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]"]');
|
||||
$this->assertFieldByXPath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]', '', 'The styles textarea exists and is empty.');
|
||||
$this->assertTrue(count($styles_textarea) === 1, 'The "styles" textarea exists.');
|
||||
|
||||
// Submit the form to save the selection of CKEditor as the chosen editor.
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
|
||||
// Ensure an Editor object exists now, with the proper settings.
|
||||
$expected_settings = $expected_default_settings;
|
||||
$expected_settings['plugins']['stylescombo']['styles'] = '';
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists now.');
|
||||
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
|
||||
// Configure the Styles plugin, and ensure the updated settings are saved.
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
$edit = array(
|
||||
'editor[settings][plugins][stylescombo][styles]' => "h1.title|Title\np.callout|Callout\n\n",
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$expected_settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.callout|Callout\n\n";
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
|
||||
// Change the buttons that appear on the toolbar (in JavaScript, this is
|
||||
// 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']['rows'][0][] = array(
|
||||
'name' => 'Action history',
|
||||
'items' => array('Undo', '|', 'Redo', 'JustifyCenter'),
|
||||
);
|
||||
$edit = array(
|
||||
'editor[settings][toolbar][button_groups]' => json_encode($expected_settings['toolbar']['rows']),
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
|
||||
// Check that the markup we're setting for the toolbar buttons (actually in
|
||||
// JavaScript's drupalSettings, and Unicode-escaped) is correctly rendered.
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
// Create function to encode HTML as we expect it in drupalSettings.
|
||||
$json_encode = function($html) {
|
||||
return trim(Json::encode($html), '"');
|
||||
};
|
||||
// Check the Button separator.
|
||||
$this->assertRaw($json_encode('<li data-drupal-ckeditor-button-name="-" class="ckeditor-button-separator ckeditor-multiple-button" data-drupal-ckeditor-type="separator"><a href="#" role="button" aria-label="Button separator" class="ckeditor-separator"></a></li>'));
|
||||
// Check the Format dropdown.
|
||||
$this->assertRaw($json_encode('<li data-drupal-ckeditor-button-name="Format" class="ckeditor-button"><a href="#" role="button" aria-label="Format"><span class="ckeditor-button-dropdown">Format<span class="ckeditor-button-arrow"></span></span></a></li>'));
|
||||
// Check the Styles dropdown.
|
||||
$this->assertRaw($json_encode('<li data-drupal-ckeditor-button-name="Styles" class="ckeditor-button"><a href="#" role="button" aria-label="Styles"><span class="ckeditor-button-dropdown">Styles<span class="ckeditor-button-arrow"></span></span></a></li>'));
|
||||
// Check strikethrough.
|
||||
$this->assertRaw($json_encode('<li data-drupal-ckeditor-button-name="Strike" class="ckeditor-button"><a href="#" class="cke-icon-only cke_ltr" role="button" title="strike" aria-label="strike"><span class="cke_button_icon cke_button__strike_icon">strike</span></a></li>'));
|
||||
|
||||
// Now enable the ckeditor_test module, which provides one configurable
|
||||
// CKEditor plugin — this should not affect the Editor config entity.
|
||||
\Drupal::service('module_installer')->install(array('ckeditor_test'));
|
||||
$this->resetAll();
|
||||
$this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
$ultra_llama_mode_checkbox = $this->xpath('//input[@type="checkbox" and @name="editor[settings][plugins][llama_contextual_and_button][ultra_llama_mode]" and not(@checked)]');
|
||||
$this->assertTrue(count($ultra_llama_mode_checkbox) === 1, 'The "Ultra llama mode" checkbox exists and is not checked.');
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
|
||||
// Finally, check the "Ultra llama mode" checkbox.
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
$edit = array(
|
||||
'editor[settings][plugins][llama_contextual_and_button][ultra_llama_mode]' => '1',
|
||||
);
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
$ultra_llama_mode_checkbox = $this->xpath('//input[@type="checkbox" and @name="editor[settings][plugins][llama_contextual_and_button][ultra_llama_mode]" and @checked="checked"]');
|
||||
$this->assertTrue(count($ultra_llama_mode_checkbox) === 1, 'The "Ultra llama mode" checkbox exists and is checked.');
|
||||
$expected_settings['plugins']['llama_contextual_and_button']['ultra_llama_mode'] = TRUE;
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertEqual($expected_settings, $editor->getSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests configuring a text editor for a new text format.
|
||||
*
|
||||
* This test only needs to ensure that the basics of the CKEditor
|
||||
* configuration form work; details are tested in testExistingFormat().
|
||||
*/
|
||||
function testNewFormat() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/content/formats/add');
|
||||
|
||||
// Verify the "Text Editor" <select> when a text editor is available.
|
||||
$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) === 0, 'The Text Editor select is not disabled.');
|
||||
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');
|
||||
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
|
||||
$this->assertTrue(((string) $options[1]) === 'CKEditor', 'Option 2 in the Text Editor select is "CKEditor".');
|
||||
$this->assertTrue(((string) $options[0]['selected']) === 'selected', 'Option 1 ("None") is selected.');
|
||||
|
||||
// Name our fancy new text format, select the "CKEditor" editor and click
|
||||
// the "Configure" button.
|
||||
$edit = array(
|
||||
'name' => 'My amazing text format',
|
||||
'format' => 'amazing_format',
|
||||
'editor[editor]' => 'ckeditor',
|
||||
);
|
||||
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
|
||||
$filter_format = FilterFormat::load('amazing_format');
|
||||
$this->assertFalse($filter_format, 'No FilterFormat config entity exists yet.');
|
||||
$editor = Editor::load('amazing_format');
|
||||
$this->assertFalse($editor, 'No Editor config entity exists yet.');
|
||||
|
||||
// Ensure the toolbar buttons configuration value is initialized to the
|
||||
// default value.
|
||||
$ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
|
||||
$default_settings = $ckeditor->getDefaultSettings();
|
||||
$expected_buttons_value = json_encode($default_settings['toolbar']['rows']);
|
||||
$this->assertFieldByName('editor[settings][toolbar][button_groups]', $expected_buttons_value);
|
||||
|
||||
// Regression test for https://www.drupal.org/node/2606460.
|
||||
$this->assertTrue(strpos($this->drupalSettings['ckeditor']['toolbarAdmin'], '<li data-drupal-ckeditor-button-name="Bold" class="ckeditor-button"><a href="#" class="cke-icon-only cke_ltr" role="button" title="bold" aria-label="bold"><span class="cke_button_icon cke_button__bold_icon">bold</span></a></li>') !== FALSE);
|
||||
|
||||
// Ensure the styles textarea exists and is initialized empty.
|
||||
$styles_textarea = $this->xpath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]');
|
||||
$this->assertFieldByXPath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]', '', 'The styles textarea exists and is empty.');
|
||||
$this->assertTrue(count($styles_textarea) === 1, 'The "styles" textarea exists.');
|
||||
|
||||
// Submit the form to create both a new text format and an associated text
|
||||
// editor.
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
|
||||
// Ensure a FilterFormat object exists now.
|
||||
$filter_format = FilterFormat::load('amazing_format');
|
||||
$this->assertTrue($filter_format instanceof FilterFormatInterface, 'A FilterFormat config entity exists now.');
|
||||
|
||||
// Ensure an Editor object exists now, with the proper settings.
|
||||
$expected_settings = $default_settings;
|
||||
$expected_settings['plugins']['stylescombo']['styles'] = '';
|
||||
$editor = Editor::load('amazing_format');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists now.');
|
||||
$this->assertEqual($this->castSafeStrings($expected_settings), $this->castSafeStrings($editor->getSettings()), 'The Editor config entity has the correct settings.');
|
||||
}
|
||||
|
||||
}
|
245
web/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
Normal file
245
web/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
Normal file
|
@ -0,0 +1,245 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Tests;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* Tests loading of CKEditor.
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorLoadingTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'editor', 'ckeditor', 'node');
|
||||
|
||||
/**
|
||||
* An untrusted user with access to only the 'plain_text' format.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $untrustedUser;
|
||||
|
||||
/**
|
||||
* A normal user with access to the 'plain_text' and 'filtered_html' formats.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $normalUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create text format, associate CKEditor.
|
||||
$filtered_html_format = FilterFormat::create(array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$editor = Editor::create([
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
// Create a second format without an associated editor so a drop down select
|
||||
// list is created when selecting formats.
|
||||
$full_html_format = FilterFormat::create(array(
|
||||
'format' => 'full_html',
|
||||
'name' => 'Full HTML',
|
||||
'weight' => 1,
|
||||
'filters' => array(),
|
||||
));
|
||||
$full_html_format->save();
|
||||
|
||||
// Create node type.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
));
|
||||
|
||||
$this->untrustedUser = $this->drupalCreateUser(array('create article content', 'edit any article content'));
|
||||
$this->normalUser = $this->drupalCreateUser(array('create article content', 'edit any article content', 'use text format filtered_html', 'use text format full_html'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests loading of CKEditor CSS, JS and JS settings.
|
||||
*/
|
||||
function testLoading() {
|
||||
// The untrusted user:
|
||||
// - has access to 1 text format (plain_text);
|
||||
// - doesn't have access to the filtered_html text format, so: no text editor.
|
||||
$this->drupalLogin($this->untrustedUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
|
||||
$this->assertFalse($editor_settings_present, 'No Text Editor module settings.');
|
||||
$this->assertFalse($editor_js_present, 'No Text Editor JavaScript.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page.');
|
||||
$hidden_input = $this->xpath('//input[@type="hidden" and contains(@class, "editor")]');
|
||||
$this->assertTrue(count($hidden_input) === 0, 'A single text format hidden input does not exist on the page.');
|
||||
$this->assertNoRaw(drupal_get_path('module', 'ckeditor') . '/js/ckeditor.js', 'CKEditor glue JS is absent.');
|
||||
|
||||
// On pages where there would never be a text editor, CKEditor JS is absent.
|
||||
$this->drupalGet('user');
|
||||
$this->assertNoRaw(drupal_get_path('module', 'ckeditor') . '/js/ckeditor.js', 'CKEditor glue JS is absent.');
|
||||
|
||||
// The normal user:
|
||||
// - has access to 2 text formats;
|
||||
// - does have access to the filtered_html text format, so: CKEditor.
|
||||
$this->drupalLogin($this->normalUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
|
||||
$ckeditor_plugin = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
|
||||
$editor = Editor::load('filtered_html');
|
||||
$expected = array('formats' => array('filtered_html' => array(
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
'editorSettings' => $this->castSafeStrings($ckeditor_plugin->getJSSettings($editor)),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
)));
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $this->castSafeStrings($settings['editor']), "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
|
||||
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and @data-editor-for="edit-body-0-value"]');
|
||||
$this->assertTrue(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has a "data-editor-for" attribute with the correct value.');
|
||||
$this->assertTrue(in_array('ckeditor/drupal.ckeditor', explode(',', $settings['ajaxPageState']['libraries'])), 'CKEditor glue library is present.');
|
||||
|
||||
// Enable the ckeditor_test module, customize configuration. In this case,
|
||||
// there is additional CSS and JS to be loaded.
|
||||
// NOTE: the tests in CKEditorTest already ensure that changing the
|
||||
// configuration also results in modified CKEditor configuration, so we
|
||||
// don't test that here.
|
||||
\Drupal::service('module_installer')->install(array('ckeditor_test'));
|
||||
$this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
|
||||
$editor_settings = $editor->getSettings();
|
||||
$editor_settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
|
||||
$editor->setSettings($editor_settings);
|
||||
$editor->save();
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
|
||||
$expected = array(
|
||||
'formats' => array(
|
||||
'filtered_html' => array(
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
'editorSettings' => $this->castSafeStrings($ckeditor_plugin->getJSSettings($editor)),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
)));
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $this->castSafeStrings($settings['editor']), "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(in_array('ckeditor/drupal.ckeditor', explode(',', $settings['ajaxPageState']['libraries'])), 'CKEditor glue library is present.');
|
||||
|
||||
// Assert that CKEditor uses Drupal's cache-busting query string by
|
||||
// comparing the setting sent with the page with the current query string.
|
||||
$settings = $this->getDrupalSettings();
|
||||
$expected = $settings['ckeditor']['timestamp'];
|
||||
$this->assertIdentical($expected, \Drupal::state()->get('system.css_js_query_string'), "CKEditor scripts cache-busting string is correct before flushing all caches.");
|
||||
// Flush all caches then make sure that $settings['ckeditor']['timestamp']
|
||||
// still matches.
|
||||
drupal_flush_all_caches();
|
||||
$this->assertIdentical($expected, \Drupal::state()->get('system.css_js_query_string'), "CKEditor scripts cache-busting string is correct after flushing all caches.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests presence of essential configuration even without Internal's buttons.
|
||||
*/
|
||||
protected function testLoadingWithoutInternalButtons() {
|
||||
// Change the CKEditor text editor configuration to only have link buttons.
|
||||
// This means:
|
||||
// - 0 buttons are from \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal
|
||||
// - 2 buttons are from \Drupal\ckeditor\Plugin\CKEditorPlugin\DrupalLink
|
||||
$filtered_html_editor = Editor::load('filtered_html');
|
||||
$settings = $filtered_html_editor->getSettings();
|
||||
$settings['toolbar']['rows'] = [
|
||||
0 => [
|
||||
0 => [
|
||||
'name' => 'Links',
|
||||
'items' => [
|
||||
'DrupalLink',
|
||||
'DrupalUnlink',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$filtered_html_editor->setSettings($settings)->save();
|
||||
|
||||
// Even when no buttons of \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal
|
||||
// are in use, its configuration (Internal::getConfig()) is still essential:
|
||||
// this is configuration that is associated with the (custom, optimized)
|
||||
// build of CKEditor that Drupal core ships with. For example, it configures
|
||||
// CKEditor to not perform its default action of loading a config.js file,
|
||||
// to not convert special characters into HTML entities, and the allowedContent
|
||||
// setting to configure CKEditor's Advanced Content Filter.
|
||||
$this->drupalLogin($this->normalUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
$editor_settings = $this->getDrupalSettings()['editor']['formats']['filtered_html']['editorSettings'];
|
||||
$this->assertTrue(isset($editor_settings['customConfig']));
|
||||
$this->assertTrue(isset($editor_settings['entities']));
|
||||
$this->assertTrue(isset($editor_settings['allowedContent']));
|
||||
$this->assertTrue(isset($editor_settings['disallowedContent']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests loading of theme's CKEditor stylesheets defined in the .info file.
|
||||
*/
|
||||
function testExternalStylesheets() {
|
||||
$theme_handler = \Drupal::service('theme_handler');
|
||||
// Case 1: Install theme which has an absolute external CSS URL.
|
||||
$theme_handler->install(['test_ckeditor_stylesheets_external']);
|
||||
$theme_handler->setDefault('test_ckeditor_stylesheets_external');
|
||||
$expected = [
|
||||
'https://fonts.googleapis.com/css?family=Open+Sans',
|
||||
];
|
||||
$this->assertIdentical($expected, _ckeditor_theme_css('test_ckeditor_stylesheets_external'));
|
||||
|
||||
// Case 2: Install theme which has an external protocol-relative CSS URL.
|
||||
$theme_handler->install(['test_ckeditor_stylesheets_protocol_relative']);
|
||||
$theme_handler->setDefault('test_ckeditor_stylesheets_protocol_relative');
|
||||
$expected = [
|
||||
'//fonts.googleapis.com/css?family=Open+Sans',
|
||||
];
|
||||
$this->assertIdentical($expected, _ckeditor_theme_css('test_ckeditor_stylesheets_protocol_relative'));
|
||||
|
||||
// Case 3: Install theme which has a relative CSS URL.
|
||||
$theme_handler->install(['test_ckeditor_stylesheets_relative']);
|
||||
$theme_handler->setDefault('test_ckeditor_stylesheets_relative');
|
||||
$expected = [
|
||||
'core/modules/system/tests/themes/test_ckeditor_stylesheets_relative/css/yokotsoko.css',
|
||||
];
|
||||
$this->assertIdentical($expected, _ckeditor_theme_css('test_ckeditor_stylesheets_relative'));
|
||||
}
|
||||
|
||||
protected function getThingsToCheck() {
|
||||
$settings = $this->getDrupalSettings();
|
||||
return array(
|
||||
// JavaScript settings.
|
||||
$settings,
|
||||
// Editor.module's JS settings present.
|
||||
isset($settings['editor']),
|
||||
// Editor.module's JS present. Note: ckeditor/drupal.ckeditor depends on
|
||||
// editor/drupal.editor, hence presence of the former implies presence of
|
||||
// the latter.
|
||||
isset($settings['ajaxPageState']['libraries']) && in_array('ckeditor/drupal.ckeditor', explode(',', $settings['ajaxPageState']['libraries'])),
|
||||
// Body field.
|
||||
$this->xpath('//textarea[@id="edit-body-0-value"]'),
|
||||
// Format selector.
|
||||
$this->xpath('//select[contains(@class, "filter-list")]'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Tests;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* Tests administration of the CKEditor StylesCombo plugin.
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorStylesComboAdminTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['filter', 'editor', 'ckeditor'];
|
||||
|
||||
/**
|
||||
* A user with the 'administer filters' permission.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* A random generated format machine name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $format;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->format = strtolower($this->randomMachineName());
|
||||
$filter_format = FilterFormat::create([
|
||||
'format' => $this->format,
|
||||
'name' => $this->randomString(),
|
||||
'filters' => [],
|
||||
]);
|
||||
$filter_format->save();
|
||||
$editor = Editor::create([
|
||||
'format' => $this->format,
|
||||
'editor' => 'ckeditor',
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser(['administer filters']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests StylesCombo settings for an existing text format.
|
||||
*/
|
||||
function testExistingFormat() {
|
||||
$ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
|
||||
$default_settings = $ckeditor->getDefaultSettings();
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
|
||||
|
||||
// Ensure an Editor config entity exists, with the proper settings.
|
||||
$expected_settings = $default_settings;
|
||||
$editor = Editor::load($this->format);
|
||||
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
|
||||
// Case 1: Configure the Styles plugin with different labels for each style,
|
||||
// and ensure the updated settings are saved.
|
||||
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
|
||||
$edit = [
|
||||
'editor[settings][plugins][stylescombo][styles]' => "h1.title|Title\np.callout|Callout\n\n",
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$expected_settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.callout|Callout\n\n";
|
||||
$editor = Editor::load($this->format);
|
||||
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
|
||||
// Case 2: Configure the Styles plugin with same labels for each style, and
|
||||
// ensure that an error is displayed and that the updated settings are not
|
||||
// saved.
|
||||
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
|
||||
$edit = [
|
||||
'editor[settings][plugins][stylescombo][styles]' => "h1.title|Title\np.callout|Title\n\n",
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$this->assertRaw(t('Each style must have a unique label.'));
|
||||
$expected_settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.callout|Callout\n\n";
|
||||
$editor = Editor::load($this->format);
|
||||
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor\Tests;
|
||||
|
||||
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
|
||||
/**
|
||||
* Tests CKEditor toolbar buttons when the language direction is RTL.
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorToolbarButtonTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable for this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['filter', 'editor', 'ckeditor', 'locale'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a text format and associate this with CKEditor.
|
||||
FilterFormat::create([
|
||||
'format' => 'full_html',
|
||||
'name' => 'Full HTML',
|
||||
'weight' => 1,
|
||||
'filters' => [],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'full_html',
|
||||
'editor' => 'ckeditor',
|
||||
])->save();
|
||||
|
||||
// Create a new user with admin rights.
|
||||
$this->admin_user = $this->drupalCreateUser([
|
||||
'administer languages',
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'administer filters',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method tests CKEditor image buttons.
|
||||
*/
|
||||
public function testImageButtonDisplay() {
|
||||
$this->drupalLogin($this->admin_user);
|
||||
|
||||
// Install the Arabic language (which is RTL) and configure as the default.
|
||||
$edit = [];
|
||||
$edit['predefined_langcode'] = 'ar';
|
||||
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
|
||||
|
||||
$edit = ['site_default_language' => 'ar'];
|
||||
$this->drupalPostForm('admin/config/regional/language', $edit, t('Save configuration'));
|
||||
// Once the default language is changed, go to the tested text format
|
||||
// configuration page.
|
||||
$this->drupalGet('admin/config/content/formats/manage/full_html');
|
||||
|
||||
// Check if any image button is loaded in CKEditor json.
|
||||
$json_encode = function($html) {
|
||||
return trim(Json::encode($html), '"');
|
||||
};
|
||||
$markup = $json_encode(file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupalimage/icons/drupalimage.png')));
|
||||
$this->assertRaw($markup);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
{#
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation for the CKEditor settings toolbar.
|
||||
*
|
||||
* Available variables:
|
||||
* - multiple_buttons: A list of buttons that may be added multiple times.
|
||||
* - disabled_buttons: A list of disabled buttons.
|
||||
* - active_buttons: A list of active button rows.
|
||||
*
|
||||
* @see template_preprocess_ckeditor_settings_toolbar()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
#}
|
||||
{% spaceless %}
|
||||
<fieldset role="form" aria-labelledby="ckeditor-button-configuration ckeditor-button-description">
|
||||
<legend id="ckeditor-button-configuration">{{ 'Toolbar configuration'|t }}</legend>
|
||||
<div class="fieldset-wrapper">
|
||||
<div id="ckeditor-button-description" class="fieldset-description">
|
||||
{%- trans -%}
|
||||
Move a button into the <em>Active toolbar</em> to enable it, or into the list of <em>Available buttons</em> to disable it. Buttons may be moved with the mouse or keyboard arrow keys. Toolbar group names are provided to support screen reader users. Empty toolbar groups will be removed upon save.
|
||||
{%- endtrans -%}
|
||||
</div>
|
||||
<div class="ckeditor-toolbar-disabled clearfix">
|
||||
{# Available buttons. #}
|
||||
<div class="ckeditor-toolbar-available">
|
||||
<label for="ckeditor-available-buttons">{{ 'Available buttons'|t }}</label>
|
||||
<ul id="ckeditor-available-buttons" class="ckeditor-buttons clearfix" role="form" data-drupal-ckeditor-button-sorting="source">
|
||||
{% for disabled_button in disabled_buttons %}
|
||||
<li{{ disabled_button.attributes.addClass('ckeditor-button') }}>{{ disabled_button.value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{# Dividers. #}
|
||||
<div class="ckeditor-toolbar-dividers">
|
||||
<label for="ckeditor-multiple-buttons">{{ 'Button divider'|t }}</label>
|
||||
<ul id="ckeditor-multiple-buttons" class="ckeditor-multiple-buttons" role="form" data-drupal-ckeditor-button-sorting="dividers">
|
||||
{% for multiple_button in multiple_buttons %}
|
||||
<li{{ multiple_button.attributes.addClass('ckeditor-multiple-button') }}>{{ multiple_button.value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{# Active toolbar. #}
|
||||
<div class="clearfix">
|
||||
<label id="ckeditor-active-toolbar">{{ 'Active toolbar'|t }}</label>
|
||||
</div>
|
||||
<div data-toolbar="active" role="form" class="ckeditor-toolbar ckeditor-toolbar-active clearfix">
|
||||
<ul class="ckeditor-active-toolbar-configuration" role="presentation" aria-label="{{ 'CKEditor toolbar and button configuration.'|t }}">
|
||||
{% for button_row in active_buttons %}
|
||||
<li class="ckeditor-row" role="group" aria-labelledby="ckeditor-active-toolbar">
|
||||
<ul class="ckeditor-toolbar-groups clearfix">
|
||||
{% for group_name, button_group in button_row %}
|
||||
<li class="ckeditor-toolbar-group" role="presentation" data-drupal-ckeditor-type="group" data-drupal-ckeditor-toolbar-group-name="{{ group_name }}" tabindex="0">
|
||||
<h3 class="ckeditor-toolbar-group-name" id="ckeditor-toolbar-group-aria-label-for-{{ button_group.group_name_class }}">{{ group_name }}</h3>
|
||||
<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target" aria-labelledby="ckeditor-toolbar-group-aria-label-for-{{ button_group.group_name_class }}">
|
||||
{% for active_button in button_group.buttons %}
|
||||
<li{{ active_button.attributes.addClass(active_button.multiple ? 'ckeditor-multiple-button' : 'ckeditor-button') }}>{{ active_button.value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<ul class="ckeditor-buttons"></ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endspaceless %}
|
|
@ -0,0 +1,6 @@
|
|||
name: CKEditor test
|
||||
type: module
|
||||
description: Support module for the CKEditor module tests.
|
||||
core: 8.x
|
||||
package: Testing
|
||||
version: VERSION
|
15
web/core/modules/ckeditor/tests/modules/ckeditor_test.module
Normal file
15
web/core/modules/ckeditor/tests/modules/ckeditor_test.module
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Helper module for the CKEditor tests.
|
||||
*/
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Implements hook_ckeditor_css_alter().
|
||||
*/
|
||||
function ckeditor_test_ckeditor_css_alter(array &$css, Editor $editor) {
|
||||
$css[] = drupal_get_path('module', 'ckeditor_test') . '/ckeditor_test.css';
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
ckeditor.plugin.llama_contextual_and_button:
|
||||
type: mapping
|
||||
label: 'Contextual Llama With Button'
|
||||
mapping:
|
||||
ultra_llama_mode:
|
||||
type: boolean
|
||||
label: 'Ultra llama mode'
|
||||
|
||||
filter_settings.test_attribute_filter:
|
||||
type: filter
|
||||
label: 'Test Attribute Filter'
|
||||
mapping:
|
||||
tags:
|
||||
type: sequence
|
||||
sequence:
|
||||
type: string
|
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\ckeditor\Kernel;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* Tests different ways of enabling CKEditor plugins.
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorPluginManagerTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('system', 'user', 'filter', 'editor', 'ckeditor');
|
||||
|
||||
/**
|
||||
* The manager for "CKEditor plugin" plugins.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Install the Filter module.
|
||||
|
||||
// Create text format, associate CKEditor.
|
||||
$filtered_html_format = FilterFormat::create(array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$editor = Editor::create([
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
]);
|
||||
$editor->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the enabling of plugins.
|
||||
*/
|
||||
function testEnabledPlugins() {
|
||||
$this->manager = $this->container->get('plugin.manager.ckeditor.plugin');
|
||||
$editor = Editor::load('filtered_html');
|
||||
|
||||
// Case 1: no CKEditor plugins.
|
||||
$definitions = array_keys($this->manager->getDefinitions());
|
||||
sort($definitions);
|
||||
$this->assertIdentical(array('drupalimage', 'drupalimagecaption', 'drupallink', 'internal', 'language', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.');
|
||||
$enabled_plugins = array(
|
||||
'drupalimage' => drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/plugin.js',
|
||||
'drupallink' => drupal_get_path('module', 'ckeditor') . '/js/plugins/drupallink/plugin.js',
|
||||
);
|
||||
$this->assertIdentical($enabled_plugins, $this->manager->getEnabledPluginFiles($editor), 'Only built-in plugins are enabled.');
|
||||
$this->assertIdentical(array('internal' => NULL) + $enabled_plugins, $this->manager->getEnabledPluginFiles($editor, TRUE), 'Only the "internal" plugin is enabled.');
|
||||
|
||||
// Enable the CKEditor Test module, which has the Llama plugin (plus four
|
||||
// variations of it, to cover all possible ways a plugin can be enabled) and
|
||||
// clear the editor manager's cache so it is picked up.
|
||||
$this->enableModules(array('ckeditor_test'));
|
||||
$this->manager = $this->container->get('plugin.manager.ckeditor.plugin');
|
||||
$this->manager->clearCachedDefinitions();
|
||||
|
||||
// Case 2: CKEditor plugins are available.
|
||||
$plugin_ids = array_keys($this->manager->getDefinitions());
|
||||
sort($plugin_ids);
|
||||
$this->assertIdentical(array('drupalimage', 'drupalimagecaption', 'drupallink', 'internal', 'language', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'llama_css', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.');
|
||||
$this->assertIdentical($enabled_plugins, $this->manager->getEnabledPluginFiles($editor), 'Only the internal plugins are enabled.');
|
||||
$this->assertIdentical(array('internal' => NULL) + $enabled_plugins, $this->manager->getEnabledPluginFiles($editor, TRUE), 'Only the "internal" plugin is enabled.');
|
||||
|
||||
// Case 3: enable each of the newly available plugins, if possible:
|
||||
// a. Llama: cannot be enabled, since it does not implement
|
||||
// CKEditorPluginContextualInterface nor CKEditorPluginButtonsInterface.
|
||||
// b. LlamaContextual: enabled by adding the 'Strike' button, which is
|
||||
// part of another plugin!
|
||||
// c. LlamaButton: automatically enabled by adding its 'Llama' button.
|
||||
// d. LlamaContextualAndButton: enabled by either b or c.
|
||||
// e. LlamaCSS: automatically enabled by add its 'LlamaCSS' button.
|
||||
// Below, we will first enable the "Llama" button, which will cause the
|
||||
// LlamaButton and LlamaContextualAndButton plugins to be enabled. Then we
|
||||
// will remove the "Llama" button and add the "Strike" button, which will
|
||||
// cause the LlamaContextual and LlamaContextualAndButton plugins to be
|
||||
// enabled. Then we will add the "Strike" button back again, which would
|
||||
// cause LlamaButton, LlamaContextual and LlamaContextualAndButton to be
|
||||
// enabled. Finally, we will add the "LlamaCSS" button which would cause
|
||||
// all four plugins to be enabled.
|
||||
$settings = $editor->getSettings();
|
||||
$original_toolbar = $settings['toolbar'];
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$file = array();
|
||||
$file['b'] = drupal_get_path('module', 'ckeditor_test') . '/js/llama_button.js';
|
||||
$file['c'] = drupal_get_path('module', 'ckeditor_test') . '/js/llama_contextual.js';
|
||||
$file['cb'] = drupal_get_path('module', 'ckeditor_test') . '/js/llama_contextual_and_button.js';
|
||||
$file['css'] = drupal_get_path('module', 'ckeditor_test') . '/js/llama_css.js';
|
||||
$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.');
|
||||
$settings['toolbar'] = $original_toolbar;
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Strike';
|
||||
$editor->setSettings($settings);
|
||||
$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.');
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
|
||||
$editor->setSettings($settings);
|
||||
$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.');
|
||||
$this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPluginFiles($editor, TRUE), 'The LLamaButton, LlamaContextual and LlamaContextualAndButton plugins are enabled.');
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'LlamaCSS';
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected = $enabled_plugins + array('llama_button' => $file['b'], 'llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb'], 'llama_css' => $file['css']);
|
||||
$this->assertIdentical($expected, $this->manager->getEnabledPluginFiles($editor), 'The LlamaButton, LlamaContextual, LlamaContextualAndButton and LlamaCSS plugins are enabled.');
|
||||
$this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPluginFiles($editor, TRUE), 'The LLamaButton, LlamaContextual, LlamaContextualAndButton and LlamaCSS plugins are enabled.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the iframe instance CSS files of plugins.
|
||||
*/
|
||||
function testCssFiles() {
|
||||
$this->manager = $this->container->get('plugin.manager.ckeditor.plugin');
|
||||
$editor = Editor::load('filtered_html');
|
||||
|
||||
// Case 1: no CKEditor iframe instance CSS file.
|
||||
$this->assertIdentical(array(), $this->manager->getCssFiles($editor), 'No iframe instance CSS file found.');
|
||||
|
||||
// Enable the CKEditor Test module, which has the LlamaCss plugin and
|
||||
// clear the editor manager's cache so it is picked up.
|
||||
$this->enableModules(array('ckeditor_test'));
|
||||
$this->manager = $this->container->get('plugin.manager.ckeditor.plugin');
|
||||
$settings = $editor->getSettings();
|
||||
// LlamaCss: automatically enabled by adding its 'LlamaCSS' button.
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'LlamaCSS';
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
|
||||
// Case 2: CKEditor iframe instance CSS file.
|
||||
$expected = array(
|
||||
'llama_css' => array(drupal_get_path('module', 'ckeditor_test') . '/css/llama.css')
|
||||
);
|
||||
$this->assertIdentical($expected, $this->manager->getCssFiles($editor), 'Iframe instance CSS file found.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,495 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\ckeditor\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* Tests for the 'CKEditor' text editor plugin.
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('system', 'user', 'filter', 'editor', 'ckeditor', 'filter_test');
|
||||
|
||||
/**
|
||||
* An instance of the "CKEditor" text editor plugin.
|
||||
*
|
||||
* @var \Drupal\ckeditor\Plugin\Editor\CKEditor;
|
||||
*/
|
||||
protected $ckeditor;
|
||||
|
||||
/**
|
||||
* The Editor Plugin Manager.
|
||||
*
|
||||
* @var \Drupal\editor\Plugin\EditorManager;
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Install the Filter module.
|
||||
|
||||
// Create text format, associate CKEditor.
|
||||
$filtered_html_format = FilterFormat::create(array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(
|
||||
'filter_html' => array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'allowed_html' => '<h2 id> <h3> <h4> <h5> <h6> <p> <br> <strong> <a href hreflang>',
|
||||
)
|
||||
),
|
||||
),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$editor = Editor::create([
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
// Create "CKEditor" text editor plugin instance.
|
||||
$this->ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests CKEditor::getJSSettings().
|
||||
*/
|
||||
function testGetJSSettings() {
|
||||
$editor = Editor::load('filtered_html');
|
||||
|
||||
// Default toolbar.
|
||||
$expected_config = $this->getDefaultInternalConfig() + array(
|
||||
'drupalImage_dialogTitleAdd' => 'Insert Image',
|
||||
'drupalImage_dialogTitleEdit' => 'Edit Image',
|
||||
'drupalLink_dialogTitleAdd' => 'Add Link',
|
||||
'drupalLink_dialogTitleEdit' => 'Edit Link',
|
||||
'allowedContent' => $this->getDefaultAllowedContentConfig(),
|
||||
'disallowedContent' => $this->getDefaultDisallowedContentConfig(),
|
||||
'toolbar' => $this->getDefaultToolbarConfig(),
|
||||
'contentsCss' => $this->getDefaultContentsCssConfig(),
|
||||
'extraPlugins' => 'drupalimage,drupallink',
|
||||
'language' => 'en',
|
||||
'stylesSet' => FALSE,
|
||||
'drupalExternalPlugins' => array(
|
||||
'drupalimage' => file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupalimage/plugin.js')),
|
||||
'drupallink' => file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupallink/plugin.js')),
|
||||
),
|
||||
);
|
||||
$expected_config = $this->castSafeStrings($expected_config);
|
||||
ksort($expected_config);
|
||||
ksort($expected_config['allowedContent']);
|
||||
$this->assertIdentical($expected_config, $this->castSafeStrings($this->ckeditor->getJSSettings($editor)), 'Generated JS settings are correct for default configuration.');
|
||||
|
||||
// Customize the configuration: add button, have two contextually enabled
|
||||
// buttons, and configure a CKEditor plugin setting.
|
||||
$this->enableModules(array('ckeditor_test'));
|
||||
$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();
|
||||
$settings = $editor->getSettings();
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Strike';
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Format';
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected_config['toolbar'][0]['items'][] = 'Strike';
|
||||
$expected_config['toolbar'][0]['items'][] = 'Format';
|
||||
$expected_config['format_tags'] = 'p;h2;h3;h4;h5;h6';
|
||||
$expected_config['extraPlugins'] .= ',llama_contextual,llama_contextual_and_button';
|
||||
$expected_config['drupalExternalPlugins']['llama_contextual'] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js'));
|
||||
$expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js'));
|
||||
$expected_config['contentsCss'][] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css'));
|
||||
ksort($expected_config);
|
||||
$this->assertIdentical($expected_config, $this->castSafeStrings($this->ckeditor->getJSSettings($editor)), 'Generated JS settings are correct for customized configuration.');
|
||||
|
||||
// Change the allowed HTML tags; the "allowedContent" and "format_tags"
|
||||
// settings for CKEditor should automatically be updated as well.
|
||||
$format = $editor->getFilterFormat();
|
||||
$format->filters('filter_html')->settings['allowed_html'] .= '<pre class> <h1> <blockquote class="*"> <address class="foo bar-* *">';
|
||||
$format->save();
|
||||
|
||||
$expected_config['allowedContent']['pre'] = array('attributes' => 'class', 'styles' => FALSE, 'classes' => TRUE);
|
||||
$expected_config['allowedContent']['h1'] = array('attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE);
|
||||
$expected_config['allowedContent']['blockquote'] = array('attributes' => 'class', 'styles' => FALSE, 'classes' => TRUE);
|
||||
$expected_config['allowedContent']['address'] = array('attributes' => 'class', 'styles' => FALSE, 'classes' => 'foo,bar-*');
|
||||
$expected_config['format_tags'] = 'p;h1;h2;h3;h4;h5;h6;pre';
|
||||
ksort($expected_config['allowedContent']);
|
||||
$this->assertIdentical($expected_config, $this->castSafeStrings($this->ckeditor->getJSSettings($editor)), 'Generated JS settings are correct for customized configuration.');
|
||||
|
||||
// Disable the filter_html filter: allow *all *tags.
|
||||
$format->setFilterConfig('filter_html', array('status' => 0));
|
||||
$format->save();
|
||||
|
||||
$expected_config['allowedContent'] = TRUE;
|
||||
$expected_config['disallowedContent'] = FALSE;
|
||||
$expected_config['format_tags'] = 'p;h1;h2;h3;h4;h5;h6;pre';
|
||||
$this->assertIdentical($expected_config, $this->castSafeStrings($this->ckeditor->getJSSettings($editor)), 'Generated JS settings are correct for customized configuration.');
|
||||
|
||||
// Enable the filter_test_restrict_tags_and_attributes filter.
|
||||
$format->setFilterConfig('filter_test_restrict_tags_and_attributes', array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'restrictions' => array(
|
||||
'allowed' => array(
|
||||
'p' => TRUE,
|
||||
'a' => array(
|
||||
'href' => TRUE,
|
||||
'rel' => array('nofollow' => TRUE),
|
||||
'class' => array('external' => TRUE),
|
||||
'target' => array('_blank' => FALSE),
|
||||
),
|
||||
'span' => array(
|
||||
'class' => array('dodo' => FALSE),
|
||||
'property' => array('dc:*' => TRUE),
|
||||
'rel' => array('foaf:*' => FALSE),
|
||||
'style' => array('underline' => FALSE, 'color' => FALSE, 'font-size' => TRUE),
|
||||
),
|
||||
'*' => array(
|
||||
'style' => FALSE,
|
||||
'on*' => FALSE,
|
||||
'class' => array('is-a-hipster-llama' => TRUE, 'and-more' => TRUE),
|
||||
'data-*' => TRUE,
|
||||
),
|
||||
'del' => FALSE,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
$format->save();
|
||||
|
||||
$expected_config['allowedContent'] = array(
|
||||
'p' => array(
|
||||
'attributes' => TRUE,
|
||||
'styles' => FALSE,
|
||||
'classes' => 'is-a-hipster-llama,and-more',
|
||||
),
|
||||
'a' => array(
|
||||
'attributes' => 'href,rel,class,target',
|
||||
'styles' => FALSE,
|
||||
'classes' => 'external',
|
||||
),
|
||||
'span' => array(
|
||||
'attributes' => 'class,property,rel,style',
|
||||
'styles' => 'font-size',
|
||||
'classes' => FALSE,
|
||||
),
|
||||
'*' => array(
|
||||
'attributes' => 'class,data-*',
|
||||
'styles' => FALSE,
|
||||
'classes' => 'is-a-hipster-llama,and-more',
|
||||
),
|
||||
'del' => array(
|
||||
'attributes' => FALSE,
|
||||
'styles' => FALSE,
|
||||
'classes' => FALSE,
|
||||
),
|
||||
);
|
||||
$expected_config['disallowedContent'] = array(
|
||||
'span' => array(
|
||||
'styles' => 'underline,color',
|
||||
'classes' => 'dodo',
|
||||
),
|
||||
'*' => array(
|
||||
'attributes' => 'on*',
|
||||
),
|
||||
);
|
||||
$expected_config['format_tags'] = 'p';
|
||||
ksort($expected_config);
|
||||
ksort($expected_config['allowedContent']);
|
||||
ksort($expected_config['disallowedContent']);
|
||||
$this->assertIdentical($expected_config, $this->castSafeStrings($this->ckeditor->getJSSettings($editor)), 'Generated JS settings are correct for customized configuration.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests CKEditor::buildToolbarJSSetting().
|
||||
*/
|
||||
function testBuildToolbarJSSetting() {
|
||||
$editor = Editor::load('filtered_html');
|
||||
|
||||
// Default toolbar.
|
||||
$expected = $this->getDefaultToolbarConfig();
|
||||
$this->assertIdentical($expected, $this->castSafeStrings($this->ckeditor->buildToolbarJSSetting($editor)), '"toolbar" configuration part of JS settings built correctly for default toolbar.');
|
||||
|
||||
// Customize the configuration.
|
||||
$settings = $editor->getSettings();
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Strike';
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected[0]['items'][] = 'Strike';
|
||||
$this->assertIdentical($expected, $this->castSafeStrings($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();
|
||||
// Override the label of a toolbar component.
|
||||
$settings['toolbar']['rows'][0][0]['name'] = 'JunkScience';
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected[0]['name'] = 'JunkScience';
|
||||
$expected[0]['items'][] = 'Llama';
|
||||
$this->assertIdentical($expected, $this->castSafeStrings($this->ckeditor->buildToolbarJSSetting($editor)), '"toolbar" configuration part of JS settings built correctly for customized toolbar with contrib module-provided CKEditor plugin.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests CKEditor::buildContentsCssJSSetting().
|
||||
*/
|
||||
function testBuildContentsCssJSSetting() {
|
||||
$editor = Editor::load('filtered_html');
|
||||
|
||||
// Default toolbar.
|
||||
$expected = $this->getDefaultContentsCssConfig();
|
||||
$this->assertIdentical($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly for default toolbar.');
|
||||
|
||||
// Enable the editor_test module, which implements hook_ckeditor_css_alter().
|
||||
$this->enableModules(array('ckeditor_test'));
|
||||
$expected[] = file_url_transform_relative(file_create_url(drupal_get_path('module', 'ckeditor_test') . '/ckeditor_test.css'));
|
||||
$this->assertIdentical($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a hook_ckeditor_css_alter() implementation exists.');
|
||||
|
||||
// Enable LlamaCss plugin, which adds an additional CKEditor stylesheet.
|
||||
$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();
|
||||
$settings = $editor->getSettings();
|
||||
// LlamaCss: automatically enabled by adding its 'LlamaCSS' button.
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'LlamaCSS';
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected[] = file_url_transform_relative(file_create_url(drupal_get_path('module', 'ckeditor_test') . '/css/llama.css'));
|
||||
$this->assertIdentical($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a CKEditorPluginInterface implementation exists.');
|
||||
|
||||
// Enable the Bartik theme, which specifies a CKEditor stylesheet.
|
||||
\Drupal::service('theme_handler')->install(['bartik']);
|
||||
\Drupal::service('theme_handler')->setDefault('bartik');
|
||||
$expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/base/elements.css'));
|
||||
$expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/captions.css'));
|
||||
$expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/table.css'));
|
||||
$expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/text-formatted.css'));
|
||||
$this->assertIdentical($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a theme providing a CKEditor stylesheet exists.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests Internal::getConfig().
|
||||
*/
|
||||
function testInternalGetConfig() {
|
||||
$editor = Editor::load('filtered_html');
|
||||
$internal_plugin = $this->container->get('plugin.manager.ckeditor.plugin')->createInstance('internal');
|
||||
|
||||
// Default toolbar.
|
||||
$expected = $this->getDefaultInternalConfig();
|
||||
$expected['disallowedContent'] = $this->getDefaultDisallowedContentConfig();
|
||||
$expected['allowedContent'] = $this->getDefaultAllowedContentConfig();
|
||||
$this->assertEqual($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for default toolbar.');
|
||||
|
||||
// Format dropdown/button enabled: new setting should be present.
|
||||
$settings = $editor->getSettings();
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Format';
|
||||
$editor->setSettings($settings);
|
||||
$expected['format_tags'] = 'p;h2;h3;h4;h5;h6';
|
||||
$this->assertEqual($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for customized toolbar.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests StylesCombo::getConfig().
|
||||
*/
|
||||
function testStylesComboGetConfig() {
|
||||
$editor = Editor::load('filtered_html');
|
||||
$stylescombo_plugin = $this->container->get('plugin.manager.ckeditor.plugin')->createInstance('stylescombo');
|
||||
|
||||
// Styles dropdown/button enabled: new setting should be present.
|
||||
$settings = $editor->getSettings();
|
||||
$settings['toolbar']['rows'][0][0]['items'][] = 'Styles';
|
||||
$settings['plugins']['stylescombo']['styles'] = '';
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected['stylesSet'] = array();
|
||||
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.');
|
||||
|
||||
// Configure the optional "styles" setting in odd ways that shouldn't affect
|
||||
// the end result.
|
||||
$settings['plugins']['stylescombo']['styles'] = " \n";
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor));
|
||||
$settings['plugins']['stylescombo']['styles'] = "\r\n \n \r \n ";
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.');
|
||||
|
||||
// Now configure it properly, the end result should change.
|
||||
$settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.mAgical.Callout|Callout";
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected['stylesSet'] = array(
|
||||
array('name' => 'Title', 'element' => 'h1', 'attributes' => array('class' => 'title')),
|
||||
array('name' => 'Callout', 'element' => 'p', 'attributes' => array('class' => 'mAgical Callout')),
|
||||
);
|
||||
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.');
|
||||
|
||||
// Same configuration, but now interspersed with nonsense. Should yield the
|
||||
// same result.
|
||||
$settings['plugins']['stylescombo']['styles'] = " h1 .title | Title \r \n\r \np.mAgical .Callout|Callout\r";
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.');
|
||||
|
||||
// Slightly different configuration: class names are optional.
|
||||
$settings['plugins']['stylescombo']['styles'] = " h1 | Title ";
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected['stylesSet'] = array(array('name' => 'Title', 'element' => 'h1'));
|
||||
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.');
|
||||
|
||||
// Invalid syntax should cause stylesSet to be set to FALSE.
|
||||
$settings['plugins']['stylescombo']['styles'] = "h1";
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$expected['stylesSet'] = FALSE;
|
||||
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests language list availability in CKEditor.
|
||||
*/
|
||||
function testLanguages() {
|
||||
// Get CKEditor supported language codes and spot-check.
|
||||
$this->enableModules(array('language'));
|
||||
$this->installConfig(array('language'));
|
||||
$langcodes = $this->ckeditor->getLangcodes();
|
||||
|
||||
// Language codes transformed with browser mappings.
|
||||
$this->assertTrue($langcodes['pt-pt'] == 'pt', '"pt" properly resolved');
|
||||
$this->assertTrue($langcodes['zh-hans'] == 'zh-cn', '"zh-hans" properly resolved');
|
||||
|
||||
// Language code both in Drupal and CKEditor.
|
||||
$this->assertTrue($langcodes['gl'] == 'gl', '"gl" properly resolved');
|
||||
|
||||
// Language codes only in CKEditor.
|
||||
$this->assertTrue($langcodes['en-au'] == 'en-au', '"en-au" properly resolved');
|
||||
$this->assertTrue($langcodes['sr-latn'] == 'sr-latn', '"sr-latn" properly resolved');
|
||||
|
||||
// No locale module, so even though languages are enabled, CKEditor should
|
||||
// still be in English.
|
||||
$this->assertCKEditorLanguage('en');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that CKEditor plugins participate in JS translation.
|
||||
*/
|
||||
function testJSTranslation() {
|
||||
$this->enableModules(array('language', 'locale'));
|
||||
$this->installSchema('locale', 'locales_source');
|
||||
$this->installSchema('locale', 'locales_location');
|
||||
$this->installSchema('locale', 'locales_target');
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->ckeditor->getJSSettings($editor);
|
||||
$localeStorage = $this->container->get('locale.storage');
|
||||
$string = $localeStorage->findString(array('source' => 'Edit Link', 'context' => ''));
|
||||
$this->assertTrue(!empty($string), 'String from JavaScript file saved.');
|
||||
|
||||
// With locale module, CKEditor should not adhere to the language selected.
|
||||
$this->assertCKEditorLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that CKEditor picks the expected language when French is default.
|
||||
*
|
||||
* @param string $langcode
|
||||
* Language code to assert for. Defaults to French. That is the default
|
||||
* language set in this assertion.
|
||||
*/
|
||||
protected function assertCKEditorLanguage($langcode = 'fr') {
|
||||
// Set French as the site default language.
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
$this->config('system.site')->set('default_langcode', 'fr')->save();
|
||||
|
||||
// Reset the language manager so new negotiations attempts will fall back on
|
||||
// French. Reinject the language manager CKEditor to use the current one.
|
||||
$this->container->get('language_manager')->reset();
|
||||
$this->ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
|
||||
|
||||
// Test that we now get the expected language.
|
||||
$editor = Editor::load('filtered_html');
|
||||
$settings = $this->ckeditor->getJSSettings($editor);
|
||||
$this->assertEqual($settings['language'], $langcode);
|
||||
}
|
||||
|
||||
protected function getDefaultInternalConfig() {
|
||||
return array(
|
||||
'customConfig' => '',
|
||||
'pasteFromWordPromptCleanup' => TRUE,
|
||||
'resize_dir' => 'vertical',
|
||||
'justifyClasses' => array('text-align-left', 'text-align-center', 'text-align-right', 'text-align-justify'),
|
||||
'entities' => FALSE,
|
||||
'disableNativeSpellChecker' => FALSE,
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultAllowedContentConfig() {
|
||||
return [
|
||||
'h2' => ['attributes' => 'id', 'styles' => FALSE, 'classes' => FALSE],
|
||||
'h3' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
|
||||
'h4' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
|
||||
'h5' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
|
||||
'h6' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
|
||||
'p' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
|
||||
'br' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
|
||||
'strong' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
|
||||
'a' => ['attributes' => 'href,hreflang', 'styles' => FALSE, 'classes' => FALSE],
|
||||
'*' => ['attributes' => 'lang,dir', 'styles' => FALSE, 'classes' => FALSE],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getDefaultDisallowedContentConfig() {
|
||||
return array(
|
||||
'*' => array('attributes' => 'on*'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultToolbarConfig() {
|
||||
return array(
|
||||
array(
|
||||
'name' => 'Formatting',
|
||||
'items' => array('Bold', 'Italic',),
|
||||
),
|
||||
array(
|
||||
'name' => 'Links',
|
||||
'items' => array('DrupalLink', 'DrupalUnlink',),
|
||||
),
|
||||
array(
|
||||
'name' => 'Lists',
|
||||
'items' => array('BulletedList', 'NumberedList',),
|
||||
),
|
||||
array(
|
||||
'name' => 'Media',
|
||||
'items' => array('Blockquote', 'DrupalImage',),
|
||||
),
|
||||
array(
|
||||
'name' => 'Tools',
|
||||
'items' => array('Source',),
|
||||
),
|
||||
'/',
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultContentsCssConfig() {
|
||||
return array(
|
||||
file_url_transform_relative(file_create_url('core/modules/ckeditor/css/ckeditor-iframe.css')),
|
||||
file_url_transform_relative(file_create_url('core/modules/system/css/components/align.module.css')),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginInterface;
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines the "Llama" plugin, with a CKEditor "llama" feature.
|
||||
*
|
||||
* This feature does not correspond to a toolbar button. Because this plugin
|
||||
* does not implement the CKEditorPluginContextualInterface nor the
|
||||
* CKEditorPluginButtonsInterface interface, there is no way of actually loading
|
||||
* this plugin.
|
||||
*
|
||||
* @see \Drupal\ckeditor_test\Plugin\CKEditorPlugin\LlamaContextual
|
||||
* @see \Drupal\ckeditor_test\Plugin\CKEditorPlugin\LlamaButton
|
||||
* @see \Drupal\ckeditor_test\Plugin\CKEditorPlugin\LlamaContextualAndButton
|
||||
* @see \Drupal\ckeditor_test\Plugin\CKEditorPlugin\LlamaCss
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "llama",
|
||||
* label = @Translation("Llama")
|
||||
* )
|
||||
*/
|
||||
class Llama extends PluginBase implements CKEditorPluginInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getDependencies(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getLibraries(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function isInternal() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginButtonsInterface;
|
||||
|
||||
/**
|
||||
* Defines a "LlamaButton" plugin, with a toolbar builder-enabled "llama" feature.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "llama_button",
|
||||
* label = @Translation("Llama Button")
|
||||
* )
|
||||
*/
|
||||
class LlamaButton extends Llama implements CKEditorPluginButtonsInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getButtons() {
|
||||
return array(
|
||||
'Llama' => array(
|
||||
'label' => t('Insert Llama'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama_button.js';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines a "Llama" plugin, with a contextually enabled "llama" feature.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "llama_contextual",
|
||||
* label = @Translation("Contextual Llama")
|
||||
* )
|
||||
*/
|
||||
class LlamaContextual extends Llama implements CKEditorPluginContextualInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function isEnabled(Editor $editor) {
|
||||
// Automatically enable this plugin if the Underline button is enabled.
|
||||
$settings = $editor->getSettings();
|
||||
foreach ($settings['toolbar']['rows'] as $row) {
|
||||
foreach ($row as $group) {
|
||||
if (in_array('Strike', $group['items'])) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama_contextual.js';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginButtonsInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines a "LlamaContextualAndbutton" plugin, with a contextually OR toolbar
|
||||
* builder-enabled "llama" feature.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "llama_contextual_and_button",
|
||||
* label = @Translation("Contextual Llama With Button")
|
||||
* )
|
||||
*/
|
||||
class LlamaContextualAndButton extends Llama implements CKEditorPluginContextualInterface, CKEditorPluginButtonsInterface, CKEditorPluginConfigurableInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function isEnabled(Editor $editor) {
|
||||
// Automatically enable this plugin if the Strike button is enabled.
|
||||
$settings = $editor->getSettings();
|
||||
foreach ($settings['toolbar']['rows'] as $row) {
|
||||
foreach ($row as $group) {
|
||||
if (in_array('Strike', $group['items'])) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getButtons() {
|
||||
return array(
|
||||
'Llama' => array(
|
||||
'label' => t('Insert Llama'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama_contextual_and_button.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
|
||||
// Defaults.
|
||||
$config = array('ultra_llama_mode' => FALSE);
|
||||
$settings = $editor->getSettings();
|
||||
if (isset($settings['plugins']['llama_contextual_and_button'])) {
|
||||
$config = $settings['plugins']['llama_contextual_and_button'];
|
||||
}
|
||||
|
||||
$form['ultra_llama_mode'] = array(
|
||||
'#title' => t('Ultra llama mode'),
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $config['ultra_llama_mode'],
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginButtonsInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginCssInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines a "LlamaCss" plugin, with an associated "llama" CSS.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "llama_css",
|
||||
* label = @Translation("Llama CSS")
|
||||
* )
|
||||
*/
|
||||
class LlamaCss extends Llama implements CKEditorPluginButtonsInterface, CKEditorPluginCssInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getButtons() {
|
||||
return array(
|
||||
'LlamaCSS' => array(
|
||||
'label' => t('Insert Llama CSS'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getCssFiles(Editor $editor) {
|
||||
return array(
|
||||
drupal_get_path('module', 'ckeditor_test') . '/css/llama.css'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama_css.js';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\Filter;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\filter\FilterProcessResult;
|
||||
use Drupal\filter\Plugin\FilterBase;
|
||||
|
||||
/**
|
||||
* A filter that adds a test attribute to any configured HTML tags.
|
||||
*
|
||||
* @Filter(
|
||||
* id = "test_attribute_filter",
|
||||
* title = @Translation("Test Attribute Filter"),
|
||||
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE,
|
||||
* settings = {
|
||||
* "tags" = {},
|
||||
* },
|
||||
* weight = -10
|
||||
* )
|
||||
*/
|
||||
class TestAttributeFilter extends FilterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process($text, $langcode) {
|
||||
$document = Html::load($text);
|
||||
foreach ($this->settings['tags'] as $tag) {
|
||||
$tag_elements = $document->getElementsByTagName($tag);
|
||||
foreach ($tag_elements as $tag_element) {
|
||||
$tag_element->setAttribute('test_attribute', 'test attribute value');
|
||||
}
|
||||
}
|
||||
return new FilterProcessResult(Html::serialize($document));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\ckeditor\FunctionalJavascript;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityFormDisplay;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* Tests the integration of CKEditor.
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorIntegrationTest extends JavascriptTestBase {
|
||||
|
||||
/**
|
||||
* The account.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $account;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['node', 'ckeditor', 'filter'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a text format and associate CKEditor.
|
||||
$filtered_html_format = FilterFormat::create([
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
]);
|
||||
$filtered_html_format->save();
|
||||
|
||||
Editor::create([
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
])->save();
|
||||
|
||||
// Create a node type for testing.
|
||||
NodeType::create(['type' => 'page', 'name' => 'page'])->save();
|
||||
|
||||
$field_storage = FieldStorageConfig::loadByName('node', 'body');
|
||||
|
||||
// Create a body field instance for the 'page' node type.
|
||||
FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'page',
|
||||
'label' => 'Body',
|
||||
'settings' => ['display_summary' => TRUE],
|
||||
'required' => TRUE,
|
||||
])->save();
|
||||
|
||||
// Assign widget settings for the 'default' form mode.
|
||||
EntityFormDisplay::create([
|
||||
'targetEntityType' => 'node',
|
||||
'bundle' => 'page',
|
||||
'mode' => 'default',
|
||||
'status' => TRUE,
|
||||
])->setComponent('body', ['type' => 'text_textarea_with_summary'])
|
||||
->save();
|
||||
|
||||
$this->account = $this->drupalCreateUser([
|
||||
'administer nodes',
|
||||
'create page content',
|
||||
'use text format filtered_html',
|
||||
]);
|
||||
$this->drupalLogin($this->account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the fragment link to a textarea works with CKEditor enabled.
|
||||
*/
|
||||
public function testFragmentLink() {
|
||||
$session = $this->getSession();
|
||||
$web_assert = $this->assertSession();
|
||||
$ckeditor_id = '#cke_edit-body-0-value';
|
||||
|
||||
$this->drupalGet('node/add/page');
|
||||
|
||||
$session->getPage();
|
||||
|
||||
// Add a bottom margin to the title field to be sure the body field is not
|
||||
// visible. PhantomJS runs with a resolution of 1024x768px.
|
||||
$session->executeScript("document.getElementById('edit-title-0-value').style.marginBottom = '800px';");
|
||||
|
||||
// Check that the CKEditor-enabled body field is currently not visible in
|
||||
// the viewport.
|
||||
$web_assert->assertNotVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is visible.');
|
||||
|
||||
$before_url = $session->getCurrentUrl();
|
||||
|
||||
// Trigger a hash change with as target the hidden textarea.
|
||||
$session->executeScript("location.hash = '#edit-body-0-value';");
|
||||
|
||||
// Check that the CKEditor-enabled body field is visible in the viewport.
|
||||
$web_assert->assertVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is not visible.');
|
||||
|
||||
// Use JavaScript to go back in the history instead of
|
||||
// \Behat\Mink\Session::back() because that function doesn't work after a
|
||||
// hash change.
|
||||
$session->executeScript("history.back();");
|
||||
|
||||
$after_url = $session->getCurrentUrl();
|
||||
|
||||
// Check that going back in the history worked.
|
||||
self::assertEquals($before_url, $after_url, 'History back works.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\ckeditor\Kernel\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class InternalTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'ckeditor',
|
||||
'ckeditor_test',
|
||||
'filter',
|
||||
'editor',
|
||||
];
|
||||
|
||||
/**
|
||||
* A testing text format.
|
||||
*
|
||||
* @var \Drupal\filter\Entity\FilterFormat
|
||||
*/
|
||||
protected $format;
|
||||
|
||||
/**
|
||||
* A testing text editor.
|
||||
*
|
||||
* @var \Drupal\editor\Entity\Editor
|
||||
*/
|
||||
protected $editor;
|
||||
|
||||
/**
|
||||
* The CKEditor plugin manager.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $ckeditorPluginManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installEntitySchema('editor');
|
||||
$this->installEntitySchema('filter_format');
|
||||
|
||||
$this->format = FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => $this->randomMachineName(),
|
||||
]);
|
||||
$this->format->save();
|
||||
|
||||
$this->editor = Editor::create([
|
||||
'editor' => 'ckeditor',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'rows' => [
|
||||
[
|
||||
[
|
||||
'name' => 'Enabled Buttons',
|
||||
'items' => [
|
||||
'Format',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->editor->save();
|
||||
|
||||
$this->ckeditorPluginManager = $this->container->get('plugin.manager.ckeditor.plugin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the format tags settings.
|
||||
*
|
||||
* @dataProvider formatTagsSettingsTestCases
|
||||
*/
|
||||
public function testFormatTagsSettings($filter_plugins, $expected_format_tags) {
|
||||
foreach ($filter_plugins as $filter_plugin_id => $filter_plugin_settings) {
|
||||
$this->format->setFilterConfig($filter_plugin_id, $filter_plugin_settings);
|
||||
}
|
||||
$this->format->save();
|
||||
|
||||
$internal_plugin = $this->ckeditorPluginManager->createInstance('internal', []);
|
||||
$plugin_config = $internal_plugin->getConfig($this->editor);
|
||||
$this->assertEquals($expected_format_tags, explode(';', $plugin_config['format_tags']));
|
||||
}
|
||||
|
||||
/**
|
||||
* A data provider for testFormatTagsSettings.
|
||||
*/
|
||||
public function formatTagsSettingsTestCases() {
|
||||
$all_tags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'];
|
||||
|
||||
return [
|
||||
'No filter plugins enabled (all tags allowed)' => [
|
||||
[],
|
||||
$all_tags,
|
||||
],
|
||||
'HTML filter plugin enabled (some tags filtered out)' => [
|
||||
[
|
||||
'filter_html' => [
|
||||
'status' => 1,
|
||||
'settings' => [
|
||||
'allowed_html' => '<h1> <h2>',
|
||||
'filter_html_help' => 1,
|
||||
'filter_html_nofollow' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
['p', 'h1', 'h2'],
|
||||
],
|
||||
'Test attribute filter enabled (all tags allowed)' => [
|
||||
[
|
||||
'test_attribute_filter' => [
|
||||
'status' => 1,
|
||||
'settings' => [
|
||||
'tags' => ['h1', 'h2'],
|
||||
],
|
||||
],
|
||||
],
|
||||
$all_tags,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\ckeditor\Unit;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginManager;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor\CKEditorPluginManager
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorPluginManagerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Provides a list of configs to test.
|
||||
*/
|
||||
public function providerGetEnabledButtons() {
|
||||
return [
|
||||
'empty' => [
|
||||
[],
|
||||
[]
|
||||
],
|
||||
'1 row, 1 group' => [
|
||||
[
|
||||
// Row 1.
|
||||
[
|
||||
// Group 1.
|
||||
['name' => 'Formatting', 'items' => ['Bold', 'Italic']],
|
||||
]
|
||||
],
|
||||
['Bold', 'Italic']
|
||||
],
|
||||
'1 row, >1 groups' => [
|
||||
[
|
||||
// Row 1.
|
||||
[
|
||||
// Group 1.
|
||||
['name' => 'Formatting', 'items' => ['Bold', 'Italic']],
|
||||
// Group 2.
|
||||
['name' => 'Linking', 'items' => ['Link']],
|
||||
],
|
||||
],
|
||||
['Bold', 'Italic', 'Link']
|
||||
],
|
||||
'2 rows, 1 group each' => [
|
||||
[
|
||||
// Row 1.
|
||||
[
|
||||
// Group 1.
|
||||
['name' => 'Formatting', 'items' => ['Bold', 'Italic']],
|
||||
],
|
||||
// Row 2.
|
||||
[
|
||||
// Group 1.
|
||||
['name' => 'Tools', 'items' => ['Source']],
|
||||
],
|
||||
],
|
||||
['Bold', 'Italic', 'Source'],
|
||||
],
|
||||
'2 rows, >1 groups each' => [
|
||||
[
|
||||
// Row 1.
|
||||
[
|
||||
// Group 1.
|
||||
['name' => 'Formatting', 'items' => ['Bold', 'Italic']],
|
||||
// Group 2.
|
||||
['name' => 'Linking', 'items' => ['Link']],
|
||||
],
|
||||
// Row 2.
|
||||
[
|
||||
// Group 1.
|
||||
['name' => 'Tools', 'items' => ['Source']],
|
||||
// Group 2.
|
||||
['name' => 'Advanced', 'items' => ['Llama']],
|
||||
],
|
||||
],
|
||||
['Bold', 'Italic', 'Link', 'Source', 'Llama']
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getEnabledButtons
|
||||
* @dataProvider providerGetEnabledButtons
|
||||
*/
|
||||
public function testGetEnabledButtons(array $toolbar_rows, array $expected_buttons) {
|
||||
$editor = $this->prophesize(Editor::class);
|
||||
$editor->getSettings()
|
||||
->willReturn(['toolbar' => ['rows' => $toolbar_rows]]);
|
||||
|
||||
$enabled_buttons = CKEditorPluginManager::getEnabledButtons($editor->reveal());
|
||||
$this->assertEquals($expected_buttons, $enabled_buttons);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\ckeditor\Unit\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\Plugin\CKEditorPlugin\Language;
|
||||
use Drupal\Core\Language\LanguageManager;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor\Plugin\CKEditorPlugin\Language
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class LanguageTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The plugin under test.
|
||||
*
|
||||
* @var \Drupal\ckeditor\Plugin\CKEditorPlugin\Language
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
$this->plugin = new Language([], $this->randomMachineName(), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of configs to test.
|
||||
*/
|
||||
public function providerGetConfig() {
|
||||
return [
|
||||
['un', count(LanguageManager::getUnitedNationsLanguageList())],
|
||||
['all', count(LanguageManager::getStandardLanguageList())],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getConfig
|
||||
*
|
||||
* @dataProvider providerGetConfig
|
||||
*/
|
||||
public function testGetConfig($language_list, $expected_number) {
|
||||
$editor = $this->getMockBuilder('Drupal\editor\Entity\Editor')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$editor->expects($this->once())
|
||||
->method('getSettings')
|
||||
->willReturn(['plugins' => ['language' => ['language_list' => $language_list]]]);
|
||||
|
||||
$config = $this->plugin->getConfig($editor);
|
||||
|
||||
$this->assertInternalType('array', $config);
|
||||
$this->assertTrue(in_array('ar:Arabic:rtl', $config['language_list']));
|
||||
$this->assertTrue(in_array('zh-hans:Chinese, Simplified', $config['language_list']));
|
||||
$this->assertTrue(in_array('en:English', $config['language_list']));
|
||||
$this->assertTrue(in_array('fr:French', $config['language_list']));
|
||||
$this->assertTrue(in_array('ru:Russian', $config['language_list']));
|
||||
$this->assertTrue(in_array('ar:Arabic:rtl', $config['language_list']));
|
||||
$this->assertEquals($expected_number, count($config['language_list']));
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue