/**
* @copyright 2017 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/* eslint-env browser */
/**
* @module bajaux/mixin/responsiveMixIn
*/
define([ 'bajaux/Widget',
'jquery',
'Promise',
'underscore' ], function (
Widget,
$,
Promise,
_) {
'use strict';
var MIXIN_NAME = 'responsive';
/**
* @typedef {Object} module:bajaux/mixin/responsiveMixIn~ResponsiveMediaInfo
*
* @property {number} width The width in pixels to use in responsive layout.
* @property {number} height The height in pixels to use in responsive layout.
*/
/**
* An object that defines some conditions for the associated class to be added
* to the widget's DOM element.
*
* @typedef {Object} module:bajaux/mixin/responsiveMixIn~ResponsiveCondition
*
* @property {Number} [maxWidth] The widget's width in pixels must be less
* than or equal to this value.
* @property {Number} [maxHeight] The widget's height in pixels must be less
* than or equal to this value.
* @property {Number} [minWidth] The widget's width in pixels must be greater
* than or equal to this value.
* @property {Number} [minHeight] The widget's height in pixels must be
* greater than or equal to this value.
*/
/**
* A callback that returns true if the condition is met.
*
* @callback {Function} module:bajaux/mixin/responsiveMixIn~ResponsiveCallback
*
* @param {module:bajaux/mixin/responsiveMixIn~ResponsiveMediaInfo} info The
* widget's current width, height, and any other associated media information.
* @returns {boolean} true if the current media info satisfies the condition.
*/
/**
* Applies the `responsive` mixin to the target Widget.
*
* This mixin provides responsive layout for a widget based upon its dimensions.
* It does this by adding or removing CSS classes to/from the widget's DOM element.
*
* The mixin injects itself by cross cutting the widget's layout behavior.
*
* As of Niagara 4.8, omit the `conditions` argument to use a set of default
* classes:
*
* - `phone-only`: less than 600px wide
* - `tablet-portrait-up`: 600px wide or greater
* - `tablet-landscape-up`: 900px wide or greater
* - `desktop-up`: 1200px wide or greater
*
* @example
* <caption>Apply responsive layout to a widget</caption>
* var ResponsiveWidget = function () {
* Widget.apply(this, arguments);
* responsiveMixIn(this, {
* 'my-css-class-to-use-when-small': { maxHeight: 768, maxWidth: 1024 },
* 'my-css-class-to-use-when-square': function (info) { return info.width === info.height; }
* });
* };
* ResponsiveWidget.prototype = Object.create(Widget.prototype);
* ResponsiveWidget.prototype.doInitialize = function (dom) {
* dom.addClass('ResponsiveWidget');
* dom.html(myHtmlTemplate());
* };
*
* // in css:
* // .ResponsiveWidget.my-css-class-to-use-when-small {
* // background-color: white;
* // }
*
* @example
* <caption>Use a set of default classes</caption>
* // ...
* responsiveMixIn(this);
*
* // in css:
* // .ResponsiveWidget.tablet-portrait-up {
* // display: flex;
* // flex-flow: row wrap;
* // }
* // .ResponsiveWidget.tablet-landscape-up {
* // flex-flow: row nowrap;
* // }
* @class
* @alias module:bajaux/mixin/responsiveMixIn
* @extends module:bajaux/Widget
* @param {module:bajaux/Widget} target The widget to apply the mixin to.
* @param {Object.<String, module:bajaux/mixin/responsiveMixIn~ResponsiveCondition|module:bajaux/mixin/responsiveMixIn~ResponsiveCallback>} conditions An
* object that maps class names to set of conditions. If all conditions are
* met, the class name will be added to the widget's DOM element. If the
* conditions aren't met, the class name will be removed.
*/
var responsiveMixIn = function (target, conditions) {
if (!(target instanceof Widget)) {
throw new Error("Responsive mixin only applies to instances or sub-classes of Widget");
}
conditions = conditions || responsiveMixIn.DEFAULT_CONDITIONS;
var mixins = target.$mixins,
layout = target.layout;
if (!_.contains(mixins, MIXIN_NAME)) {
mixins.push(MIXIN_NAME);
}
target.$responsiveConditions = conditions;
/**
* Returns true if the class can be added/removed to/from a widget's
* DOM element.
*
* @function module:bajaux/mixin/responsiveMixIn#canApplyResponsiveClass
* @param {String} className The name of the class to test for.
* @param {module:bajaux/mixin/responsiveMixIn~ResponsiveMediaInfo} [mediaInfo=this.getResponsiveMediaInfo()]
* The widget media info.
* @returns {Boolean} Returns true if all the conditions for applying
* the class are met.
*/
target.canApplyResponsiveClass = target.canApplyResponsiveClass || function (className, info) {
info = info || this.getResponsiveMediaInfo();
var that = this,
condition = that.$responsiveConditions[className],
width = info.width,
height = info.height;
if (!condition) {
return false;
}
if (typeof condition === 'function') {
return condition.call(that, info);
}
if (isNumber(condition.maxWidth) && width > condition.maxWidth) {
return false;
}
if (isNumber(condition.minWidth) && width < condition.minWidth) {
return false;
}
if (isNumber(condition.maxHeight) && height > condition.maxHeight) {
return false;
}
if (isNumber(condition.minHeight) && height < condition.minHeight) {
return false;
}
return true;
};
/**
* Return media information for the widget. This is used in deciding
* whether to add/remove a CSS class to/from a widget when it's laid out.
* The information will come from the widget's container, not the widget
* itself. By default, it will look up the DOM hierarchy to find an element
* with the class `bajaux-widget-container`, which will be provided to you
* in most profiles (the HTML5 Hx Profile's content window, the element
* containing a widget in a Px view, etc.). If none is found, will use the
* dimensions of the browser window itself.
*
* This method returns width and height but could contain more properties
* in future. Please note, any calculated pixel values should be
* floored to the nearest integer.
*
* A developer can override this method to calculate the media information
* differently.
*
* @function module:bajaux/mixin/responsiveMixIn#getResponsiveMediaInfo
* @returns {module:bajaux/mixin/responsiveMixIn~ResponsiveMediaInfo} The media information for the widget.
*/
target.getResponsiveMediaInfo = target.getResponsiveMediaInfo || function () {
var container = this.jq().closest('.bajaux-widget-container');
if (container.length) {
return {
width: Math.floor(container.innerWidth() || 0),
height: Math.floor(container.innerHeight() || 0)
};
} else {
var w = $(window);
return {
width: w.width(),
height: w.height()
};
}
};
/**
* Overrides the bajaux Widget's `layout` method. This will add/remove
* class names to/from a widget's DOM element depending on whether
* its responsive conditions are met.
*
* @see module:bajaux/Widget#layout
* @see module:bajaux/Widget#doLayout
*
* @override
* @function module:bajaux/mixin/responsiveMixIn#layout
* @returns {Promise}
*/
target.layout = function () {
// Short circuit if the widget isn't initialized yet.
if (!this.isInitialized()) { return Promise.resolve(); }
var jq = this.jq(),
info = this.getResponsiveMediaInfo(),
conditions = this.$responsiveConditions;
for (var className in conditions) {
if (conditions.hasOwnProperty(className)) {
jq.toggleClass(className, this.canApplyResponsiveClass(className, info));
}
}
return layout.apply(this, arguments);
};
}; // responsiveMixIn
/**
* Default conditions to use when conditions are not specified. Changing these
* can potentially change the behavior of all Widgets that use them.
* @private
* @type {object}
*/
responsiveMixIn.DEFAULT_CONDITIONS = {
'phone-only': { maxWidth: 599 },
'tablet-portrait-up': { minWidth: 600 },
'tablet-landscape-up': { minWidth: 900 },
'desktop-up': { minWidth: 1200 }
};
function isNumber(o) { return typeof o === 'number'; }
return responsiveMixIn;
});