8 (function ($, Drupal, drupalSettings, CKEDITOR) {
9 function parseAttributes(editor, element) {
10 const parsedAttributes = {};
12 const domElement = element.$;
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) {
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;
27 // Remove any cke_* classes.
28 if (parsedAttributes.class) {
29 parsedAttributes.class = CKEDITOR.tools.trim(parsedAttributes.class.replace(/cke_\S+/, ''));
32 return parsedAttributes;
35 function getAttributes(editor, data) {
37 for (const attributeName in data) {
38 if (data.hasOwnProperty(attributeName)) {
39 set[attributeName] = data[attributeName];
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;
47 // Remove all attributes which are not currently set.
49 for (const s in set) {
50 if (set.hasOwnProperty(s)) {
57 removed: CKEDITOR.tools.objectKeys(removed),
61 CKEDITOR.plugins.add('drupallink', {
62 icons: 'drupallink,drupalunlink',
66 // Add the commands for link and unlink.
67 editor.addCommand('drupallink', {
76 requiredContent: new CKEDITOR.style({
82 modes: { wysiwyg: 1 },
85 const drupalImageUtils = CKEDITOR.plugins.drupalimage;
86 const focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
87 let linkElement = getSelectedLink(editor);
89 // Set existing values based on selected element.
90 let existingValues = {};
91 if (linkElement && linkElement.$) {
92 existingValues = parseAttributes(editor, linkElement);
94 // Or, if an image widget is focused, we're editing a link wrapping
96 else if (focusedImageWidget && focusedImageWidget.data.link) {
97 existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
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');
110 editor.fire('saveSnapshot');
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];
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);
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);
131 // Set the link so individual properties may be set below.
132 linkElement = getSelectedLink(editor);
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);
144 // Delete the property if set to an empty string.
146 linkElement.removeAttribute(attrName);
152 // Save snapshot for undo support.
153 editor.fire('saveSnapshot');
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',
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);
167 editor.addCommand('drupalunlink', {
170 requiredContent: new CKEDITOR.style({
177 const style = new CKEDITOR.style({ element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 });
178 editor.removeStyle(style);
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);
186 this.setState(CKEDITOR.TRISTATE_DISABLED);
192 editor.setKeystroke(CKEDITOR.CTRL + 75, 'drupallink');
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',
200 editor.ui.addButton('DrupalUnlink', {
201 label: Drupal.t('Unlink'),
202 command: 'drupalunlink',
206 editor.on('doubleclick', (evt) => {
207 const element = getSelectedLink(editor) || evt.data.element;
209 if (!element.isReadOnly()) {
210 if (element.is('a')) {
211 editor.getSelection().selectElement(element);
212 editor.getCommand('drupallink').exec();
217 // If the "menu" plugin is loaded, register the menu items.
218 if (editor.addMenuItems) {
219 editor.addMenuItems({
221 label: Drupal.t('Edit Link'),
222 command: 'drupallink',
228 label: Drupal.t('Unlink'),
229 command: 'drupalunlink',
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()) {
242 const anchor = getSelectedLink(editor);
248 if (anchor.getAttribute('href') && anchor.getChildCount()) {
249 menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF };
258 * Get the surrounding link element of current selection.
260 * The following selection will all return the link element.
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>
270 * @param {CKEDITOR.editor} editor
271 * The CKEditor editor object
273 * @return {?HTMLElement}
274 * The selected link element, or null.
277 function getSelectedLink(editor) {
278 const selection = editor.getSelection();
279 const selectedElement = selection.getSelectedElement();
280 if (selectedElement && selectedElement.is('a')) {
281 return selectedElement;
284 const range = selection.getRanges(true)[0];
287 range.shrink(CKEDITOR.SHRINK_TEXT);
288 return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
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,
300 }(jQuery, Drupal, drupalSettings, CKEDITOR));