Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
139
core/modules/ckeditor/ckeditor.admin.inc
Normal file
139
core/modules/ckeditor/ckeditor.admin.inc
Normal file
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Callbacks and theming for the CKEditor toolbar configuration UI.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
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.
|
||||
*/
|
||||
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 = SafeMarkup::set($button['image_alternative' . $rtl]);
|
||||
}
|
||||
elseif (isset($button['image_alternative'])) {
|
||||
$value = SafeMarkup::set($button['image_alternative']);
|
||||
}
|
||||
elseif (isset($button['image'])) {
|
||||
$value = array(
|
||||
'#theme' => 'image',
|
||||
'#uri' => $button['image' . $rtl],
|
||||
'#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) {
|
||||
$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 $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);
|
||||
}
|
||||
}
|
60
core/modules/ckeditor/ckeditor.api.php
Normal file
60
core/modules/ckeditor/ckeditor.api.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?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 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 relative to the Drupal root.
|
||||
* @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
core/modules/ckeditor/ckeditor.info.yml
Normal file
8
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
|
75
core/modules/ckeditor/ckeditor.libraries.yml
Normal file
75
core/modules/ckeditor/ckeditor.libraries.yml
Normal file
|
@ -0,0 +1,75 @@
|
|||
drupal.ckeditor:
|
||||
version: VERSION
|
||||
js:
|
||||
js/ckeditor.js: {}
|
||||
css:
|
||||
state:
|
||||
css/ckeditor.css: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/drupal.debounce
|
||||
- core/ckeditor
|
||||
- editor/drupal.editor
|
||||
|
||||
drupal.ckeditor.plugins.drupalimagecaption:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css: {}
|
||||
dependencies:
|
||||
- filter/caption
|
||||
|
||||
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/jquery.once
|
||||
- core/drupal.vertical-tabs
|
||||
- core/drupalSettings
|
173
core/modules/ckeditor/ckeditor.module
Normal file
173
core/modules/ckeditor/ckeditor.module
Normal file
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provides integration with the CKEditor WYSIWYG editor.
|
||||
*/
|
||||
|
||||
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 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 <a href="!doc_url">the 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 .= '</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';
|
||||
}
|
||||
|
||||
// Add the filter caption CSS if the text format associated with this text
|
||||
// editor uses the filter_align filter. This is used by the included
|
||||
// CKEditor DrupalImageCaption plugin.
|
||||
if ($editor->getFilterFormat()->filters('filter_align')->status) {
|
||||
$css[] = drupal_get_path('module', 'ckeditor') . '/css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default theme's CKEditor stylesheets defined in the .info file.
|
||||
*
|
||||
* Themes may specify iframe-specific CSS files for use with CKEditor by
|
||||
* including a "ckeditor_stylesheets" key in the theme .info file.
|
||||
*
|
||||
* @code
|
||||
* ckeditor_stylesheets[] = css/ckeditor-iframe.css
|
||||
* @endcode
|
||||
*/
|
||||
function _ckeditor_theme_css($theme = NULL) {
|
||||
$css = array();
|
||||
if (!isset($theme)) {
|
||||
$theme = \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 => $path) {
|
||||
$css[$key] = $theme_path . '/' . $path;
|
||||
}
|
||||
}
|
||||
if (isset($info['base theme'])) {
|
||||
$css = array_merge(_ckeditor_theme_css($info['base theme']), $css);
|
||||
}
|
||||
}
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_insert() for 'filter_format'.
|
||||
*
|
||||
* Recalculates the 'format_tags' CKEditor setting when a text format is added.
|
||||
*
|
||||
* @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
|
||||
* @see ckeditor_rebuild()
|
||||
*/
|
||||
function ckeditor_filter_format_insert() {
|
||||
ckeditor_rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_update() for 'filter_format'.
|
||||
*
|
||||
* Recalculates the 'format_tags' CKEditor setting when a text format changes.
|
||||
*
|
||||
* @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
|
||||
* @see ckeditor_rebuild()
|
||||
*/
|
||||
function ckeditor_filter_format_update() {
|
||||
ckeditor_rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_rebuild().
|
||||
*
|
||||
* Calculates the 'format_tags' CKEditor setting for each text format.
|
||||
*
|
||||
* If this wouldn't happen in hook_rebuild(), then the first drupal_render()
|
||||
* call that occurs for a page that contains a #type 'text_format' element will
|
||||
* cause the CKEditor::getJSSettings() to be called, which will cause
|
||||
* Internal::generateFormatTagsSetting() to be called, which calls
|
||||
* check_markup(), which finally calls drupal_render() non-recursively, because
|
||||
* a filter might add placeholders to replace.
|
||||
* This would be a root call inside a root call, which breaks the stack-based
|
||||
* logic for bubbling rendering metadata.
|
||||
* Therefore this pre-calculates the needed values, and hence performs the
|
||||
* check_markup() calls outside of a drupal_render() call tree.
|
||||
*
|
||||
* @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
|
||||
* @see ckeditor_filter_format_insert()
|
||||
* @see ckeditor_filter_format_update()
|
||||
*/
|
||||
function ckeditor_rebuild() {
|
||||
/** @var \Drupal\filter\FilterFormatInterface[] $formats */
|
||||
$formats = filter_formats();
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$key = 'ckeditor_internal_format_tags:' . $format->id();
|
||||
|
||||
// The <p> tag is always allowed — HTML without <p> tags is nonsensical.
|
||||
$format_tags = array('p');
|
||||
|
||||
// Given the list of possible format tags, automatically determine whether
|
||||
// the current text format allows this tag, and thus whether it should show
|
||||
// up in the "Format" dropdown.
|
||||
$possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre');
|
||||
foreach ($possible_format_tags as $tag) {
|
||||
$input = '<' . $tag . '>TEST</' . $tag . '>';
|
||||
$output = trim(check_markup($input, $format->id()));
|
||||
if ($input == $output) {
|
||||
$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, which is
|
||||
// guaranteed by the hook_ENTITY_TYPE_update() and hook_ENTITY_TYPE_insert()
|
||||
// hook implementations.
|
||||
\Drupal::state()->set($key, $format_tags);
|
||||
}
|
||||
}
|
4
core/modules/ckeditor/ckeditor.services.yml
Normal file
4
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
|
43
core/modules/ckeditor/config/schema/ckeditor.schema.yml
Normal file
43
core/modules/ckeditor/config/schema/ckeditor.schema.yml
Normal file
|
@ -0,0 +1,43 @@
|
|||
# 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\ckeditor\plugin\StylesCombo
|
||||
ckeditor.plugin.stylescombo:
|
||||
type: mapping
|
||||
label: 'Styles dropdown'
|
||||
mapping:
|
||||
styles:
|
||||
type: string
|
||||
label: 'List of styles'
|
23
core/modules/ckeditor/css/ckeditor-iframe.css
Normal file
23
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;
|
||||
}
|
311
core/modules/ckeditor/css/ckeditor.admin.css
Normal file
311
core/modules/ckeditor/css/ckeditor.admin.css
Normal file
|
@ -0,0 +1,311 @@
|
|||
/**
|
||||
* @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;
|
||||
}
|
||||
.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;
|
||||
width: 80%;
|
||||
}
|
||||
.ckeditor-toolbar-disabled .ckeditor-toolbar-dividers {
|
||||
float: right;
|
||||
width: 20%;
|
||||
}
|
||||
.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;
|
||||
direction: ltr;
|
||||
/* Firefox includes the offscreen text in the focus indicator, resulting in a
|
||||
far too wide focus indicator. This fixes that. */
|
||||
overflow: hidden;
|
||||
}
|
||||
.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
core/modules/ckeditor/css/ckeditor.css
Normal file
39
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;
|
||||
}
|
477
core/modules/ckeditor/js/ckeditor.admin.js
Normal file
477
core/modules/ckeditor/js/ckeditor.admin.js
Normal file
|
@ -0,0 +1,477 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor button and group configuration user interface.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, _, CKEDITOR) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.ckeditor = Drupal.ckeditor || {};
|
||||
|
||||
/**
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
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('.form-item-editor-settings-toolbar-button-groups')
|
||||
.hide()
|
||||
// Return the textarea child node from this expression.
|
||||
.find('textarea');
|
||||
|
||||
// The HTML for the CKEditor configuration is assembled on the server and
|
||||
// and sent to the client as a serialized DOM fragment.
|
||||
$configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
|
||||
|
||||
// Create a configuration model.
|
||||
var model = Drupal.ckeditor.models.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 a change in CKEditor config DOM structure into 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 a change in CKEditor config DOM structure into 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 Drupal 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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
|
||||
return '<a class="ckeditor-groupnames-toggle" role="button" aria-pressed="false"></a>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a button that will prompt the user to name a new button group.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
Drupal.theme.ckeditorNewButtonGroup = function () {
|
||||
return '<li class="ckeditor-add-new-group"><button role="button" aria-label="' + Drupal.t('Add a CKEditor button group to the end of this row.') + '">' + Drupal.t('Add group') + '</button></li>';
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, _, CKEDITOR);
|
42
core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
Normal file
42
core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor 'drupalimage' plugin admin behavior.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Provides the summary for the "drupalimage" plugin settings vertical tab.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
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);
|
259
core/modules/ckeditor/js/ckeditor.js
vendored
Normal file
259
core/modules/ckeditor/js/ckeditor.js
vendored
Normal file
|
@ -0,0 +1,259 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor implementation of {@link Drupal.editors} API.
|
||||
*/
|
||||
|
||||
(function (Drupal, debounce, CKEDITOR, $) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editors.ckeditor = {
|
||||
|
||||
/**
|
||||
* Editor attach callback.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {string} format
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
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});
|
||||
|
||||
// CKEditor initializes itself in a read-only state if the 'disabled'
|
||||
// attribute is set. It does not respect the 'readonly' attribute,
|
||||
// however, so we set the 'readOnly' configuration property manually in
|
||||
// that case, for the CKEditor instance that's about to be created.
|
||||
format.editorSettings.readOnly = element.hasAttribute('readonly');
|
||||
|
||||
return !!CKEDITOR.replace(element, format.editorSettings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Editor detach callback.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {string} format
|
||||
* @param {string} trigger
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {function} callback
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {object} format
|
||||
* @param {string} mainToolbarId
|
||||
* @param {string} floatedToolbarId
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} format
|
||||
*/
|
||||
_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('editor-dialog');
|
||||
dialogSettings.dialogClass = classes.join(' ');
|
||||
dialogSettings.autoResize = Drupal.checkWidthBreakpoint(600);
|
||||
|
||||
// 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) {
|
||||
$('.editor-dialog').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;
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Drupal.debounce, CKEDITOR, jQuery);
|
121
core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
Normal file
121
core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor SylesCombo 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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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);
|
70
core/modules/ckeditor/js/models/Model.js
Normal file
70
core/modules/ckeditor/js/models/Model.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* @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, 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);
|
BIN
core/modules/ckeditor/js/plugins/drupalimage/image.png
Normal file
BIN
core/modules/ckeditor/js/plugins/drupalimage/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 470 B |
230
core/modules/ckeditor/js/plugins/drupalimage/plugin.js
Normal file
230
core/modules/ckeditor/js/plugins/drupalimage/plugin.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
/**
|
||||
* @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',
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Override requiredContent & allowedContent.
|
||||
widgetDefinition.requiredContent = 'img[alt,src,width,height,data-entity-type,data-entity-uuid]';
|
||||
widgetDefinition.allowedContent.img.attributes += ',!data-entity-type,!data-entity-uuid';
|
||||
// We don't allow <figure>, <figcaption>, <div> or <p> in our downcast.
|
||||
delete widgetDefinition.allowedContent.figure;
|
||||
delete widgetDefinition.allowedContent.figcaption;
|
||||
delete widgetDefinition.allowedContent.div;
|
||||
delete widgetDefinition.allowedContent.p;
|
||||
|
||||
// Override the 'link' part, to completely disable image2's link
|
||||
// support: http://dev.ckeditor.com/ticket/11341.
|
||||
widgetDefinition.parts.link = 'This is a nonsensical selector to disable this functionality completely';
|
||||
|
||||
// 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-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;
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
// 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,width,height,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',
|
||||
icon: this.path + '/image.png'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Disable image2's integration with the link/drupallink plugins: don't
|
||||
// allow the widget itself to become a link. Support for that may be added
|
||||
// by an text filter that adds a data- attribute specifically for that.
|
||||
afterInit: function (editor) {
|
||||
if (editor.plugins.drupallink) {
|
||||
var cmd = editor.getCommand('drupallink');
|
||||
// Needs to be refreshed on selection changes.
|
||||
cmd.contextSensitive = 1;
|
||||
// Disable command and cancel event when the image widget is selected.
|
||||
cmd.on('refresh', function (evt) {
|
||||
var widget = editor.widgets.focused;
|
||||
if (widget && widget.name === 'image') {
|
||||
this.setState(CKEDITOR.TRISTATE_DISABLED);
|
||||
evt.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, CKEDITOR);
|
247
core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
Normal file
247
core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
Normal file
|
@ -0,0 +1,247 @@
|
|||
/**
|
||||
* @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);
|
||||
|
||||
// Override requiredContent & allowedContent.
|
||||
widgetDefinition.requiredContent = 'img[alt,src,width,height,data-entity-type,data-entity-uuid,data-align,data-caption]';
|
||||
widgetDefinition.allowedContent.img.attributes += ',data-align,data-caption';
|
||||
|
||||
// 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.
|
||||
widgetDefinition.downcast = function (element) {
|
||||
// Find an image element in the one being downcasted (can be itself).
|
||||
var img = findElementByName(element, '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;
|
||||
}
|
||||
}
|
||||
attrs['data-entity-type'] = this.data['data-entity-type'];
|
||||
attrs['data-entity-uuid'] = this.data['data-entity-uuid'];
|
||||
|
||||
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.
|
||||
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;
|
||||
}
|
||||
|
||||
var attrs = element.attributes;
|
||||
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);
|
||||
}
|
||||
};
|
||||
};
|
||||
}, null, null, 20); // Low priority to ensure drupalimage's event handler runs first.
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {string} name
|
||||
*
|
||||
* @return {CKEDITOR.htmlParser.element}
|
||||
*/
|
||||
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);
|
BIN
core/modules/ckeditor/js/plugins/drupallink/link.png
Normal file
BIN
core/modules/ckeditor/js/plugins/drupallink/link.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 328 B |
232
core/modules/ckeditor/js/plugins/drupallink/plugin.js
Normal file
232
core/modules/ckeditor/js/plugins/drupallink/plugin.js
Normal file
|
@ -0,0 +1,232 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Link plugin.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, CKEDITOR) {
|
||||
|
||||
"use strict";
|
||||
|
||||
CKEDITOR.plugins.add('drupallink', {
|
||||
init: function (editor) {
|
||||
// Add the commands for link and unlink.
|
||||
editor.addCommand('drupallink', {
|
||||
allowedContent: 'a[!href,target]',
|
||||
requiredContent: 'a[href]',
|
||||
modes: {wysiwyg: 1},
|
||||
canUndo: true,
|
||||
exec: function (editor) {
|
||||
var linkElement = getSelectedLink(editor);
|
||||
var linkDOMElement = null;
|
||||
|
||||
// Set existing values based on selected element.
|
||||
var existingValues = {};
|
||||
if (linkElement && linkElement.$) {
|
||||
linkDOMElement = linkElement.$;
|
||||
|
||||
// Populate an array with the link's current attributes.
|
||||
var attribute = null;
|
||||
var attributeName;
|
||||
for (var attrIndex = 0; attrIndex < linkDOMElement.attributes.length; attrIndex++) {
|
||||
attribute = linkDOMElement.attributes.item(attrIndex);
|
||||
attributeName = attribute.nodeName.toLowerCase();
|
||||
// Don't consider data-cke-saved- attributes; they're just there to
|
||||
// work around browser quirks.
|
||||
if (attributeName.substring(0, 15) === 'data-cke-saved-') {
|
||||
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.
|
||||
existingValues[attributeName] = linkElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare a save callback to be used upon saving the dialog.
|
||||
var saveCallback = function (returnValues) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Ignore a disabled target attribute.
|
||||
if (returnValues.attributes.target === 0) {
|
||||
delete returnValues.attributes.target;
|
||||
}
|
||||
|
||||
// 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,
|
||||
allowedContent: 'a[!href]',
|
||||
requiredContent: 'a[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',
|
||||
icon: this.path + '/link.png'
|
||||
});
|
||||
editor.ui.addButton('DrupalUnlink', {
|
||||
label: Drupal.t('Unlink'),
|
||||
command: 'drupalunlink',
|
||||
icon: this.path + '/unlink.png'
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* @return {?bool}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, CKEDITOR);
|
BIN
core/modules/ckeditor/js/plugins/drupallink/unlink.png
Normal file
BIN
core/modules/ckeditor/js/plugins/drupallink/unlink.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 312 B |
228
core/modules/ckeditor/js/views/AuralView.js
Normal file
228
core/modules/ckeditor/js/views/AuralView.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
/**
|
||||
* @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
|
||||
* @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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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);
|
362
core/modules/ckeditor/js/views/ControllerView.js
Normal file
362
core/modules/ckeditor/js/views/ControllerView.js
Normal file
|
@ -0,0 +1,362 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
|
||||
"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.
|
||||
var features = {};
|
||||
for (var featureName in CKEFeatureRulesMap) {
|
||||
if (CKEFeatureRulesMap.hasOwnProperty(featureName)) {
|
||||
var feature = new Drupal.EditorFeature(featureName);
|
||||
convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]);
|
||||
features[featureName] = feature;
|
||||
}
|
||||
}
|
||||
|
||||
callback(features);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 = 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.
|
||||
*/
|
||||
disableFeaturesDisallowedByFilters: function (features) {
|
||||
this.model.set('featuresMetadata', features);
|
||||
|
||||
// Ensure that toolbar configuration changes are broadcast.
|
||||
this.broadcastConfigurationChanges(this.$el);
|
||||
|
||||
// Initialization: not all of the default toolbar buttons may be allowed
|
||||
// by the current filter settings. Remove any of the default toolbar
|
||||
// buttons that require more permissive filter settings. The remaining
|
||||
// default toolbar buttons are marked as "added".
|
||||
var existingButtons = [];
|
||||
// Loop through each button group after flattening the groups from the
|
||||
// toolbar row arrays.
|
||||
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 featuresMetadata = this.model.get('featuresMetadata');
|
||||
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.
|
||||
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, '-');
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, jQuery);
|
263
core/modules/ckeditor/js/views/KeyboardView.js
Normal file
263
core/modules/ckeditor/js/views/KeyboardView.js
Normal file
|
@ -0,0 +1,263 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, jQuery);
|
262
core/modules/ckeditor/js/views/VisualView.js
Normal file
262
core/modules/ckeditor/js/views/VisualView.js
Normal file
|
@ -0,0 +1,262 @@
|
|||
/**
|
||||
* @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();
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} model
|
||||
* @param {string} [value]
|
||||
* @param {object} changedAttributes
|
||||
*
|
||||
* @return {Drupal.ckeditor.VisualView}
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
* @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
|
||||
* @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
|
||||
* @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);
|
44
core/modules/ckeditor/src/Annotation/CKEditorPlugin.php
Normal file
44
core/modules/ckeditor/src/Annotation/CKEditorPlugin.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Annotation\CKEditorPlugin.
|
||||
*/
|
||||
|
||||
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 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;
|
||||
|
||||
}
|
58
core/modules/ckeditor/src/CKEditorPluginBase.php
Normal file
58
core/modules/ckeditor/src/CKEditorPluginBase.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\CKEditorPluginBase.
|
||||
*/
|
||||
|
||||
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 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) though.
|
||||
*
|
||||
* 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();
|
||||
}
|
||||
}
|
61
core/modules/ckeditor/src/CKEditorPluginButtonsInterface.php
Normal file
61
core/modules/ckeditor/src/CKEditorPluginButtonsInterface.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\CKEditorPluginButtonsInterface.
|
||||
*/
|
||||
|
||||
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\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,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\CKEditorPluginConfigurableInterface.
|
||||
*/
|
||||
|
||||
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\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|FALSE
|
||||
* A render array for the settings form, or FALSE if there is none.
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor);
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\CKEditorPluginContextualInterface.
|
||||
*/
|
||||
|
||||
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 (e.g. 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\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);
|
||||
|
||||
}
|
102
core/modules/ckeditor/src/CKEditorPluginInterface.php
Normal file
102
core/modules/ckeditor/src/CKEditorPluginInterface.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\CKEditorPluginInterface.
|
||||
*/
|
||||
|
||||
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\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);
|
||||
}
|
185
core/modules/ckeditor/src/CKEditorPluginManager.php
Normal file
185
core/modules/ckeditor/src/CKEditorPluginManager.php
Normal file
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\CKEditorPluginManager.
|
||||
*/
|
||||
|
||||
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\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());
|
||||
// Flatten each row.
|
||||
$toolbar_rows = array();
|
||||
$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']);
|
||||
}, array());
|
||||
}
|
||||
$toolbar_buttons = array_unique(NestedArray::mergeDeepArray($toolbar_rows));
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Plugin\CKEditorPlugin\DrupalImage.
|
||||
*/
|
||||
|
||||
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/image.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,102 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Plugin\CKEditorPlugin\DrupalImageCaption.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\ckeditor\CKEditorPluginInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
|
||||
/**
|
||||
* Defines the "drupalimagecaption" plugin.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "drupalimagecaption",
|
||||
* label = @Translation("Drupal image caption widget"),
|
||||
* module = "ckeditor"
|
||||
* )
|
||||
*/
|
||||
class DrupalImageCaption extends PluginBase implements CKEditorPluginInterface, CKEditorPluginContextualInterface {
|
||||
|
||||
/**
|
||||
* {@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}
|
||||
*/
|
||||
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,67 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Plugin\CKEditorPlugin\DrupalLink.
|
||||
*/
|
||||
|
||||
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 . '/link.png',
|
||||
),
|
||||
'DrupalUnlink' => array(
|
||||
'label' => t('Unlink'),
|
||||
'image' => $path . '/unlink.png',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
522
core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
Normal file
522
core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
Normal file
|
@ -0,0 +1,522 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginBase;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 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')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal().
|
||||
*/
|
||||
public function isInternal() {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getFile().
|
||||
*/
|
||||
public function getFile() {
|
||||
// This plugin is already part of Drupal core's CKEditor build.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getConfig().
|
||||
*/
|
||||
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,
|
||||
);
|
||||
|
||||
// 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_rows = array();
|
||||
$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']);
|
||||
}, array());
|
||||
}
|
||||
$toolbar_buttons = array_unique(NestedArray::mergeDeepArray($toolbar_rows));
|
||||
if (in_array('Format', $toolbar_buttons)) {
|
||||
$config['format_tags'] = $this->generateFormatTagsSetting($editor);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons().
|
||||
*/
|
||||
public function getButtons() {
|
||||
$button = function($name, $direction = 'ltr') {
|
||||
return '<a href="#" class="cke-icon-only cke_' . $direction . '" role="button" title="' . $name . '" aria-label="' . $name . '"><span class="cke_button_icon cke_button__' . str_replace(' ', '', $name) . '_icon">' . $name . '</span></a>';
|
||||
};
|
||||
|
||||
return array(
|
||||
// "basicstyles" plugin.
|
||||
'Bold' => array(
|
||||
'label' => t('Bold'),
|
||||
'image_alternative' => $button('bold'),
|
||||
),
|
||||
'Italic' => array(
|
||||
'label' => t('Italic'),
|
||||
'image_alternative' => $button('italic'),
|
||||
),
|
||||
'Underline' => array(
|
||||
'label' => t('Underline'),
|
||||
'image_alternative' => $button('underline'),
|
||||
),
|
||||
'Strike' => array(
|
||||
'label' => t('Strike-through'),
|
||||
'image_alternative' => $button('strike'),
|
||||
),
|
||||
'Superscript' => array(
|
||||
'label' => t('Superscript'),
|
||||
'image_alternative' => $button('super script'),
|
||||
),
|
||||
'Subscript' => array(
|
||||
'label' => t('Subscript'),
|
||||
'image_alternative' => $button('sub script'),
|
||||
),
|
||||
// "removeformat" plugin.
|
||||
'RemoveFormat' => array(
|
||||
'label' => t('Remove format'),
|
||||
'image_alternative' => $button('remove format'),
|
||||
),
|
||||
// "justify" plugin.
|
||||
'JustifyLeft' => array(
|
||||
'label' => t('Align left'),
|
||||
'image_alternative' => $button('justify left'),
|
||||
),
|
||||
'JustifyCenter' => array(
|
||||
'label' => t('Align center'),
|
||||
'image_alternative' => $button('justify center'),
|
||||
),
|
||||
'JustifyRight' => array(
|
||||
'label' => t('Align right'),
|
||||
'image_alternative' => $button('justify right'),
|
||||
),
|
||||
'JustifyBlock' => array(
|
||||
'label' => t('Justify'),
|
||||
'image_alternative' => $button('justify block'),
|
||||
),
|
||||
// "list" plugin.
|
||||
'BulletedList' => array(
|
||||
'label' => t('Bullet list'),
|
||||
'image_alternative' => $button('bulleted list'),
|
||||
'image_alternative_rtl' => $button('bulleted list', 'rtl'),
|
||||
),
|
||||
'NumberedList' => array(
|
||||
'label' => t('Numbered list'),
|
||||
'image_alternative' => $button('numbered list'),
|
||||
'image_alternative_rtl' => $button('numbered list', 'rtl'),
|
||||
),
|
||||
// "indent" plugin.
|
||||
'Outdent' => array(
|
||||
'label' => t('Outdent'),
|
||||
'image_alternative' => $button('outdent'),
|
||||
'image_alternative_rtl' => $button('outdent', 'rtl'),
|
||||
),
|
||||
'Indent' => array(
|
||||
'label' => t('Indent'),
|
||||
'image_alternative' => $button('indent'),
|
||||
'image_alternative_rtl' => $button('indent', 'rtl'),
|
||||
),
|
||||
// "undo" plugin.
|
||||
'Undo' => array(
|
||||
'label' => t('Undo'),
|
||||
'image_alternative' => $button('undo'),
|
||||
'image_alternative_rtl' => $button('undo', 'rtl'),
|
||||
),
|
||||
'Redo' => array(
|
||||
'label' => t('Redo'),
|
||||
'image_alternative' => $button('redo'),
|
||||
'image_alternative_rtl' => $button('redo', 'rtl'),
|
||||
),
|
||||
// "blockquote" plugin.
|
||||
'Blockquote' => array(
|
||||
'label' => t('Blockquote'),
|
||||
'image_alternative' => $button('blockquote'),
|
||||
),
|
||||
// "horizontalrule" plugin
|
||||
'HorizontalRule' => array(
|
||||
'label' => t('Horizontal rule'),
|
||||
'image_alternative' => $button('horizontal rule'),
|
||||
),
|
||||
// "clipboard" plugin.
|
||||
'Cut' => array(
|
||||
'label' => t('Cut'),
|
||||
'image_alternative' => $button('cut'),
|
||||
'image_alternative_rtl' => $button('cut', 'rtl'),
|
||||
),
|
||||
'Copy' => array(
|
||||
'label' => t('Copy'),
|
||||
'image_alternative' => $button('copy'),
|
||||
'image_alternative_rtl' => $button('copy', 'rtl'),
|
||||
),
|
||||
'Paste' => array(
|
||||
'label' => t('Paste'),
|
||||
'image_alternative' => $button('paste'),
|
||||
'image_alternative_rtl' => $button('paste', 'rtl'),
|
||||
),
|
||||
// "pastetext" plugin.
|
||||
'PasteText' => array(
|
||||
'label' => t('Paste Text'),
|
||||
'image_alternative' => $button('paste text'),
|
||||
'image_alternative_rtl' => $button('paste text', 'rtl'),
|
||||
),
|
||||
// "pastefromword" plugin.
|
||||
'PasteFromWord' => array(
|
||||
'label' => t('Paste from Word'),
|
||||
'image_alternative' => $button('paste from word'),
|
||||
'image_alternative_rtl' => $button('paste from word', 'rtl'),
|
||||
),
|
||||
// "specialchar" plugin.
|
||||
'SpecialChar' => array(
|
||||
'label' => t('Character map'),
|
||||
'image_alternative' => $button('special char'),
|
||||
),
|
||||
'Format' => array(
|
||||
'label' => t('HTML block format'),
|
||||
'image_alternative' => '<a href="#" role="button" aria-label="' . t('Format') . '"><span class="ckeditor-button-dropdown">' . t('Format') . '<span class="ckeditor-button-arrow"></span></span></a>',
|
||||
),
|
||||
// "table" plugin.
|
||||
'Table' => array(
|
||||
'label' => t('Table'),
|
||||
'image_alternative' => $button('table'),
|
||||
),
|
||||
// "showblocks" plugin.
|
||||
'ShowBlocks' => array(
|
||||
'label' => t('Show blocks'),
|
||||
'image_alternative' => $button('show blocks'),
|
||||
'image_alternative_rtl' => $button('show blocks', 'rtl'),
|
||||
),
|
||||
// "sourcearea" plugin.
|
||||
'Source' => array(
|
||||
'label' => t('Source code'),
|
||||
'image_alternative' => $button('source'),
|
||||
),
|
||||
// "maximize" plugin.
|
||||
'Maximize' => array(
|
||||
'label' => t('Maximize'),
|
||||
'image_alternative' => $button('maximize'),
|
||||
),
|
||||
// No plugin, separator "button" for toolbar builder UI use only.
|
||||
'-' => array(
|
||||
'label' => t('Separator'),
|
||||
'image_alternative' => '<a href="#" role="button" aria-label="' . t('Button separator') . '" class="ckeditor-separator"></a>',
|
||||
'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.
|
||||
*
|
||||
* @see ckeditor_rebuild()
|
||||
* @see ckeditor_filter_format_insert()
|
||||
* @see ckeditor_filter_format_update()
|
||||
*/
|
||||
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();
|
||||
// The <p> tag is always allowed — HTML without <p> tags is nonsensical.
|
||||
$default = 'p';
|
||||
return \Drupal::state()->get('ckeditor_internal_format_tags:' . $format->id(), $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 to e.g. define any style, no
|
||||
// matter what the "*" tag's restrictions may be. If there's 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)) {
|
||||
// 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']) && is_array($allowed_attributes['style'])) {
|
||||
$allowed_styles = $get_attribute_values($allowed_attributes['style'], TRUE);
|
||||
if (isset($allowed_styles)) {
|
||||
$allowed[$tag]['styles'] = $allowed_styles;
|
||||
}
|
||||
}
|
||||
if (isset($allowed_attributes['class']) && 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array($allowed, $disallowed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
159
core/modules/ckeditor/src/Plugin/CKEditorPlugin/StylesCombo.php
Normal file
159
core/modules/ckeditor/src/Plugin/CKEditorPlugin/StylesCombo.php
Normal file
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Plugin\CKEditorPlugin\StylesCombo.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginBase;
|
||||
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal().
|
||||
*/
|
||||
public function isInternal() {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getFile().
|
||||
*/
|
||||
public function getFile() {
|
||||
// This plugin is already part of Drupal core's CKEditor build.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getConfig().
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons().
|
||||
*/
|
||||
public function getButtons() {
|
||||
return array(
|
||||
'Styles' => array(
|
||||
'label' => t('Font style'),
|
||||
'image_alternative' => '<a href="#" role="button" aria-label="' . t('Styles') . '"><span class="ckeditor-button-dropdown">' . t('Styles') . '<span class="ckeditor-button-arrow"></span></span></a>',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginConfigurableInterface::settingsForm().
|
||||
*/
|
||||
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 class on each line in the format: element.class|Label. Example: h1.title|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) {
|
||||
if ($this->generateStylesSetSetting($element['#value']) === FALSE) {
|
||||
$form_state->setError($element, t('The provided list of styles is syntactically incorrect.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the "stylesSet" configuration part of the CKEditor JS settings.
|
||||
*
|
||||
* @see getConfig()
|
||||
*
|
||||
* @param string $styles
|
||||
* The "styles" setting.
|
||||
* @return array|FALSE
|
||||
* An array containing the "stylesSet" configuration, or FALSE when the
|
||||
* syntax is invalid.
|
||||
*/
|
||||
protected function generateStylesSetSetting($styles) {
|
||||
$styles_set = array();
|
||||
|
||||
// Early-return when empty.
|
||||
$styles = trim($styles);
|
||||
if (empty($styles)) {
|
||||
return $styles_set;
|
||||
}
|
||||
|
||||
$styles = str_replace(array("\r\n", "\r"), "\n", $styles);
|
||||
foreach (explode("\n", $styles) as $style) {
|
||||
$style = trim($style);
|
||||
|
||||
// Ignore empty lines in between non-empty lines.
|
||||
if (empty($style)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate syntax: 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;
|
||||
}
|
||||
|
||||
}
|
426
core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
Normal file
426
core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
Normal file
|
@ -0,0 +1,426 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Plugin\Editor\CKEditor.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor\Plugin\Editor;
|
||||
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
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 as EditorEntity;
|
||||
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' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, EditorEntity $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' => $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' (e.g. 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 = entity_create('editor', 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(EditorEntity $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.
|
||||
$settings += array(
|
||||
'drupalExternalPlugins' => array_map('file_create_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.
|
||||
$ckeditor_languages = new \GlobIterator(\Drupal::root() . '/core/assets/vendor/ckeditor/lang/*.js');
|
||||
foreach ($ckeditor_languages as $language_file) {
|
||||
$langcode = $language_file->getBasename('.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.
|
||||
// e.g. 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(EditorEntity $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(EditorEntity $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(EditorEntity $editor) {
|
||||
$css = array(
|
||||
drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css',
|
||||
drupal_get_path('module', 'system') . '/css/system.module.css',
|
||||
);
|
||||
$this->moduleHandler->alter('ckeditor_css', $css, $editor);
|
||||
$css = array_merge($css, _ckeditor_theme_css());
|
||||
$css = array_map('file_create_url', $css);
|
||||
|
||||
return array_values($css);
|
||||
}
|
||||
|
||||
}
|
256
core/modules/ckeditor/src/Tests/CKEditorAdminTest.php
Normal file
256
core/modules/ckeditor/src/Tests/CKEditorAdminTest.php
Normal file
|
@ -0,0 +1,256 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Tests\CKEditorAdminTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor\Tests;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* 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 = entity_create('filter_format', 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 = entity_load('editor', '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' => 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' => array(),
|
||||
);
|
||||
$this->assertIdentical($ckeditor->getDefaultSettings(), $expected_default_settings);
|
||||
|
||||
// Keep the "CKEditor" editor selected and click the "Configure" button.
|
||||
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
|
||||
$editor = entity_load('editor', 'filtered_html');
|
||||
$this->assertFalse($editor, 'No Editor config entity exists yet.');
|
||||
|
||||
// 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 = entity_load('editor', 'filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists now.');
|
||||
$this->assertIdentical($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 = entity_load('editor', 'filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertIdentical($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 = entity_load('editor', 'filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertIdentical($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
|
||||
// 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 = entity_load('editor', 'filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertIdentical($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 = entity_load('editor', 'filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertIdentical($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 = entity_load('filter_format', 'amazing_format');
|
||||
$this->assertFalse($filter_format, 'No FilterFormat config entity exists yet.');
|
||||
$editor = entity_load('editor', '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);
|
||||
|
||||
// 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 = entity_load('filter_format', '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 = entity_load('editor', 'amazing_format');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists now.');
|
||||
$this->assertIdentical($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
|
||||
}
|
||||
|
||||
}
|
168
core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
Normal file
168
core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
Normal file
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Tests\CKEditorLoadingTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor\Tests;
|
||||
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* 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 = entity_create('filter_format', array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$editor = entity_create('editor', array(
|
||||
'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 = entity_create('filter_format', 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 = entity_load('editor', 'filtered_html');
|
||||
$expected = array('formats' => array('filtered_html' => array(
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
'editorSettings' => $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, $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' => $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, $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.');
|
||||
}
|
||||
|
||||
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")]'),
|
||||
);
|
||||
}
|
||||
}
|
127
core/modules/ckeditor/src/Tests/CKEditorPluginManagerTest.php
Normal file
127
core/modules/ckeditor/src/Tests/CKEditorPluginManagerTest.php
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Tests\CKEditorPluginManagerTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor\Tests;
|
||||
|
||||
use Drupal\simpletest\KernelTestBase;
|
||||
use Drupal\ckeditor\CKEditorPluginManager;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
$this->installSchema('system', 'url_alias');
|
||||
|
||||
// Create text format, associate CKEditor.
|
||||
$filtered_html_format = entity_create('filter_format', array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
));
|
||||
$editor->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the enabling of plugins.
|
||||
*/
|
||||
function testEnabledPlugins() {
|
||||
$this->manager = $this->container->get('plugin.manager.ckeditor.plugin');
|
||||
$editor = entity_load('editor', 'filtered_html');
|
||||
|
||||
// Case 1: no CKEditor plugins.
|
||||
$definitions = array_keys($this->manager->getDefinitions());
|
||||
sort($definitions);
|
||||
$this->assertIdentical(array('drupalimage', 'drupalimagecaption', 'drupallink', 'internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.');
|
||||
$enabled_plugins = array(
|
||||
'drupalimage' => 'core/modules/ckeditor/js/plugins/drupalimage/plugin.js',
|
||||
'drupallink' => 'core/modules/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 three
|
||||
// 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', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', '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.
|
||||
// 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. Finally, we will add the "Strike" button back again, which would
|
||||
// cause all three 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'] = 'core/modules/ckeditor/tests/modules/js/llama_button.js';
|
||||
$file['c'] = 'core/modules/ckeditor/tests/modules/js/llama_contextual.js';
|
||||
$file['cb'] = 'core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.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.');
|
||||
}
|
||||
|
||||
}
|
482
core/modules/ckeditor/src/Tests/CKEditorTest.php
Normal file
482
core/modules/ckeditor/src/Tests/CKEditorTest.php
Normal file
|
@ -0,0 +1,482 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor\Tests\CKEditorTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor\Tests;
|
||||
|
||||
use Drupal\simpletest\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
$this->installSchema('system', 'url_alias');
|
||||
|
||||
// Create text format, associate CKEditor.
|
||||
$filtered_html_format = entity_create('filter_format', array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(
|
||||
'filter_html' => array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'allowed_html' => '<h4> <h5> <h6> <p> <br> <strong> <a>',
|
||||
)
|
||||
),
|
||||
),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$editor = entity_create('editor', array(
|
||||
'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 = entity_load('editor', '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_create_url('core/modules/ckeditor/js/plugins/drupalimage/plugin.js'),
|
||||
'drupallink' => file_create_url('core/modules/ckeditor/js/plugins/drupallink/plugin.js'),
|
||||
),
|
||||
);
|
||||
ksort($expected_config);
|
||||
$this->assertIdentical($expected_config, $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();
|
||||
// KernelTestBase::enableModules() unfortunately doesn't invoke
|
||||
// hook_rebuild() just like a "real" Drupal site would. Do it manually.
|
||||
\Drupal::moduleHandler()->invoke('ckeditor', 'rebuild');
|
||||
$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;h4;h5;h6';
|
||||
$expected_config['extraPlugins'] .= ',llama_contextual,llama_contextual_and_button';
|
||||
$expected_config['drupalExternalPlugins']['llama_contextual'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js');
|
||||
$expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js');
|
||||
$expected_config['contentsCss'][] = file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css');
|
||||
ksort($expected_config);
|
||||
$this->assertIdentical($expected_config, $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> <h3>';
|
||||
$format->save();
|
||||
|
||||
$expected_config['allowedContent']['pre'] = array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE);
|
||||
$expected_config['allowedContent']['h3'] = array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE);
|
||||
$expected_config['format_tags'] = 'p;h3;h4;h5;h6;pre';
|
||||
$this->assertIdentical($expected_config, $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->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',
|
||||
'classes' => 'external',
|
||||
),
|
||||
'span' => array(
|
||||
'attributes' => 'class,property,rel,style',
|
||||
'styles' => 'font-size',
|
||||
),
|
||||
'*' => array(
|
||||
'attributes' => 'class,data-*',
|
||||
'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);
|
||||
$this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
|
||||
|
||||
// Assert that we're robust enough to withstand people messing with State
|
||||
// manually.
|
||||
\Drupal::state()->delete('ckeditor_internal_format_tags:' . $format->id());
|
||||
$this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Even when somebody manually deleted the key-value pair in State with the pre-calculated format_tags setting, it returns "p" — because the <p> tag is always allowed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests CKEditor::buildToolbarJSSetting().
|
||||
*/
|
||||
function testBuildToolbarJSSetting() {
|
||||
$editor = entity_load('editor', 'filtered_html');
|
||||
|
||||
// Default toolbar.
|
||||
$expected = $this->getDefaultToolbarConfig();
|
||||
$this->assertIdentical($expected, $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->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->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 = entity_load('editor', '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_create_url('core/modules/ckeditor/tests/modules/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 the Bartik theme, which specifies a CKEditor stylesheet.
|
||||
\Drupal::service('theme_handler')->install(['bartik']);
|
||||
$this->config('system.theme')->set('default', 'bartik')->save();
|
||||
$expected[] = file_create_url('core/themes/bartik/css/base/elements.css');
|
||||
$expected[] = file_create_url('core/themes/bartik/css/components/captions.css');
|
||||
$expected[] = file_create_url('core/themes/bartik/css/components/content.css');
|
||||
$expected[] = file_create_url('core/themes/bartik/css/components/table.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 = entity_load('editor', '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;h4;h5;h6';
|
||||
$this->assertEqual($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for customized toolbar.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests StylesCombo::getConfig().
|
||||
*/
|
||||
function testStylesComboGetConfig() {
|
||||
$editor = entity_load('editor', '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,
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultAllowedContentConfig() {
|
||||
return array(
|
||||
'h4' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
|
||||
'h5' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
|
||||
'h6' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
|
||||
'p' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
|
||||
'br' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
|
||||
'strong' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
|
||||
'a' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultDisallowedContentConfig() {
|
||||
return array(
|
||||
'*' => array('attributes' => 'on*'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultToolbarConfig() {
|
||||
return 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',),
|
||||
),
|
||||
'/',
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultContentsCssConfig() {
|
||||
return array(
|
||||
file_create_url('core/modules/ckeditor/css/ckeditor-iframe.css'),
|
||||
file_create_url('core/modules/system/css/system.module.css'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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" 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
core/modules/ckeditor/tests/modules/ckeditor_test.module
Normal file
15
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,7 @@
|
|||
ckeditor.plugin.llama_contextual_and_button:
|
||||
type: mapping
|
||||
label: 'Contextual Llama With Button'
|
||||
mapping:
|
||||
ultra_llama_mode:
|
||||
type: boolean
|
||||
label: 'Ultra llama mode'
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor_test\Plugin\CKEditorPlugin\Llama.
|
||||
*/
|
||||
|
||||
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 MetaContextual
|
||||
* @see MetaButton
|
||||
* @see MetaContextualAndButton
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "llama",
|
||||
* label = @Translation("Llama")
|
||||
* )
|
||||
*/
|
||||
class Llama extends PluginBase implements CKEditorPluginInterface {
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getDependencies().
|
||||
*/
|
||||
function getDependencies(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getLibraries().
|
||||
*/
|
||||
function getLibraries(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal().
|
||||
*/
|
||||
function isInternal() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getFile().
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getConfig().
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor_test\Plugin\CKEditorPlugin\LlamaButton.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginButtonsInterface;
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons().
|
||||
*/
|
||||
function getButtons() {
|
||||
return array(
|
||||
'Llama' => array(
|
||||
'label' => t('Insert Llama'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getFile().
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama_button.js';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor_test\Plugin\CKEditorPlugin\LlamaContextual.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginContextualInterface::isEnabled().
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getFile().
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama_contextual.js';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\ckeditor_test\Plugin\CKEditorPlugin\LlamaContextualAndButton.
|
||||
*/
|
||||
|
||||
namespace Drupal\ckeditor_test\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginButtonsInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginContextualInterface::isEnabled().
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons().
|
||||
*/
|
||||
function getButtons() {
|
||||
return array(
|
||||
'Llama' => array(
|
||||
'label' => t('Insert Llama'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getFile().
|
||||
*/
|
||||
function getFile() {
|
||||
return drupal_get_path('module', 'ckeditor_test') . '/js/llama_contextual_and_button.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\ckeditor\Plugin\CKEditorPluginConfigurableInterface::settingsForm().
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue