c9d2f44b98fb927b8a026bc13df4c9c52884c55a
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Public / JavaScript / extjs / components / pagetree / javascript / tree.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 Ext.namespace('TYPO3.Components.PageTree');
14
15 /**
16 * @class TYPO3.Components.PageTree.Tree
17 *
18 * Generic Tree Panel
19 *
20 * @namespace TYPO3.Components.PageTree
21 * @extends Ext.tree.TreePanel
22 */
23 TYPO3.Components.PageTree.Tree = Ext.extend(Ext.tree.TreePanel, {
24 /**
25 * Border
26 *
27 * @type {Boolean}
28 */
29 border: false,
30
31 /**
32 * Indicates if the root node is visible
33 *
34 * @type {Boolean}
35 */
36 rootVisible: false,
37
38 /**
39 * Tree Editor Instance (Inline Edit)
40 *
41 * @type {TYPO3.Components.PageTree.TreeEditor}
42 */
43 treeEditor: null,
44
45 /**
46 * Currently Selected Node
47 *
48 * @type {Ext.tree.TreeNode}
49 */
50 currentSelectedNode: null,
51
52 /**
53 * Enable the drag and drop feature
54 *
55 * @cfg {Boolean}
56 */
57 enableDD: true,
58
59 /**
60 * Drag and Drop Group
61 *
62 * @cfg {String}
63 */
64 ddGroup: '',
65
66 /**
67 * Indicates if the label should be editable
68 *
69 * @cfg {Boolean}
70 */
71 labelEdit: true,
72
73 /**
74 * User Interface Provider
75 *
76 * @cfg {Ext.tree.TreeNodeUI}
77 */
78 uiProvider: null,
79
80 /**
81 * Data Provider
82 *
83 * @cfg {Object}
84 */
85 treeDataProvider: null,
86
87 /**
88 * Command Provider
89 *
90 * @cfg {Object}
91 */
92 commandProvider : null,
93
94 /**
95 * Id of the deletion drop zone if any
96 *
97 * @cfg {String}
98 */
99 deletionDropZoneId: '',
100
101 /**
102 * Main applicaton
103 *
104 * @cfg {TYPO3.Components.PageTree.App}
105 */
106 app: null,
107
108 /**
109 * Root Node Configuration
110 *
111 * @type {Object}
112 */
113 rootNodeConfig: {
114 id: 'root',
115 expanded: true,
116 nodeData: {
117 id: 'root'
118 }
119 },
120
121 /**
122 * Indicator if the control key is pressed
123 *
124 * @type {Boolean}
125 */
126 isControlPressed: false,
127
128 /**
129 * Context Node
130 *
131 * @type {Ext.tree.TreeNode}
132 */
133 t3ContextNode: null,
134
135 /**
136 * Context Information
137 *
138 * @type {Object}
139 */
140 t3ContextInfo: {
141 inCopyMode: false,
142 inCutMode: false
143 },
144
145 /**
146 * Registered clicks for the double click feature
147 *
148 * @type {int}
149 */
150 clicksRegistered: 0,
151
152 /**
153 * Indicator if the control key was pressed
154 *
155 * @type {Boolean}
156 */
157 controlKeyPressed: false,
158
159 /**
160 * Listeners
161 *
162 * Event handlers that handle click events and synchronizes the label edit,
163 * double click and single click events in a useful way.
164 */
165 listeners: {
166 // single click handler that only triggers after a delay to let the double click event
167 // a possibility to be executed (needed for label edit)
168 click: {
169 fn: function(node, event) {
170 if (this.clicksRegistered === 2) {
171 this.clicksRegistered = 0;
172 event.stopEvent();
173 return false;
174 }
175
176 if (event.target.tagName === 'IMG') {
177 // Stop this event if click event was triggered via the node icon to prevent loading the action of the node
178 return false;
179 }
180
181 this.clicksRegistered = 0;
182 if (this.commandProvider.singleClick) {
183 this.commandProvider.singleClick(node, this);
184 }
185 },
186 delay: 400
187 },
188
189 // prevent the expanding / collapsing on double click
190 beforedblclick: {
191 fn: function() {
192 return false;
193 }
194 },
195
196 // prevents label edit on a selected node
197 beforeclick: {
198 fn: function(node, event) {
199 if (event.target.tagName === 'IMG') {
200 // Node icon was clicked
201 // This is a workaround to prevent TreeEditor being triggered, which also requires a patch within TreeEditor
202 node.attributes.editable = false;
203 } else {
204 node.attributes.editable = true;
205
206 if (!this.clicksRegistered && this.getSelectionModel().isSelected(node)) {
207 node.fireEvent('click', node, event);
208 ++this.clicksRegistered;
209 return false;
210 }
211 ++this.clicksRegistered;
212 }
213 }
214 }
215 },
216
217 /**
218 * Initializes the component
219 *
220 * @return {void}
221 */
222 initComponent: function() {
223 if (!this.uiProvider) {
224 this.uiProvider = TYPO3.Components.PageTree.PageTreeNodeUI;
225 }
226 Ext.dd.DragDropMgr.useCache = false;
227 this.root = new Ext.tree.AsyncTreeNode(this.rootNodeConfig);
228 this.addTreeLoader();
229
230 if (this.labelEdit) {
231 this.enableInlineEditor();
232 }
233
234 if (this.enableDD) {
235 this.dragConfig = {ddGroup: this.ddGroup};
236 this.enableDragAndDrop();
237 }
238
239 TYPO3.Components.PageTree.Tree.superclass.initComponent.apply(this, arguments);
240 },
241
242 /**
243 * Refreshes the tree
244 *
245 * @param {Function} callback
246 * @param {Object} scope
247 * return {void}
248 */
249 refreshTree: function(callback, scope) {
250 // remove readable rootline elements while refreshing
251 if (!this.inRefreshingMode) {
252 var rootlineElements = Ext.select('.x-tree-node-readableRootline');
253 if (rootlineElements) {
254 rootlineElements.each(function(element) {
255 element.remove();
256 });
257 }
258 }
259
260 this.refreshNode(this.root, callback, scope);
261 },
262
263 /**
264 * Refreshes a given node
265 *
266 * @param {Ext.tree.TreeNode} node
267 * @param {Function} callback
268 * @param {Object} scope
269 * return {void}
270 */
271 refreshNode: function(node, callback, scope) {
272 if (this.inRefreshingMode) {
273 return;
274 }
275
276 scope = scope || node;
277 this.inRefreshingMode = true;
278 var loadCallback = function(node) {
279 node.ownerTree.inRefreshingMode = false;
280 if (node.ownerTree.restoreState) {
281 node.ownerTree.restoreState(node.getPath());
282 }
283 };
284
285 if (callback) {
286 loadCallback = callback.createSequence(loadCallback);
287 }
288
289 this.getLoader().load(node, loadCallback, scope);
290 },
291
292 /**
293 * Adds a tree loader implementation that uses the directFn feature
294 *
295 * return {void}
296 */
297 addTreeLoader: function() {
298 this.loader = new Ext.tree.TreeLoader({
299 directFn: this.treeDataProvider.getNextTreeLevel,
300 paramOrder: 'nodeId,attributes',
301 nodeParameter: 'nodeId',
302 baseAttrs: {
303 uiProvider: this.uiProvider
304 },
305
306 // an id can never be zero in ExtJS, but this is needed
307 // for the root line feature or it will never be working!
308 createNode: function(attr) {
309 if (attr.id == 0) {
310 attr.id = 'siteRootNode';
311 }
312
313 return Ext.tree.TreeLoader.prototype.createNode.call(this, attr);
314 },
315
316 listeners: {
317 beforeload: function(treeLoader, node) {
318 treeLoader.baseParams.nodeId = node.id;
319 treeLoader.baseParams.attributes = node.attributes.nodeData;
320 }
321 }
322 });
323 },
324
325 /**
326 * Initialize the inline editor for the given tree.
327 *
328 * @return {void}
329 */
330 enableInlineEditor: function() {
331 this.treeEditor = new TYPO3.Components.PageTree.TreeEditor(this);
332 },
333
334 /**
335 * Triggers the editing of the node if the tree editor is available
336 *
337 * @param {Ext.tree.TreeNode} node
338 * @return {void}
339 */
340 triggerEdit: function(node) {
341 if (this.treeEditor) {
342 this.treeEditor.triggerEdit(node);
343 }
344 },
345
346 /**
347 * Enables the drag and drop feature
348 *
349 * return {void}
350 */
351 enableDragAndDrop: function() {
352 // init proxy element
353 this.on('startdrag', this.initDd, this);
354 this.on('enddrag', this.stopDd, this);
355 this.on('nodedragover', this.nodeDragOver, this);
356
357 // node is moved
358 this.on('movenode', this.moveNode, this);
359
360 // new node is created/copied
361 this.on('beforenodedrop', this.beforeDropNode, this);
362 this.on('nodedrop', this.dropNode, this);
363
364 // listens on the ctrl key to toggle the copy mode
365 (new Ext.KeyMap(document, {
366 key: Ext.EventObject.CONTROL,
367 scope: this,
368 buffer: 250,
369 fn: function() {
370 if (!this.controlKeyPressed && this.dragZone.dragging && this.copyHint) {
371 if (this.shouldCopyNode) {
372 this.copyHint.show();
373 } else {
374 this.copyHint.hide();
375 }
376
377 this.shouldCopyNode = !this.shouldCopyNode;
378 this.dragZone.proxy.el.toggleClass('typo3-pagetree-copy');
379 }
380 this.controlKeyPressed = true;
381 }
382 }, 'keydown'));
383
384 (new Ext.KeyMap(document, {
385 key: Ext.EventObject.CONTROL,
386 scope: this,
387 fn: function() {
388 this.controlKeyPressed = false;
389 }
390 }, 'keyup'));
391
392 // listens on the escape key to stop the dragging
393 (new Ext.KeyMap(document, {
394 key: Ext.EventObject.ESC,
395 scope: this,
396 buffer: 250,
397 fn: function(event) {
398 if (this.dragZone.dragging) {
399 Ext.dd.DragDropMgr.stopDrag(event);
400 this.dragZone.onInvalidDrop(event);
401 }
402 }
403 }, 'keydown'));
404 },
405
406 /**
407 * Disables the deletion drop zone if configured
408 *
409 * @return {void}
410 */
411 stopDd: function() {
412 if (this.deletionDropZoneId) {
413 Ext.getCmp(this.deletionDropZoneId).hide();
414 this.app.doLayout();
415 }
416 },
417
418 /**
419 * Enables the deletion drop zone if configured. Also it creates the
420 * shown dd proxy element.
421 *
422 * @param {TYPO3.Components.PageTree.Tree} treePanel
423 * @param {Ext.tree.TreeNode} node
424 * @return {void}
425 */
426 initDd: function(treePanel, node) {
427 var nodeHasChildNodes = (node.hasChildNodes() || node.isExpandable());
428 if (this.deletionDropZoneId &&
429 (!nodeHasChildNodes ||
430 (nodeHasChildNodes && TYPO3.Components.PageTree.Configuration.canDeleteRecursivly)
431 )) {
432 Ext.getCmp(this.deletionDropZoneId).show();
433 this.app.doLayout();
434 }
435 this.initDDProxyElement();
436 },
437
438 /**
439 * Adds the copy hint to the proxy element
440 *
441 * @return {void}
442 */
443 initDDProxyElement: function() {
444 this.shouldCopyNode = false;
445 this.copyHint = new Ext.Element(document.createElement('div')).addClass(this.id + '-copy');
446 this.copyHint.dom.appendChild(document.createTextNode(TYPO3.Components.PageTree.LLL.copyHint));
447 this.copyHint.setVisibilityMode(Ext.Element.DISPLAY);
448 this.dragZone.proxy.el.shadow = false;
449 this.dragZone.proxy.ghost.dom.appendChild(this.copyHint.dom);
450 },
451
452 /**
453 * Cancels the drop possibility for the position above and below a mount page
454 *
455 * @param {Object} event
456 * @return {void}
457 */
458 nodeDragOver: function(event) {
459 var isMountPage = (event.target.attributes.realId == 0 || event.target.attributes.nodeData.isMountPoint);
460 return !((event.point === 'above' || event.point === 'below') && isMountPage);
461 },
462
463 /**
464 * Creates a Fake Node
465 *
466 * This must be done to prevent the calling of the moveNode event.
467 *
468 * @param {Object} dragElement
469 */
470 beforeDropNode: function(dragElement) {
471 if (dragElement.data && dragElement.data.item && dragElement.data.item.shouldCreateNewNode) {
472 this.t3ContextInfo.serverNodeType = dragElement.data.item.nodeType;
473 dragElement.dropNode = new Ext.tree.TreeNode({
474 text: TYPO3.Components.PageTree.LLL.fakeNodeHint,
475 leaf: true,
476 isInsertedNode: true
477 });
478
479 // fix incorrect cancel value
480 dragElement.cancel = false;
481
482 } else if (this.shouldCopyNode) {
483 dragElement.dropNode.ui.onOut();
484 var attributes = dragElement.dropNode.attributes;
485 attributes.isCopiedNode = true;
486 attributes.id = 'fakeNode';
487 dragElement.dropNode = new Ext.tree.TreeNode(attributes);
488 }
489
490 return true;
491 },
492
493 /**
494 * Differentiate between the copy and insert event
495 *
496 * @param {Ext.tree.TreeDropZone} dragElement
497 * return {void}
498 */
499 dropNode: function(dragElement) {
500 this.controlKeyPressed = false;
501 if (dragElement.dropNode.attributes.isInsertedNode) {
502 dragElement.dropNode.attributes.isInsertedNode = false;
503 this.insertNode(dragElement.dropNode);
504 } else if (dragElement.dropNode.attributes.isCopiedNode) {
505 dragElement.dropNode.attributes.isCopiedNode = false;
506 this.copyNode(dragElement.dropNode)
507 }
508 },
509
510 /**
511 * Moves a node
512 *
513 * @param {TYPO3.Components.PageTree.Tree} tree
514 * @param {Ext.tree.TreeNode} movedNode
515 * @param {Ext.tree.TreeNode} oldParent
516 * @param {Ext.tree.TreeNode} newParent
517 * @param {int} position
518 * return {void}
519 */
520 moveNode: function(tree, movedNode, oldParent, newParent, position) {
521 this.controlKeyPressed = false;
522 tree.t3ContextNode = movedNode;
523
524 if (position === 0) {
525 this.commandProvider.moveNodeToFirstChildOfDestination(newParent, tree);
526 } else {
527 var previousSiblingNode = newParent.childNodes[position - 1];
528 this.commandProvider.moveNodeAfterDestination(previousSiblingNode, tree);
529 }
530 },
531
532 /**
533 * Inserts a node
534 *
535 * @param {Ext.tree.TreeNode} movedNode
536 * return {void}
537 */
538 insertNode: function(movedNode) {
539 this.t3ContextNode = movedNode.parentNode;
540
541 movedNode.disable();
542 if (movedNode.previousSibling) {
543 this.commandProvider.insertNodeAfterDestination(movedNode, this);
544 } else {
545 this.commandProvider.insertNodeToFirstChildOfDestination(movedNode, this);
546 }
547 },
548
549 /**
550 * Copies a node
551 *
552 * @param {Ext.tree.TreeNode} movedNode
553 * return {void}
554 */
555 copyNode: function(movedNode) {
556 this.t3ContextNode = movedNode;
557
558 movedNode.disable();
559
560 // This is hard stuff to do. So increase the timeout for the AJAX request
561 Ext.Ajax.timeout = 3600000;
562
563 if (movedNode.previousSibling) {
564 this.commandProvider.copyNodeAfterDestination(movedNode, this);
565 } else {
566 this.commandProvider.copyNodeToFirstChildOfDestination(movedNode, this);
567 }
568 }
569 });
570
571 // XTYPE Registration
572 Ext.reg('TYPO3.Components.PageTree.Tree', TYPO3.Components.PageTree.Tree);