summaryrefslogtreecommitdiffstats
path: root/core/modules/ckeditor/js/ckeditor.js
blob: 18c4632d47d2b9bb497e7b4ca8b5d0c9b8ab6a27 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
(function (Drupal, CKEDITOR, $) {

"use strict";

Drupal.editors.ckeditor = {

  attach: function (element, format) {
    this._loadExternalPlugins(format);
    this._ACF_HACK_to_support_blacklisted_attributes(element, format);
    return !!CKEDITOR.replace(element, format.editorSettings);
  },

  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;
  },

  onChange: function (element, callback) {
    var editor = CKEDITOR.dom.element.get(element).getEditor();
    if (editor) {
      var changed = function () {
        window.setTimeout(function () {
          callback(editor.getData());
        }, 0);
      };
      // @todo Make this more elegant once http://dev.ckeditor.com/ticket/9794
      // is fixed.
      editor.on('key', changed);
      editor.on('paste', changed);
      editor.on('afterCommandExec', changed);
    }
    return !!editor;
  },

  attachInlineEditor: function (element, format, mainToolbarId, floatedToolbarId) {
    this._loadExternalPlugins(format);
    this._ACF_HACK_to_support_blacklisted_attributes(element, 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);
  },

  _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;
    }
  },

  /**
   * This is a huge hack to do ONE thing: to allow Drupal to fully mandate what
   * CKEditor should allow by setting CKEditor's allowedContent setting. The
   * problem is that allowedContent only allows for whitelisting, whereas
   * Drupal's default HTML filtering (the filter_html filter) also blacklists
   * the "style" and "on*" ("onClick" etc.) attributes.
   *
   * So this function hacks in explicit support for Drupal's filter_html's need
   * to blacklist specifically those attributes, until ACF supports blacklisting
   * of properties: http://dev.ckeditor.com/ticket/10276.
   *
   * Limitations:
   *   - This does not support blacklisting of other attributes, it's only
   *     intended to implement filter_html's blacklisted attributes.
   *   - This is only a temporary work-around; it assumes the filter_html
   *     filter is being used whenever *any* restriction exists. This is a valid
   *     assumption for the default text formats in Drupal 8 core, but obviously
   *     won't work for release.
   *
   * This is the only way we could get https://drupal.org/node/1936392 committed
   * before Drupal 8 code freeze on July 1, 2013. CKEditor has committed to
   * explicitly supporting this in some way.
   *
   * @todo D8 remove this once http://dev.ckeditor.com/ticket/10276 is done.
   */
  _ACF_HACK_to_support_blacklisted_attributes: function (element, format) {
    function override(rule) {
      var oldValue = rule.attributes;
      function filter_html_override_attributes (attribute) {
        // Disallow the "style" and "on*" attributes on any tag.
        if (attribute === 'style' || attribute.substr(0, 2) === 'on') {
          return false;
        }

        // Ensure the original logic still runs, if any.
        if (typeof oldValue === 'function') {
          return oldValue(attribute);
        }
        else if (typeof oldValue === 'boolean') {
          return oldValue;
        }

        // Otherwise, accept this attribute.
        return true;
      }
      rule.attributes = filter_html_override_attributes;
    }

    CKEDITOR.once('instanceLoaded', function(e) {
      if (e.editor.name === element.id) {
        // If everything is allowed, everything is allowed.
        if (format.editorSettings.allowedContent === true) {
          return;
        }
        // Otherwise, assume Drupal's filter_html filter is being used.
        else {
          // Get the filter object (ACF).
          var filter = e.editor.filter;
          // Find the "config" rule (the one caused by the allowedContent
          // setting) for each HTML tag, and override its "attributes" value.
          for (var el in filter._.rules.elements) {
            if (filter._.rules.elements.hasOwnProperty(el)) {
              for (var i = 0; i < filter._.rules.elements[el].length; i++) {
                if (filter._.rules.elements[el][i].featureName === 'config') {
                  override(filter._.rules.elements[el][i]);
                }
              }
            }
          }
        }
      }
    });
  }
};

})(Drupal, CKEDITOR, jQuery);