1 | define([ |
---|
2 | "dojo/_base/array", // array.filter array.forEach array.map |
---|
3 | "dojo/_base/connect", // connect.isCopyKey |
---|
4 | "dojo/_base/declare", // declare |
---|
5 | "dojo/_base/kernel", // global |
---|
6 | "dojo/_base/lang", // lang.hitch |
---|
7 | "dojo/dom", // isDescendant |
---|
8 | "dojo/mouse", // mouse.isLeft |
---|
9 | "dojo/on", |
---|
10 | "dojo/touch", |
---|
11 | "../a11yclick", |
---|
12 | "./_dndContainer" |
---|
13 | ], function(array, connect, declare, kernel, lang, dom, mouse, on, touch, a11yclick, _dndContainer){ |
---|
14 | |
---|
15 | // module: |
---|
16 | // dijit/tree/_dndSelector |
---|
17 | |
---|
18 | return declare("dijit.tree._dndSelector", _dndContainer, { |
---|
19 | // summary: |
---|
20 | // This is a base class for `dijit/tree/dndSource`, and isn't meant to be used directly. |
---|
21 | // It's based on `dojo/dnd/Selector`. |
---|
22 | // tags: |
---|
23 | // protected |
---|
24 | |
---|
25 | /*===== |
---|
26 | // selection: Object |
---|
27 | // (id to DomNode) map for every TreeNode that's currently selected. |
---|
28 | // The DOMNode is the TreeNode.rowNode. |
---|
29 | selection: {}, |
---|
30 | =====*/ |
---|
31 | |
---|
32 | constructor: function(){ |
---|
33 | // summary: |
---|
34 | // Initialization |
---|
35 | // tags: |
---|
36 | // private |
---|
37 | |
---|
38 | this.selection={}; |
---|
39 | this.anchor = null; |
---|
40 | |
---|
41 | this.events.push( |
---|
42 | // listeners setup here but no longer used (left for backwards compatibility |
---|
43 | on(this.tree.domNode, touch.press, lang.hitch(this,"onMouseDown")), |
---|
44 | on(this.tree.domNode, touch.release, lang.hitch(this,"onMouseUp")), |
---|
45 | |
---|
46 | // listeners used in this module |
---|
47 | on(this.tree.domNode, touch.move, lang.hitch(this,"onMouseMove")), |
---|
48 | on(this.tree.domNode, a11yclick.press, lang.hitch(this,"onClickPress")), |
---|
49 | on(this.tree.domNode, a11yclick.release, lang.hitch(this,"onClickRelease")) |
---|
50 | ); |
---|
51 | }, |
---|
52 | |
---|
53 | // singular: Boolean |
---|
54 | // Allows selection of only one element, if true. |
---|
55 | // Tree hasn't been tested in singular=true mode, unclear if it works. |
---|
56 | singular: false, |
---|
57 | |
---|
58 | // methods |
---|
59 | getSelectedTreeNodes: function(){ |
---|
60 | // summary: |
---|
61 | // Returns a list of selected node(s). |
---|
62 | // Used by dndSource on the start of a drag. |
---|
63 | // tags: |
---|
64 | // protected |
---|
65 | var nodes=[], sel = this.selection; |
---|
66 | for(var i in sel){ |
---|
67 | nodes.push(sel[i]); |
---|
68 | } |
---|
69 | return nodes; |
---|
70 | }, |
---|
71 | |
---|
72 | selectNone: function(){ |
---|
73 | // summary: |
---|
74 | // Unselects all items |
---|
75 | // tags: |
---|
76 | // private |
---|
77 | |
---|
78 | this.setSelection([]); |
---|
79 | return this; // self |
---|
80 | }, |
---|
81 | |
---|
82 | destroy: function(){ |
---|
83 | // summary: |
---|
84 | // Prepares the object to be garbage-collected |
---|
85 | this.inherited(arguments); |
---|
86 | this.selection = this.anchor = null; |
---|
87 | }, |
---|
88 | addTreeNode: function(/*dijit/Tree._TreeNode*/ node, /*Boolean?*/isAnchor){ |
---|
89 | // summary: |
---|
90 | // add node to current selection |
---|
91 | // node: Node |
---|
92 | // node to add |
---|
93 | // isAnchor: Boolean |
---|
94 | // Whether the node should become anchor. |
---|
95 | |
---|
96 | this.setSelection(this.getSelectedTreeNodes().concat( [node] )); |
---|
97 | if(isAnchor){ this.anchor = node; } |
---|
98 | return node; |
---|
99 | }, |
---|
100 | removeTreeNode: function(/*dijit/Tree._TreeNode*/ node){ |
---|
101 | // summary: |
---|
102 | // remove node and it's descendants from current selection |
---|
103 | // node: Node |
---|
104 | // node to remove |
---|
105 | var newSelection = array.filter(this.getSelectedTreeNodes(), function(selectedNode){ |
---|
106 | return !dom.isDescendant(selectedNode.domNode, node.domNode); // also matches when selectedNode == node |
---|
107 | }); |
---|
108 | this.setSelection(newSelection); |
---|
109 | return node; |
---|
110 | }, |
---|
111 | isTreeNodeSelected: function(/*dijit/Tree._TreeNode*/ node){ |
---|
112 | // summary: |
---|
113 | // return true if node is currently selected |
---|
114 | // node: Node |
---|
115 | // the node to check whether it's in the current selection |
---|
116 | |
---|
117 | return node.id && !!this.selection[node.id]; |
---|
118 | }, |
---|
119 | setSelection: function(/*dijit/Tree._TreeNode[]*/ newSelection){ |
---|
120 | // summary: |
---|
121 | // set the list of selected nodes to be exactly newSelection. All changes to the |
---|
122 | // selection should be passed through this function, which ensures that derived |
---|
123 | // attributes are kept up to date. Anchor will be deleted if it has been removed |
---|
124 | // from the selection, but no new anchor will be added by this function. |
---|
125 | // newSelection: Node[] |
---|
126 | // list of tree nodes to make selected |
---|
127 | var oldSelection = this.getSelectedTreeNodes(); |
---|
128 | array.forEach(this._setDifference(oldSelection, newSelection), lang.hitch(this, function(node){ |
---|
129 | node.setSelected(false); |
---|
130 | if(this.anchor == node){ |
---|
131 | delete this.anchor; |
---|
132 | } |
---|
133 | delete this.selection[node.id]; |
---|
134 | })); |
---|
135 | array.forEach(this._setDifference(newSelection, oldSelection), lang.hitch(this, function(node){ |
---|
136 | node.setSelected(true); |
---|
137 | this.selection[node.id] = node; |
---|
138 | })); |
---|
139 | this._updateSelectionProperties(); |
---|
140 | }, |
---|
141 | _setDifference: function(xs,ys){ |
---|
142 | // summary: |
---|
143 | // Returns a copy of xs which lacks any objects |
---|
144 | // occurring in ys. Checks for membership by |
---|
145 | // modifying and then reading the object, so it will |
---|
146 | // not properly handle sets of numbers or strings. |
---|
147 | |
---|
148 | array.forEach(ys, function(y){ y.__exclude__ = true; }); |
---|
149 | var ret = array.filter(xs, function(x){ return !x.__exclude__; }); |
---|
150 | |
---|
151 | // clean up after ourselves. |
---|
152 | array.forEach(ys, function(y){ delete y['__exclude__'] }); |
---|
153 | return ret; |
---|
154 | }, |
---|
155 | _updateSelectionProperties: function(){ |
---|
156 | // summary: |
---|
157 | // Update the following tree properties from the current selection: |
---|
158 | // path[s], selectedItem[s], selectedNode[s] |
---|
159 | |
---|
160 | var selected = this.getSelectedTreeNodes(); |
---|
161 | var paths = [], nodes = [], selects = []; |
---|
162 | array.forEach(selected, function(node){ |
---|
163 | var ary = node.getTreePath(), model = this.tree.model; |
---|
164 | nodes.push(node); |
---|
165 | paths.push(ary); |
---|
166 | ary = array.map(ary, function(item){ |
---|
167 | return model.getIdentity(item); |
---|
168 | }, this); |
---|
169 | selects.push(ary.join("/")) |
---|
170 | }, this); |
---|
171 | var items = array.map(nodes,function(node){ return node.item; }); |
---|
172 | this.tree._set("paths", paths); |
---|
173 | this.tree._set("path", paths[0] || []); |
---|
174 | this.tree._set("selectedNodes", nodes); |
---|
175 | this.tree._set("selectedNode", nodes[0] || null); |
---|
176 | this.tree._set("selectedItems", items); |
---|
177 | this.tree._set("selectedItem", items[0] || null); |
---|
178 | }, |
---|
179 | |
---|
180 | // selection related events |
---|
181 | onClickPress: function(e){ |
---|
182 | // summary: |
---|
183 | // Event processor for onmousedown/ontouchstart/onkeydown corresponding to a click event |
---|
184 | // e: Event |
---|
185 | // onmousedown/ontouchstart/onkeydown event |
---|
186 | // tags: |
---|
187 | // protected |
---|
188 | |
---|
189 | // ignore mouse or touch on expando node |
---|
190 | if(this.current && this.current.isExpandable && this.tree.isExpandoNode(e.target, this.current)){ return; } |
---|
191 | |
---|
192 | if(mouse.isLeft(e)){ |
---|
193 | // Prevent text selection while dragging on desktop, see #16328. But don't call preventDefault() |
---|
194 | // for mobile because it will break things completely, see #15838. |
---|
195 | e.preventDefault(); |
---|
196 | } |
---|
197 | |
---|
198 | var treeNode = e.type == "keydown" ? this.tree.focusedChild : this.current; |
---|
199 | |
---|
200 | if(!treeNode){ |
---|
201 | // Click must be on the Tree but not on a TreeNode, happens especially when Tree is stretched to fill |
---|
202 | // a pane of a BorderContainer, etc. |
---|
203 | return; |
---|
204 | } |
---|
205 | |
---|
206 | var copy = connect.isCopyKey(e), id = treeNode.id; |
---|
207 | |
---|
208 | // if shift key is not pressed, and the node is already in the selection, |
---|
209 | // delay deselection until onmouseup so in the case of DND, deselection |
---|
210 | // will be canceled by onmousemove. |
---|
211 | if(!this.singular && !e.shiftKey && this.selection[id]){ |
---|
212 | this._doDeselect = true; |
---|
213 | return; |
---|
214 | }else{ |
---|
215 | this._doDeselect = false; |
---|
216 | } |
---|
217 | this.userSelect(treeNode, copy, e.shiftKey); |
---|
218 | }, |
---|
219 | |
---|
220 | onClickRelease: function(e){ |
---|
221 | // summary: |
---|
222 | // Event processor for onmouseup/ontouchend/onkeyup corresponding to a click event |
---|
223 | // e: Event |
---|
224 | // onmouseup/ontouchend/onkeyup event |
---|
225 | // tags: |
---|
226 | // protected |
---|
227 | |
---|
228 | // _doDeselect is the flag to indicate that the user wants to either ctrl+click on |
---|
229 | // an already selected item (to deselect the item), or click on a not-yet selected item |
---|
230 | // (which should remove all current selection, and add the clicked item). This can not |
---|
231 | // be done in onMouseDown, because the user may start a drag after mousedown. By moving |
---|
232 | // the deselection logic here, the user can drag an already selected item. |
---|
233 | if(!this._doDeselect){ return; } |
---|
234 | this._doDeselect = false; |
---|
235 | this.userSelect(e.type == "keyup" ? this.tree.focusedChild : this.current, connect.isCopyKey(e), e.shiftKey); |
---|
236 | }, |
---|
237 | onMouseMove: function(/*===== e =====*/){ |
---|
238 | // summary: |
---|
239 | // event processor for onmousemove/ontouchmove |
---|
240 | // e: Event |
---|
241 | // onmousemove/ontouchmove event |
---|
242 | this._doDeselect = false; |
---|
243 | }, |
---|
244 | |
---|
245 | // mouse/touch events that are no longer used |
---|
246 | onMouseDown: function(){ |
---|
247 | // summary: |
---|
248 | // Event processor for onmousedown/ontouchstart |
---|
249 | // e: Event |
---|
250 | // onmousedown/ontouchstart event |
---|
251 | // tags: |
---|
252 | // protected |
---|
253 | }, |
---|
254 | onMouseUp: function(){ |
---|
255 | // summary: |
---|
256 | // Event processor for onmouseup/ontouchend |
---|
257 | // e: Event |
---|
258 | // onmouseup/ontouchend event |
---|
259 | // tags: |
---|
260 | // protected |
---|
261 | }, |
---|
262 | |
---|
263 | _compareNodes: function(n1, n2){ |
---|
264 | if(n1 === n2){ |
---|
265 | return 0; |
---|
266 | } |
---|
267 | |
---|
268 | if('sourceIndex' in document.documentElement){ //IE |
---|
269 | //TODO: does not yet work if n1 and/or n2 is a text node |
---|
270 | return n1.sourceIndex - n2.sourceIndex; |
---|
271 | }else if('compareDocumentPosition' in document.documentElement){ //FF, Opera |
---|
272 | return n1.compareDocumentPosition(n2) & 2 ? 1: -1; |
---|
273 | }else if(document.createRange){ //Webkit |
---|
274 | var r1 = doc.createRange(); |
---|
275 | r1.setStartBefore(n1); |
---|
276 | |
---|
277 | var r2 = doc.createRange(); |
---|
278 | r2.setStartBefore(n2); |
---|
279 | |
---|
280 | return r1.compareBoundaryPoints(r1.END_TO_END, r2); |
---|
281 | }else{ |
---|
282 | throw Error("dijit.tree._compareNodes don't know how to compare two different nodes in this browser"); |
---|
283 | } |
---|
284 | }, |
---|
285 | |
---|
286 | userSelect: function(node, multi, range){ |
---|
287 | // summary: |
---|
288 | // Add or remove the given node from selection, responding |
---|
289 | // to a user action such as a click or keypress. |
---|
290 | // multi: Boolean |
---|
291 | // Indicates whether this is meant to be a multi-select action (e.g. ctrl-click) |
---|
292 | // range: Boolean |
---|
293 | // Indicates whether this is meant to be a ranged action (e.g. shift-click) |
---|
294 | // tags: |
---|
295 | // protected |
---|
296 | |
---|
297 | if(this.singular){ |
---|
298 | if(this.anchor == node && multi){ |
---|
299 | this.selectNone(); |
---|
300 | }else{ |
---|
301 | this.setSelection([node]); |
---|
302 | this.anchor = node; |
---|
303 | } |
---|
304 | }else{ |
---|
305 | if(range && this.anchor){ |
---|
306 | var cr = this._compareNodes(this.anchor.rowNode, node.rowNode), |
---|
307 | begin, end, anchor = this.anchor; |
---|
308 | |
---|
309 | if(cr < 0){ //current is after anchor |
---|
310 | begin = anchor; |
---|
311 | end = node; |
---|
312 | }else{ //current is before anchor |
---|
313 | begin = node; |
---|
314 | end = anchor; |
---|
315 | } |
---|
316 | var nodes = []; |
---|
317 | //add everything betweeen begin and end inclusively |
---|
318 | while(begin != end){ |
---|
319 | nodes.push(begin); |
---|
320 | begin = this.tree._getNext(begin); |
---|
321 | } |
---|
322 | nodes.push(end); |
---|
323 | |
---|
324 | this.setSelection(nodes); |
---|
325 | }else{ |
---|
326 | if( this.selection[ node.id ] && multi ){ |
---|
327 | this.removeTreeNode( node ); |
---|
328 | }else if(multi){ |
---|
329 | this.addTreeNode(node, true); |
---|
330 | }else{ |
---|
331 | this.setSelection([node]); |
---|
332 | this.anchor = node; |
---|
333 | } |
---|
334 | } |
---|
335 | } |
---|
336 | }, |
---|
337 | |
---|
338 | getItem: function(/*String*/ key){ |
---|
339 | // summary: |
---|
340 | // Returns the dojo/dnd/Container._Item (representing a dragged node) by it's key (id). |
---|
341 | // Called by dojo/dnd/Source.checkAcceptance(). |
---|
342 | // tags: |
---|
343 | // protected |
---|
344 | |
---|
345 | var widget = this.selection[key]; |
---|
346 | return { |
---|
347 | data: widget, |
---|
348 | type: ["treeNode"] |
---|
349 | }; // dojo/dnd/Container._Item |
---|
350 | }, |
---|
351 | |
---|
352 | forInSelectedItems: function(/*Function*/ f, /*Object?*/ o){ |
---|
353 | // summary: |
---|
354 | // Iterates over selected items; |
---|
355 | // see `dojo/dnd/Container.forInItems()` for details |
---|
356 | o = o || kernel.global; |
---|
357 | for(var id in this.selection){ |
---|
358 | // console.log("selected item id: " + id); |
---|
359 | f.call(o, this.getItem(id), id, this); |
---|
360 | } |
---|
361 | } |
---|
362 | }); |
---|
363 | }); |
---|