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