3 * Attaches behavior for updating filter_html's settings automatically.
6 (function($, Drupal, _, document) {
7 if (Drupal.filterConfiguration) {
9 * Implement a live setting parser to prevent text editors from automatically
10 * enabling buttons that are not allowed by this filter's configuration.
14 Drupal.filterConfiguration.liveSettingParsers.filter_html = {
17 * An array of filter rules.
20 const currentValue = $(
21 '#edit-filters-filter-html-settings-allowed-html',
23 const rules = Drupal.behaviors.filterFilterHtmlUpdating._parseSetting(
27 // Build a FilterHTMLRule that reflects the hard-coded behavior that
28 // strips all "style" attribute and all "on*" attributes.
29 const rule = new Drupal.FilterHTMLRule();
30 rule.restrictedTags.tags = ['*'];
31 rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
40 * Displays and updates what HTML tags are allowed to use in a filter.
42 * @type {Drupal~behavior}
44 * @todo Remove everything but 'attach' and 'detach' and make a proper object.
46 * @prop {Drupal~behaviorAttach} attach
47 * Attaches behavior for updating allowed HTML tags.
49 Drupal.behaviors.filterFilterHtmlUpdating = {
50 // The form item contains the "Allowed HTML tags" setting.
51 $allowedHTMLFormItem: null,
53 // The description for the "Allowed HTML tags" field.
54 $allowedHTMLDescription: null,
57 * The parsed, user-entered tag list of $allowedHTMLFormItem
59 * @var {Object.<string, Drupal.FilterHTMLRule>}
63 // The auto-created tag list thus far added.
66 // Track which new features have been added to the text editor.
69 attach(context, settings) {
72 .find('[name="filters[filter_html][settings][allowed_html]"]')
73 .once('filter-filter_html-updating')
75 that.$allowedHTMLFormItem = $(this);
76 that.$allowedHTMLDescription = that.$allowedHTMLFormItem
77 .closest('.js-form-item')
78 .find('.description');
79 that.userTags = that._parseSetting(this.value);
81 // Update the new allowed tags based on added text editor features.
83 .on('drupalEditorFeatureAdded', (e, feature) => {
84 that.newFeatures[feature.name] = feature.rules;
85 that._updateAllowedTags();
87 .on('drupalEditorFeatureModified', (e, feature) => {
88 if (that.newFeatures.hasOwnProperty(feature.name)) {
89 that.newFeatures[feature.name] = feature.rules;
90 that._updateAllowedTags();
93 .on('drupalEditorFeatureRemoved', (e, feature) => {
94 if (that.newFeatures.hasOwnProperty(feature.name)) {
95 delete that.newFeatures[feature.name];
96 that._updateAllowedTags();
100 // When the allowed tags list is manually changed, update userTags.
101 that.$allowedHTMLFormItem.on('change.updateUserTags', function() {
102 that.userTags = _.difference(
103 that._parseSetting(this.value),
111 * Updates the "Allowed HTML tags" setting and shows an informative message.
113 _updateAllowedTags() {
114 // Update the list of auto-created tags.
115 this.autoTags = this._calculateAutoAllowedTags(
120 // Remove any previous auto-created tag message.
121 this.$allowedHTMLDescription.find('.editor-update-message').remove();
123 // If any auto-created tags: insert message and update form item.
124 if (!_.isEmpty(this.autoTags)) {
125 this.$allowedHTMLDescription.append(
126 Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags),
128 const userTagsWithoutOverrides = _.omit(
130 _.keys(this.autoTags),
132 this.$allowedHTMLFormItem.val(
133 `${this._generateSetting(
134 userTagsWithoutOverrides,
135 )} ${this._generateSetting(this.autoTags)}`,
138 // Restore to original state.
140 this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags));
145 * Calculates which HTML tags the added text editor buttons need to work.
147 * The filter_html filter is only concerned with the required tags, not with
148 * any properties, nor with each feature's "allowed" tags.
150 * @param {Array} userAllowedTags
151 * The list of user-defined allowed tags.
152 * @param {object} newFeatures
153 * A list of {@link Drupal.EditorFeature} objects' rules, keyed by
157 * A list of new allowed tags.
159 _calculateAutoAllowedTags(userAllowedTags, newFeatures) {
160 const editorRequiredTags = {};
162 // Map the newly added Text Editor features to Drupal.FilterHtmlRule
163 // objects (to allow comparing userTags with autoTags).
164 Object.keys(newFeatures || {}).forEach(featureName => {
165 const feature = newFeatures[featureName];
170 for (let f = 0; f < feature.length; f++) {
171 featureRule = feature[f];
172 for (let t = 0; t < featureRule.required.tags.length; t++) {
173 tag = featureRule.required.tags[t];
174 if (!_.has(editorRequiredTags, tag)) {
175 filterRule = new Drupal.FilterHTMLRule();
176 filterRule.restrictedTags.tags = [tag];
177 // @todo Neither Drupal.FilterHtmlRule nor
178 // Drupal.EditorFeatureHTMLRule allow for generic attribute
179 // value restrictions, only for the "class" and "style"
180 // attribute's values to be restricted. The filter_html filter
181 // always disallows the "style" attribute, so we only need to
182 // support "class" attribute value restrictions. Fix once
183 // https://www.drupal.org/node/2567801 lands.
184 filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(
187 filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(
190 editorRequiredTags[tag] = filterRule;
192 // The tag is already allowed, add any additionally allowed
195 filterRule = editorRequiredTags[tag];
196 filterRule.restrictedTags.allowed.attributes = _.union(
197 filterRule.restrictedTags.allowed.attributes,
198 featureRule.required.attributes,
200 filterRule.restrictedTags.allowed.classes = _.union(
201 filterRule.restrictedTags.allowed.classes,
202 featureRule.required.classes,
209 // Now compare userAllowedTags with editorRequiredTags, and build
210 // autoAllowedTags, which contains:
211 // - any tags in editorRequiredTags but not in userAllowedTags (i.e. tags
212 // that are additionally going to be allowed)
213 // - any tags in editorRequiredTags that already exists in userAllowedTags
214 // but does not allow all attributes or attribute values
215 const autoAllowedTags = {};
216 Object.keys(editorRequiredTags).forEach(tag => {
217 // If userAllowedTags does not contain a rule for this editor-required
218 // tag, then add it to the list of automatically allowed tags.
219 if (!_.has(userAllowedTags, tag)) {
220 autoAllowedTags[tag] = editorRequiredTags[tag];
222 // Otherwise, if userAllowedTags already allows this tag, then check if
223 // additional attributes and classes on this tag are required by the
226 const requiredAttributes =
227 editorRequiredTags[tag].restrictedTags.allowed.attributes;
228 const allowedAttributes =
229 userAllowedTags[tag].restrictedTags.allowed.attributes;
230 const needsAdditionalAttributes =
231 requiredAttributes.length &&
232 _.difference(requiredAttributes, allowedAttributes).length;
233 const requiredClasses =
234 editorRequiredTags[tag].restrictedTags.allowed.classes;
235 const allowedClasses =
236 userAllowedTags[tag].restrictedTags.allowed.classes;
237 const needsAdditionalClasses =
238 requiredClasses.length &&
239 _.difference(requiredClasses, allowedClasses).length;
240 if (needsAdditionalAttributes || needsAdditionalClasses) {
241 autoAllowedTags[tag] = userAllowedTags[tag].clone();
243 if (needsAdditionalAttributes) {
244 autoAllowedTags[tag].restrictedTags.allowed.attributes = _.union(
249 if (needsAdditionalClasses) {
250 autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(
258 return autoAllowedTags;
262 * Parses the value of this.$allowedHTMLFormItem.
264 * @param {string} setting
265 * The string representation of the setting. For example:
266 * <p class="callout"> <br> <a href hreflang>
268 * @return {Object.<string, Drupal.FilterHTMLRule>}
269 * The corresponding text filter HTML rule objects, one per tag, keyed by
272 _parseSetting(setting) {
278 const allowedTags = setting.match(/(<[^>]+>)/g);
279 const sandbox = document.createElement('div');
281 for (let t = 0; t < allowedTags.length; t++) {
282 // Let the browser do the parsing work for us.
283 sandbox.innerHTML = allowedTags[t];
284 node = sandbox.firstChild;
285 tag = node.tagName.toLowerCase();
287 // Build the Drupal.FilterHtmlRule object.
288 rule = new Drupal.FilterHTMLRule();
289 // We create one rule per allowed tag, so always one tag.
290 rule.restrictedTags.tags = [tag];
291 // Add the attribute restrictions.
292 attributes = node.attributes;
293 for (let i = 0; i < attributes.length; i++) {
294 attribute = attributes.item(i);
295 const attributeName = attribute.nodeName;
296 // @todo Drupal.FilterHtmlRule does not allow for generic attribute
297 // value restrictions, only for the "class" and "style" attribute's
298 // values. The filter_html filter always disallows the "style"
299 // attribute, so we only need to support "class" attribute value
300 // restrictions. Fix once https://www.drupal.org/node/2567801 lands.
301 if (attributeName === 'class') {
302 const attributeValue = attribute.textContent;
303 rule.restrictedTags.allowed.classes = attributeValue.split(' ');
305 rule.restrictedTags.allowed.attributes.push(attributeName);
315 * Generates the value of this.$allowedHTMLFormItem.
317 * @param {Object.<string, Drupal.FilterHTMLRule>} tags
318 * The parsed representation of the setting.
321 * The string representation of the setting. e.g. "<p> <br> <a>"
323 _generateSetting(tags) {
326 (setting, rule, tag) => {
327 if (setting.length) {
331 setting += `<${tag}`;
332 if (rule.restrictedTags.allowed.attributes.length) {
333 setting += ` ${rule.restrictedTags.allowed.attributes.join(' ')}`;
335 // @todo Drupal.FilterHtmlRule does not allow for generic attribute
336 // value restrictions, only for the "class" and "style" attribute's
337 // values. The filter_html filter always disallows the "style"
338 // attribute, so we only need to support "class" attribute value
339 // restrictions. Fix once https://www.drupal.org/node/2567801 lands.
340 if (rule.restrictedTags.allowed.classes.length) {
341 setting += ` class="${rule.restrictedTags.allowed.classes.join(
355 * Theme function for the filter_html update message.
357 * @param {Array} tags
358 * An array of the new tags that are to be allowed.
361 * The corresponding HTML.
363 Drupal.theme.filterFilterHTMLUpdateMessage = function(tags) {
365 const tagList = Drupal.behaviors.filterFilterHtmlUpdating._generateSetting(
368 html += '<p class="editor-update-message">';
370 'Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.',
371 { '@tag-list': tagList },
376 })(jQuery, Drupal, _, document);