1 /**
  2  * Olives http://flams.github.com/olives
  3  * The MIT License (MIT)
  4  * Copyright (c) 2012-2014 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
  5  */
  6 "use strict";
  7 
  8 var Tools = require("emily").Tools;
  9 /**
 10  * @class
 11  * A Stack is a tool for managing DOM elements as groups. Within a group, dom elements
 12  * can be added, removed, moved around. The group can be moved to another parent node
 13  * while keeping the DOM elements in the same order, excluding the parent dom elements's
 14  * children that are not in the Stack.
 15  */
 16 module.exports = function StackConstructor($parent) {
 17 
 18 	/**
 19 	 * The parent DOM element is a documentFragment by default
 20 	 * @private
 21 	 */
 22 	var _parent = document.createDocumentFragment(),
 23 
 24 	/**
 25 	 * The place where the dom elements hide
 26 	 * @private
 27 	 */
 28 	_hidePlace = document.createElement("div"),
 29 
 30 	/**
 31 	 * The list of dom elements that are part of the stack
 32 	 * Helps for excluding elements that are not part of it
 33 	 * @private
 34 	 */
 35 	_childNodes = [],
 36 
 37 	_lastTransit = null;
 38 
 39 	/**
 40 	 * Add a DOM element to the stack. It will be appended.
 41 	 * @param {HTMLElement} dom the DOM element to add
 42 	 * @returns {HTMLElement} dom
 43 	 */
 44 	this.add = function add(dom) {
 45 		if (!this.has(dom) && dom instanceof HTMLElement) {
 46 			_parent.appendChild(dom);
 47 			_childNodes.push(dom);
 48 			return dom;
 49 		} else {
 50 			return false;
 51 		}
 52 	};
 53 
 54 	/**
 55 	 * Remove a DOM element from the stack.
 56 	 * @param {HTMLElement} dom the DOM element to remove
 57 	 * @returns {HTMLElement} dom
 58 	 */
 59 	this.remove = function remove(dom) {
 60 		var index;
 61 		if (this.has(dom)) {
 62 			index = _childNodes.indexOf(dom);
 63 			_parent.removeChild(dom);
 64 			_childNodes.splice(index, 1);
 65 			return dom;
 66 		} else {
 67 			return false;
 68 		}
 69 	};
 70 
 71 	/**
 72 	 * Place a stack by appending its DOM elements to a new parent
 73 	 * @param {HTMLElement} newParentDom the new DOM element to append the stack to
 74 	 * @returns {HTMLElement} newParentDom
 75 	 */
 76 	this.place = function place(newParentDom) {
 77 		if (newParentDom instanceof HTMLElement) {
 78 			[].slice.call(_parent.childNodes).forEach(function (childDom) {
 79 				if (this.has(childDom)) {
 80 					newParentDom.appendChild(childDom);
 81 				}
 82 			}, this);
 83 			return this._setParent(newParentDom);
 84 		} else {
 85 			return false;
 86 		}
 87 	};
 88 
 89 	/**
 90 	 * Move an element up in the stack
 91 	 * @param {HTMLElement} dom the dom element to move up
 92 	 * @returns {HTMLElement} dom
 93 	 */
 94 	this.up = function up(dom) {
 95 		if (this.has(dom)) {
 96 			var domPosition = this.getPosition(dom);
 97 			this.move(dom, domPosition + 1);
 98 			return dom;
 99 		} else {
100 			return false;
101 		}
102 	};
103 
104 	/**
105 	 * Move an element down in the stack
106 	 * @param {HTMLElement} dom the dom element to move down
107 	 * @returns {HTMLElement} dom
108 	 */
109 	this.down = function down(dom) {
110 		if (this.has(dom)) {
111 			var domPosition = this.getPosition(dom);
112 			this.move(dom, domPosition - 1);
113 			return dom;
114 		} else {
115 			return false;
116 		}
117 	};
118 
119 	/**
120 	 * Move an element that is already in the stack to a new position
121 	 * @param {HTMLElement} dom the dom element to move
122 	 * @param {Number} position the position to which to move the DOM element
123 	 * @returns {HTMLElement} dom
124 	 */
125 	this.move = function move(dom, position) {
126 		if (this.has(dom)) {
127 			var domIndex = _childNodes.indexOf(dom);
128 			_childNodes.splice(domIndex, 1);
129 			// Preventing a bug in IE when insertBefore is not given a valid
130 			// second argument
131 			var nextElement = getNextElementInDom(position);
132 			if (nextElement) {
133 				_parent.insertBefore(dom, nextElement);
134 			} else {
135 				_parent.appendChild(dom);
136 			}
137 			_childNodes.splice(position, 0, dom);
138 			return dom;
139 		} else {
140 			return false;
141 		}
142 	};
143 
144 	function getNextElementInDom(position) {
145 		if (position >= _childNodes.length) {
146 			return;
147 		}
148 		var nextElement = _childNodes[position];
149 		if (Tools.toArray(_parent.childNodes).indexOf(nextElement) == -1) {
150 			return getNextElementInDom(position +1);
151 		} else {
152 			return nextElement;
153 		}
154 	}
155 
156 	/**
157 	 * Insert a new element at a specific position in the stack
158 	 * @param {HTMLElement} dom the dom element to insert
159 	 * @param {Number} position the position to which to insert the DOM element
160 	 * @returns {HTMLElement} dom
161 	 */
162 	this.insert = function insert(dom, position) {
163 		if (!this.has(dom) && dom instanceof HTMLElement) {
164 			_childNodes.splice(position, 0, dom);
165 			_parent.insertBefore(dom, _parent.childNodes[position]);
166 			return dom;
167 		} else {
168 			return false;
169 		}
170 	};
171 
172 	/**
173 	 * Get the position of an element in the stack
174 	 * @param {HTMLElement} dom the dom to get the position from
175 	 * @returns {HTMLElement} dom
176 	 */
177 	this.getPosition = function getPosition(dom) {
178 		return _childNodes.indexOf(dom);
179 	};
180 
181 	/**
182 	 * Count the number of elements in a stack
183 	 * @returns {Number} the number of items
184 	 */
185 	this.count = function count() {
186 		return _parent.childNodes.length;
187 	};
188 
189 	/**
190 	 * Tells if a DOM element is in the stack
191 	 * @param {HTMLElement} dom the dom to tell if its in the stack
192 	 * @returns {HTMLElement} dom
193 	 */
194 	this.has = function has(childDom) {
195 		return this.getPosition(childDom) >= 0;
196 	};
197 
198 	/**
199 	 * Hide a dom element that was previously added to the stack
200 	 * It will be taken out of the dom until displayed again
201 	 * @param {HTMLElement} dom the dom to hide
202 	 * @return {boolean} if dom element is in the stack
203 	 */
204 	this.hide = function hide(dom) {
205 		if (this.has(dom)) {
206 			_hidePlace.appendChild(dom);
207 			return true;
208 		} else {
209 			return false;
210 		}
211 	};
212 
213 	/**
214 	 * Show a dom element that was previously hidden
215 	 * It will be added back to the dom
216 	 * @param {HTMLElement} dom the dom to show
217 	 * @return {boolean} if dom element is current hidden
218 	 */
219 	this.show = function show(dom) {
220 		if (this.has(dom) && dom.parentNode === _hidePlace) {
221 			this.move(dom, _childNodes.indexOf(dom));
222 			return true;
223 		} else {
224 			return false;
225 		}
226 	};
227 
228 	/**
229 	 * Helper function for hiding all the dom elements
230 	 */
231 	this.hideAll = function hideAll() {
232 		_childNodes.forEach(this.hide, this);
233 	};
234 
235 	/**
236 	 * Helper function for showing all the dom elements
237 	 */
238 	this.showAll = function showAll() {
239 		_childNodes.forEach(this.show, this);
240 	};
241 
242 	/**
243 	 * Get the parent node that a stack is currently attached to
244 	 * @returns {HTMLElement} parent node
245 	 */
246 	this.getParent = function _getParent() {
247 			return _parent;
248 	};
249 
250 	/**
251 	 * Set the parent element (without appending the stacks dom elements to)
252 	 * @private
253 	 */
254 	this._setParent = function _setParent(parent) {
255 		if (parent instanceof HTMLElement) {
256 			_parent = parent;
257 			return _parent;
258 		} else {
259 			return false;
260 		}
261 	};
262 
263 	/**
264 	 * Get the place where the DOM elements are hidden
265 	 * @private
266 	 */
267 	this.getHidePlace = function getHidePlace() {
268 		return _hidePlace;
269 	};
270 
271 	/**
272 	 * Set the place where the DOM elements are hidden
273 	 * @private
274 	 */
275 	this.setHidePlace = function setHidePlace(hidePlace) {
276 		if (hidePlace instanceof HTMLElement) {
277 			_hidePlace = hidePlace;
278 			return true;
279 		} else {
280 			return false;
281 		}
282 	};
283 
284 	/**
285 	 * Get the last dom element that the stack transitted to
286 	 * @returns {HTMLElement} the last dom element
287 	 */
288 	this.getLastTransit = function getLastTransit() {
289 		return _lastTransit;
290 	};
291 
292 	/**
293 	 * Transit between views, will show the new one and hide the previous
294 	 * element that the stack transitted to, if any.
295 	 * @param {HTMLElement} dom the element to transit to
296 	 * @returns {Boolean} false if the element can't be shown
297 	 */
298 	this.transit = function transit(dom) {
299 		if (_lastTransit) {
300 			this.hide(_lastTransit);
301 		}
302 		if (this.show(dom)) {
303 			_lastTransit = dom;
304 			return true;
305 		} else {
306 			return false;
307 		}
308 	};
309 
310 	this._setParent($parent);
311 
312 };