a097fdbd2afda9e6070fe7cfe95797396fb7821a
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / Plugins / TYPO3Link.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13
14 /**
15 * TYPO3Link plugin for htmlArea RTE
16 */
17 define(['TYPO3/CMS/Rtehtmlarea/HTMLArea/Plugin/Plugin',
18 'TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
20 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util'],
21 function (Plugin, UserAgent, Dom, Util) {
22
23 var TYPO3Link = function (editor, pluginName) {
24 this.constructor.super.call(this, editor, pluginName);
25 };
26 Util.inherit(TYPO3Link, Plugin);
27 Util.apply(TYPO3Link.prototype, {
28
29 /**
30 * This function gets called by the class constructor
31 */
32 configurePlugin: function (editor) {
33 this.pageTSConfiguration = this.editorConfiguration.buttons.link;
34 this.modulePath = this.pageTSConfiguration.pathLinkModule;
35 this.classesAnchorUrl = this.pageTSConfiguration.classesAnchorUrl;
36
37 /**
38 * Registering plugin "About" information
39 */
40 var pluginInformation = {
41 version : '2.2',
42 developer : 'Stanislas Rolland',
43 developerUrl : 'http://www.sjbr.ca/',
44 copyrightOwner : 'Stanislas Rolland',
45 sponsor : 'SJBR',
46 sponsorUrl : 'http://www.sjbr.ca/',
47 license : 'GPL'
48 };
49 this.registerPluginInformation(pluginInformation);
50 /**
51 * Registering the buttons
52 */
53 var buttonList = this.buttonList, buttonId;
54 for (var i = 0; i < buttonList.length; ++i) {
55 var button = buttonList[i];
56 buttonId = button[0];
57 var buttonConfiguration = {
58 id : buttonId,
59 tooltip : this.localize(buttonId.toLowerCase()),
60 iconCls : 'htmlarea-action-' + button[4],
61 action : 'onButtonPress',
62 hotKey : (this.pageTSConfiguration ? this.pageTSConfiguration.hotKey : null),
63 context : button[1],
64 selection : button[2],
65 dialog : button[3]
66 };
67 this.registerButton(buttonConfiguration);
68 }
69 return true;
70 },
71 /**
72 * The list of buttons added by this plugin
73 */
74 buttonList: [
75 ['CreateLink', 'a,img', false, true, 'link-edit'],
76 ['UnLink', 'a', false, false, 'unlink']
77 ],
78 /**
79 * This function is invoked when the editor is being generated
80 */
81 onGenerate: function () {
82 // Download the definition of special anchor classes if not yet done
83 if (this.classesAnchorUrl && typeof HTMLArea.classesAnchorSetup === 'undefined') {
84 this.getJavascriptFile(this.classesAnchorUrl, function (options, success, response) {
85 if (success) {
86 try {
87 if (typeof HTMLArea.classesAnchorSetup === 'undefined') {
88 eval(response.responseText);
89 }
90 } catch(e) {
91 this.appendToLog('ongenerate', 'Error evaluating contents of Javascript file: ' + this.classesAnchorUrl, 'error');
92 }
93 }
94 });
95 }
96 },
97 /**
98 * This function gets called when the button was pressed
99 *
100 * @param {Object} editor The editor instance
101 * @param {String} id The button id or the key
102 * @param {Object} target The target element of the contextmenu event, when invoked from the context menu
103 *
104 * @return {Boolean}false if action is completed
105 */
106 onButtonPress: function(editor, id, target) {
107 // Could be a button or its hotkey
108 var buttonId = this.translateHotKey(id);
109 buttonId = buttonId ? buttonId : id;
110 // Download the definition of special anchor classes if not yet done
111 if (this.classesAnchorUrl && typeof HTMLArea.classesAnchorSetup === 'undefined') {
112 this.getJavascriptFile(this.classesAnchorUrl, function (options, success, response) {
113 if (success) {
114 try {
115 if (typeof HTMLArea.classesAnchorSetup === 'undefined') {
116 eval(response.responseText);
117 }
118 this.onButtonPress(editor, id, target);
119 } catch(e) {
120 this.appendToLog('onButtonPress', 'Error evaluating contents of Javascript file: ' + this.classesAnchorUrl, 'error');
121 }
122 }
123 });
124 } else {
125 if (buttonId === 'UnLink') {
126 this.unLink(true);
127 return false;
128 }
129 var node = this.editor.getSelection().getParentElement();
130 var el = this.editor.getSelection().getFirstAncestorOfType('a');
131 if (el != null) {
132 node = el;
133 }
134 var additionalParameter = '';
135 if (node != null && /^a$/i.test(node.nodeName)) {
136 additionalParameter = '&curUrl[url]=' + encodeURIComponent(node.getAttribute('href'));
137 if (node.target) additionalParameter += '&curUrl[target]=' + encodeURIComponent(node.target);
138 if (node.className) additionalParameter += '&curUrl[class]=' + encodeURIComponent(node.className);
139 if (node.title) additionalParameter += '&curUrl[title]=' + encodeURIComponent(node.title);
140 if (this.pageTSConfiguration && this.pageTSConfiguration.additionalAttributes) {
141 var additionalAttributes = this.pageTSConfiguration.additionalAttributes.split(',');
142 for (var i = additionalAttributes.length; --i >= 0;) {
143 if (node.hasAttribute(additionalAttributes[i])) {
144 additionalParameter += '&curUrl[' + additionalAttributes[i] + ']=' + encodeURIComponent(node.getAttribute(additionalAttributes[i]));
145 }
146 }
147 }
148 } else if (!this.editor.getSelection().isEmpty()) {
149 var text = this.editor.getSelection().getHtml();
150 if (text && text != null) {
151 var offset = text.toLowerCase().indexOf('<a');
152 if (offset != -1) {
153 var ATagContent = text.substring(offset+2);
154 offset = ATagContent.toUpperCase().indexOf('>');
155 ATagContent = ATagContent.substring(0, offset);
156 additionalParameter = '&curUrl[all]=' + encodeURIComponent(ATagContent);
157 }
158 }
159 }
160 this.openContainerWindow(
161 buttonId,
162 this.getButton(buttonId).tooltip,
163 TYPO3.settings.Textarea.RTEPopupWindow.height - 20,
164 this.makeUrlFromModulePath(this.modulePath, additionalParameter)
165 );
166 }
167 return false;
168 },
169 /**
170 * Add a link to the selection.
171 * This function is called from the TYPO3 link popup.
172 *
173 * @param {String} theLink The href attribute of the link to be created
174 * @param {String} cur_target Value for the target attribute
175 * @param {String} cur_class Value for the class attribute
176 * @param {String} cur_title Value for the title attribute
177 * @param {Object} additionalValues Values for additional attributes (may be used by extension)
178 */
179 createLink: function (theLink, cur_target, cur_class, cur_title, additionalValues) {
180 var range, anchorClass, imageNode = null, addIconAfterLink;
181 this.restoreSelection();
182 var node = this.editor.getSelection().getFirstAncestorOfType('a');
183 if (!node) {
184 node = this.editor.getSelection().getParentElement();
185 }
186 if (HTMLArea.classesAnchorSetup && cur_class) {
187 for (var i = HTMLArea.classesAnchorSetup.length; --i >= 0;) {
188 anchorClass = HTMLArea.classesAnchorSetup[i];
189 if (anchorClass.name == cur_class && anchorClass.image) {
190 imageNode = this.editor.document.createElement('img');
191 imageNode.src = anchorClass.image;
192 imageNode.alt = anchorClass.altText;
193 addIconAfterLink = anchorClass.addIconAfterLink;
194 break;
195 }
196 }
197 }
198 if (node != null && /^a$/i.test(node.nodeName)) {
199 // Update existing link
200 this.editor.getSelection().selectNode(node);
201 range = this.editor.getSelection().createRange();
202 // Clean images, keep links
203 if (HTMLArea.classesAnchorSetup) {
204 this.cleanAllLinks(node, range, true);
205 }
206 // Update link href
207 // In IE, setting href may update the content of the element. We don't want this feature.
208 if (UserAgent.isIE) {
209 var content = node.innerHTML;
210 }
211 node.href = UserAgent.isGecko ? encodeURI(theLink) : theLink;
212 if (UserAgent.isIE) {
213 node.innerHTML = content;
214 }
215 // Update link attributes
216 this.setLinkAttributes(node, range, cur_target, cur_class, cur_title, imageNode, addIconAfterLink, additionalValues);
217 } else {
218 // Create new link
219 // Cleanup selected range
220 range = this.editor.getSelection().createRange();
221 // Clean existing anchors otherwise Mozilla may create nested anchors
222 // Selection may be lost when cleaning links
223 var bookMark = this.editor.getBookMark().get(range);
224 this.cleanAllLinks(node, range);
225 range = this.editor.getBookMark().moveTo(bookMark);
226 this.editor.getSelection().selectRange(range);
227 if (UserAgent.isGecko) {
228 this.editor.getSelection().execCommand('CreateLink', false, encodeURI(theLink));
229 } else {
230 this.editor.getSelection().execCommand('CreateLink', false, theLink);
231 }
232 // Get the created link or parent
233 node = this.editor.getSelection().getParentElement();
234 // Re-establish the range of the selection
235 range = this.editor.getSelection().createRange();
236 if (node) {
237 // Export trailing br that IE may include in the link
238 if (UserAgent.isIE) {
239 if (node.lastChild && /^br$/i.test(node.lastChild.nodeName)) {
240 Dom.removeFromParent(node.lastChild);
241 node.parentNode.insertBefore(this.editor.document.createElement('br'), node.nextSibling);
242 }
243 }
244 // We may have created multiple links in as many blocks
245 this.setLinkAttributes(node, range, cur_target, cur_class, cur_title, imageNode, addIconAfterLink, additionalValues);
246 }
247 // Set the selection on the last link created
248 this.editor.getSelection().selectNodeContents(node);
249 }
250 this.close();
251 },
252
253 /**
254 * Unlink the selection.
255 * This function is called from the TYPO3 link popup and from unlink button pressed in toolbar or context menu.
256 *
257 * @param {String} buttonPressed True if the unlink button was pressed
258 */
259 unLink: function (buttonPressed) {
260 // If no dialogue window was opened, the selection should not be restored
261 if (!buttonPressed) {
262 this.restoreSelection();
263 }
264 var node = this.editor.getSelection().getParentElement();
265 var el = this.editor.getSelection().getFirstAncestorOfType('a');
266 if (el != null) {
267 node = el;
268 }
269 if (node != null && /^a$/i.test(node.nodeName)) {
270 this.editor.getSelection().selectNode(node);
271 }
272 if (HTMLArea.classesAnchorSetup) {
273 var range = this.editor.getSelection().createRange();
274 this.cleanAllLinks(node, range, false);
275 } else {
276 this.editor.getSelection().execCommand('Unlink', false, '');
277 }
278 if (this.dialog) {
279 this.close();
280 }
281 },
282
283 /**
284 * Set attributes of anchors intersecting a range in the given node
285 *
286 * @param {Object} node A node that may interesect the range
287 * @param {Object} range Set attributes on all nodes intersecting this range
288 * @param {String} cur_target Value for the target attribute
289 * @param {String} cur_class Value for the class attribute
290 * @param {String} cur_title Value for the title attribute
291 * @param {Object} imageNode Image to clone and append to the anchor
292 * @param {Boolean} addIconAfterLink Add icon after rather than before the link
293 * @param {Object} additionalValues Values for additional attributes (may be used by extension)
294 */
295 setLinkAttributes: function(node, range, cur_target, cur_class, cur_title, imageNode, addIconAfterLink, additionalValues) {
296 if (/^a$/i.test(node.nodeName)) {
297 this.editor.focus();
298 var nodeInRange = Dom.rangeIntersectsNode(range, node);
299 if (nodeInRange) {
300 if (imageNode != null) {
301 if (addIconAfterLink) {
302 node.appendChild(imageNode.cloneNode(false));
303 } else {
304 node.insertBefore(imageNode.cloneNode(false), node.firstChild);
305 }
306 }
307 if (UserAgent.isGecko) {
308 node.href = decodeURI(node.getAttributeNode('href').value);
309 }
310 if (cur_target.trim()) node.target = cur_target.trim();
311 else node.removeAttribute('target');
312 if (!UserAgent.isOpera) {
313 node.removeAttribute('class');
314 } else {
315 node.className = '';
316 }
317 if (cur_class.trim()) {
318 Dom.addClass(node, cur_class.trim());
319 }
320 if (cur_title.trim()) {
321 node.title = cur_title.trim();
322 } else {
323 node.removeAttribute('title');
324 node.removeAttribute('rtekeep');
325 }
326 if (this.pageTSConfiguration && this.pageTSConfiguration.additionalAttributes && typeof(additionalValues) == 'object') {
327 for (var additionalAttribute in additionalValues) {
328 if (additionalValues.hasOwnProperty(additionalAttribute)) {
329 if (additionalValues[additionalAttribute].toString().trim()) {
330 node.setAttribute(additionalAttribute, additionalValues[additionalAttribute]);
331 } else {
332 node.removeAttribute(additionalAttribute);
333 }
334 }
335 }
336 }
337 }
338 } else {
339 for (var i = node.firstChild; i; i = i.nextSibling) {
340 if (i.nodeType === Dom.ELEMENT_NODE || i.nodeType === Dom.DOCUMENT_FRAGMENT_NODE) {
341 this.setLinkAttributes(i, range, cur_target, cur_class, cur_title, imageNode, addIconAfterLink, additionalValues);
342 }
343 }
344 }
345 },
346
347 /**
348 * Clean up images in special anchor classes
349 */
350 cleanClassesAnchorImages: function(node) {
351 var nodeArray = [], splitArray1 = [], splitArray2 = [];
352 for (var childNode = node.firstChild; childNode; childNode = childNode.nextSibling) {
353 if (/^img$/i.test(childNode.nodeName)) {
354 splitArray1 = childNode.src.split('/');
355 for (var i = HTMLArea.classesAnchorSetup.length; --i >= 0;) {
356 if (HTMLArea.classesAnchorSetup[i]['image']) {
357 splitArray2 = HTMLArea.classesAnchorSetup[i]['image'].split('/');
358 if (splitArray1[splitArray1.length-1] == splitArray2[splitArray2.length-1]) {
359 nodeArray.push(childNode);
360 break;
361 }
362 }
363 }
364 }
365 }
366 for (i = nodeArray.length; --i >= 0;) {
367 node.removeChild(nodeArray[i]);
368 }
369 },
370
371 /**
372 * Clean up all anchors intersecting with the range in the given node
373 */
374 cleanAllLinks: function(node, range, keepLinks) {
375 if (/^a$/i.test(node.nodeName)) {
376 this.editor.focus();
377 var intersection = Dom.rangeIntersectsNode(range, node);
378 if (intersection) {
379 this.cleanClassesAnchorImages(node);
380 if (!keepLinks) {
381 while (node.firstChild) {
382 node.parentNode.insertBefore(node.firstChild, node);
383 }
384 node.parentNode.removeChild(node);
385 }
386 }
387 } else {
388 var child = node.firstChild,
389 nextSibling;
390 while (child) {
391 // Save next sibling as child may be removed
392 nextSibling = child.nextSibling;
393 if (child.nodeType === Dom.ELEMENT_NODE || child.nodeType === Dom.DOCUMENT_FRAGMENT_NODE) {
394 this.cleanAllLinks(child, range, keepLinks);
395 }
396 child = nextSibling;
397 }
398 }
399 },
400
401 /**
402 * This function gets called when the toolbar is updated
403 */
404 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
405 button.setInactive(true);
406 if (mode === 'wysiwyg' && this.editor.isEditable()) {
407 var node;
408 switch (button.itemId) {
409 case 'CreateLink':
410 button.setDisabled(selectionEmpty && !button.isInContext(mode, selectionEmpty, ancestors));
411 if (!button.disabled) {
412 node = this.editor.getSelection().getParentElement();
413 var el = this.editor.getSelection().getFirstAncestorOfType('a');
414 if (el != null) {
415 node = el;
416 }
417 if (node != null && /^a$/i.test(node.nodeName)) {
418 button.setTooltip(this.localize('Modify link'));
419 button.setInactive(false);
420 } else {
421 button.setTooltip(this.localize('Insert link'));
422 }
423 }
424 break;
425 case 'UnLink':
426 var link = false;
427 // Let's see if a link was double-clicked in Firefox
428 if (UserAgent.isGecko && !selectionEmpty) {
429 var range = this.editor.getSelection().createRange();
430 if (range.startContainer.nodeType === Dom.ELEMENT_NODE && range.startContainer == range.endContainer && (range.endOffset - range.startOffset == 1)) {
431 node = range.startContainer.childNodes[range.startOffset];
432 if (node && /^a$/i.test(node.nodeName) && node.textContent == range.toString()) {
433 link = true;
434 }
435 }
436 }
437 button.setDisabled(!link && !button.isInContext(mode, selectionEmpty, ancestors));
438 break;
439 }
440 }
441 }
442 });
443
444 return TYPO3Link;
445 });