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 };