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