7e1f4e8abb801239d3a4682ecd748e4be3403b48
[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 this.getWindowDimensions(
164 {
165 width: top.TYPO3.configuration.RTEPopupWindow.width,
166 height: top.TYPO3.configuration.RTEPopupWindow.height
167 },
168 buttonId
169 ),
170 this.makeUrlFromModulePath(this.modulePath, additionalParameter)
171 );
172 }
173 return false;
174 },
175 /*
176 * Add a link to the selection.
177 * This function is called from the TYPO3 link popup.
178 *
179 * @param string theLink: the href attribute of the link to be created
180 * @param string cur_target: value for the target attribute
181 * @param string cur_class: value for the class attribute
182 * @param string cur_title: value for the title attribute
183 * @param object additionalValues: values for additional attributes (may be used by extension)
184 *
185 * @return void
186 */
187 createLink: function(theLink,cur_target,cur_class,cur_title,additionalValues) {
188 var range, anchorClass, imageNode = null, addIconAfterLink;
189 this.restoreSelection();
190 var node = this.editor.getSelection().getFirstAncestorOfType('a');
191 if (!node) {
192 node = this.editor.getSelection().getParentElement();
193 }
194 if (HTMLArea.classesAnchorSetup && cur_class) {
195 for (var i = HTMLArea.classesAnchorSetup.length; --i >= 0;) {
196 anchorClass = HTMLArea.classesAnchorSetup[i];
197 if (anchorClass.name == cur_class && anchorClass.image) {
198 imageNode = this.editor.document.createElement('img');
199 imageNode.src = anchorClass.image;
200 imageNode.alt = anchorClass.altText;
201 addIconAfterLink = anchorClass.addIconAfterLink;
202 break;
203 }
204 }
205 }
206 if (node != null && /^a$/i.test(node.nodeName)) {
207 // Update existing link
208 this.editor.getSelection().selectNode(node);
209 range = this.editor.getSelection().createRange();
210 // Clean images, keep links
211 if (HTMLArea.classesAnchorSetup) {
212 this.cleanAllLinks(node, range, true);
213 }
214 // Update link href
215 // In IE, setting href may update the content of the element. We don't want this feature.
216 if (UserAgent.isIE) {
217 var content = node.innerHTML;
218 }
219 node.href = UserAgent.isGecko ? encodeURI(theLink) : theLink;
220 if (UserAgent.isIE) {
221 node.innerHTML = content;
222 }
223 // Update link attributes
224 this.setLinkAttributes(node, range, cur_target, cur_class, cur_title, imageNode, addIconAfterLink, additionalValues);
225 } else {
226 // Create new link
227 // Cleanup selected range
228 range = this.editor.getSelection().createRange();
229 // Clean existing anchors otherwise Mozilla may create nested anchors
230 // Selection may be lost when cleaning links
231 var bookMark = this.editor.getBookMark().get(range);
232 this.cleanAllLinks(node, range);
233 range = this.editor.getBookMark().moveTo(bookMark);
234 this.editor.getSelection().selectRange(range);
235 if (UserAgent.isGecko) {
236 this.editor.getSelection().execCommand('CreateLink', false, encodeURI(theLink));
237 } else {
238 this.editor.getSelection().execCommand('CreateLink', false, theLink);
239 }
240 // Get the created link or parent
241 node = this.editor.getSelection().getParentElement();
242 // Re-establish the range of the selection
243 range = this.editor.getSelection().createRange();
244 if (node) {
245 // Export trailing br that IE may include in the link
246 if (UserAgent.isIE) {
247 if (node.lastChild && /^br$/i.test(node.lastChild.nodeName)) {
248 Dom.removeFromParent(node.lastChild);
249 node.parentNode.insertBefore(this.editor.document.createElement('br'), node.nextSibling);
250 }
251 }
252 // We may have created multiple links in as many blocks
253 this.setLinkAttributes(node, range, cur_target, cur_class, cur_title, imageNode, addIconAfterLink, additionalValues);
254 }
255 // Set the selection on the last link created
256 this.editor.getSelection().selectNodeContents(node);
257 }
258 this.close();
259 },
260
261 /**
262 * Unlink the selection.
263 * This function is called from the TYPO3 link popup and from unlink button pressed in toolbar or context menu.
264 *
265 * @param string buttonPressd: true if the unlink button was pressed
266 *
267 * @return void
268 */
269 unLink: function (buttonPressed) {
270 // If no dialogue window was opened, the selection should not be restored
271 if (!buttonPressed) {
272 this.restoreSelection();
273 }
274 var node = this.editor.getSelection().getParentElement();
275 var el = this.editor.getSelection().getFirstAncestorOfType('a');
276 if (el != null) {
277 node = el;
278 }
279 if (node != null && /^a$/i.test(node.nodeName)) {
280 this.editor.getSelection().selectNode(node);
281 }
282 if (HTMLArea.classesAnchorSetup) {
283 var range = this.editor.getSelection().createRange();
284 this.cleanAllLinks(node, range, false);
285 } else {
286 this.editor.getSelection().execCommand('Unlink', false, '');
287 }
288 if (this.dialog) {
289 this.close();
290 }
291 },
292
293 /**
294 * Set attributes of anchors intersecting a range in the given node
295 *
296 * @param object node: a node that may interesect the range
297 * @param object range: set attributes on all nodes intersecting this range
298 * @param string cur_target: value for the target attribute
299 * @param string cur_class: value for the class attribute
300 * @param string cur_title: value for the title attribute
301 * @param object imageNode: image to clone and append to the anchor
302 * @param boolean addIconAfterLink: add icon after rather than before the link
303 * @param object additionalValues: values for additional attributes (may be used by extension)
304 * @return void
305 */
306 setLinkAttributes: function(node, range, cur_target, cur_class, cur_title, imageNode, addIconAfterLink, additionalValues) {
307 if (/^a$/i.test(node.nodeName)) {
308 var nodeInRange = false;
309 this.editor.focus();
310 nodeInRange = Dom.rangeIntersectsNode(range, node);
311 if (nodeInRange) {
312 if (imageNode != null) {
313 if (addIconAfterLink) {
314 node.appendChild(imageNode.cloneNode(false));
315 } else {
316 node.insertBefore(imageNode.cloneNode(false), node.firstChild);
317 }
318 }
319 if (UserAgent.isGecko) {
320 node.href = decodeURI(node.getAttributeNode('href').value);
321 }
322 if (cur_target.trim()) node.target = cur_target.trim();
323 else node.removeAttribute('target');
324 if (cur_class.trim()) {
325 node.className = cur_class.trim();
326 } else {
327 if (!UserAgent.isOpera) {
328 node.removeAttribute('class');
329 } else {
330 node.className = '';
331 }
332 }
333 if (cur_title.trim()) {
334 node.title = cur_title.trim();
335 } else {
336 node.removeAttribute('title');
337 node.removeAttribute('rtekeep');
338 }
339 if (this.pageTSConfiguration && this.pageTSConfiguration.additionalAttributes && typeof(additionalValues) == 'object') {
340 for (additionalAttribute in additionalValues) {
341 if (additionalValues.hasOwnProperty(additionalAttribute)) {
342 if (additionalValues[additionalAttribute].toString().trim()) {
343 node.setAttribute(additionalAttribute, additionalValues[additionalAttribute]);
344 } else {
345 node.removeAttribute(additionalAttribute);
346 }
347 }
348 }
349 }
350 }
351 } else {
352 for (var i = node.firstChild; i; i = i.nextSibling) {
353 if (i.nodeType === Dom.ELEMENT_NODE || i.nodeType === Dom.DOCUMENT_FRAGMENT_NODE) {
354 this.setLinkAttributes(i, range, cur_target, cur_class, cur_title, imageNode, addIconAfterLink, additionalValues);
355 }
356 }
357 }
358 },
359
360 /**
361 * Clean up images in special anchor classes
362 */
363 cleanClassesAnchorImages: function(node) {
364 var nodeArray = [], splitArray1 = [], splitArray2 = [];
365 for (var childNode = node.firstChild; childNode; childNode = childNode.nextSibling) {
366 if (/^img$/i.test(childNode.nodeName)) {
367 splitArray1 = childNode.src.split('/');
368 for (var i = HTMLArea.classesAnchorSetup.length; --i >= 0;) {
369 if (HTMLArea.classesAnchorSetup[i]['image']) {
370 splitArray2 = HTMLArea.classesAnchorSetup[i]['image'].split('/');
371 if (splitArray1[splitArray1.length-1] == splitArray2[splitArray2.length-1]) {
372 nodeArray.push(childNode);
373 break;
374 }
375 }
376 }
377 }
378 }
379 for (i = nodeArray.length; --i >= 0;) {
380 node.removeChild(nodeArray[i]);
381 }
382 },
383
384 /**
385 * Clean up all anchors intesecting with the range in the given node
386 */
387 cleanAllLinks: function(node, range, keepLinks) {
388 if (/^a$/i.test(node.nodeName)) {
389 var intersection = false;
390 this.editor.focus();
391 intersection = Dom.rangeIntersectsNode(range, node);
392 if (intersection) {
393 this.cleanClassesAnchorImages(node);
394 if (!keepLinks) {
395 while (node.firstChild) {
396 node.parentNode.insertBefore(node.firstChild, node);
397 }
398 node.parentNode.removeChild(node);
399 }
400 }
401 } else {
402 var child = node.firstChild,
403 nextSibling;
404 while (child) {
405 // Save next sibling as child may be removed
406 nextSibling = child.nextSibling;
407 if (child.nodeType === Dom.ELEMENT_NODE || child.nodeType === Dom.DOCUMENT_FRAGMENT_NODE) {
408 this.cleanAllLinks(child, range, keepLinks);
409 }
410 child = nextSibling;
411 }
412 }
413 },
414
415 /**
416 * This function gets called when the toolbar is updated
417 */
418 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
419 button.setInactive(true);
420 if (mode === 'wysiwyg' && this.editor.isEditable()) {
421 switch (button.itemId) {
422 case 'CreateLink':
423 button.setDisabled(selectionEmpty && !button.isInContext(mode, selectionEmpty, ancestors));
424 if (!button.disabled) {
425 var node = this.editor.getSelection().getParentElement();
426 var el = this.editor.getSelection().getFirstAncestorOfType('a');
427 if (el != null) {
428 node = el;
429 }
430 if (node != null && /^a$/i.test(node.nodeName)) {
431 button.setTooltip(this.localize('Modify link'));
432 button.setInactive(false);
433 } else {
434 button.setTooltip(this.localize('Insert link'));
435 }
436 }
437 break;
438 case 'UnLink':
439 var link = false;
440 // Let's see if a link was double-clicked in Firefox
441 if (UserAgent.isGecko && !selectionEmpty) {
442 var range = this.editor.getSelection().createRange();
443 if (range.startContainer.nodeType === Dom.ELEMENT_NODE && range.startContainer == range.endContainer && (range.endOffset - range.startOffset == 1)) {
444 var node = range.startContainer.childNodes[range.startOffset];
445 if (node && /^a$/i.test(node.nodeName) && node.textContent == range.toString()) {
446 link = true;
447 }
448 }
449 }
450 button.setDisabled(!link && !button.isInContext(mode, selectionEmpty, ancestors));
451 break;
452 }
453 }
454 }
455 });
456
457 return TYPO3Link;
458
459 });