Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / ckeditor / js / plugins / drupallink / plugin.es6.js
1 /**
2  * @file
3  * Drupal Link plugin.
4  *
5  * @ignore
6  */
7
8 (function ($, Drupal, drupalSettings, CKEDITOR) {
9   function parseAttributes(editor, element) {
10     const parsedAttributes = {};
11
12     const domElement = element.$;
13     let attribute;
14     let attributeName;
15     for (let attrIndex = 0; attrIndex < domElement.attributes.length; attrIndex++) {
16       attribute = domElement.attributes.item(attrIndex);
17       attributeName = attribute.nodeName.toLowerCase();
18       // Ignore data-cke-* attributes; they're CKEditor internals.
19       if (attributeName.indexOf('data-cke-') === 0) {
20         continue;
21       }
22       // Store the value for this attribute, unless there's a data-cke-saved-
23       // alternative for it, which will contain the quirk-free, original value.
24       parsedAttributes[attributeName] = element.data(`cke-saved-${attributeName}`) || attribute.nodeValue;
25     }
26
27     // Remove any cke_* classes.
28     if (parsedAttributes.class) {
29       parsedAttributes.class = CKEDITOR.tools.trim(parsedAttributes.class.replace(/cke_\S+/, ''));
30     }
31
32     return parsedAttributes;
33   }
34
35   function getAttributes(editor, data) {
36     const set = {};
37     for (const attributeName in data) {
38       if (data.hasOwnProperty(attributeName)) {
39         set[attributeName] = data[attributeName];
40       }
41     }
42
43     // CKEditor tracks the *actual* saved href in a data-cke-saved-* attribute
44     // to work around browser quirks. We need to update it.
45     set['data-cke-saved-href'] = set.href;
46
47     // Remove all attributes which are not currently set.
48     const removed = {};
49     for (const s in set) {
50       if (set.hasOwnProperty(s)) {
51         delete removed[s];
52       }
53     }
54
55     return {
56       set,
57       removed: CKEDITOR.tools.objectKeys(removed),
58     };
59   }
60
61   CKEDITOR.plugins.add('drupallink', {
62     icons: 'drupallink,drupalunlink',
63     hidpi: true,
64
65     init(editor) {
66       // Add the commands for link and unlink.
67       editor.addCommand('drupallink', {
68         allowedContent: {
69           a: {
70             attributes: {
71               '!href': true,
72             },
73             classes: {},
74           },
75         },
76         requiredContent: new CKEDITOR.style({
77           element: 'a',
78           attributes: {
79             href: '',
80           },
81         }),
82         modes: { wysiwyg: 1 },
83         canUndo: true,
84         exec(editor) {
85           const drupalImageUtils = CKEDITOR.plugins.drupalimage;
86           const focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
87           let linkElement = getSelectedLink(editor);
88
89           // Set existing values based on selected element.
90           let existingValues = {};
91           if (linkElement && linkElement.$) {
92             existingValues = parseAttributes(editor, linkElement);
93           }
94           // Or, if an image widget is focused, we're editing a link wrapping
95           // an image widget.
96           else if (focusedImageWidget && focusedImageWidget.data.link) {
97             existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
98           }
99
100           // Prepare a save callback to be used upon saving the dialog.
101           const saveCallback = function (returnValues) {
102             // If an image widget is focused, we're not editing an independent
103             // link, but we're wrapping an image widget in a link.
104             if (focusedImageWidget) {
105               focusedImageWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedImageWidget.data.link));
106               editor.fire('saveSnapshot');
107               return;
108             }
109
110             editor.fire('saveSnapshot');
111
112             // Create a new link element if needed.
113             if (!linkElement && returnValues.attributes.href) {
114               const selection = editor.getSelection();
115               const range = selection.getRanges(1)[0];
116
117               // Use link URL as text with a collapsed cursor.
118               if (range.collapsed) {
119                 // Shorten mailto URLs to just the email address.
120                 const text = new CKEDITOR.dom.text(returnValues.attributes.href.replace(/^mailto:/, ''), editor.document);
121                 range.insertNode(text);
122                 range.selectNodeContents(text);
123               }
124
125               // Create the new link by applying a style to the new text.
126               const style = new CKEDITOR.style({ element: 'a', attributes: returnValues.attributes });
127               style.type = CKEDITOR.STYLE_INLINE;
128               style.applyToRange(range);
129               range.select();
130
131               // Set the link so individual properties may be set below.
132               linkElement = getSelectedLink(editor);
133             }
134             // Update the link properties.
135             else if (linkElement) {
136               for (const attrName in returnValues.attributes) {
137                 if (returnValues.attributes.hasOwnProperty(attrName)) {
138                   // Update the property if a value is specified.
139                   if (returnValues.attributes[attrName].length > 0) {
140                     const value = returnValues.attributes[attrName];
141                     linkElement.data(`cke-saved-${attrName}`, value);
142                     linkElement.setAttribute(attrName, value);
143                   }
144                   // Delete the property if set to an empty string.
145                   else {
146                     linkElement.removeAttribute(attrName);
147                   }
148                 }
149               }
150             }
151
152             // Save snapshot for undo support.
153             editor.fire('saveSnapshot');
154           };
155           // Drupal.t() will not work inside CKEditor plugins because CKEditor
156           // loads the JavaScript file instead of Drupal. Pull translated
157           // strings from the plugin settings that are translated server-side.
158           const dialogSettings = {
159             title: linkElement ? editor.config.drupalLink_dialogTitleEdit : editor.config.drupalLink_dialogTitleAdd,
160             dialogClass: 'editor-link-dialog',
161           };
162
163           // Open the dialog for the edit form.
164           Drupal.ckeditor.openDialog(editor, Drupal.url(`editor/dialog/link/${editor.config.drupal.format}`), existingValues, saveCallback, dialogSettings);
165         },
166       });
167       editor.addCommand('drupalunlink', {
168         contextSensitive: 1,
169         startDisabled: 1,
170         requiredContent: new CKEDITOR.style({
171           element: 'a',
172           attributes: {
173             href: '',
174           },
175         }),
176         exec(editor) {
177           const style = new CKEDITOR.style({ element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 });
178           editor.removeStyle(style);
179         },
180         refresh(editor, path) {
181           const element = path.lastElement && path.lastElement.getAscendant('a', true);
182           if (element && element.getName() === 'a' && element.getAttribute('href') && element.getChildCount()) {
183             this.setState(CKEDITOR.TRISTATE_OFF);
184           }
185           else {
186             this.setState(CKEDITOR.TRISTATE_DISABLED);
187           }
188         },
189       });
190
191       // CTRL + K.
192       editor.setKeystroke(CKEDITOR.CTRL + 75, 'drupallink');
193
194       // Add buttons for link and unlink.
195       if (editor.ui.addButton) {
196         editor.ui.addButton('DrupalLink', {
197           label: Drupal.t('Link'),
198           command: 'drupallink',
199         });
200         editor.ui.addButton('DrupalUnlink', {
201           label: Drupal.t('Unlink'),
202           command: 'drupalunlink',
203         });
204       }
205
206       editor.on('doubleclick', (evt) => {
207         const element = getSelectedLink(editor) || evt.data.element;
208
209         if (!element.isReadOnly()) {
210           if (element.is('a')) {
211             editor.getSelection().selectElement(element);
212             editor.getCommand('drupallink').exec();
213           }
214         }
215       });
216
217       // If the "menu" plugin is loaded, register the menu items.
218       if (editor.addMenuItems) {
219         editor.addMenuItems({
220           link: {
221             label: Drupal.t('Edit Link'),
222             command: 'drupallink',
223             group: 'link',
224             order: 1,
225           },
226
227           unlink: {
228             label: Drupal.t('Unlink'),
229             command: 'drupalunlink',
230             group: 'link',
231             order: 5,
232           },
233         });
234       }
235
236       // If the "contextmenu" plugin is loaded, register the listeners.
237       if (editor.contextMenu) {
238         editor.contextMenu.addListener((element, selection) => {
239           if (!element || element.isReadOnly()) {
240             return null;
241           }
242           const anchor = getSelectedLink(editor);
243           if (!anchor) {
244             return null;
245           }
246
247           let menu = {};
248           if (anchor.getAttribute('href') && anchor.getChildCount()) {
249             menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF };
250           }
251           return menu;
252         });
253       }
254     },
255   });
256
257   /**
258    * Get the surrounding link element of current selection.
259    *
260    * The following selection will all return the link element.
261    *
262    * @example
263    *  <a href="#">li^nk</a>
264    *  <a href="#">[link]</a>
265    *  text[<a href="#">link]</a>
266    *  <a href="#">li[nk</a>]
267    *  [<b><a href="#">li]nk</a></b>]
268    *  [<a href="#"><b>li]nk</b></a>
269    *
270    * @param {CKEDITOR.editor} editor
271    *   The CKEditor editor object
272    *
273    * @return {?HTMLElement}
274    *   The selected link element, or null.
275    *
276    */
277   function getSelectedLink(editor) {
278     const selection = editor.getSelection();
279     const selectedElement = selection.getSelectedElement();
280     if (selectedElement && selectedElement.is('a')) {
281       return selectedElement;
282     }
283
284     const range = selection.getRanges(true)[0];
285
286     if (range) {
287       range.shrink(CKEDITOR.SHRINK_TEXT);
288       return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
289     }
290     return null;
291   }
292
293   // Expose an API for other plugins to interact with drupallink widgets.
294   // (Compatible with the official CKEditor link plugin's API:
295   // http://dev.ckeditor.com/ticket/13885.)
296   CKEDITOR.plugins.drupallink = {
297     parseLinkAttributes: parseAttributes,
298     getLinkAttributes: getAttributes,
299   };
300 }(jQuery, Drupal, drupalSettings, CKEDITOR));