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