Source: deliteful/ResponsiveColumns.js

/** @module deliteful/ResponsiveColumns */
define([
	"requirejs-dplugins/jquery!attributes/classes",
	"delite/register",
	"delite/DisplayContainer",
	"./channelBreakpoints",
	"delite/theme!./ResponsiveColumns/themes/{{theme}}/ResponsiveColumns.css"
], function ($, register, DisplayContainer, channelBreakpoints) {
	/**
	 * A container that lays out its children according to the screen width. This widget relies on CSS media queries
	 * (http://www.w3.org/TR/css3-mediaqueries). You can define any number of screen classes by setting the breakpoints
	 * attribute. Then you must then set the layout attribute on each child to configure a width for each screen class.
	 * The following example defines two screen classes: "phone" and "other" with a breakpoint at 500px. If the "phone"
	 * class is active, the first child width is 100% and the second child is hidden. If the screen is larger than 500px
	 * then the first child width is 20% and the second one fill the remaining space.
	 * @example
	 * <d-responsive-columns breakpoints="{phone: '500px', other: ''}">
	 *     <div layout="{phone: '100%', other: '20%'}">...</div>
	 *     <div layout="{phone: 'hidden', other: 'fill'}">...</div>
	 * </d-responsive-columns>
	 *
	 * When the screen width changes and a new screen class is applied by the container, a "change" event is emitted
	 * with two specific properties: `screenClass` (the new screen class) and `mediaQueryList`
	 * (the MediaQueryList instance at the origin of the change).
	 *
	 * @class module:deliteful/ResponsiveColumns
	 * @augments module:delite/DisplayContainer
	 */
	return register("d-responsive-columns", [HTMLElement, DisplayContainer],
		{
			baseClass: "d-responsive-columns",

			/**
			 * A string containing underlying media queries breakpoints definition. Each breakpoint is made of a
			 * name (which defines the screen size class) and an upper bound in pixels.
			 * The upper bound can be a string like "200px", an empty string or a number. The last breakpoint
			 * which defines the largest screen class is usually set to an empty string to match any size larger
			 * than the penultimate breakpoint bound. This property is parsed internally by `JSON.parse()`.
			 * To facilitate writing markup you can use single quotes when defining this property, single quotes
			 * will be replaced by double quotes before interpreted by `JSON.parse`.
			 *
			 * The default value of `breakpoints` uses three breakpoints: `small`, `medium`, and `large`.
			 * The value of `large` is an empty string. The default values of the breakpoints `small`
			 * and `medium` are determined by the values of properties `smallScreen"` and `"mediumScreen"`
			 * of `deliteful/channelBreakpoints`, and can be configured statically using `require.config()`.
			 * For details, see the documentation of `deliteful/channelBreakpoints`.
			 * @member {string}
			 * @default "{'small': '480px', 'medium': '1024px', 'large': ''}"
			 */
			breakpoints: "{'small': '" + channelBreakpoints.smallScreen +
				"', 'medium': '" + channelBreakpoints.mediumScreen +
				"', 'large': ''}",

			/**
			 * The current screen class currently applied by the container.
			 * It depends on the value of breakpoints attribute and current screen width. This attribute is read-only.
			 * When this attribute is modified, a "change" event is emitted which contains the new `screenClass` value.
			 * @readonly
			 * @member {string}
			 */
			screenClass: "",

			preRender: function () {
				this._breakpoints = {};
				this._layouts = [];
				// A set of MediaQueryList
				this._mqls = [];
				// Unique class for this widget
				$(this).addClass("-d-responsive-columns-" + this.widgetId);
			},

			_removeListeners: function () {
				for (var i = 0; i < this._mqls.length; i++) {
					this._mqls[i].mql.removeListener(this._mqls[i].listener);
				}
				this._mqls = [];
			},

			_checkConfiguration: function () {
				var result = true;
				for (var sc in this._breakpoints) {
					for (var i = 0; i < this._layouts.length; i++) {
						if (!this._layouts[i][sc]) {
							result = false;
							console.error("SyntaxError - Warning: ResponsiveColumns, the 'layout' attribute " +
								"of the child at index " + i + " must define a value for the key '" + sc + "'.");
						}
					}
				}
				return result;
			},

			_parseJSONAttrs: function () {
				var result = true;
				this._breakpoints = {};
				this._layouts = [];
				this._breakpoints = JSON.parse(this.breakpoints.replace(/\'/g, "\""));

				var children = this.getChildren();
				var layout;
				for (var i = 0; i < children.length; i++) {
					layout = children[i].getAttribute("layout");
					if (!layout) {
						result = false;
						console.error("SyntaxError - Warning: ResponsiveColumns child at index " + i +
							", 'layout' is not defined.");
					} else {
						layout = JSON.parse(layout.replace(/\'/g, "\""));
					}
					if (!layout) {
						result = false;
						console.error("SyntaxError - Warning: ResponsiveColumns child at index " + i +
							", 'layout' parsing failed.");
					} else {
						this._layouts.push(layout);
					}
				}
				return result;
			},

			_genCSS: function () {
				this._removeListeners();
				var thresholds = [0];
				var sizeClasses = [];
				var thr, i;
				for (var t in this._breakpoints) {
					sizeClasses.push(t);
					thr = parseInt(this._breakpoints[t].replace(/px/g, ""), 10);
					if (thr) {
						thresholds.push(thr);
					}
				}

				var children = this.getChildren();
				var content = "";
				var mqHeader = "";
				var val;
				var mediaPart = "@media screen and ";
				var minPart = "(min-width: A)";
				var maxPart = " and (max-width: B)";
				var mql;
				var listener;
				// Generates media queries blocks with columns layout definition inside
				for (i = 0; i < thresholds.length; i++) {
					mqHeader = minPart.replace("A", (thresholds[i] + 1) + "px");
					if (thresholds[i + 1]) {
						mqHeader += maxPart.replace("B", (thresholds[i + 1]) + "px");
					} else {
						// No upper bounds
						mqHeader = mqHeader.replace(" and (max-width: B)", "");
					}
					content += mediaPart + mqHeader + "{";
					for (var j = 0; j < children.length; j++) {
						content += ".-d-responsive-columns-" + this.widgetId + " > *:nth-child(" + (j + 1) + "){";
						val = this._layouts[j][sizeClasses[i]];
						if (val === "hidden") {
							content += "display: none;";
						} else if (val === "fill") {
							content += "-webkit-box-flex: 1;";
							content += "-moz-box-flex: 1;";
							content += "-webkit-flex: 1;";
							content += "-ms-flex: 1;";
							content += "flex: 1;";
						} else {
							content += "width: " + val + ";";
						}
						content += "}";
					}
					content += "}";

					// Listen for breakpoint triggers
					mql = window.matchMedia(mqHeader);
					listener = function (a) {
						if (a.matches) {
							this.target.screenClass = this.class;
							this.target.emit("change", {screenClass: this.class, mediaQueryList: mql});
						}
					}.bind({class: sizeClasses[i], mql: mql, target: this});
					mql.addListener(listener);
					this._mqls.push({mql: mql, listener: listener});
					// Initialization of screenClass
					if (mql.matches) {
						this.screenClass = sizeClasses[i];
					}
				}
				var styleBlockId = "d-responsive-columns-generated-style-" + this.widgetId;
				var styleBlock = this.ownerDocument.getElementById(styleBlockId);
				if (! styleBlock) {
					styleBlock = this.ownerDocument.createElement("style");
					styleBlock.id = styleBlockId;
					this.ownerDocument.head.appendChild(styleBlock);
				}
				styleBlock.innerHTML = content;

			},

			onAddChild: function (/*jshint unused: vars */node) {
				this.notifyCurrentValue("breakpoints");
			},

			refreshRendering: function (oldValues) {
				if ("breakpoints" in oldValues) {
					if (this._parseJSONAttrs() && this._checkConfiguration()) {
						this._genCSS();
					}
				}
			}
		});
});