Commit db0e9117 authored by alexpott's avatar alexpott

Issue #1894644 by Wim Leers, quicksketch: Unidirectional editor configuration...

Issue #1894644 by Wim Leers, quicksketch: Unidirectional editor configuration -> filter settings syncing.
parent 32eadec8
...@@ -47,6 +47,9 @@ function ckeditor_library_info() { ...@@ -47,6 +47,9 @@ function ckeditor_library_info() {
array('system', 'jquery.ui.sortable'), array('system', 'jquery.ui.sortable'),
array('system', 'jquery.ui.draggable'), array('system', 'jquery.ui.draggable'),
array('system', 'jquery.ui.touch-punch'), array('system', 'jquery.ui.touch-punch'),
array('ckeditor', 'ckeditor'),
array('editor', 'drupal.editor.admin'),
array('system', 'underscore')
), ),
); );
$libraries['drupal.ckeditor.stylescombo.admin'] = array( $libraries['drupal.ckeditor.stylescombo.admin'] = array(
...@@ -60,6 +63,9 @@ function ckeditor_library_info() { ...@@ -60,6 +63,9 @@ function ckeditor_library_info() {
array('system', 'drupal'), array('system', 'drupal'),
array('system', 'jquery.once'), array('system', 'jquery.once'),
array('system', 'drupal.vertical-tabs'), array('system', 'drupal.vertical-tabs'),
array('system', 'drupalSettings'),
// @todo D8 formUpdated event should be debounced already.
array('system', 'drupal.debounce'),
), ),
); );
$libraries['ckeditor'] = array( $libraries['ckeditor'] = array(
......
This diff is collapsed.
...@@ -87,7 +87,7 @@ Drupal.editors.ckeditor = { ...@@ -87,7 +87,7 @@ Drupal.editors.ckeditor = {
return !!CKEDITOR.inline(element, settings); return !!CKEDITOR.inline(element, settings);
}, },
_loadExternalPlugins: function(format) { _loadExternalPlugins: function (format) {
var externalPlugins = format.editorSettings.drupalExternalPlugins; var externalPlugins = format.editorSettings.drupalExternalPlugins;
// Register and load additional CKEditor plugins as necessary. // Register and load additional CKEditor plugins as necessary.
if (externalPlugins) { if (externalPlugins) {
......
(function ($, Drupal) { (function ($, Drupal, drupalSettings) {
"use strict"; "use strict";
/** /**
* Shows the "stylescombo" plugin settings only when the button is enabled. * Shows the "stylescombo" plugin settings only when the button is enabled.
*/ */
Drupal.behaviors.ckeditorStylesComboSettingsVisibility = { Drupal.behaviors.ckeditorStylesComboSettings = {
attach: function (context) { attach: function (context) {
var $context = $(context);
var $stylesComboVerticalTab = $('#edit-editor-settings-plugins-stylescombo').data('verticalTab'); var $stylesComboVerticalTab = $('#edit-editor-settings-plugins-stylescombo').data('verticalTab');
// Hide if the "Styles" button is disabled. // Hide if the "Styles" button is disabled.
...@@ -15,9 +16,9 @@ Drupal.behaviors.ckeditorStylesComboSettingsVisibility = { ...@@ -15,9 +16,9 @@ Drupal.behaviors.ckeditorStylesComboSettingsVisibility = {
} }
// React to added/removed toolbar buttons. // React to added/removed toolbar buttons.
$(context) $context
.find('.ckeditor-toolbar-active') .find('.ckeditor-toolbar-active')
.on('CKEditorToolbarChanged', function (e, action, button) { .on('CKEditorToolbarChanged.ckeditorStylesComboSettings', function (e, action, button) {
if (button === 'Styles') { if (button === 'Styles') {
if (action === 'added') { if (action === 'added') {
$stylesComboVerticalTab.tabShow(); $stylesComboVerticalTab.tabShow();
...@@ -27,6 +28,80 @@ Drupal.behaviors.ckeditorStylesComboSettingsVisibility = { ...@@ -27,6 +28,80 @@ Drupal.behaviors.ckeditorStylesComboSettingsVisibility = {
} }
} }
}); });
// 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($('#edit-editor-settings-plugins-stylescombo-styles').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 sstyles
* 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;
} }
}; };
...@@ -48,4 +123,4 @@ Drupal.behaviors.ckeditorStylesComboSettingsSummary = { ...@@ -48,4 +123,4 @@ Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
} }
}; };
})(jQuery, Drupal); })(jQuery, Drupal, drupalSettings);
...@@ -96,8 +96,6 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA ...@@ -96,8 +96,6 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA
/** /**
* Retrieves all plugins that implement CKEditorPluginButtonsInterface. * Retrieves all plugins that implement CKEditorPluginButtonsInterface.
* *
* @param \Drupal\editor\Plugin\Core\Entity\Editor $editor
* A configured text editor object.
* @return array * @return array
* A list of the CKEditor plugins that implement buttons, with the plugin * A list of the CKEditor plugins that implement buttons, with the plugin
* IDs as keys and lists of button metadata (as implemented by getButtons()) * IDs as keys and lists of button metadata (as implemented by getButtons())
...@@ -105,7 +103,7 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA ...@@ -105,7 +103,7 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA
* *
* @see CKEditorPluginButtonsInterface::getButtons() * @see CKEditorPluginButtonsInterface::getButtons()
*/ */
public function getButtonsPlugins(Editor $editor) { public function getButtonsPlugins() {
$plugins = array_keys($this->getDefinitions()); $plugins = array_keys($this->getDefinitions());
$buttons_plugins = array(); $buttons_plugins = array();
......
...@@ -44,6 +44,9 @@ public function getFile() { ...@@ -44,6 +44,9 @@ public function getFile() {
*/ */
public function getConfig(Editor $editor) { public function getConfig(Editor $editor) {
$config = array(); $config = array();
if (!isset($editor->settings['plugins']['stylescombo']['styles'])) {
return $config;
}
$styles = $editor->settings['plugins']['stylescombo']['styles']; $styles = $editor->settings['plugins']['stylescombo']['styles'];
$config['stylesSet'] = $this->generateStylesSetSetting($styles); $config['stylesSet'] = $this->generateStylesSetSetting($styles);
return $config; return $config;
...@@ -126,8 +129,8 @@ protected function generateStylesSetSetting($styles) { ...@@ -126,8 +129,8 @@ protected function generateStylesSetSetting($styles) {
continue; continue;
} }
// Validate syntax: element.class[.class...]|label pattern expected. // Validate syntax: element[.class...]|label pattern expected.
if (!preg_match('@^ *[a-zA-Z0-9]+ *(\\.[a-zA-Z0-9_-]+ *)+\\| *.+ *$@', $style)) { if (!preg_match('@^ *[a-zA-Z0-9]+ *(\\.[a-zA-Z0-9_-]+ *)*\\| *.+ *$@', $style)) {
return FALSE; return FALSE;
} }
...@@ -138,13 +141,16 @@ protected function generateStylesSetSetting($styles) { ...@@ -138,13 +141,16 @@ protected function generateStylesSetSetting($styles) {
// Build the data structure CKEditor's stylescombo plugin expects. // Build the data structure CKEditor's stylescombo plugin expects.
// @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles // @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles
$styles_set[] = array( $configured_style = array(
'name' => trim($label), 'name' => trim($label),
'element' => trim($element), 'element' => trim($element),
'attributes' => array(
'class' => implode(' ', array_map('trim', $classes))
),
); );
if (!empty($classes)) {
$configured_style['attributes'] = array(
'class' => implode(' ', array_map('trim', $classes))
);
}
$styles_set[] = $configured_style;
} }
return $styles_set; return $styles_set;
} }
......
...@@ -53,7 +53,7 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit ...@@ -53,7 +53,7 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit
$ckeditor_settings_toolbar = array( $ckeditor_settings_toolbar = array(
'#theme' => 'ckeditor_settings_toolbar', '#theme' => 'ckeditor_settings_toolbar',
'#editor' => $editor, '#editor' => $editor,
'#plugins' => $manager->getButtonsPlugins($editor), '#plugins' => $manager->getButtonsPlugins(),
); );
$form['toolbar'] = array( $form['toolbar'] = array(
'#type' => 'container', '#type' => 'container',
...@@ -87,6 +87,49 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit ...@@ -87,6 +87,49 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit
unset($form['plugin_settings']); 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($manager->getDefinitions());
$all_external_plugins = array();
foreach ($plugins as $plugin_id) {
$plugin = $manager->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($manager->getButtonsPlugins(), 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' => 'ckeditor',
'settings' => array(
// Single toolbar row that contains all existing buttons.
'toolbar' => array('buttons' => array(0 => $all_buttons)),
'plugins' => $editor->settings['plugins'],
),
));
$form['hidden_ckeditor'] = array(
'#markup' => '<div id="ckeditor-hidden" class="element-hidden"></div>',
'#attached' => array(
'js' => array(
array(
'type' => 'setting',
'data' => array('ckeditor' => array(
'hiddenCKEditorConfig' => $this->getJSSettings($fake_editor),
)),
),
),
),
);
return $form; return $form;
} }
......
...@@ -213,8 +213,14 @@ function testStylesComboGetConfig() { ...@@ -213,8 +213,14 @@ function testStylesComboGetConfig() {
$editor->save(); $editor->save();
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.'); $this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.');
// Slightly different configuration: class names are optional.
$editor->settings['plugins']['stylescombo']['styles'] = " h1 | Title ";
$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. // Invalid syntax should cause stylesSet to be set to FALSE.
$editor->settings['plugins']['stylescombo']['styles'] = "h1|Title"; $editor->settings['plugins']['stylescombo']['styles'] = "h1";
$editor->save(); $editor->save();
$expected['stylesSet'] = FALSE; $expected['stylesSet'] = FALSE;
$this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.'); $this->assertIdentical($expected, $stylescombo_plugin->getConfig($editor), '"StylesCombo" plugin configuration built correctly for customized toolbar.');
......
...@@ -65,6 +65,18 @@ function editor_element_info() { ...@@ -65,6 +65,18 @@ function editor_element_info() {
*/ */
function editor_library_info() { function editor_library_info() {
$path = drupal_get_path('module', 'editor'); $path = drupal_get_path('module', 'editor');
$libraries['drupal.editor.admin'] = array(
'title' => 'Text Editor',
'version' => VERSION,
'js' => array(
$path . '/js/editor.admin.js' => array(),
),
'dependencies' => array(
array('system', 'jquery'),
array('system', 'drupal'),
),
);
$libraries['drupal.editor'] = array( $libraries['drupal.editor'] = array(
'title' => 'Text Editor', 'title' => 'Text Editor',
'version' => VERSION, 'version' => VERSION,
...@@ -200,6 +212,11 @@ function editor_form_filter_admin_format_form_alter(&$form, &$form_state) { ...@@ -200,6 +212,11 @@ function editor_form_filter_admin_format_form_alter(&$form, &$form_state) {
'#weight' => -8, '#weight' => -8,
'#type' => 'container', '#type' => 'container',
'#id' => 'editor-settings-wrapper', '#id' => 'editor-settings-wrapper',
'#attached' => array(
'library' => array(
array('editor', 'drupal.editor.admin'),
),
),
); );
// Add editor-specific validation and submit handlers. // Add editor-specific validation and submit handlers.
......
This diff is collapsed.
/**
* @file
* Attaches behavior for updating filter_html's settings automatically.
*/
(function ($, _, document, window) {
"use strict";
/**
* Implement a live setting parser to prevent text editors from automatically
* enabling buttons that are not allowed by this filter's configuration.
*/
if (Drupal.filterConfiguration) {
Drupal.filterConfiguration.liveSettingParsers.filter_html = {
getRules: function () {
var currentValue = $('#edit-filters-filter-html-settings-allowed-html').val();
var rules = [], rule;
// Build a FilterHTMLRule that reflects the hard-coded behavior that
// strips all "style" attribute and all "on*" attributes.
rule = new Drupal.FilterHTMLRule();
rule.restrictedTags.tags = ['*'];
rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
rules.push(rule);
// Build a FilterHTMLRule that reflects the current settings.
rule = new Drupal.FilterHTMLRule();
var behavior = Drupal.behaviors.filterFilterHtmlUpdating;
rule.allow = true;
rule.tags = behavior._parseSetting(currentValue);
rules.push(rule);
return rules;
}
};
}
Drupal.behaviors.filterFilterHtmlUpdating = {
// The form item containg the "Allowed HTML tags" setting.
$allowedHTMLFormItem: null,
// The description for the "Allowed HTML tags" field.
$allowedHTMLDescription: null,
// The user-entered tag list of $allowedHTMLFormItem.
userTags: null,
// The auto-created tag list thus far added.
autoTags: null,
// Track which new features have been added to the text editor.
newFeatures: {},
attach: function (context, settings) {
var that = this;
$(context).find('[name="filters[filter_html][settings][allowed_html]"]').once('filter-filter_html-updating', function () {
that.$allowedHTMLFormItem = $(this);
that.$allowedHTMLDescription = that.$allowedHTMLFormItem.closest('.form-item').find('.description');
that.userTags = that._parseSetting(this.value);
// Update the new allowed tags based on added text editor features.
$(document)
.on('drupalEditorFeatureAdded', function (e, feature) {
that.newFeatures[feature.name] = feature.rules;
that._updateAllowedTags();
})
.on('drupalEditorFeatureModified', function (e, feature) {
if (that.newFeatures.hasOwnProperty(feature.name)) {
that.newFeatures[feature.name] = feature.rules;
that._updateAllowedTags();
}
})
.on('drupalEditorFeatureRemoved', function (e, feature) {
if (that.newFeatures.hasOwnProperty(feature.name)) {
delete that.newFeatures[feature.name];
that._updateAllowedTags();
}
});
// When the allowed tags list is manually changed, update userTags.
that.$allowedHTMLFormItem.on('change.updateUserTags', function () {
that.userTags = _.difference(that._parseSetting(this.value), that.autoTags);
});
});
},
/**
* Updates the "Allowed HTML tags" setting and shows an informative message.
*/
_updateAllowedTags: function () {
// Update the list of auto-created tags.
this.autoTags = this._calculateAutoAllowedTags(this.userTags, this.newFeatures);
// Remove any previous auto-created tag message.
this.$allowedHTMLDescription.find('.editor-update-message').remove();
// If any auto-created tags: insert message and update form item.
if (this.autoTags.length > 0) {
this.$allowedHTMLDescription.append(Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags));
this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags) + ' ' + this._generateSetting(this.autoTags));
}
// Restore to original state.
else {
this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags));
}
},
/**
* Calculates which HTML tags the added text editor buttons need to work.
*
* The filter_html filter is only concerned with the required tags, not with
* any properties, nor with each feature's "allowed" tags.
*
* @param Array userAllowedTags
* The list of user-defined allowed tags.
* @param Object newFeatures
* A list of Drupal.EditorFeature objects' rules, keyed by their name.
*
* @return Array
* A list of new allowed tags.
*/
_calculateAutoAllowedTags: function (userAllowedTags, newFeatures) {
return _
.chain(newFeatures)
// Reduce multiple features' rules.
.reduce(function (memo, featureRules) {
// Reduce a single features' rules' required tags.
return _.union(memo, _.reduce(featureRules, function (memo, featureRule) {
return _.union(memo, featureRule.required.tags);
}, []));
}, [])
// All new features' required tags are "new allowed tags", except
// for those that are already allowed in the original allowed tags.
.difference(userAllowedTags)
.value();
},
/**
* Parses the value of this.$allowedHTMLFormItem.
*
* @param String setting
* The string representation of the setting. e.g. "<p> <br> <a>"
*
* @return Array
* The array representation of the setting. e.g. ['p', 'br', 'a']
*/
_parseSetting: function (setting) {
return setting.length ? setting.substring(1, setting.length - 1).split('> <') : [];
},
/**
* Generates the value of this.$allowedHTMLFormItem.
*
* @param Array setting
* The array representation of the setting. e.g. ['p', 'br', 'a']
*
* @return Array
* The string representation of the setting. e.g. "<p> <br> <a>"
*/
_generateSetting: function (tags) {
return tags.length ? '<' + tags.join('> <') + '>' : '';
}
};
/**
* Theme function for the filter_html update message.
*
* @param Array tags
* An array of the new tags that are to be allowed.
* @return
* The corresponding HTML.
*/
Drupal.theme.filterFilterHTMLUpdateMessage = function (tags) {
var html = '';
var tagList = '<' + tags.join('> <') + '>';
html += '<p class="editor-update-message">';
html += Drupal.t('Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.', { '@tag-list' : tagList });
html += '</p>';
return html;
};
})(jQuery, _, document, window);
...@@ -1452,14 +1452,16 @@ function theme_filter_html_image_secure_image(&$variables) { ...@@ -1452,14 +1452,16 @@ function theme_filter_html_image_secure_image(&$variables) {
* Implements hook_library_info(). * Implements hook_library_info().
*/ */
function filter_library_info() { function filter_library_info() {
$path = drupal_get_path('module', 'filter');
$libraries['drupal.filter.admin'] = array( $libraries['drupal.filter.admin'] = array(
'title' => 'Filter', 'title' => 'Filter',
'version' => VERSION, 'version' => VERSION,
'js' => array( 'js' => array(
drupal_get_path('module', 'filter') . '/filter.admin.js' => array(), $path . '/filter.admin.js' => array(),
), ),
'css' => array( 'css' => array(
drupal_get_path('module', 'filter') . '/css/filter.admin.css' $path . '/css/filter.admin.css'
), ),
'dependencies' => array( 'dependencies' => array(
array('system', 'jquery'), array('system', 'jquery'),
...@@ -1468,14 +1470,26 @@ function filter_library_info() { ...@@ -1468,14 +1470,26 @@ function filter_library_info() {
array('system', 'drupal.form'), array('system', 'drupal.form'),
), ),
); );
$libraries['drupal.filter.filter_html.admin'] = array(