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