|
1 YUI.add('widget-modality', function (Y, NAME) { |
|
2 |
|
3 /** |
|
4 * Provides modality support for Widgets, though an extension |
|
5 * |
|
6 * @module widget-modality |
|
7 */ |
|
8 |
|
9 var WIDGET = 'widget', |
|
10 RENDER_UI = 'renderUI', |
|
11 BIND_UI = 'bindUI', |
|
12 SYNC_UI = 'syncUI', |
|
13 BOUNDING_BOX = 'boundingBox', |
|
14 CONTENT_BOX = 'contentBox', |
|
15 RENDERED = 'rendered', |
|
16 VISIBLE = 'visible', |
|
17 Z_INDEX = 'zIndex', |
|
18 CHANGE = 'Change', |
|
19 isBoolean = Y.Lang.isBoolean, |
|
20 getCN = Y.ClassNameManager.getClassName, |
|
21 MaskShow = "maskShow", |
|
22 MaskHide = "maskHide", |
|
23 ClickOutside = "clickoutside", |
|
24 FocusOutside = "focusoutside", |
|
25 |
|
26 supportsPosFixed = (function(){ |
|
27 |
|
28 /*! IS_POSITION_FIXED_SUPPORTED - Juriy Zaytsev (kangax) - http://yura.thinkweb2.com/cft/ */ |
|
29 |
|
30 var doc = Y.config.doc, |
|
31 isSupported = null, |
|
32 el, root; |
|
33 |
|
34 if (doc.createElement) { |
|
35 el = doc.createElement('div'); |
|
36 if (el && el.style) { |
|
37 el.style.position = 'fixed'; |
|
38 el.style.top = '10px'; |
|
39 root = doc.body; |
|
40 if (root && root.appendChild && root.removeChild) { |
|
41 root.appendChild(el); |
|
42 isSupported = (el.offsetTop === 10); |
|
43 root.removeChild(el); |
|
44 } |
|
45 } |
|
46 } |
|
47 |
|
48 return isSupported; |
|
49 }()); |
|
50 |
|
51 /** |
|
52 * Widget extension, which can be used to add modality support to the base Widget class, |
|
53 * through the Base.create method. |
|
54 * |
|
55 * @class WidgetModality |
|
56 * @param {Object} config User configuration object |
|
57 */ |
|
58 function WidgetModal(config) {} |
|
59 |
|
60 var MODAL = 'modal', |
|
61 MASK = 'mask', |
|
62 MODAL_CLASSES = { |
|
63 modal : getCN(WIDGET, MODAL), |
|
64 mask : getCN(WIDGET, MASK) |
|
65 }; |
|
66 |
|
67 /** |
|
68 * Static property used to define the default attribute |
|
69 * configuration introduced by WidgetModality. |
|
70 * |
|
71 * @property ATTRS |
|
72 * @static |
|
73 * @type Object |
|
74 */ |
|
75 WidgetModal.ATTRS = { |
|
76 /** |
|
77 * @attribute maskNode |
|
78 * @type Y.Node |
|
79 * |
|
80 * @description Returns a Y.Node instance of the node being used as the mask. |
|
81 */ |
|
82 maskNode : { |
|
83 getter : '_getMaskNode', |
|
84 readOnly : true |
|
85 }, |
|
86 |
|
87 |
|
88 /** |
|
89 * @attribute modal |
|
90 * @type boolean |
|
91 * |
|
92 * @description Whether the widget should be modal or not. |
|
93 */ |
|
94 modal: { |
|
95 value:false, |
|
96 validator: isBoolean |
|
97 }, |
|
98 |
|
99 /** |
|
100 * @attribute focusOn |
|
101 * @type array |
|
102 * |
|
103 * @description An array of objects corresponding to the nodes and events that will trigger a re-focus back on the widget. |
|
104 * The implementer can supply an array of objects, with each object having the following properties: |
|
105 * <p>eventName: (string, required): The eventName to listen to.</p> |
|
106 * <p>node: (Y.Node, optional): The Y.Node that will fire the event (defaults to the boundingBox of the widget)</p> |
|
107 * <p>By default, this attribute consists of two objects which will cause the widget to re-focus if anything |
|
108 * outside the widget is clicked on or focussed upon.</p> |
|
109 */ |
|
110 focusOn: { |
|
111 valueFn: function() { |
|
112 return [ |
|
113 { |
|
114 // node: this.get(BOUNDING_BOX), |
|
115 eventName: ClickOutside |
|
116 }, |
|
117 { |
|
118 //node: this.get(BOUNDING_BOX), |
|
119 eventName: FocusOutside |
|
120 } |
|
121 ]; |
|
122 }, |
|
123 |
|
124 validator: Y.Lang.isArray |
|
125 } |
|
126 |
|
127 }; |
|
128 |
|
129 |
|
130 WidgetModal.CLASSES = MODAL_CLASSES; |
|
131 |
|
132 |
|
133 /** |
|
134 * Returns the mask if it exists on the page - otherwise creates a mask. There's only |
|
135 * one mask on a page at a given time. |
|
136 * <p> |
|
137 * This method in invoked internally by the getter of the maskNode ATTR. |
|
138 * </p> |
|
139 * @method _GET_MASK |
|
140 * @protected |
|
141 * @static |
|
142 */ |
|
143 WidgetModal._GET_MASK = function() { |
|
144 |
|
145 var mask = Y.one('.' + MODAL_CLASSES.mask), |
|
146 win = Y.one('win'); |
|
147 |
|
148 if (mask) { |
|
149 return mask; |
|
150 } |
|
151 |
|
152 mask = Y.Node.create('<div></div>').addClass(MODAL_CLASSES.mask); |
|
153 |
|
154 if (supportsPosFixed) { |
|
155 mask.setStyles({ |
|
156 position: 'fixed', |
|
157 width : '100%', |
|
158 height : '100%', |
|
159 top : '0', |
|
160 left : '0', |
|
161 display : 'block' |
|
162 }); |
|
163 } else { |
|
164 mask.setStyles({ |
|
165 position: 'absolute', |
|
166 width : win.get('winWidth') +'px', |
|
167 height : win.get('winHeight') + 'px', |
|
168 top : '0', |
|
169 left : '0', |
|
170 display : 'block' |
|
171 }); |
|
172 } |
|
173 |
|
174 return mask; |
|
175 }; |
|
176 |
|
177 /** |
|
178 * A stack of Y.Widget objects representing the current hierarchy of modal widgets presently displayed on the screen |
|
179 * @property STACK |
|
180 */ |
|
181 WidgetModal.STACK = []; |
|
182 |
|
183 |
|
184 WidgetModal.prototype = { |
|
185 |
|
186 initializer: function () { |
|
187 Y.after(this._renderUIModal, this, RENDER_UI); |
|
188 Y.after(this._syncUIModal, this, SYNC_UI); |
|
189 Y.after(this._bindUIModal, this, BIND_UI); |
|
190 }, |
|
191 |
|
192 destructor: function () { |
|
193 // Hack to remove this thing from the STACK. |
|
194 this._uiSetHostVisibleModal(false); |
|
195 }, |
|
196 |
|
197 // *** Instance Members *** // |
|
198 |
|
199 _uiHandlesModal: null, |
|
200 |
|
201 |
|
202 /** |
|
203 * Adds modal class to the bounding box of the widget |
|
204 * <p> |
|
205 * This method in invoked after renderUI is invoked for the Widget class |
|
206 * using YUI's aop infrastructure. |
|
207 * </p> |
|
208 * @method _renderUIModal |
|
209 * @protected |
|
210 */ |
|
211 _renderUIModal : function () { |
|
212 |
|
213 var bb = this.get(BOUNDING_BOX); |
|
214 //cb = this.get(CONTENT_BOX); |
|
215 |
|
216 //this makes the content box content appear over the mask |
|
217 // cb.setStyles({ |
|
218 // position: "" |
|
219 // }); |
|
220 |
|
221 this._repositionMask(this); |
|
222 bb.addClass(MODAL_CLASSES.modal); |
|
223 |
|
224 }, |
|
225 |
|
226 |
|
227 /** |
|
228 * Hooks up methods to be executed when the widget's visibility or z-index changes |
|
229 * <p> |
|
230 * This method in invoked after bindUI is invoked for the Widget class |
|
231 * using YUI's aop infrastructure. |
|
232 * </p> |
|
233 * @method _bindUIModal |
|
234 * @protected |
|
235 */ |
|
236 _bindUIModal : function () { |
|
237 |
|
238 this.after(VISIBLE+CHANGE, this._afterHostVisibleChangeModal); |
|
239 this.after(Z_INDEX+CHANGE, this._afterHostZIndexChangeModal); |
|
240 this.after("focusOnChange", this._afterFocusOnChange); |
|
241 |
|
242 // Re-align the mask in the viewport if `position: fixed;` is not |
|
243 // supported. iOS < 5 and Android < 3 don't actually support it even |
|
244 // though they both pass the feature test; the UA sniff is here to |
|
245 // account for that. Ideally this should be replaced with a better |
|
246 // feature test. |
|
247 if (!supportsPosFixed || |
|
248 (Y.UA.ios && Y.UA.ios < 5) || |
|
249 (Y.UA.android && Y.UA.android < 3)) { |
|
250 |
|
251 Y.one('win').on('scroll', this._resyncMask, this); |
|
252 } |
|
253 }, |
|
254 |
|
255 /** |
|
256 * Syncs the mask with the widget's current state, namely the visibility and z-index of the widget |
|
257 * <p> |
|
258 * This method in invoked after syncUI is invoked for the Widget class |
|
259 * using YUI's aop infrastructure. |
|
260 * </p> |
|
261 * @method _syncUIModal |
|
262 * @protected |
|
263 */ |
|
264 _syncUIModal : function () { |
|
265 |
|
266 //var host = this.get(HOST); |
|
267 |
|
268 this._uiSetHostVisibleModal(this.get(VISIBLE)); |
|
269 |
|
270 }, |
|
271 |
|
272 /** |
|
273 * Provides mouse and tab focus to the widget's bounding box. |
|
274 * |
|
275 * @method _focus |
|
276 * @protected |
|
277 */ |
|
278 _focus : function (e) { |
|
279 |
|
280 var bb = this.get(BOUNDING_BOX), |
|
281 oldTI = bb.get('tabIndex'); |
|
282 |
|
283 bb.set('tabIndex', oldTI >= 0 ? oldTI : 0); |
|
284 this.focus(); |
|
285 }, |
|
286 /** |
|
287 * Blurs the widget. |
|
288 * |
|
289 * @method _blur |
|
290 * @protected |
|
291 */ |
|
292 _blur : function () { |
|
293 |
|
294 this.blur(); |
|
295 }, |
|
296 |
|
297 /** |
|
298 * Returns the Y.Node instance of the maskNode |
|
299 * |
|
300 * @method _getMaskNode |
|
301 * @protected |
|
302 * @return {Node} The Y.Node instance of the mask, as returned from WidgetModal._GET_MASK |
|
303 */ |
|
304 _getMaskNode : function () { |
|
305 |
|
306 return WidgetModal._GET_MASK(); |
|
307 }, |
|
308 |
|
309 /** |
|
310 * Performs events attaching/detaching, stack shifting and mask repositioning based on the visibility of the widget |
|
311 * |
|
312 * @method _uiSetHostVisibleModal |
|
313 * @protected |
|
314 * @param {boolean} Whether the widget is visible or not |
|
315 */ |
|
316 _uiSetHostVisibleModal : function (visible) { |
|
317 var stack = WidgetModal.STACK, |
|
318 maskNode = this.get('maskNode'), |
|
319 isModal = this.get('modal'), |
|
320 topModal, index; |
|
321 |
|
322 if (visible) { |
|
323 |
|
324 Y.Array.each(stack, function(modal){ |
|
325 modal._detachUIHandlesModal(); |
|
326 modal._blur(); |
|
327 }); |
|
328 |
|
329 // push on top of stack |
|
330 stack.unshift(this); |
|
331 |
|
332 this._repositionMask(this); |
|
333 this._uiSetHostZIndexModal(this.get(Z_INDEX)); |
|
334 |
|
335 if (isModal) { |
|
336 maskNode.show(); |
|
337 Y.later(1, this, '_attachUIHandlesModal'); |
|
338 if (this.get(RENDERED)) { |
|
339 this._focus(); |
|
340 } |
|
341 } |
|
342 |
|
343 |
|
344 } else { |
|
345 |
|
346 index = Y.Array.indexOf(stack, this); |
|
347 if (index >= 0) { |
|
348 // Remove modal widget from global stack. |
|
349 stack.splice(index, 1); |
|
350 } |
|
351 |
|
352 this._detachUIHandlesModal(); |
|
353 this._blur(); |
|
354 |
|
355 if (stack.length) { |
|
356 topModal = stack[0]; |
|
357 this._repositionMask(topModal); |
|
358 //topModal._attachUIHandlesModal(); |
|
359 topModal._uiSetHostZIndexModal(topModal.get(Z_INDEX)); |
|
360 |
|
361 if (topModal.get('modal')) { |
|
362 //topModal._attachUIHandlesModal(); |
|
363 Y.later(1, topModal, '_attachUIHandlesModal'); |
|
364 topModal._focus(); |
|
365 } |
|
366 |
|
367 } else { |
|
368 |
|
369 if (maskNode.getStyle('display') === 'block') { |
|
370 maskNode.hide(); |
|
371 } |
|
372 |
|
373 } |
|
374 |
|
375 } |
|
376 }, |
|
377 |
|
378 /** |
|
379 * Sets the z-index of the mask node. |
|
380 * |
|
381 * @method _uiSetHostZIndexModal |
|
382 * @protected |
|
383 * @param {Number} zIndex Z-Index of the widget |
|
384 */ |
|
385 _uiSetHostZIndexModal : function (zIndex) { |
|
386 |
|
387 if (this.get('modal')) { |
|
388 this.get('maskNode').setStyle(Z_INDEX, zIndex || 0); |
|
389 } |
|
390 |
|
391 }, |
|
392 |
|
393 /** |
|
394 * Attaches UI Listeners for "clickoutside" and "focusoutside" on the |
|
395 * widget. When these events occur, and the widget is modal, focus is |
|
396 * shifted back onto the widget. |
|
397 * |
|
398 * @method _attachUIHandlesModal |
|
399 * @protected |
|
400 */ |
|
401 _attachUIHandlesModal : function () { |
|
402 |
|
403 if (this._uiHandlesModal || WidgetModal.STACK[0] !== this) { |
|
404 // Quit early if we have ui handles, or if we not at the top |
|
405 // of the global stack. |
|
406 return; |
|
407 } |
|
408 |
|
409 var bb = this.get(BOUNDING_BOX), |
|
410 maskNode = this.get('maskNode'), |
|
411 focusOn = this.get('focusOn'), |
|
412 focus = Y.bind(this._focus, this), |
|
413 uiHandles = [], |
|
414 i, len, o; |
|
415 |
|
416 for (i = 0, len = focusOn.length; i < len; i++) { |
|
417 |
|
418 o = {}; |
|
419 o.node = focusOn[i].node; |
|
420 o.ev = focusOn[i].eventName; |
|
421 o.keyCode = focusOn[i].keyCode; |
|
422 |
|
423 //no keycode or node defined |
|
424 if (!o.node && !o.keyCode && o.ev) { |
|
425 uiHandles.push(bb.on(o.ev, focus)); |
|
426 } |
|
427 |
|
428 //node defined, no keycode (not a keypress) |
|
429 else if (o.node && !o.keyCode && o.ev) { |
|
430 uiHandles.push(o.node.on(o.ev, focus)); |
|
431 } |
|
432 |
|
433 //node defined, keycode defined, event defined (its a key press) |
|
434 else if (o.node && o.keyCode && o.ev) { |
|
435 uiHandles.push(o.node.on(o.ev, focus, o.keyCode)); |
|
436 } |
|
437 |
|
438 else { |
|
439 } |
|
440 |
|
441 } |
|
442 |
|
443 if ( ! supportsPosFixed) { |
|
444 uiHandles.push(Y.one('win').on('scroll', Y.bind(function(e){ |
|
445 maskNode.setStyle('top', maskNode.get('docScrollY')); |
|
446 }, this))); |
|
447 } |
|
448 |
|
449 this._uiHandlesModal = uiHandles; |
|
450 }, |
|
451 |
|
452 /** |
|
453 * Detaches all UI Listeners that were set in _attachUIHandlesModal from the widget. |
|
454 * |
|
455 * @method _detachUIHandlesModal |
|
456 * @protected |
|
457 */ |
|
458 _detachUIHandlesModal : function () { |
|
459 Y.each(this._uiHandlesModal, function(h){ |
|
460 h.detach(); |
|
461 }); |
|
462 this._uiHandlesModal = null; |
|
463 }, |
|
464 |
|
465 /** |
|
466 * Default function that is called when visibility is changed on the widget. |
|
467 * |
|
468 * @method _afterHostVisibleChangeModal |
|
469 * @protected |
|
470 * @param {EventFacade} e The event facade of the change |
|
471 */ |
|
472 _afterHostVisibleChangeModal : function (e) { |
|
473 |
|
474 this._uiSetHostVisibleModal(e.newVal); |
|
475 }, |
|
476 |
|
477 /** |
|
478 * Default function that is called when z-index is changed on the widget. |
|
479 * |
|
480 * @method _afterHostZIndexChangeModal |
|
481 * @protected |
|
482 * @param {EventFacade} e The event facade of the change |
|
483 */ |
|
484 _afterHostZIndexChangeModal : function (e) { |
|
485 |
|
486 this._uiSetHostZIndexModal(e.newVal); |
|
487 }, |
|
488 |
|
489 /** |
|
490 * Returns a boolean representing whether the current widget is in a "nested modality" state. |
|
491 * This is done by checking the number of widgets currently on the stack. |
|
492 * |
|
493 * @method isNested |
|
494 * @public |
|
495 */ |
|
496 isNested: function() { |
|
497 var length = WidgetModal.STACK.length, |
|
498 retval = (length > 1) ? true : false; |
|
499 return retval; |
|
500 }, |
|
501 |
|
502 /** |
|
503 * Repositions the mask in the DOM for nested modality cases. |
|
504 * |
|
505 * @method _repositionMask |
|
506 * @protected |
|
507 * @param {Widget} nextElem The Y.Widget instance that will be visible in the stack once the current widget is closed. |
|
508 */ |
|
509 _repositionMask: function(nextElem) { |
|
510 |
|
511 var currentModal = this.get('modal'), |
|
512 nextModal = nextElem.get('modal'), |
|
513 maskNode = this.get('maskNode'), |
|
514 bb, bbParent; |
|
515 |
|
516 //if this is modal and host is not modal |
|
517 if (currentModal && !nextModal) { |
|
518 //leave the mask where it is, since the host is not modal. |
|
519 maskNode.remove(); |
|
520 this.fire(MaskHide); |
|
521 } |
|
522 |
|
523 //if the main widget is not modal but the host is modal, or both of them are modal |
|
524 else if ((!currentModal && nextModal) || (currentModal && nextModal)) { |
|
525 |
|
526 //then remove the mask off DOM, reposition it, and reinsert it into the DOM |
|
527 maskNode.remove(); |
|
528 this.fire(MaskHide); |
|
529 bb = nextElem.get(BOUNDING_BOX); |
|
530 bbParent = bb.get('parentNode') || Y.one('body'); |
|
531 bbParent.insert(maskNode, bbParent.get('firstChild')); |
|
532 this.fire(MaskShow); |
|
533 } |
|
534 |
|
535 }, |
|
536 |
|
537 /** |
|
538 * Resyncs the mask in the viewport for browsers that don't support fixed positioning |
|
539 * |
|
540 * @method _resyncMask |
|
541 * @param {Widget} nextElem The Y.Widget instance that will be visible in the stack once the current widget is closed. |
|
542 * @private |
|
543 */ |
|
544 _resyncMask: function (e) { |
|
545 var o = e.currentTarget, |
|
546 offsetX = o.get('docScrollX'), |
|
547 offsetY = o.get('docScrollY'), |
|
548 w = o.get('innerWidth') || o.get('winWidth'), |
|
549 h = o.get('innerHeight') || o.get('winHeight'), |
|
550 mask = this.get('maskNode'); |
|
551 |
|
552 mask.setStyles({ |
|
553 "top": offsetY + "px", |
|
554 "left": offsetX + "px", |
|
555 "width": w + 'px', |
|
556 "height": h + 'px' |
|
557 }); |
|
558 }, |
|
559 |
|
560 /** |
|
561 * Default function called when focusOn Attribute is changed. Remove existing listeners and create new listeners. |
|
562 * |
|
563 * @method _afterFocusOnChange |
|
564 * @protected |
|
565 */ |
|
566 _afterFocusOnChange : function(e) { |
|
567 this._detachUIHandlesModal(); |
|
568 |
|
569 if (this.get(VISIBLE)) { |
|
570 this._attachUIHandlesModal(); |
|
571 } |
|
572 } |
|
573 }; |
|
574 |
|
575 Y.WidgetModality = WidgetModal; |
|
576 |
|
577 |
|
578 |
|
579 }, '@VERSION@', {"requires": ["base-build", "event-outside", "widget"], "skinnable": true}); |