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