]>
Commit | Line | Data |
---|---|---|
f0cfe83e AD |
1 | define("dojo/dnd/Container", [ |
2 | "../_base/array", | |
3 | "../_base/declare", | |
4 | "../_base/event", | |
5 | "../_base/kernel", | |
6 | "../_base/lang", | |
7 | "../_base/window", | |
8 | "../dom", | |
9 | "../dom-class", | |
10 | "../dom-construct", | |
11 | "../Evented", | |
12 | "../has", | |
13 | "../on", | |
14 | "../query", | |
15 | "../ready", | |
16 | "../touch", | |
17 | "./common" | |
18 | ], function( | |
19 | array, declare, event, kernel, lang, win, | |
20 | dom, domClass, domConstruct, Evented, has, on, query, ready, touch, dnd){ | |
21 | ||
22 | // module: | |
23 | // dojo/dnd/Container | |
24 | ||
25 | /* | |
26 | Container states: | |
27 | "" - normal state | |
28 | "Over" - mouse over a container | |
29 | Container item states: | |
30 | "" - normal state | |
31 | "Over" - mouse over a container item | |
32 | */ | |
33 | ||
34 | ||
35 | ||
36 | var Container = declare("dojo.dnd.Container", Evented, { | |
37 | // summary: | |
38 | // a Container object, which knows when mouse hovers over it, | |
39 | // and over which element it hovers | |
40 | ||
41 | // object attributes (for markup) | |
42 | skipForm: false, | |
43 | // allowNested: Boolean | |
44 | // Indicates whether to allow dnd item nodes to be nested within other elements. | |
45 | // By default this is false, indicating that only direct children of the container can | |
46 | // be draggable dnd item nodes | |
47 | allowNested: false, | |
48 | /*===== | |
49 | // current: DomNode | |
50 | // The DOM node the mouse is currently hovered over | |
51 | current: null, | |
52 | ||
53 | // map: Hash<String, Container.Item> | |
54 | // Map from an item's id (which is also the DOMNode's id) to | |
55 | // the dojo/dnd/Container.Item itself. | |
56 | map: {}, | |
57 | =====*/ | |
58 | ||
59 | constructor: function(node, params){ | |
60 | // summary: | |
61 | // a constructor of the Container | |
62 | // node: Node | |
63 | // node or node's id to build the container on | |
64 | // params: Container.__ContainerArgs | |
65 | // a dictionary of parameters | |
66 | this.node = dom.byId(node); | |
67 | if(!params){ params = {}; } | |
68 | this.creator = params.creator || null; | |
69 | this.skipForm = params.skipForm; | |
70 | this.parent = params.dropParent && dom.byId(params.dropParent); | |
71 | ||
72 | // class-specific variables | |
73 | this.map = {}; | |
74 | this.current = null; | |
75 | ||
76 | // states | |
77 | this.containerState = ""; | |
78 | domClass.add(this.node, "dojoDndContainer"); | |
79 | ||
80 | // mark up children | |
81 | if(!(params && params._skipStartup)){ | |
82 | this.startup(); | |
83 | } | |
84 | ||
85 | // set up events | |
86 | this.events = [ | |
87 | on(this.node, touch.over, lang.hitch(this, "onMouseOver")), | |
88 | on(this.node, touch.out, lang.hitch(this, "onMouseOut")), | |
89 | // cancel text selection and text dragging | |
90 | on(this.node, "dragstart", lang.hitch(this, "onSelectStart")), | |
91 | on(this.node, "selectstart", lang.hitch(this, "onSelectStart")) | |
92 | ]; | |
93 | }, | |
94 | ||
95 | // object attributes (for markup) | |
96 | creator: function(){ | |
97 | // summary: | |
98 | // creator function, dummy at the moment | |
99 | }, | |
100 | ||
101 | // abstract access to the map | |
102 | getItem: function(/*String*/ key){ | |
103 | // summary: | |
104 | // returns a data item by its key (id) | |
105 | return this.map[key]; // Container.Item | |
106 | }, | |
107 | setItem: function(/*String*/ key, /*Container.Item*/ data){ | |
108 | // summary: | |
109 | // associates a data item with its key (id) | |
110 | this.map[key] = data; | |
111 | }, | |
112 | delItem: function(/*String*/ key){ | |
113 | // summary: | |
114 | // removes a data item from the map by its key (id) | |
115 | delete this.map[key]; | |
116 | }, | |
117 | forInItems: function(/*Function*/ f, /*Object?*/ o){ | |
118 | // summary: | |
119 | // iterates over a data map skipping members that | |
120 | // are present in the empty object (IE and/or 3rd-party libraries). | |
121 | o = o || kernel.global; | |
122 | var m = this.map, e = dnd._empty; | |
123 | for(var i in m){ | |
124 | if(i in e){ continue; } | |
125 | f.call(o, m[i], i, this); | |
126 | } | |
127 | return o; // Object | |
128 | }, | |
129 | clearItems: function(){ | |
130 | // summary: | |
131 | // removes all data items from the map | |
132 | this.map = {}; | |
133 | }, | |
134 | ||
135 | // methods | |
136 | getAllNodes: function(){ | |
137 | // summary: | |
138 | // returns a list (an array) of all valid child nodes | |
139 | return query((this.allowNested ? "" : "> ") + ".dojoDndItem", this.parent); // NodeList | |
140 | }, | |
141 | sync: function(){ | |
142 | // summary: | |
143 | // sync up the node list with the data map | |
144 | var map = {}; | |
145 | this.getAllNodes().forEach(function(node){ | |
146 | if(node.id){ | |
147 | var item = this.getItem(node.id); | |
148 | if(item){ | |
149 | map[node.id] = item; | |
150 | return; | |
151 | } | |
152 | }else{ | |
153 | node.id = dnd.getUniqueId(); | |
154 | } | |
155 | var type = node.getAttribute("dndType"), | |
156 | data = node.getAttribute("dndData"); | |
157 | map[node.id] = { | |
158 | data: data || node.innerHTML, | |
159 | type: type ? type.split(/\s*,\s*/) : ["text"] | |
160 | }; | |
161 | }, this); | |
162 | this.map = map; | |
163 | return this; // self | |
164 | }, | |
165 | insertNodes: function(data, before, anchor){ | |
166 | // summary: | |
167 | // inserts an array of new nodes before/after an anchor node | |
168 | // data: Array | |
169 | // a list of data items, which should be processed by the creator function | |
170 | // before: Boolean | |
171 | // insert before the anchor, if true, and after the anchor otherwise | |
172 | // anchor: Node | |
173 | // the anchor node to be used as a point of insertion | |
174 | if(!this.parent.firstChild){ | |
175 | anchor = null; | |
176 | }else if(before){ | |
177 | if(!anchor){ | |
178 | anchor = this.parent.firstChild; | |
179 | } | |
180 | }else{ | |
181 | if(anchor){ | |
182 | anchor = anchor.nextSibling; | |
183 | } | |
184 | } | |
185 | var i, t; | |
186 | if(anchor){ | |
187 | for(i = 0; i < data.length; ++i){ | |
188 | t = this._normalizedCreator(data[i]); | |
189 | this.setItem(t.node.id, {data: t.data, type: t.type}); | |
190 | anchor.parentNode.insertBefore(t.node, anchor); | |
191 | } | |
192 | }else{ | |
193 | for(i = 0; i < data.length; ++i){ | |
194 | t = this._normalizedCreator(data[i]); | |
195 | this.setItem(t.node.id, {data: t.data, type: t.type}); | |
196 | this.parent.appendChild(t.node); | |
197 | } | |
198 | } | |
199 | return this; // self | |
200 | }, | |
201 | destroy: function(){ | |
202 | // summary: | |
203 | // prepares this object to be garbage-collected | |
204 | array.forEach(this.events, function(handle){ handle.remove(); }); | |
205 | this.clearItems(); | |
206 | this.node = this.parent = this.current = null; | |
207 | }, | |
208 | ||
209 | // markup methods | |
210 | markupFactory: function(params, node, Ctor){ | |
211 | params._skipStartup = true; | |
212 | return new Ctor(node, params); | |
213 | }, | |
214 | startup: function(){ | |
215 | // summary: | |
216 | // collects valid child items and populate the map | |
217 | ||
218 | // set up the real parent node | |
219 | if(!this.parent){ | |
220 | // use the standard algorithm, if not assigned | |
221 | this.parent = this.node; | |
222 | if(this.parent.tagName.toLowerCase() == "table"){ | |
223 | var c = this.parent.getElementsByTagName("tbody"); | |
224 | if(c && c.length){ this.parent = c[0]; } | |
225 | } | |
226 | } | |
227 | this.defaultCreator = dnd._defaultCreator(this.parent); | |
228 | ||
229 | // process specially marked children | |
230 | this.sync(); | |
231 | }, | |
232 | ||
233 | // mouse events | |
234 | onMouseOver: function(e){ | |
235 | // summary: | |
236 | // event processor for onmouseover or touch, to mark that element as the current element | |
237 | // e: Event | |
238 | // mouse event | |
239 | var n = e.relatedTarget; | |
240 | while(n){ | |
241 | if(n == this.node){ break; } | |
242 | try{ | |
243 | n = n.parentNode; | |
244 | }catch(x){ | |
245 | n = null; | |
246 | } | |
247 | } | |
248 | if(!n){ | |
249 | this._changeState("Container", "Over"); | |
250 | this.onOverEvent(); | |
251 | } | |
252 | n = this._getChildByEvent(e); | |
253 | if(this.current == n){ return; } | |
254 | if(this.current){ this._removeItemClass(this.current, "Over"); } | |
255 | if(n){ this._addItemClass(n, "Over"); } | |
256 | this.current = n; | |
257 | }, | |
258 | onMouseOut: function(e){ | |
259 | // summary: | |
260 | // event processor for onmouseout | |
261 | // e: Event | |
262 | // mouse event | |
263 | for(var n = e.relatedTarget; n;){ | |
264 | if(n == this.node){ return; } | |
265 | try{ | |
266 | n = n.parentNode; | |
267 | }catch(x){ | |
268 | n = null; | |
269 | } | |
270 | } | |
271 | if(this.current){ | |
272 | this._removeItemClass(this.current, "Over"); | |
273 | this.current = null; | |
274 | } | |
275 | this._changeState("Container", ""); | |
276 | this.onOutEvent(); | |
277 | }, | |
278 | onSelectStart: function(e){ | |
279 | // summary: | |
280 | // event processor for onselectevent and ondragevent | |
281 | // e: Event | |
282 | // mouse event | |
283 | if(!this.skipForm || !dnd.isFormElement(e)){ | |
284 | event.stop(e); | |
285 | } | |
286 | }, | |
287 | ||
288 | // utilities | |
289 | onOverEvent: function(){ | |
290 | // summary: | |
291 | // this function is called once, when mouse is over our container | |
292 | }, | |
293 | onOutEvent: function(){ | |
294 | // summary: | |
295 | // this function is called once, when mouse is out of our container | |
296 | }, | |
297 | _changeState: function(type, newState){ | |
298 | // summary: | |
299 | // changes a named state to new state value | |
300 | // type: String | |
301 | // a name of the state to change | |
302 | // newState: String | |
303 | // new state | |
304 | var prefix = "dojoDnd" + type; | |
305 | var state = type.toLowerCase() + "State"; | |
306 | //domClass.replace(this.node, prefix + newState, prefix + this[state]); | |
307 | domClass.replace(this.node, prefix + newState, prefix + this[state]); | |
308 | this[state] = newState; | |
309 | }, | |
310 | _addItemClass: function(node, type){ | |
311 | // summary: | |
312 | // adds a class with prefix "dojoDndItem" | |
313 | // node: Node | |
314 | // a node | |
315 | // type: String | |
316 | // a variable suffix for a class name | |
317 | domClass.add(node, "dojoDndItem" + type); | |
318 | }, | |
319 | _removeItemClass: function(node, type){ | |
320 | // summary: | |
321 | // removes a class with prefix "dojoDndItem" | |
322 | // node: Node | |
323 | // a node | |
324 | // type: String | |
325 | // a variable suffix for a class name | |
326 | domClass.remove(node, "dojoDndItem" + type); | |
327 | }, | |
328 | _getChildByEvent: function(e){ | |
329 | // summary: | |
330 | // gets a child, which is under the mouse at the moment, or null | |
331 | // e: Event | |
332 | // a mouse event | |
333 | var node = e.target; | |
334 | if(node){ | |
335 | for(var parent = node.parentNode; parent; node = parent, parent = node.parentNode){ | |
336 | if((parent == this.parent || this.allowNested) && domClass.contains(node, "dojoDndItem")){ return node; } | |
337 | } | |
338 | } | |
339 | return null; | |
340 | }, | |
341 | _normalizedCreator: function(/*Container.Item*/ item, /*String*/ hint){ | |
342 | // summary: | |
343 | // adds all necessary data to the output of the user-supplied creator function | |
344 | var t = (this.creator || this.defaultCreator).call(this, item, hint); | |
345 | if(!lang.isArray(t.type)){ t.type = ["text"]; } | |
346 | if(!t.node.id){ t.node.id = dnd.getUniqueId(); } | |
347 | domClass.add(t.node, "dojoDndItem"); | |
348 | return t; | |
349 | } | |
350 | }); | |
351 | ||
352 | dnd._createNode = function(tag){ | |
353 | // summary: | |
354 | // returns a function, which creates an element of given tag | |
355 | // (SPAN by default) and sets its innerHTML to given text | |
356 | // tag: String | |
357 | // a tag name or empty for SPAN | |
358 | if(!tag){ return dnd._createSpan; } | |
359 | return function(text){ // Function | |
360 | return domConstruct.create(tag, {innerHTML: text}); // Node | |
361 | }; | |
362 | }; | |
363 | ||
364 | dnd._createTrTd = function(text){ | |
365 | // summary: | |
366 | // creates a TR/TD structure with given text as an innerHTML of TD | |
367 | // text: String | |
368 | // a text for TD | |
369 | var tr = domConstruct.create("tr"); | |
370 | domConstruct.create("td", {innerHTML: text}, tr); | |
371 | return tr; // Node | |
372 | }; | |
373 | ||
374 | dnd._createSpan = function(text){ | |
375 | // summary: | |
376 | // creates a SPAN element with given text as its innerHTML | |
377 | // text: String | |
378 | // a text for SPAN | |
379 | return domConstruct.create("span", {innerHTML: text}); // Node | |
380 | }; | |
381 | ||
382 | // dnd._defaultCreatorNodes: Object | |
383 | // a dictionary that maps container tag names to child tag names | |
384 | dnd._defaultCreatorNodes = {ul: "li", ol: "li", div: "div", p: "div"}; | |
385 | ||
386 | dnd._defaultCreator = function(node){ | |
387 | // summary: | |
388 | // takes a parent node, and returns an appropriate creator function | |
389 | // node: Node | |
390 | // a container node | |
391 | var tag = node.tagName.toLowerCase(); | |
392 | var c = tag == "tbody" || tag == "thead" ? dnd._createTrTd : | |
393 | dnd._createNode(dnd._defaultCreatorNodes[tag]); | |
394 | return function(item, hint){ // Function | |
395 | var isObj = item && lang.isObject(item), data, type, n; | |
396 | if(isObj && item.tagName && item.nodeType && item.getAttribute){ | |
397 | // process a DOM node | |
398 | data = item.getAttribute("dndData") || item.innerHTML; | |
399 | type = item.getAttribute("dndType"); | |
400 | type = type ? type.split(/\s*,\s*/) : ["text"]; | |
401 | n = item; // this node is going to be moved rather than copied | |
402 | }else{ | |
403 | // process a DnD item object or a string | |
404 | data = (isObj && item.data) ? item.data : item; | |
405 | type = (isObj && item.type) ? item.type : ["text"]; | |
406 | n = (hint == "avatar" ? dnd._createSpan : c)(String(data)); | |
407 | } | |
408 | if(!n.id){ | |
409 | n.id = dnd.getUniqueId(); | |
410 | } | |
411 | return {node: n, data: data, type: type}; | |
412 | }; | |
413 | }; | |
414 | ||
415 | /*===== | |
416 | Container.__ContainerArgs = declare([], { | |
417 | creator: function(){ | |
418 | // summary: | |
419 | // a creator function, which takes a data item, and returns an object like that: | |
420 | // {node: newNode, data: usedData, type: arrayOfStrings} | |
421 | }, | |
422 | ||
423 | // skipForm: Boolean | |
424 | // don't start the drag operation, if clicked on form elements | |
425 | skipForm: false, | |
426 | ||
427 | // dropParent: Node||String | |
428 | // node or node's id to use as the parent node for dropped items | |
429 | // (must be underneath the 'node' parameter in the DOM) | |
430 | dropParent: null, | |
431 | ||
432 | // _skipStartup: Boolean | |
433 | // skip startup(), which collects children, for deferred initialization | |
434 | // (this is used in the markup mode) | |
435 | _skipStartup: false | |
436 | }); | |
437 | ||
438 | Container.Item = function(){ | |
439 | // summary: | |
440 | // Represents (one of) the source node(s) being dragged. | |
441 | // Contains (at least) the "type" and "data" attributes. | |
442 | // type: String[] | |
443 | // Type(s) of this item, by default this is ["text"] | |
444 | // data: Object | |
445 | // Logical representation of the object being dragged. | |
446 | // If the drag object's type is "text" then data is a String, | |
447 | // if it's another type then data could be a different Object, | |
448 | // perhaps a name/value hash. | |
449 | ||
450 | this.type = type; | |
451 | this.data = data; | |
452 | }; | |
453 | =====*/ | |
454 | ||
455 | return Container; | |
456 | }); |