8 (function($, Drupal, drupalSettings, CKEDITOR) {
9 function parseAttributes(editor, element) {
10 const parsedAttributes = {};
12 const domElement = element.$;
17 attrIndex < domElement.attributes.length;
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) {
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;
32 // Remove any cke_* classes.
33 if (parsedAttributes.class) {
34 parsedAttributes.class = CKEDITOR.tools.trim(
35 parsedAttributes.class.replace(/cke_\S+/, ''),
39 return parsedAttributes;
42 function getAttributes(editor, data) {
44 Object.keys(data || {}).forEach(attributeName => {
45 set[attributeName] = data[attributeName];
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;
52 // Remove all attributes which are not currently set.
54 Object.keys(set).forEach(s => {
60 removed: CKEDITOR.tools.objectKeys(removed),
65 * Get the surrounding link element of current selection.
67 * The following selection will all return the link element.
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>
77 * @param {CKEDITOR.editor} editor
78 * The CKEditor editor object
80 * @return {?HTMLElement}
81 * The selected link element, or null.
84 function getSelectedLink(editor) {
85 const selection = editor.getSelection();
86 const selectedElement = selection.getSelectedElement();
87 if (selectedElement && selectedElement.is('a')) {
88 return selectedElement;
91 const range = selection.getRanges(true)[0];
94 range.shrink(CKEDITOR.SHRINK_TEXT);
95 return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
100 CKEDITOR.plugins.add('drupallink', {
101 icons: 'drupallink,drupalunlink',
105 // Add the commands for link and unlink.
106 editor.addCommand('drupallink', {
115 requiredContent: new CKEDITOR.style({
121 modes: { wysiwyg: 1 },
124 const drupalImageUtils = CKEDITOR.plugins.drupalimage;
125 const focusedImageWidget =
126 drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
127 let linkElement = getSelectedLink(editor);
129 // Set existing values based on selected element.
130 let existingValues = {};
131 if (linkElement && linkElement.$) {
132 existingValues = parseAttributes(editor, linkElement);
134 // Or, if an image widget is focused, we're editing a link wrapping
136 else if (focusedImageWidget && focusedImageWidget.data.link) {
137 existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
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(
147 CKEDITOR.tools.extend(
148 returnValues.attributes,
149 focusedImageWidget.data.link,
152 editor.fire('saveSnapshot');
156 editor.fire('saveSnapshot');
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];
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:/, ''),
170 range.insertNode(text);
171 range.selectNodeContents(text);
174 // Create the new link by applying a style to the new text.
175 const style = new CKEDITOR.style({
177 attributes: returnValues.attributes,
179 style.type = CKEDITOR.STYLE_INLINE;
180 style.applyToRange(range);
183 // Set the link so individual properties may be set below.
184 linkElement = getSelectedLink(editor);
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);
195 // Delete the property if set to an empty string.
197 linkElement.removeAttribute(attrName);
202 // Save snapshot for undo support.
203 editor.fire('saveSnapshot');
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 = {
210 ? editor.config.drupalLink_dialogTitleEdit
211 : editor.config.drupalLink_dialogTitleAdd,
212 dialogClass: 'editor-link-dialog',
215 // Open the dialog for the edit form.
216 Drupal.ckeditor.openDialog(
218 Drupal.url(`editor/dialog/link/${editor.config.drupal.format}`),
225 editor.addCommand('drupalunlink', {
228 requiredContent: new CKEDITOR.style({
235 const style = new CKEDITOR.style({
237 type: CKEDITOR.STYLE_INLINE,
238 alwaysRemoveElement: 1,
240 editor.removeStyle(style);
242 refresh(editor, path) {
244 path.lastElement && path.lastElement.getAscendant('a', true);
247 element.getName() === 'a' &&
248 element.getAttribute('href') &&
249 element.getChildCount()
251 this.setState(CKEDITOR.TRISTATE_OFF);
253 this.setState(CKEDITOR.TRISTATE_DISABLED);
259 editor.setKeystroke(CKEDITOR.CTRL + 75, 'drupallink');
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',
267 editor.ui.addButton('DrupalUnlink', {
268 label: Drupal.t('Unlink'),
269 command: 'drupalunlink',
273 editor.on('doubleclick', evt => {
274 const element = getSelectedLink(editor) || evt.data.element;
276 if (!element.isReadOnly()) {
277 if (element.is('a')) {
278 editor.getSelection().selectElement(element);
279 editor.getCommand('drupallink').exec();
284 // If the "menu" plugin is loaded, register the menu items.
285 if (editor.addMenuItems) {
286 editor.addMenuItems({
288 label: Drupal.t('Edit Link'),
289 command: 'drupallink',
295 label: Drupal.t('Unlink'),
296 command: 'drupalunlink',
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()) {
309 const anchor = getSelectedLink(editor);
315 if (anchor.getAttribute('href') && anchor.getChildCount()) {
317 link: CKEDITOR.TRISTATE_OFF,
318 unlink: CKEDITOR.TRISTATE_OFF,
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,
334 })(jQuery, Drupal, drupalSettings, CKEDITOR);