Source: widgets/widget.js

/**
 * @fileOverview Requirejs module containing the antie.widgets.Widget abstract base class.
 * @preserve Copyright (c) 2013-present British Broadcasting Corporation. All rights reserved.
 * @license See https://github.com/fmtvp/tal/blob/master/LICENSE for full licence
 */

/**
 * User interface widgets.
 * @name antie.widgets.Widget
 * @namespace
 */

define(
    'antie/widgets/widget',
    [
        'antie/class',
        'antie/runtimecontext'
    ],
    function(Class, RuntimeContext) {
        'use strict';

        /**
         * Keep a count of generated IDs so we can ensure they're always unique
         * @private
         */
        var widgetUniqueIDIndex = 0;

        /**
         * The base widget class. A widget is a UI-component which can be rendered to device-specific output
         * via a {@link antie.devices.Device} object.
         * @name antie.widgets.Widget
         * @class
         * @abstract
         * @extends antie.Class
         * @param {String} [id] The unique ID of the widget. If excluded, a temporary internal ID will be used (but not included in any output).
         */
        return Class.extend(/** @lends antie.widgets.Widget.prototype */ {
            /**
             * @constructor
             * @ignore
             */
            init: function init (id) {
                this._classNames = {'widget':true};
                this.parentWidget = null;
                this.outputElement = null;
                this._eventListeners = {};
                this._dataItem = null; // Any data item bound to this widget
                this._isFocussed = false;

                function createUniqueID() {
                    return '#' + (new Date() * 1) + '_' + (widgetUniqueIDIndex++);
                }

                // ensure all widgets have an ID
                this.id = id ? id : createUniqueID();
            },
            /**
             * Renders the widget to device-specific output.
             * @param {antie.devices.Device} device The device to render to.
             * @returns A device-specific object that represents the widget as displayed on the device (in a browser, a DOMElement);
             */
            render: function render (/*device*/) {
                throw new Error('Widget::render called - the subclass for widget \'' + this.id + '\' must have not overridden the render method.');
            },
            /**
             * Adds a CSS class to the widget if not already present.
             * @param {String} className The class name to add.
             */
            addClass: function addClass (className) {
                if (!this._classNames[className]) {
                    this._classNames[className] = true;
                    if (this.outputElement) {
                        var device = this.getCurrentApplication().getDevice();
                        device.setElementClasses(this.outputElement, this.getClasses());
                    }
                }
            },
            /**
             * Removes a CSS class from the widget if present.
             * @param {String} className The class name to remove.
             */
            removeClass: function removeClass (className) {
                if (this._classNames[className]) {
                    delete(this._classNames[className]);
                    if (this.outputElement) {
                        var device = this.getCurrentApplication().getDevice();
                        device.setElementClasses(this.outputElement, this.getClasses());
                    }
                }
            },
            /**
             * Checks to see if the widget has a given CSS class.
             * @param {String} className The class name to check.
             * @returns Boolean true if the device has the className. Otherwise boolean false.
             */
            hasClass: function hasClass (className) {
                return (this._classNames[className] ? true : false);
            },
            /**
             * Get an array of class names that this widget has.
             * @returns An array of class names (Strings)
             */
            getClasses: function getClasses () {
                var _names = [];
                for (var i in this._classNames) {
                    if(this._classNames.hasOwnProperty(i)) {
                        _names.push(i);
                    }
                }
                return _names;
            },
            /**
             * Add an event listener function to this widget.
             * @param {String} ev The event type to listen for (e.g. <code>keydown</code>)
             * @param {Function} func The handler to be called when the event is fired.
             * @see antie.events.Event
             */
            addEventListener: function addEventListener (ev, func) {
                var listeners = this._eventListeners[ev];
                if (typeof listeners === 'undefined') {
                    listeners = [];
                    this._eventListeners[ev] = listeners;
                }
                if (!~listeners.indexOf(func)) {
                    listeners.push(func);
                }
            },
            /**
             * Removes an event listener function to this widget.
             * @param {String} ev The event type that the listener is to be removed from (e.g. <code>keydown</code>)
             * @param {Function} func The handler to be removed.
             * @see antie.events.Event
             */
            removeEventListener: function removeEventListener (ev, func) {
                var listeners = this._eventListeners[ev],
                    listener;

                if (!listeners) {
                    RuntimeContext.getDevice().getLogger().error('Attempting to remove non-existent event listener');
                    return false;
                }

                listener = listeners.indexOf(func);
                if (~listener) {
                    listeners.splice(listener, 1);
                }
            },
            /**
             * Fires an event on this object, triggering any event listeners bound to this widget only.
             * Note: this does not bubble or propagate the event to other widgets, for that functionality
             * see {@link #bubbleEvent}.
             * @param {antie.events.Event} ev The event to fire.
             * @see antie.events.Event
             */
            fireEvent: function fireEvent (ev) {
                var listeners = this._eventListeners[ev.type];
                if (listeners) {
                    for (var func in listeners) {
                        if(listeners.hasOwnProperty(func)) {
                            try {
                                listeners[func](ev);
                            } catch (exception) {
                                var logger = this.getCurrentApplication().getDevice().getLogger();
                                logger.error('Error in ' + ev.type + ' event listener on widget ' + this.id + ': ' + exception.message, exception, listeners[func]);
                            }
                        }
                    }
                }
            },
            /**
             * Bubbles an event from object, triggering any event listeners bound to this widget and any
             * parent widgets.
             * To halt bubbling of the event, see {@link antie.events.Event#stopPropagation}.
             * @param {antie.events.Event} ev The event to bubble.
             * @see antie.events.Event
             */
            bubbleEvent: function bubbleEvent (ev) {
                this.fireEvent(ev);
                if (!ev.isPropagationStopped()) {
                    if (this.parentWidget) {
                        this.parentWidget.bubbleEvent(ev);
                    } else {
                        ev.stopPropagation();
                    }

                }
            },

            /**
             * Broadcast an event from object, triggering any event listeners bound to this widget and any
             * parent widgets.
             * To halt bubbling of the event, see {@link antie.events.Event#stopPropagation}.
             * @param {antie.events.Event} ev The event to bubble.
             * @see antie.events.Event
             */
            broadcastEvent: function broadcastEvent (ev) {
                this.fireEvent(ev);
            },

            /**
             * Checks to see if a widget is focussable, i.e. contains an enabled button.
             * @see antie.widgets.Button
             */
            isFocusable: function isFocusable () {
                // a widget can receive focus if any of it's children or children-of-children are Buttons
                // We're not a button and we have no children, so we're not.
                return false;
            },
            /**
             * Gets a reference to the application responsible for creating the widget.
             * @see antie.RuntimeContext
             */
            getCurrentApplication: function getCurrentApplication () {
                try {
                    return RuntimeContext.getCurrentApplication();
                } catch (ex) {
                    return null;
                }
            },
            /**
             * Get any data item associated with this widget.
             */
            getDataItem: function getDataItem () {
                return this._dataItem;
            },
            /**
             * Associate a data item with this widget.
             * @param {object} dataItem Object to associate with this widget.
             */
            setDataItem: function setDataItem (dataItem) {
                this._dataItem = dataItem;
            },
            /**
             * Returns the component this widget is a descendant of
             */
            getComponent: function getComponent () {
                var widget = this;
                while (widget && !(widget.isComponent())) {
                    widget = widget.parentWidget;
                }
                return widget;
            },
            /**
             * Remove focus state from this widget.
             */
            removeFocus: function removeFocus () {
                this.removeClass('focus');
                this._isFocussed = false;
            },
            /**
             * Get if this widget is in the current focus path.
             * @returns Boolean true if this widget is in the focus path, otherwise false.
             */
            isFocussed: function isFocussed () {
                return this._isFocussed;
            },
            /**
             * Returns whether the widget is a Component.
             * @returns {Boolean} True if the widget is a Component.
             */
            isComponent: function isComponent () {
                return false;
            },
            /**
             * Shows a widget. If animation is enabled the widget will be faded in.
             * @param {Object}    options Details of the element to be shown, with optional parameters.
             * @param {Boolean} [options.skipAnim] By default the showing of the element will be animated (faded in). Pass <code>true</code> here to prevent animation.
             * @param {Function} [options.onComplete] Callback function to be called when the element has been shown.
             * @param {Number}    [options.fps=25] Frames per second for fade animation.
             * @param {Number}    [options.duration=840] Duration of fade animation, in milliseconds (ms).
             * @param {String}    [options.easing=linear] Easing style for fade animation.
             * @returns Boolean true if animation was called, otherwise false
             */
            show : function(options) {
                if (this.outputElement) {
                    options.el = this.outputElement;
                    var device = this.getCurrentApplication().getDevice();
                    device.showElement(options);
                } else {
                    throw new Error('Widget::show called - the current widget has not yet been rendered.');
                }
            },
            /**
             * Hides a widget. If animation is enabled the widget will be faded out of view.
             * @param {Object}    options Details of the element to be shown, with optional parameters.
             * @param {Boolean} [options.skipAnim] By default the showing of the element will be animated (faded in). Pass <code>true</code> here to prevent animation.
             * @param {Function} [options.onComplete] Callback function to be called when the element has been shown.
             * @param {Number}    [options.fps=25] Frames per second for fade animation.
             * @param {Number}    [options.duration=840] Duration of fade animation, in milliseconds (ms).
             * @param {String}    [options.easing=linear] Easing style for fade animation.
             * @returns Boolean true if animation was called, otherwise false
             */
            hide : function(options) {
                if (this.outputElement) {
                    options.el = this.outputElement;
                    var device = this.getCurrentApplication().getDevice();
                    device.hideElement(options);
                } else {
                    throw new Error('Widget::hide called - the current widget has not yet been rendered.');
                }
            },
            /**
             * Moves a widget so that its top-left corner is at the given position.
             * @param {Object}    options Details of the element to be shown, with optional parameters.
             * @param {Boolean} [options.skipAnim] By default the showing of the element will be animated (faded in). Pass <code>true</code> here to prevent animation.
             * @param {Function} [options.onComplete] Callback function to be called when the element has been shown.
             * @param {Number}    [options.fps=25] Frames per second for fade animation.
             * @param {Number}    [options.duration=840] Duration of fade animation, in milliseconds (ms).
             * @param {String}    [options.easing=linear] Easing style for fade animation.
             * @returns Boolean true if animation was called, otherwise false
             */
            moveTo : function(options) {
                if (this.outputElement) {
                    options.el = this.outputElement;
                    var device = this.getCurrentApplication().getDevice();
                    device.moveElementTo(options);
                } else {
                    throw new Error('Widget::moveTo called - the current widget has not yet been rendered.');
                }
            }
        });
    }
);