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