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