3 * Attaches behavior for updating filter_html's settings automatically.
6 (function ($, Drupal, _, document) {
10 if (Drupal.filterConfiguration) {
13 * Implement a live setting parser to prevent text editors from automatically
14 * enabling buttons that are not allowed by this filter's configuration.
18 Drupal.filterConfiguration.liveSettingParsers.filter_html = {
22 * An array of filter rules.
24 getRules: function () {
25 var currentValue = $('#edit-filters-filter-html-settings-allowed-html').val();
26 var rules = Drupal.behaviors.filterFilterHtmlUpdating._parseSetting(currentValue);
28 // Build a FilterHTMLRule that reflects the hard-coded behavior that
29 // strips all "style" attribute and all "on*" attributes.
30 var rule = new Drupal.FilterHTMLRule();
31 rule.restrictedTags.tags = ['*'];
32 rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
41 * Displays and updates what HTML tags are allowed to use in a filter.
43 * @type {Drupal~behavior}
45 * @todo Remove everything but 'attach' and 'detach' and make a proper object.
47 * @prop {Drupal~behaviorAttach} attach
48 * Attaches behavior for updating allowed HTML tags.
50 Drupal.behaviors.filterFilterHtmlUpdating = {
52 // The form item contains the "Allowed HTML tags" setting.
53 $allowedHTMLFormItem: null,
55 // The description for the "Allowed HTML tags" field.
56 $allowedHTMLDescription: null,
59 * The parsed, user-entered tag list of $allowedHTMLFormItem
61 * @var {Object.<string, Drupal.FilterHTMLRule>}
65 // The auto-created tag list thus far added.
68 // Track which new features have been added to the text editor.
71 attach: function (context, settings) {
73 $(context).find('[name="filters[filter_html][settings][allowed_html]"]').once('filter-filter_html-updating').each(function () {
74 that.$allowedHTMLFormItem = $(this);
75 that.$allowedHTMLDescription = that.$allowedHTMLFormItem.closest('.js-form-item').find('.description');
76 that.userTags = that._parseSetting(this.value);
78 // Update the new allowed tags based on added text editor features.
80 .on('drupalEditorFeatureAdded', function (e, feature) {
81 that.newFeatures[feature.name] = feature.rules;
82 that._updateAllowedTags();
84 .on('drupalEditorFeatureModified', function (e, feature) {
85 if (that.newFeatures.hasOwnProperty(feature.name)) {
86 that.newFeatures[feature.name] = feature.rules;
87 that._updateAllowedTags();
90 .on('drupalEditorFeatureRemoved', function (e, feature) {
91 if (that.newFeatures.hasOwnProperty(feature.name)) {
92 delete that.newFeatures[feature.name];
93 that._updateAllowedTags();
97 // When the allowed tags list is manually changed, update userTags.
98 that.$allowedHTMLFormItem.on('change.updateUserTags', function () {
99 that.userTags = _.difference(that._parseSetting(this.value), that.autoTags);
105 * Updates the "Allowed HTML tags" setting and shows an informative message.
107 _updateAllowedTags: function () {
108 // Update the list of auto-created tags.
109 this.autoTags = this._calculateAutoAllowedTags(this.userTags, this.newFeatures);
111 // Remove any previous auto-created tag message.
112 this.$allowedHTMLDescription.find('.editor-update-message').remove();
114 // If any auto-created tags: insert message and update form item.
115 if (!_.isEmpty(this.autoTags)) {
116 this.$allowedHTMLDescription.append(Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags));
117 var userTagsWithoutOverrides = _.omit(this.userTags, _.keys(this.autoTags));
118 this.$allowedHTMLFormItem.val(this._generateSetting(userTagsWithoutOverrides) + ' ' + this._generateSetting(this.autoTags));
120 // Restore to original state.
122 this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags));
127 * Calculates which HTML tags the added text editor buttons need to work.
129 * The filter_html filter is only concerned with the required tags, not with
130 * any properties, nor with each feature's "allowed" tags.
132 * @param {Array} userAllowedTags
133 * The list of user-defined allowed tags.
134 * @param {object} newFeatures
135 * A list of {@link Drupal.EditorFeature} objects' rules, keyed by
139 * A list of new allowed tags.
141 _calculateAutoAllowedTags: function (userAllowedTags, newFeatures) {
147 var editorRequiredTags = {};
148 // Map the newly added Text Editor features to Drupal.FilterHtmlRule
149 // objects (to allow comparing userTags with autoTags).
150 for (featureName in newFeatures) {
151 if (newFeatures.hasOwnProperty(featureName)) {
152 feature = newFeatures[featureName];
153 for (var f = 0; f < feature.length; f++) {
154 featureRule = feature[f];
155 for (var t = 0; t < featureRule.required.tags.length; t++) {
156 tag = featureRule.required.tags[t];
157 if (!_.has(editorRequiredTags, tag)) {
158 filterRule = new Drupal.FilterHTMLRule();
159 filterRule.restrictedTags.tags = [tag];
160 // @todo Neither Drupal.FilterHtmlRule nor
161 // Drupal.EditorFeatureHTMLRule allow for generic attribute
162 // value restrictions, only for the "class" and "style"
163 // attribute's values to be restricted. The filter_html filter
164 // always disallows the "style" attribute, so we only need to
165 // support "class" attribute value restrictions. Fix once
166 // https://www.drupal.org/node/2567801 lands.
167 filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(0);
168 filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(0);
169 editorRequiredTags[tag] = filterRule;
171 // The tag is already allowed, add any additionally allowed
174 filterRule = editorRequiredTags[tag];
175 filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes);
176 filterRule.restrictedTags.allowed.classes = _.union(filterRule.restrictedTags.allowed.classes, featureRule.required.classes);
183 // Now compare userAllowedTags with editorRequiredTags, and build
184 // autoAllowedTags, which contains:
185 // - any tags in editorRequiredTags but not in userAllowedTags (i.e. tags
186 // that are additionally going to be allowed)
187 // - any tags in editorRequiredTags that already exists in userAllowedTags
188 // but does not allow all attributes or attribute values
189 var autoAllowedTags = {};
190 for (tag in editorRequiredTags) {
191 // If userAllowedTags does not contain a rule for this editor-required
192 // tag, then add it to the list of automatically allowed tags.
193 if (!_.has(userAllowedTags, tag)) {
194 autoAllowedTags[tag] = editorRequiredTags[tag];
196 // Otherwise, if userAllowedTags already allows this tag, then check if
197 // additional attributes and classes on this tag are required by the
200 var requiredAttributes = editorRequiredTags[tag].restrictedTags.allowed.attributes;
201 var allowedAttributes = userAllowedTags[tag].restrictedTags.allowed.attributes;
202 var needsAdditionalAttributes = requiredAttributes.length && _.difference(requiredAttributes, allowedAttributes).length;
203 var requiredClasses = editorRequiredTags[tag].restrictedTags.allowed.classes;
204 var allowedClasses = userAllowedTags[tag].restrictedTags.allowed.classes;
205 var needsAdditionalClasses = requiredClasses.length && _.difference(requiredClasses, allowedClasses).length;
206 if (needsAdditionalAttributes || needsAdditionalClasses) {
207 autoAllowedTags[tag] = userAllowedTags[tag].clone();
209 if (needsAdditionalAttributes) {
210 autoAllowedTags[tag].restrictedTags.allowed.attributes = _.union(allowedAttributes, requiredAttributes);
212 if (needsAdditionalClasses) {
213 autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(allowedClasses, requiredClasses);
218 return autoAllowedTags;
222 * Parses the value of this.$allowedHTMLFormItem.
224 * @param {string} setting
225 * The string representation of the setting. For example:
226 * <p class="callout"> <br> <a href hreflang>
228 * @return {Object.<string, Drupal.FilterHTMLRule>}
229 * The corresponding text filter HTML rule objects, one per tag, keyed by
232 _parseSetting: function (setting) {
238 var allowedTags = setting.match(/(<[^>]+>)/g);
239 var sandbox = document.createElement('div');
241 for (var t = 0; t < allowedTags.length; t++) {
242 // Let the browser do the parsing work for us.
243 sandbox.innerHTML = allowedTags[t];
244 node = sandbox.firstChild;
245 tag = node.tagName.toLowerCase();
247 // Build the Drupal.FilterHtmlRule object.
248 rule = new Drupal.FilterHTMLRule();
249 // We create one rule per allowed tag, so always one tag.
250 rule.restrictedTags.tags = [tag];
251 // Add the attribute restrictions.
252 attributes = node.attributes;
253 for (var i = 0; i < attributes.length; i++) {
254 attribute = attributes.item(i);
255 var attributeName = attribute.nodeName;
256 // @todo Drupal.FilterHtmlRule does not allow for generic attribute
257 // value restrictions, only for the "class" and "style" attribute's
258 // values. The filter_html filter always disallows the "style"
259 // attribute, so we only need to support "class" attribute value
260 // restrictions. Fix once https://www.drupal.org/node/2567801 lands.
261 if (attributeName === 'class') {
262 var attributeValue = attribute.textContent;
263 rule.restrictedTags.allowed.classes = attributeValue.split(' ');
266 rule.restrictedTags.allowed.attributes.push(attributeName);
276 * Generates the value of this.$allowedHTMLFormItem.
278 * @param {Object.<string, Drupal.FilterHTMLRule>} tags
279 * The parsed representation of the setting.
282 * The string representation of the setting. e.g. "<p> <br> <a>"
284 _generateSetting: function (tags) {
285 return _.reduce(tags, function (setting, rule, tag) {
286 if (setting.length) {
290 setting += '<' + tag;
291 if (rule.restrictedTags.allowed.attributes.length) {
292 setting += ' ' + rule.restrictedTags.allowed.attributes.join(' ');
294 // @todo Drupal.FilterHtmlRule does not allow for generic attribute
295 // value restrictions, only for the "class" and "style" attribute's
296 // values. The filter_html filter always disallows the "style"
297 // attribute, so we only need to support "class" attribute value
298 // restrictions. Fix once https://www.drupal.org/node/2567801 lands.
299 if (rule.restrictedTags.allowed.classes.length) {
300 setting += ' class="' + rule.restrictedTags.allowed.classes.join(' ') + '"';
311 * Theme function for the filter_html update message.
313 * @param {Array} tags
314 * An array of the new tags that are to be allowed.
317 * The corresponding HTML.
319 Drupal.theme.filterFilterHTMLUpdateMessage = function (tags) {
321 var tagList = Drupal.behaviors.filterFilterHtmlUpdating._generateSetting(tags);
322 html += '<p class="editor-update-message">';
323 html += Drupal.t('Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.', {'@tag-list': tagList});
328 })(jQuery, Drupal, _, document);