8 (function ($, Drupal, drupalSettings, CKEDITOR) {
12 function parseAttributes(editor, element) {
13 var parsedAttributes = {};
15 var domElement = element.$;
18 for (var attrIndex = 0; attrIndex < domElement.attributes.length; attrIndex++) {
19 attribute = domElement.attributes.item(attrIndex);
20 attributeName = attribute.nodeName.toLowerCase();
21 // Ignore data-cke-* attributes; they're CKEditor internals.
22 if (attributeName.indexOf('data-cke-') === 0) {
25 // Store the value for this attribute, unless there's a data-cke-saved-
26 // alternative for it, which will contain the quirk-free, original value.
27 parsedAttributes[attributeName] = element.data('cke-saved-' + attributeName) || attribute.nodeValue;
30 // Remove any cke_* classes.
31 if (parsedAttributes.class) {
32 parsedAttributes.class = CKEDITOR.tools.trim(parsedAttributes.class.replace(/cke_\S+/, ''));
35 return parsedAttributes;
38 function getAttributes(editor, data) {
40 for (var attributeName in data) {
41 if (data.hasOwnProperty(attributeName)) {
42 set[attributeName] = data[attributeName];
46 // CKEditor tracks the *actual* saved href in a data-cke-saved-* attribute
47 // to work around browser quirks. We need to update it.
48 set['data-cke-saved-href'] = set.href;
50 // Remove all attributes which are not currently set.
53 if (set.hasOwnProperty(s)) {
60 removed: CKEDITOR.tools.objectKeys(removed)
64 CKEDITOR.plugins.add('drupallink', {
65 icons: 'drupallink,drupalunlink',
68 init: function (editor) {
69 // Add the commands for link and unlink.
70 editor.addCommand('drupallink', {
79 requiredContent: new CKEDITOR.style({
87 exec: function (editor) {
88 var drupalImageUtils = CKEDITOR.plugins.drupalimage;
89 var focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
90 var linkElement = getSelectedLink(editor);
92 // Set existing values based on selected element.
93 var existingValues = {};
94 if (linkElement && linkElement.$) {
95 existingValues = parseAttributes(editor, linkElement);
97 // Or, if an image widget is focused, we're editing a link wrapping
99 else if (focusedImageWidget && focusedImageWidget.data.link) {
100 existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
103 // Prepare a save callback to be used upon saving the dialog.
104 var saveCallback = function (returnValues) {
105 // If an image widget is focused, we're not editing an independent
106 // link, but we're wrapping an image widget in a link.
107 if (focusedImageWidget) {
108 focusedImageWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedImageWidget.data.link));
109 editor.fire('saveSnapshot');
113 editor.fire('saveSnapshot');
115 // Create a new link element if needed.
116 if (!linkElement && returnValues.attributes.href) {
117 var selection = editor.getSelection();
118 var range = selection.getRanges(1)[0];
120 // Use link URL as text with a collapsed cursor.
121 if (range.collapsed) {
122 // Shorten mailto URLs to just the email address.
123 var text = new CKEDITOR.dom.text(returnValues.attributes.href.replace(/^mailto:/, ''), editor.document);
124 range.insertNode(text);
125 range.selectNodeContents(text);
128 // Create the new link by applying a style to the new text.
129 var style = new CKEDITOR.style({element: 'a', attributes: returnValues.attributes});
130 style.type = CKEDITOR.STYLE_INLINE;
131 style.applyToRange(range);
134 // Set the link so individual properties may be set below.
135 linkElement = getSelectedLink(editor);
137 // Update the link properties.
138 else if (linkElement) {
139 for (var attrName in returnValues.attributes) {
140 if (returnValues.attributes.hasOwnProperty(attrName)) {
141 // Update the property if a value is specified.
142 if (returnValues.attributes[attrName].length > 0) {
143 var value = returnValues.attributes[attrName];
144 linkElement.data('cke-saved-' + attrName, value);
145 linkElement.setAttribute(attrName, value);
147 // Delete the property if set to an empty string.
149 linkElement.removeAttribute(attrName);
155 // Save snapshot for undo support.
156 editor.fire('saveSnapshot');
158 // Drupal.t() will not work inside CKEditor plugins because CKEditor
159 // loads the JavaScript file instead of Drupal. Pull translated
160 // strings from the plugin settings that are translated server-side.
161 var dialogSettings = {
162 title: linkElement ? editor.config.drupalLink_dialogTitleEdit : editor.config.drupalLink_dialogTitleAdd,
163 dialogClass: 'editor-link-dialog'
166 // Open the dialog for the edit form.
167 Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/link/' + editor.config.drupal.format), existingValues, saveCallback, dialogSettings);
170 editor.addCommand('drupalunlink', {
173 requiredContent: new CKEDITOR.style({
179 exec: function (editor) {
180 var style = new CKEDITOR.style({element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1});
181 editor.removeStyle(style);
183 refresh: function (editor, path) {
184 var element = path.lastElement && path.lastElement.getAscendant('a', true);
185 if (element && element.getName() === 'a' && element.getAttribute('href') && element.getChildCount()) {
186 this.setState(CKEDITOR.TRISTATE_OFF);
189 this.setState(CKEDITOR.TRISTATE_DISABLED);
195 editor.setKeystroke(CKEDITOR.CTRL + 75, 'drupallink');
197 // Add buttons for link and unlink.
198 if (editor.ui.addButton) {
199 editor.ui.addButton('DrupalLink', {
200 label: Drupal.t('Link'),
201 command: 'drupallink'
203 editor.ui.addButton('DrupalUnlink', {
204 label: Drupal.t('Unlink'),
205 command: 'drupalunlink'
209 editor.on('doubleclick', function (evt) {
210 var element = getSelectedLink(editor) || evt.data.element;
212 if (!element.isReadOnly()) {
213 if (element.is('a')) {
214 editor.getSelection().selectElement(element);
215 editor.getCommand('drupallink').exec();
220 // If the "menu" plugin is loaded, register the menu items.
221 if (editor.addMenuItems) {
222 editor.addMenuItems({
224 label: Drupal.t('Edit Link'),
225 command: 'drupallink',
231 label: Drupal.t('Unlink'),
232 command: 'drupalunlink',
239 // If the "contextmenu" plugin is loaded, register the listeners.
240 if (editor.contextMenu) {
241 editor.contextMenu.addListener(function (element, selection) {
242 if (!element || element.isReadOnly()) {
245 var anchor = getSelectedLink(editor);
251 if (anchor.getAttribute('href') && anchor.getChildCount()) {
252 menu = {link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF};
261 * Get the surrounding link element of current selection.
263 * The following selection will all return the link element.
266 * <a href="#">li^nk</a>
267 * <a href="#">[link]</a>
268 * text[<a href="#">link]</a>
269 * <a href="#">li[nk</a>]
270 * [<b><a href="#">li]nk</a></b>]
271 * [<a href="#"><b>li]nk</b></a>
273 * @param {CKEDITOR.editor} editor
274 * The CKEditor editor object
276 * @return {?HTMLElement}
277 * The selected link element, or null.
280 function getSelectedLink(editor) {
281 var selection = editor.getSelection();
282 var selectedElement = selection.getSelectedElement();
283 if (selectedElement && selectedElement.is('a')) {
284 return selectedElement;
287 var range = selection.getRanges(true)[0];
290 range.shrink(CKEDITOR.SHRINK_TEXT);
291 return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
296 // Expose an API for other plugins to interact with drupallink widgets.
297 // (Compatible with the official CKEditor link plugin's API:
298 // http://dev.ckeditor.com/ticket/13885.)
299 CKEDITOR.plugins.drupallink = {
300 parseLinkAttributes: parseAttributes,
301 getLinkAttributes: getAttributes
304 })(jQuery, Drupal, drupalSettings, CKEDITOR);