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