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/
8
YUI.add('widget-modality', function (Y, NAME) {
11
* Provides modality support for Widgets, though an extension
13
* @module widget-modality
16
var WIDGET = 'widget',
17
RENDER_UI = 'renderUI',
20
BOUNDING_BOX = 'boundingBox',
21
CONTENT_BOX = 'contentBox',
25
isBoolean = Y.Lang.isBoolean,
26
getCN = Y.ClassNameManager.getClassName,
27
MaskShow = "maskShow",
28
MaskHide = "maskHide",
29
ClickOutside = "clickoutside",
30
FocusOutside = "focusoutside",
32
supportsPosFixed = (function(){
34
/*! IS_POSITION_FIXED_SUPPORTED - Juriy Zaytsev (kangax) - http://yura.thinkweb2.com/cft/ */
36
var doc = Y.config.doc,
40
if (doc.createElement) {
41
el = doc.createElement('div');
43
el.style.position = 'fixed';
44
el.style.top = '10px';
46
if (root && root.appendChild && root.removeChild) {
48
isSupported = (el.offsetTop === 10);
58
* Widget extension, which can be used to add modality support to the base Widget class,
59
* through the Base.create method.
61
* @class WidgetModality
62
* @param {Object} config User configuration object
64
function WidgetModal(config) {}
69
modal : getCN(WIDGET, MODAL),
70
mask : getCN(WIDGET, MASK)
74
* Static property used to define the default attribute
75
* configuration introduced by WidgetModality.
86
* @description Returns a Y.Node instance of the node being used as the mask.
89
getter : '_getMaskNode',
98
* @description Whether the widget should be modal or not.
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>
117
valueFn: function() {
120
// node: this.get(BOUNDING_BOX),
121
eventName: ClickOutside
124
//node: this.get(BOUNDING_BOX),
125
eventName: FocusOutside
130
validator: Y.Lang.isArray
136
WidgetModal.CLASSES = MODAL_CLASSES;
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.
143
* This method in invoked internally by the getter of the maskNode ATTR.
148
WidgetModal._GET_MASK = function() {
150
var mask = Y.one('.' + MODAL_CLASSES.mask),
157
mask = Y.Node.create('<div></div>').addClass(MODAL_CLASSES.mask);
159
if (supportsPosFixed) {
170
position: 'absolute',
171
width : win.get('winWidth') +'px',
172
height : win.get('winHeight') + 'px',
183
* A stack of Y.Widget objects representing the current hierarchy of modal widgets presently displayed on the screen
186
WidgetModal.STACK = [];
189
WidgetModal.prototype = {
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);
197
destructor: function () {
198
// Hack to remove this thing from the STACK.
199
this._uiSetHostVisibleModal(false);
202
// *** Instance Members *** //
204
_uiHandlesModal: null,
208
* Adds modal class to the bounding box of the widget
210
* This method in invoked after renderUI is invoked for the Widget class
211
* using YUI's aop infrastructure.
213
* @method _renderUIModal
216
_renderUIModal : function () {
218
var bb = this.get(BOUNDING_BOX);
219
//cb = this.get(CONTENT_BOX);
221
//this makes the content box content appear over the mask
226
this._repositionMask(this);
227
bb.addClass(MODAL_CLASSES.modal);
233
* Hooks up methods to be executed when the widget's visibility or z-index changes
235
* This method in invoked after bindUI is invoked for the Widget class
236
* using YUI's aop infrastructure.
238
* @method _bindUIModal
241
_bindUIModal : function () {
243
this.after(VISIBLE+CHANGE, this._afterHostVisibleChangeModal);
244
this.after(Z_INDEX+CHANGE, this._afterHostZIndexChangeModal);
245
this.after("focusOnChange", this._afterFocusOnChange);
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
252
if (!supportsPosFixed ||
253
(Y.UA.ios && Y.UA.ios < 5) ||
254
(Y.UA.android && Y.UA.android < 3)) {
256
Y.one('win').on('scroll', this._resyncMask, this);
261
* Syncs the mask with the widget's current state, namely the visibility and z-index of the widget
263
* This method in invoked after syncUI is invoked for the Widget class
264
* using YUI's aop infrastructure.
266
* @method _syncUIModal
269
_syncUIModal : function () {
271
//var host = this.get(HOST);
273
this._uiSetHostVisibleModal(this.get(VISIBLE));
278
* Provides mouse and tab focus to the widget's bounding box.
282
_focus : function (e) {
284
var bb = this.get(BOUNDING_BOX),
285
oldTI = bb.get('tabIndex');
287
bb.set('tabIndex', oldTI >= 0 ? oldTI : 0);
295
_blur : function () {
301
* Returns the Y.Node instance of the maskNode
303
* @method _getMaskNode
304
* @return {Node} The Y.Node instance of the mask, as returned from WidgetModal._GET_MASK
306
_getMaskNode : function () {
308
return WidgetModal._GET_MASK();
312
* Performs events attaching/detaching, stack shifting and mask repositioning based on the visibility of the widget
314
* @method _uiSetHostVisibleModal
315
* @param {boolean} Whether the widget is visible or not
317
_uiSetHostVisibleModal : function (visible) {
318
var stack = WidgetModal.STACK,
319
maskNode = this.get('maskNode'),
320
isModal = this.get('modal'),
325
Y.Array.each(stack, function(modal){
326
modal._detachUIHandlesModal();
330
// push on top of stack
333
this._repositionMask(this);
334
this._uiSetHostZIndexModal(this.get(Z_INDEX));
338
Y.later(1, this, '_attachUIHandlesModal');
345
index = Y.Array.indexOf(stack, this);
347
// Remove modal widget from global stack.
348
stack.splice(index, 1);
351
this._detachUIHandlesModal();
356
this._repositionMask(topModal);
357
//topModal._attachUIHandlesModal();
358
topModal._uiSetHostZIndexModal(topModal.get(Z_INDEX));
360
if (topModal.get('modal')) {
361
//topModal._attachUIHandlesModal();
362
Y.later(1, topModal, '_attachUIHandlesModal');
368
if (maskNode.getStyle('display') === 'block') {
378
* Sets the z-index of the mask node.
380
* @method _uiSetHostZIndexModal
381
* @param {Number} Z-Index of the widget
383
_uiSetHostZIndexModal : function (zIndex) {
385
if (this.get('modal')) {
386
this.get('maskNode').setStyle(Z_INDEX, zIndex || 0);
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.
396
* @method _attachUIHandlesModal
398
_attachUIHandlesModal : function () {
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.
406
var bb = this.get(BOUNDING_BOX),
407
maskNode = this.get('maskNode'),
408
focusOn = this.get('focusOn'),
409
focus = Y.bind(this._focus, this),
413
for (i = 0, len = focusOn.length; i < len; i++) {
416
o.node = focusOn[i].node;
417
o.ev = focusOn[i].eventName;
418
o.keyCode = focusOn[i].keyCode;
420
//no keycode or node defined
421
if (!o.node && !o.keyCode && o.ev) {
422
uiHandles.push(bb.on(o.ev, focus));
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));
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));
436
Y.Log('focusOn ATTR Error: The event with name "'+o.ev+'" could not be attached.');
441
if ( ! supportsPosFixed) {
442
uiHandles.push(Y.one('win').on('scroll', Y.bind(function(e){
443
maskNode.setStyle('top', maskNode.get('docScrollY'));
447
this._uiHandlesModal = uiHandles;
451
* Detaches all UI Listeners that were set in _attachUIHandlesModal from the widget.
453
* @method _detachUIHandlesModal
455
_detachUIHandlesModal : function () {
456
Y.each(this._uiHandlesModal, function(h){
459
this._uiHandlesModal = null;
463
* Default function that is called when visibility is changed on the widget.
465
* @method _afterHostVisibleChangeModal
466
* @param {EventFacade} e The event facade of the change
468
_afterHostVisibleChangeModal : function (e) {
470
this._uiSetHostVisibleModal(e.newVal);
474
* Default function that is called when z-index is changed on the widget.
476
* @method _afterHostZIndexChangeModal
477
* @param {EventFacade} e The event facade of the change
479
_afterHostZIndexChangeModal : function (e) {
481
this._uiSetHostZIndexModal(e.newVal);
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.
491
isNested: function() {
492
var length = WidgetModal.STACK.length,
493
retval = (length > 1) ? true : false;
498
* Repositions the mask in the DOM for nested modality cases.
500
* @method _repositionMask
501
* @param {Widget} nextElem The Y.Widget instance that will be visible in the stack once the current widget is closed.
503
_repositionMask: function(nextElem) {
505
var currentModal = this.get('modal'),
506
nextModal = nextElem.get('modal'),
507
maskNode = this.get('maskNode'),
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.
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)) {
520
//then remove the mask off DOM, reposition it, and reinsert it into the DOM
523
bb = nextElem.get(BOUNDING_BOX);
524
bbParent = bb.get('parentNode') || Y.one('body');
525
bbParent.insert(maskNode, bbParent.get('firstChild'));
532
* Resyncs the mask in the viewport for browsers that don't support fixed positioning
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.
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');
547
"top": offsetY + "px",
548
"left": offsetX + "px",
555
* Default function called when focusOn Attribute is changed. Remove existing listeners and create new listeners.
557
* @method _afterFocusOnChange
559
_afterFocusOnChange : function(e) {
560
this._detachUIHandlesModal();
562
if (this.get(VISIBLE)) {
563
this._attachUIHandlesModal();
568
Y.WidgetModality = WidgetModal;
572
}, '3.10.3', {"requires": ["base-build", "event-outside", "widget"], "skinnable": true});