Version 3.18.1
Show:

File: widget-position-align/js/Widget-PositionAlign.js

            /**
            Provides extended/advanced XY positioning support for Widgets, through an
            extension.
            
            It builds on top of the `widget-position` module, to provide alignment and
            centering support. Future releases aim to add constrained and fixed positioning
            support.
            
            @module widget-position-align
            **/
            var Lang = Y.Lang,
            
                ALIGN        = 'align',
                ALIGN_ON     = 'alignOn',
            
                VISIBLE      = 'visible',
                BOUNDING_BOX = 'boundingBox',
            
                OFFSET_WIDTH    = 'offsetWidth',
                OFFSET_HEIGHT   = 'offsetHeight',
                REGION          = 'region',
                VIEWPORT_REGION = 'viewportRegion';
            
            /**
            Widget extension, which can be used to add extended XY positioning support to
            the base Widget class, through the `Base.create` method.
            
            **Note:** This extension requires that the `WidgetPosition` extension be added
            to the Widget (before `WidgetPositionAlign`, if part of the same extension list
            passed to `Base.build`).
            
            @class WidgetPositionAlign
            @param {Object} config User configuration object.
            @constructor
            **/
            function PositionAlign (config) {}
            
            PositionAlign.ATTRS = {
            
                /**
                The alignment configuration for this widget.
            
                The `align` attribute is used to align a reference point on the widget, with
                the reference point on another `Node`, or the viewport. The object which
                `align` expects has the following properties:
            
                  * __`node`__: The `Node` to which the widget is to be aligned. If set to
                    `null`, or not provided, the widget is aligned to the viewport.
            
                  * __`points`__: A two element Array, defining the two points on the widget
                    and `Node`/viewport which are to be aligned. The first element is the
                    point on the widget, and the second element is the point on the
                    `Node`/viewport. Supported alignment points are defined as static
                    properties on `WidgetPositionAlign`.
            
                @example Aligns the top-right corner of the widget with the top-left corner
                of the viewport:
            
                    myWidget.set('align', {
                        points: [Y.WidgetPositionAlign.TR, Y.WidgetPositionAlign.TL]
                    });
            
                @attribute align
                @type Object
                @default null
                **/
                align: {
                    value: null
                },
            
                /**
                A convenience Attribute, which can be used as a shortcut for the `align`
                Attribute.
            
                If set to `true`, the widget is centered in the viewport. If set to a `Node`
                reference or valid selector String, the widget will be centered within the
                `Node`. If set to `false`, no center positioning is applied.
            
                @attribute centered
                @type Boolean|Node
                @default false
                **/
                centered: {
                    setter : '_setAlignCenter',
                    lazyAdd:false,
                    value  :false
                },
            
                /**
                An Array of Objects corresponding to the `Node`s and events that will cause
                the alignment of this widget to be synced to the DOM.
            
                The `alignOn` Attribute is expected to be an Array of Objects with the
                following properties:
            
                  * __`eventName`__: The String event name to listen for.
            
                  * __`node`__: The optional `Node` that will fire the event, it can be a
                    `Node` reference or a selector String. This will default to the widget's
                    `boundingBox`.
            
                @example Sync this widget's alignment on window resize:
            
                    myWidget.set('alignOn', [
                        {
                            node     : Y.one('win'),
                            eventName: 'resize'
                        }
                    ]);
            
                @attribute alignOn
                @type Array
                @default []
                **/
                alignOn: {
                    value    : [],
                    validator: Y.Lang.isArray
                }
            };
            
            /**
            Constant used to specify the top-left corner for alignment
            
            @property TL
            @type String
            @value 'tl'
            @static
            **/
            PositionAlign.TL = 'tl';
            
            /**
            Constant used to specify the top-right corner for alignment
            
            @property TR
            @type String
            @value 'tr'
            @static
            **/
            PositionAlign.TR = 'tr';
            
            /**
            Constant used to specify the bottom-left corner for alignment
            
            @property BL
            @type String
            @value 'bl'
            @static
            **/
            PositionAlign.BL = 'bl';
            
            /**
            Constant used to specify the bottom-right corner for alignment
            
            @property BR
            @type String
            @value 'br'
            @static
            **/
            PositionAlign.BR = 'br';
            
            /**
            Constant used to specify the top edge-center point for alignment
            
            @property TC
            @type String
            @value 'tc'
            @static
            **/
            PositionAlign.TC = 'tc';
            
            /**
            Constant used to specify the right edge, center point for alignment
            
            @property RC
            @type String
            @value 'rc'
            @static
            **/
            PositionAlign.RC = 'rc';
            
            /**
            Constant used to specify the bottom edge, center point for alignment
            
            @property BC
            @type String
            @value 'bc'
            @static
            **/
            PositionAlign.BC = 'bc';
            
            /**
            Constant used to specify the left edge, center point for alignment
            
            @property LC
            @type String
            @value 'lc'
            @static
            **/
            PositionAlign.LC = 'lc';
            
            /**
            Constant used to specify the center of widget/node/viewport for alignment
            
            @property CC
            @type String
            @value 'cc'
            @static
            */
            PositionAlign.CC = 'cc';
            
            PositionAlign.prototype = {
                // -- Protected Properties -------------------------------------------------
            
            
                initializer : function() {
                    if (!this._posNode) {
                        Y.error('WidgetPosition needs to be added to the Widget, ' +
                            'before WidgetPositionAlign is added');
                    }
            
                    Y.after(this._bindUIPosAlign, this, 'bindUI');
                    Y.after(this._syncUIPosAlign, this, 'syncUI');
                },
            
                /**
                Holds the alignment-syncing event handles.
            
                @property _posAlignUIHandles
                @type Array
                @default null
                @protected
                **/
                _posAlignUIHandles: null,
            
                // -- Lifecycle Methods ----------------------------------------------------
            
                destructor: function () {
                    this._detachPosAlignUIHandles();
                },
            
                /**
                Bind event listeners responsible for updating the UI state in response to
                the widget's position-align related state changes.
            
                This method is invoked after `bindUI` has been invoked for the `Widget`
                class using the AOP infrastructure.
            
                @method _bindUIPosAlign
                @protected
                **/
                _bindUIPosAlign: function () {
                    this.after('alignChange', this._afterAlignChange);
                    this.after('alignOnChange', this._afterAlignOnChange);
                    this.after('visibleChange', this._syncUIPosAlign);
                },
            
                /**
                Synchronizes the current `align` Attribute value to the DOM.
            
                This method is invoked after `syncUI` has been invoked for the `Widget`
                class using the AOP infrastructure.
            
                @method _syncUIPosAlign
                @protected
                **/
                _syncUIPosAlign: function () {
                    var align = this.get(ALIGN);
            
                    this._uiSetVisiblePosAlign(this.get(VISIBLE));
            
                    if (align) {
                        this._uiSetAlign(align.node, align.points);
                    }
                },
            
                // -- Public Methods -------------------------------------------------------
            
                /**
                Aligns this widget to the provided `Node` (or viewport) using the provided
                points. This method can be invoked with no arguments which will cause the
                widget's current `align` Attribute value to be synced to the DOM.
            
                @example Aligning to the top-left corner of the `<body>`:
            
                    myWidget.align('body',
                        [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.TR]);
            
                @method align
                @param {Node|String|null} [node] A reference (or selector String) for the
                  `Node` which with the widget is to be aligned. If null is passed in, the
                  widget will be aligned with the viewport.
                @param {Array[2]} [points] A two item array specifying the points on the
                  widget and `Node`/viewport which will to be aligned. The first entry is
                  the point on the widget, and the second entry is the point on the
                  `Node`/viewport. Valid point references are defined as static constants on
                  the `WidgetPositionAlign` extension.
                @chainable
                **/
                align: function (node, points) {
                    if (arguments.length) {
                        // Set the `align` Attribute.
                        this.set(ALIGN, {
                            node  : node,
                            points: points
                        });
                    } else {
                        // Sync the current `align` Attribute value to the DOM.
                        this._syncUIPosAlign();
                    }
            
                    return this;
                },
            
                /**
                Centers the widget in the viewport, or if a `Node` is passed in, it will
                be centered to that `Node`.
            
                @method centered
                @param {Node|String} [node] A `Node` reference or selector String defining
                  the `Node` which the widget should be centered. If a `Node` is not  passed
                  in, then the widget will be centered to the viewport.
                @chainable
                **/
                centered: function (node) {
                    return this.align(node, [PositionAlign.CC, PositionAlign.CC]);
                },
            
                // -- Protected Methods ----------------------------------------------------
            
                /**
                Default setter for `center` Attribute changes. Sets up the appropriate
                value, and passes it through the to the align attribute.
            
                @method _setAlignCenter
                @param {Boolean|Node} val The Attribute value being set.
                @return {Boolean|Node} the value passed in.
                @protected
                **/
                _setAlignCenter: function (val) {
                    if (val) {
                        this.set(ALIGN, {
                            node  : val === true ? null : val,
                            points: [PositionAlign.CC, PositionAlign.CC]
                        });
                    }
            
                    return val;
                },
            
                /**
                Updates the UI to reflect the `align` value passed in.
            
                **Note:** See the `align` Attribute documentation, for the Object structure
                expected.
            
                @method _uiSetAlign
                @param {Node|String|null} [node] The node to align to, or null to indicate
                  the viewport.
                @param {Array} points The alignment points.
                @protected
                **/
                _uiSetAlign: function (node, points) {
                    if ( ! Lang.isArray(points) || points.length !== 2) {
                        Y.error('align: Invalid Points Arguments');
                        return;
                    }
            
                    var nodeRegion = this._getRegion(node),
                        widgetPoint, nodePoint, xy;
            
                    if ( ! nodeRegion) {
                        // No-op, nothing to align to.
                        return;
                    }
            
                    widgetPoint = points[0];
                    nodePoint   = points[1];
            
                    // TODO: Optimize KWeight - Would lookup table help?
                    switch (nodePoint) {
                    case PositionAlign.TL:
                        xy = [nodeRegion.left, nodeRegion.top];
                        break;
            
                    case PositionAlign.TR:
                        xy = [nodeRegion.right, nodeRegion.top];
                        break;
            
                    case PositionAlign.BL:
                        xy = [nodeRegion.left, nodeRegion.bottom];
                        break;
            
                    case PositionAlign.BR:
                        xy = [nodeRegion.right, nodeRegion.bottom];
                        break;
            
                    case PositionAlign.TC:
                        xy = [
                            nodeRegion.left + Math.floor(nodeRegion.width / 2),
                            nodeRegion.top
                        ];
                        break;
            
                    case PositionAlign.BC:
                        xy = [
                            nodeRegion.left + Math.floor(nodeRegion.width / 2),
                            nodeRegion.bottom
                        ];
                        break;
            
                    case PositionAlign.LC:
                        xy = [
                            nodeRegion.left,
                            nodeRegion.top + Math.floor(nodeRegion.height / 2)
                        ];
                        break;
            
                    case PositionAlign.RC:
                        xy = [
                            nodeRegion.right,
                            nodeRegion.top + Math.floor(nodeRegion.height / 2)
                        ];
                        break;
            
                    case PositionAlign.CC:
                        xy = [
                            nodeRegion.left + Math.floor(nodeRegion.width / 2),
                            nodeRegion.top + Math.floor(nodeRegion.height / 2)
                        ];
                        break;
            
                    default:
                        Y.log('align: Invalid Points Arguments', 'info',
                            'widget-position-align');
                        break;
            
                    }
            
                    if (xy) {
                        this._doAlign(widgetPoint, xy[0], xy[1]);
                    }
                },
            
                /**
                Attaches or detaches alignment-syncing event handlers based on the widget's
                `visible` Attribute state.
            
                @method _uiSetVisiblePosAlign
                @param {Boolean} visible The current value of the widget's `visible`
                  Attribute.
                @protected
                **/
                _uiSetVisiblePosAlign: function (visible) {
                    if (visible) {
                        this._attachPosAlignUIHandles();
                    } else {
                        this._detachPosAlignUIHandles();
                    }
                },
            
                /**
                Attaches the alignment-syncing event handlers.
            
                @method _attachPosAlignUIHandles
                @protected
                **/
                _attachPosAlignUIHandles: function () {
                    if (this._posAlignUIHandles) {
                        // No-op if we have already setup the event handlers.
                        return;
                    }
            
                    var bb        = this.get(BOUNDING_BOX),
                        syncAlign = Y.bind(this._syncUIPosAlign, this),
                        handles   = [];
            
                    Y.Array.each(this.get(ALIGN_ON), function (o) {
                        var event = o.eventName,
                            node  = Y.one(o.node) || bb;
            
                        if (event) {
                            handles.push(node.on(event, syncAlign));
                        }
                    });
            
                    this._posAlignUIHandles = handles;
                },
            
                /**
                Detaches the alignment-syncing event handlers.
            
                @method _detachPosAlignUIHandles
                @protected
                **/
                _detachPosAlignUIHandles: function () {
                    var handles = this._posAlignUIHandles;
                    if (handles) {
                        new Y.EventHandle(handles).detach();
                        this._posAlignUIHandles = null;
                    }
                },
            
                // -- Private Methods ------------------------------------------------------
            
                /**
                Helper method, used to align the given point on the widget, with the XY page
                coordinates provided.
            
                @method _doAlign
                @param {String} widgetPoint Supported point constant
                  (e.g. WidgetPositionAlign.TL)
                @param {Number} x X page coordinate to align to.
                @param {Number} y Y page coordinate to align to.
                @private
                **/
                _doAlign: function (widgetPoint, x, y) {
                    var widgetNode = this._posNode,
                        xy;
            
                    switch (widgetPoint) {
                    case PositionAlign.TL:
                        xy = [x, y];
                        break;
            
                    case PositionAlign.TR:
                        xy = [
                            x - widgetNode.get(OFFSET_WIDTH),
                            y
                        ];
                        break;
            
                    case PositionAlign.BL:
                        xy = [
                            x,
                            y - widgetNode.get(OFFSET_HEIGHT)
                        ];
                        break;
            
                    case PositionAlign.BR:
                        xy = [
                            x - widgetNode.get(OFFSET_WIDTH),
                            y - widgetNode.get(OFFSET_HEIGHT)
                        ];
                        break;
            
                    case PositionAlign.TC:
                        xy = [
                            x - (widgetNode.get(OFFSET_WIDTH) / 2),
                            y
                        ];
                        break;
            
                    case PositionAlign.BC:
                        xy = [
                            x - (widgetNode.get(OFFSET_WIDTH) / 2),
                            y - widgetNode.get(OFFSET_HEIGHT)
                        ];
                        break;
            
                    case PositionAlign.LC:
                        xy = [
                            x,
                            y - (widgetNode.get(OFFSET_HEIGHT) / 2)
                        ];
                        break;
            
                    case PositionAlign.RC:
                        xy = [
                            x - widgetNode.get(OFFSET_WIDTH),
                            y - (widgetNode.get(OFFSET_HEIGHT) / 2)
                        ];
                        break;
            
                    case PositionAlign.CC:
                        xy = [
                            x - (widgetNode.get(OFFSET_WIDTH) / 2),
                            y - (widgetNode.get(OFFSET_HEIGHT) / 2)
                        ];
                        break;
            
                    default:
                        Y.log('align: Invalid Points Argument', 'info',
                            'widget-position-align');
                        break;
            
                    }
            
                    if (xy) {
                        this.move(xy);
                    }
                },
            
                /**
                Returns the region of the passed-in `Node`, or the viewport region if
                calling with passing in a `Node`.
            
                @method _getRegion
                @param {Node} [node] The node to get the region of.
                @return {Object} The node's region.
                @private
                **/
                _getRegion: function (node) {
                    var nodeRegion;
            
                    if ( ! node) {
                        nodeRegion = this._posNode.get(VIEWPORT_REGION);
                    } else {
                        node = Y.Node.one(node);
                        if (node) {
                            nodeRegion = node.get(REGION);
                        }
                    }
            
                    return nodeRegion;
                },
            
                // -- Protected Event Handlers ---------------------------------------------
            
                /**
                Handles `alignChange` events by updating the UI in response to `align`
                Attribute changes.
            
                @method _afterAlignChange
                @param {EventFacade} e
                @protected
                **/
                _afterAlignChange: function (e) {
                    var align = e.newVal;
                    if (align) {
                        this._uiSetAlign(align.node, align.points);
                    }
                },
            
                /**
                Handles `alignOnChange` events by updating the alignment-syncing event
                handlers.
            
                @method _afterAlignOnChange
                @param {EventFacade} e
                @protected
                **/
                _afterAlignOnChange: function(e) {
                    this._detachPosAlignUIHandles();
            
                    if (this.get(VISIBLE)) {
                        this._attachPosAlignUIHandles();
                    }
                }
            };
            
            Y.WidgetPositionAlign = PositionAlign;