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 = {
18 * An array of filter rules.
21 const currentValue = $('#edit-filters-filter-html-settings-allowed-html').val();
22 const rules = Drupal.behaviors.filterFilterHtmlUpdating._parseSetting(currentValue);
24 // Build a FilterHTMLRule that reflects the hard-coded behavior that
25 // strips all "style" attribute and all "on*" attributes.
26 const rule = new Drupal.FilterHTMLRule();
27 rule.restrictedTags.tags = ['*'];
28 rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
37 * Displays and updates what HTML tags are allowed to use in a filter.
39 * @type {Drupal~behavior}
41 * @todo Remove everything but 'attach' and 'detach' and make a proper object.
43 * @prop {Drupal~behaviorAttach} attach
44 * Attaches behavior for updating allowed HTML tags.
46 Drupal.behaviors.filterFilterHtmlUpdating = {
48 // The form item contains the "Allowed HTML tags" setting.
49 $allowedHTMLFormItem: null,
51 // The description for the "Allowed HTML tags" field.
52 $allowedHTMLDescription: null,
55 * The parsed, user-entered tag list of $allowedHTMLFormItem
57 * @var {Object.<string, Drupal.FilterHTMLRule>}
61 // The auto-created tag list thus far added.
64 // Track which new features have been added to the text editor.
67 attach(context, settings) {
69 $(context).find('[name="filters[filter_html][settings][allowed_html]"]').once('filter-filter_html-updating').each(function () {
70 that.$allowedHTMLFormItem = $(this);
71 that.$allowedHTMLDescription = that.$allowedHTMLFormItem.closest('.js-form-item').find('.description');
72 that.userTags = that._parseSetting(this.value);
74 // Update the new allowed tags based on added text editor features.
76 .on('drupalEditorFeatureAdded', (e, feature) => {
77 that.newFeatures[feature.name] = feature.rules;
78 that._updateAllowedTags();
80 .on('drupalEditorFeatureModified', (e, feature) => {
81 if (that.newFeatures.hasOwnProperty(feature.name)) {
82 that.newFeatures[feature.name] = feature.rules;
83 that._updateAllowedTags();
86 .on('drupalEditorFeatureRemoved', (e, feature) => {
87 if (that.newFeatures.hasOwnProperty(feature.name)) {
88 delete that.newFeatures[feature.name];
89 that._updateAllowedTags();
93 // When the allowed tags list is manually changed, update userTags.
94 that.$allowedHTMLFormItem.on('change.updateUserTags', function () {
95 that.userTags = _.difference(that._parseSetting(this.value), that.autoTags);
101 * Updates the "Allowed HTML tags" setting and shows an informative message.
103 _updateAllowedTags() {
104 // Update the list of auto-created tags.
105 this.autoTags = this._calculateAutoAllowedTags(this.userTags, this.newFeatures);
107 // Remove any previous auto-created tag message.
108 this.$allowedHTMLDescription.find('.editor-update-message').remove();
110 // If any auto-created tags: insert message and update form item.
111 if (!_.isEmpty(this.autoTags)) {
112 this.$allowedHTMLDescription.append(Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags));
113 const userTagsWithoutOverrides = _.omit(this.userTags, _.keys(this.autoTags));
114 this.$allowedHTMLFormItem.val(`${this._generateSetting(userTagsWithoutOverrides)} ${this._generateSetting(this.autoTags)}`);
116 // Restore to original state.
118 this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags));
123 * Calculates which HTML tags the added text editor buttons need to work.
125 * The filter_html filter is only concerned with the required tags, not with
126 * any properties, nor with each feature's "allowed" tags.
128 * @param {Array} userAllowedTags
129 * The list of user-defined allowed tags.
130 * @param {object} newFeatures
131 * A list of {@link Drupal.EditorFeature} objects' rules, keyed by
135 * A list of new allowed tags.
137 _calculateAutoAllowedTags(userAllowedTags, newFeatures) {
138 const editorRequiredTags = {};
140 // Map the newly added Text Editor features to Drupal.FilterHtmlRule
141 // objects (to allow comparing userTags with autoTags).
142 Object.keys(newFeatures || {}).forEach((featureName) => {
143 const feature = newFeatures[featureName];
148 for (let f = 0; f < feature.length; f++) {
149 featureRule = feature[f];
150 for (let t = 0; t < featureRule.required.tags.length; t++) {
151 tag = featureRule.required.tags[t];
152 if (!_.has(editorRequiredTags, tag)) {
153 filterRule = new Drupal.FilterHTMLRule();
154 filterRule.restrictedTags.tags = [tag];
155 // @todo Neither Drupal.FilterHtmlRule nor
156 // Drupal.EditorFeatureHTMLRule allow for generic attribute
157 // value restrictions, only for the "class" and "style"
158 // attribute's values to be restricted. The filter_html filter
159 // always disallows the "style" attribute, so we only need to
160 // support "class" attribute value restrictions. Fix once
161 // https://www.drupal.org/node/2567801 lands.
162 filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(0);
163 filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(0);
164 editorRequiredTags[tag] = filterRule;
166 // The tag is already allowed, add any additionally allowed
169 filterRule = editorRequiredTags[tag];
170 filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes);
171 filterRule.restrictedTags.allowed.classes = _.union(filterRule.restrictedTags.allowed.classes, featureRule.required.classes);
177 // Now compare userAllowedTags with editorRequiredTags, and build
178 // autoAllowedTags, which contains:
179 // - any tags in editorRequiredTags but not in userAllowedTags (i.e. tags
180 // that are additionally going to be allowed)
181 // - any tags in editorRequiredTags that already exists in userAllowedTags
182 // but does not allow all attributes or attribute values
183 const autoAllowedTags = {};
184 Object.keys(editorRequiredTags).forEach((tag) => {
185 // If userAllowedTags does not contain a rule for this editor-required
186 // tag, then add it to the list of automatically allowed tags.
187 if (!_.has(userAllowedTags, tag)) {
188 autoAllowedTags[tag] = editorRequiredTags[tag];
190 // Otherwise, if userAllowedTags already allows this tag, then check if
191 // additional attributes and classes on this tag are required by the
194 const requiredAttributes = editorRequiredTags[tag].restrictedTags.allowed.attributes;
195 const allowedAttributes = userAllowedTags[tag].restrictedTags.allowed.attributes;
196 const needsAdditionalAttributes = requiredAttributes.length && _.difference(requiredAttributes, allowedAttributes).length;
197 const requiredClasses = editorRequiredTags[tag].restrictedTags.allowed.classes;
198 const allowedClasses = userAllowedTags[tag].restrictedTags.allowed.classes;
199 const needsAdditionalClasses = requiredClasses.length && _.difference(requiredClasses, allowedClasses).length;
200 if (needsAdditionalAttributes || needsAdditionalClasses) {
201 autoAllowedTags[tag] = userAllowedTags[tag].clone();
203 if (needsAdditionalAttributes) {
204 autoAllowedTags[tag].restrictedTags.allowed.attributes = _.union(allowedAttributes, requiredAttributes);
206 if (needsAdditionalClasses) {
207 autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(allowedClasses, requiredClasses);
212 return autoAllowedTags;
216 * Parses the value of this.$allowedHTMLFormItem.
218 * @param {string} setting
219 * The string representation of the setting. For example:
220 * <p class="callout"> <br> <a href hreflang>
222 * @return {Object.<string, Drupal.FilterHTMLRule>}
223 * The corresponding text filter HTML rule objects, one per tag, keyed by
226 _parseSetting(setting) {
232 const allowedTags = setting.match(/(<[^>]+>)/g);
233 const sandbox = document.createElement('div');
235 for (let t = 0; t < allowedTags.length; t++) {
236 // Let the browser do the parsing work for us.
237 sandbox.innerHTML = allowedTags[t];
238 node = sandbox.firstChild;
239 tag = node.tagName.toLowerCase();
241 // Build the Drupal.FilterHtmlRule object.
242 rule = new Drupal.FilterHTMLRule();
243 // We create one rule per allowed tag, so always one tag.
244 rule.restrictedTags.tags = [tag];
245 // Add the attribute restrictions.
246 attributes = node.attributes;
247 for (let i = 0; i < attributes.length; i++) {
248 attribute = attributes.item(i);
249 const attributeName = attribute.nodeName;
250 // @todo Drupal.FilterHtmlRule does not allow for generic attribute
251 // value restrictions, only for the "class" and "style" attribute's
252 // values. The filter_html filter always disallows the "style"
253 // attribute, so we only need to support "class" attribute value
254 // restrictions. Fix once https://www.drupal.org/node/2567801 lands.
255 if (attributeName === 'class') {
256 const attributeValue = attribute.textContent;
257 rule.restrictedTags.allowed.classes = attributeValue.split(' ');
260 rule.restrictedTags.allowed.attributes.push(attributeName);
270 * Generates the value of this.$allowedHTMLFormItem.
272 * @param {Object.<string, Drupal.FilterHTMLRule>} tags
273 * The parsed representation of the setting.
276 * The string representation of the setting. e.g. "<p> <br> <a>"
278 _generateSetting(tags) {
279 return _.reduce(tags, (setting, rule, tag) => {
280 if (setting.length) {
284 setting += `<${tag}`;
285 if (rule.restrictedTags.allowed.attributes.length) {
286 setting += ` ${rule.restrictedTags.allowed.attributes.join(' ')}`;
288 // @todo Drupal.FilterHtmlRule does not allow for generic attribute
289 // value restrictions, only for the "class" and "style" attribute's
290 // values. The filter_html filter always disallows the "style"
291 // attribute, so we only need to support "class" attribute value
292 // restrictions. Fix once https://www.drupal.org/node/2567801 lands.
293 if (rule.restrictedTags.allowed.classes.length) {
294 setting += ` class="${rule.restrictedTags.allowed.classes.join(' ')}"`;
305 * Theme function for the filter_html update message.
307 * @param {Array} tags
308 * An array of the new tags that are to be allowed.
311 * The corresponding HTML.
313 Drupal.theme.filterFilterHTMLUpdateMessage = function (tags) {
315 const tagList = Drupal.behaviors.filterFilterHtmlUpdating._generateSetting(tags);
316 html += '<p class="editor-update-message">';
317 html += Drupal.t('Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.', { '@tag-list': tagList });
321 }(jQuery, Drupal, _, document));