1 /** 2 * @fileoverview activator.js 3 * Widget.Activator activates controls with highlighters, selection, and en/disabling functionality.<br /> 4 * Requires Prototype (http://www.prototypejs.org) 1.6 or later<br /><br /> 5 * 6 * Copyright (c) 2007 - 2008 Marc Heiligers (marc@eternal.co.za) http://www.eternal.co.za<br /><br /> 7 * 8 * Permission is hereby granted, free of charge, to any person obtaining a copy 9 * of this software and associated documentation files (the "Software"), to deal 10 * in the Software without restriction, including without limitation the rights 11 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 * copies of the Software, and to permit persons to whom the Software is 13 * furnished to do so, subject to the following conditions: 14 * 15 * The above copyright notice and this permission notice shall be included in 16 * all copies or substantial portions of the Software.<br /><br /> 17 * 18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 * THE SOFTWARE.<br /><br /> 25 * 26 * Change History:<br /> 27 * Version 0.2.00: 11 Oct 2008<br /> 28 * - Prepared for release, updated docs and demo pages<br /> 29 * - Added MIT license<br /> 30 * Version 0.1.15: 8 Oct 2008<br /> 31 * - BREAKING CHANGE: the event memo is no longer the element but now contains the following properties:<br /> 32 * element - the element<br /> 33 * activator - the activator<br /> 34 * event - the event that triggered this event<br /> 35 * Version 0.1.14: 5 Oct 2008<br /> 36 * - Fixed bug where elements were still firing mouseout when disabled<br /> 37 * Version 0.1.13: 8 Jun 2008<br /> 38 * - Updated setEnabled so that it removes the active and hover styles if you disable an element<br /> 39 * Version 0.1.12: 18 Apr 2008<br /> 40 * - Updated apply so that it ensures that selected and enabled elements remain so<br /> 41 * Version 0.1.11: 17 Mar 2008<br /> 42 * - Fixed a bug in setSelected and setEnabled where the default was not true<br /> 43 * Version 0.1.10: 12 Mar 2008<br /> 44 * - Added click event<br /> 45 * Version 0.1.9: 7 Mar 2008<br /> 46 * - Added this.selector to save the original selector<br /> 47 * - Added apply method which removes all and reapplies the original selector<br /> 48 * Version 0.1.8: 3 Mar 2008<br /> 49 * - Moved event name options into their own events options<br /> 50 * Version 0.1.7: 25 Feb 2008<br /> 51 * - Fixed a bug where the elements array was not correctly compacted after remove<br /> 52 * Version 0.1.6: 23 Feb 2008<br /> 53 * - getElements will return all elements if passed null or *<br /> 54 * - The above affects methods like remove: remove() or remove("*") will remove all elements<br /> 55 * - Selector in constructor is now optional<br /> 56 * - Minor improvement in constructor - just call add instead of calling add for each element returned from $$(selector) which add does anyway<br /> 57 * - Fixed a bug in add where it would incorrectly try to add a selector string passed in<br /> 58 * - Moved className options into a classNames object<br /> 59 * Version 0.1.5: 18 Feb 2008<br /> 60 * - Fixed a bugs in selectAll and enableAll where the default was not true<br /> 61 * Version 0.1.4: 19 Jan 2008<br /> 62 * - Fixed a minor bug in add<br /> 63 * - Added possibility for selector to be null - TODO: paramater mashing<br /> 64 * Version 0.1.3: 10 Dec 2007<br /> 65 * - Added mousedownEvent and mouseupEvent names to options<br /> 66 * Version 0.1.2: 19 Oct 2007<br /> 67 * - Added handlers array and updated destroy method to stopObserving all event handlers<br /> 68 * Version 0.1.1: 18 Oct 2007<br /> 69 * - Improved #isElement method to allow for sliding doors<br /> 70 * - Turned event names into properties of Widget.Activator (Widget.Activator.mousedownEvent and Widget.Activator.mouseupEvent)<br /> 71 * Version 0.1.0: 14 Oct 2007<br /> 72 * - Initial version 73 */ 74 if(typeof Widget == "undefined") Widget = {}; 75 /** 76 * @class Widget.Activator 77 * @version 0.1.13 78 * @author Marc Heiligers marc@eternal.co.za http://www.eternal.co.za 79 */ 80 Widget.Activator = Class.create(/** @scope Widget.Activator **/{ 81 /** 82 * The Activator class constructor. 83 * @constructor Activator 84 * @param {string} [selector] A CSS selector string which is defines one or more elements to be activated 85 * @param {object} [options] An object hash of options. 86 */ 87 initialize: function(selector, options) { 88 if(selector && !Object.isString(selector)) { 89 options = selector; 90 selector = null; 91 } 92 93 /** 94 * The default options.classNames.normals object. 95 * @class 96 * @param {string} [normal] The className to be applied to all activated elements. (default: null) 97 * @param {string} [hover] The className to be applied when the element is hovered. (default: "hover") 98 * @param {string} [active] The className to be applied when the element is active (mouse down). (default: "active") 99 * @param {string} [selected] The className to be applied when the element is selected. (default: "selected") 100 * @param {string} [disbaled] The className to be applied when the element is disabled. (default: "disabled") 101 */ 102 var classNames = Object.extend({ 103 normal: null, 104 hover: "hover", 105 active: "active", 106 selected: "selected", 107 disabled: "disabled" 108 }, options ? options.classNames || {} : {}); 109 110 /** 111 * The default options.classNames.normals object. 112 * @class 113 * @param {string} [mouseover] The name of the mouse over event. (default: Widget.Activator.mouseoverEvent = "activator:mouseover") 114 * @param {string} [mouseout] The name of the mouse out event. (default: Widget.Activator.mouseoutEvent = "activator:mouseout") 115 * @param {string} [mousedown] The name of the mouse down event. (default: Widget.Activator.mousedownEvent = "activator:mousedown") 116 * @param {string} [mouseup] The name of the mouse up event. (default: Widget.Activator.mouseupEvent = "activator:mouseup") 117 * @param {string} [click] The name of the click event. (default: Widget.Activator.clickEvent = "activator:click") 118 */ 119 var events = Object.extend({ 120 mouseover: Widget.Activator.mouseoverEvent, 121 mouseout: Widget.Activator.mouseoutEvent, 122 mousedown: Widget.Activator.mousedownEvent, 123 mouseup: Widget.Activator.mouseupEvent, 124 click: Widget.Activator.clickEvent 125 }, options ? options.events || {} : {}); 126 127 /** 128 * The default options object. 129 * @class 130 * @param {object} [classNames] The classNames object to be applied to all activated elements. (default: null) 131 * @param {object} [events] The event names to be used. (default: null) 132 * @param {string} [container] An element that contains all activated elements, and that is the source of all activator events. If null, document.body is used. (default: null) 133 * @param {bool} [singleSelect] If true, only one element can be selected at a time. (default: false) 134 * @param {bool} [extendElements] If true, all activated elements are extended with #getActivator(), #isSelected(), #setSelected(bool), #isEnabled(), and #setEnabled(bool) methods. (default: false) 135 */ 136 this.options = Object.extend({ 137 container: null, 138 singleSelect: false, 139 extendElements: false 140 }, options || {}); 141 this.options.classNames = classNames; 142 this.options.events = events; 143 144 this.mouseoverListener = this.mouseover.bindAsEventListener(this); 145 this.mouseoutListener = this.mouseout.bindAsEventListener(this); 146 this.mousedownListener = this.mousedown.bindAsEventListener(this); 147 this.mouseupListener = this.mouseup.bindAsEventListener(this); 148 this.clickListener = this.click.bindAsEventListener(this); 149 if(this.options.container) { 150 this.options.container = $(this.options.container); 151 this.options.container.observe("mouseover", this.mouseoverListener); 152 this.options.container.observe("mouseout", this.mouseoutListener); 153 this.options.container.observe("mousedown", this.mousedownListener); 154 this.options.container.observe("mouseup", this.mouseupListener); 155 this.options.container.observe("click", this.clickListener); 156 } 157 158 this.elements = []; 159 if(selector) { 160 this.add(selector); 161 this.selector = selector; 162 } 163 164 this.handlers = []; 165 }, 166 /** 167 * Removes all elements and then adds elements based on the selector, or the original selector if none is passed. 168 * @param {string|Array|element} elements A CSS selector string, array of elements, or single element to add. 169 */ 170 apply: function(elements) { 171 var s = this.getSelected(); 172 var d = this.getDisabled(); 173 this.remove("*"); 174 this.add(elements ? elements : this.selector); 175 this.setSelected(s); 176 this.setEnabled(d, false); 177 }, 178 /** 179 * Add elements to the activator. 180 * @param {string|Array|element} elements A CSS selector string, array of elements, or single element to add. 181 */ 182 add: function(elements) { 183 if(Object.isString(elements)) return this.add($$(elements)); 184 if(Object.isArray(elements)) { 185 elements.each(function(e) { 186 this.add(e); 187 }, this); 188 return; 189 } 190 var element = $(elements); 191 if(this.options.container) { 192 var t = element.ancestors().find(function(a) { 193 return a == this.options.container; 194 }.bind(this)); 195 if(!t) { 196 throw Widget.Activator.notContainerChild.interpolate({ element: element.identify(), container: this.options.container.identify() }); 197 } 198 } else { 199 element.observe("mouseover", this.mouseoverListener); 200 element.observe("mouseout", this.mouseoutListener); 201 element.observe("mousedown", this.mousedownListener); 202 element.observe("mouseup", this.mouseupListener); 203 element.observe("click", this.clickListener); 204 } 205 if(this.options.classNames.normal) element.addClassName(this.options.classNames.normal); 206 this.elements.push(element); 207 element[Widget.Activator.activatorAttribute] = this; 208 element[Widget.Activator.selectedAttribute] = false; 209 element[Widget.Activator.enabledAttribute] = true; 210 if(this.options.extendElements) { 211 element[Widget.Activator.getActivatorFunction] = function() { return this[Widget.Activator.activatorAttribute]; }; 212 element[Widget.Activator.setSelectedFunction] = function(selected) { this.getActivator().setSelected(this, selected); }; 213 element[Widget.Activator.isSelectedFunction] = function() { return this.getActivator().isSelected(this); }; 214 element[Widget.Activator.setEnabledFunction] = function(enabled) { this.getActivator().setEnabled(this, enabled); }; 215 element[Widget.Activator.isEnabledFunction] = function() { return this.getActivator().isEnabled(this); }; 216 } 217 }, 218 /** 219 * Remove elements from the activator. 220 * @param {string|Array|number|element} elements A CSS selector string, array of elements, index of an element, or single element to remove. 221 */ 222 remove: function(elements) { 223 this.getElements(elements).each(function(e) { 224 if(!this.options.container) { 225 e.stopObserving("mouseover", this.mouseoverListener); 226 e.stopObserving("mouseout", this.mouseoutListener); 227 e.stopObserving("mousedown", this.mousedownListener); 228 e.stopObserving("mouseup", this.mouseupListener); 229 e.stopObserving("click", this.clickListener); 230 } 231 $w("activatorAttribute selectedAttribute enabledAttribute getActivatorFunction setSelectedFunction isSelectedFunction setEnabledFunction isEnabledFunction").each(function(a) { 232 //Common.log(Widget.Activator[a] + " " + Object.isUndefined(e[Widget.Activator[a]])); 233 //if(!Object.isUndefined(e[Widget.Activator[a]])) delete e[Widget.Activator[a]]; 234 e[Widget.Activator[a]] = null; 235 }); 236 /*delete e[Widget.Activator.activatorAttribute]; 237 delete e[Widget.Activator.selectedAttribute]; 238 delete e[Widget.Activator.enabledAttribute]; 239 if(this.options.extendElements) { 240 delete e[Widget.Activator.getActivatorFunction]; 241 delete e[Widget.Activator.setSelectedFunction]; 242 delete e[Widget.Activator.isSelectedFunction]; 243 delete e[Widget.Activator.setEnabledFunction]; 244 delete e[Widget.Activator.isEnabledFunction]; 245 }*/ 246 e.removeClassName(this.options.classNames.hover); 247 e.removeClassName(this.options.classNames.active); 248 e.removeClassName(this.options.classNames.selected); 249 e.removeClassName(this.options.classNames.disabled); 250 if(this.options.classNames.normal) e.removeClassName(this.options.classNames.normal); 251 this.elements[this.elements.indexOf(e)] = null; 252 }.bind(this)); 253 this.elements = this.elements.compact(); 254 }, 255 /** 256 * The mouseover event handler 257 * @private 258 */ 259 mouseover: function(event) { 260 var e = this.isElement(event.element()); 261 if(e && !e[Widget.Activator.disabledAttribute]) { 262 e.addClassName(this.options.classNames.hover); 263 this.fire(this.options.events.mouseover, e, event); 264 } 265 }, 266 /** 267 * The mouseout event handler 268 * @private 269 */ 270 mouseout: function(event) { 271 var e = this.isElement(event.element()); 272 if(e && !e[Widget.Activator.disabledAttribute]) { 273 e.removeClassName(this.options.classNames.hover); 274 e.removeClassName(this.options.classNames.active); 275 this.fire(this.options.events.mouseout, e, event); 276 } 277 }, 278 /** 279 * The mousedown event handler 280 * @private 281 */ 282 mousedown: function(event) { 283 var e = this.isElement(event.element()); 284 if(e && !e[Widget.Activator.disabledAttribute]) { 285 e.addClassName(this.options.classNames.active); 286 this.fire(this.options.events.mousedown, e, event); 287 } 288 }, 289 /** 290 * The mouseup event handler 291 * @private 292 */ 293 mouseup: function(event) { 294 var e = this.isElement(event.element()); 295 if(e) e.removeClassName(this.options.classNames.active); 296 if(e && !e[Widget.Activator.disabledAttribute]) { 297 this.fire(this.options.events.mouseup, e, event); 298 } 299 }, 300 /** 301 * The click event handler 302 * @private 303 */ 304 click: function(event) { 305 var e = this.isElement(event.element()); 306 if(e && !e[Widget.Activator.disabledAttribute]) { 307 this.fire(this.options.events.click, e, event); 308 } 309 }, 310 /** 311 * Set an event handler for an activator event. The Activator class fires mousedown and mouseup events. 312 * Event handlers are attached to the options.container if there is one, and document.body if not. 313 * If there are multiple Activators without containers, all event handlers will receive messages for 314 * all the activators. You can test to see if the element is part of this activator using the isElement method. 315 * The element for which the event was fired is in event.memo. 316 * @param {string} eventName The name of the event. Use Widget.Activator.mousedownHandler and Widget.Activator.mouseupHandler constants. 317 * @param {function} elements Your event handler function. 318 */ 319 observe: function(eventName, handler) { 320 if(this.options.container) { 321 this.options.container.observe(eventName, handler); 322 } else { 323 $(document).observe(eventName, handler); 324 } 325 this.handlers.push({ eventName: eventName, handler: handler }); 326 return this; 327 }, 328 /** 329 * Remove an event handler for an activator event. 330 * @param {string} eventName The name of the event. Use Widget.Activator.mousedownHandler and Widget.Activator.mouseupHandler constants. 331 * @param {function} elements Your event handler function. 332 */ 333 stopObserving: function(eventName, handler) { 334 if(this.options.container) { 335 this.options.container.stopObserving(eventName, handler); 336 } else { 337 $(document).stopObserving(eventName, handler); 338 } 339 return this; 340 }, 341 /** 342 * Fires an activator event. 343 * @private 344 */ 345 fire: function(eventName, element, event) { 346 var memo = { 347 activator: this, 348 element: element, 349 event: event 350 } 351 if(this.options.container) { 352 this.options.container.fire(eventName, memo); 353 } else { 354 $(document).fire(eventName, memo); 355 } 356 return this; 357 }, 358 /** 359 * Gets an element from the activator by index or id. 360 * @param {number|string} element The index or id of the element to get. 361 * @returns {element} The element if found, null otherwise. 362 */ 363 getElement: function(element) { 364 if(Object.isNumber(element)) { 365 return this.elements[element]; 366 } 367 return this.isElement(element); 368 }, 369 /** 370 * Gets activator elements by index or CSS selector. 371 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to get. 372 * @returns {Array} The array elements found. 373 */ 374 getElements: function(elements) { 375 if(elements === null || elements === "*") return this.getElements(this.elements); 376 if(Object.isNumber(elements)) return [ this.elements[elements] ]; 377 if(Object.isString(elements)) return this.getElements($$(elements)); 378 if(Object.isArray(elements)) { 379 var es = []; 380 elements.each(function(a) { 381 var e = this.getElement(a); 382 if(e) es.push(e); 383 }.bind(this)); 384 return es; 385 } 386 if(this.isElement(elements)) return [ $(elements) ]; 387 return []; 388 }, 389 /** 390 * Determines if an element or one of it's parent elements is member of the activator's list of elements. 391 * @param {element} element The element to test. 392 * @returns {element} The element or parent element that is a member of the activator's list of elements if found, null otherwise. 393 */ 394 isElement: function(element) { 395 if(!element || element == this.options.container || element == document.body) { 396 return null; 397 } 398 element = $(element); 399 var e = this.elements.find(function(e) { 400 return e == element; 401 }); 402 if(!e) { 403 if(!element.up) return null; 404 return this.isElement(element.up()); 405 } 406 return e; 407 }, 408 /** 409 * (De)Selects one or more elements (and deselects all other elements if the singleSelect option is true). 410 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to select. 411 * @param {bool} [selected] If true, the elements are selected; they are deselected otherwise. (default: true) 412 */ 413 setSelected: function(elements, selected) { 414 var se = (Object.isUndefined(selected) || selected); 415 var fn = se ? Element.addClassName : Element.removeClassName; 416 if(se && this.options.singleSelect) { 417 this.selectAll(false); 418 } 419 this.getElements(elements).each(function(e) { 420 fn(e, this.options.classNames.selected); 421 e[Widget.Activator.selectedAttribute] = se; 422 }, this); 423 }, 424 /** 425 * Determines if an element is selected. 426 * @param {number|string} element The index or id of the element to get. 427 * @returns {bool} True if the element is selected, false otherwise. 428 */ 429 isSelected: function(element) { 430 return this.getElement(element)[Widget.Activator.selectedAttribute]; 431 }, 432 /** 433 * Gets all selected elements. 434 * @returns {Array|element} An array of selected elements if the singleSelect option is false. If the singleSelect option is true, returns the selected element if any, null otherwise. 435 */ 436 getSelected: function() { 437 var es = []; 438 this.elements.each(function(e) { 439 if(e[Widget.Activator.selectedAttribute]) es.push(e); 440 }); 441 if(this.options.singleSelect) { 442 return es.length > 0 ? es[0] : null; 443 } 444 return es; 445 }, 446 /** 447 * (Dis)Enables one or more elements. 448 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to enable. 449 * @param {bool} [enabled] If true, the elements are enabled; they are disabled otherwise. (default: true) 450 */ 451 setEnabled: function(elements, enabled) { 452 var en = (Object.isUndefined(enabled) || enabled); 453 var fn = en ? Element.removeClassName : Element.addClassName; 454 this.getElements(elements).each(function(e) { 455 fn(e, this.options.classNames.disabled); 456 e[Widget.Activator.disabledAttribute] = !en; 457 if(!en) { 458 e.removeClassName(this.options.classNames.hover); 459 e.removeClassName(this.options.classNames.active); 460 } 461 }.bind(this)); 462 }, 463 /** 464 * Determines if an element is enabled. 465 * @param {number|string} element The index or id of the element to get. 466 * @returns {bool} True if the element is enabled, false otherwise. 467 */ 468 isEnabled: function(element) { 469 return !this.getElement(element)[Widget.Activator.disabledAttribute]; 470 }, 471 /** 472 * Gets all disabled elements. 473 * @returns {Array} An array of disabled elements. 474 */ 475 getDisabled: function() { 476 var es = []; 477 this.elements.each(function(e) { 478 if(e[Widget.Activator.disabledAttribute]) es.push(e); 479 }); 480 return es; 481 }, 482 /** 483 * (De)Selects all elements. 484 * @param {bool} [selected] If true, the elements are selected; they are deselected otherwise. (default: true) 485 */ 486 selectAll: function(selected) { 487 var se = (Object.isUndefined(selected) || selected); 488 this.elements.each(function(e) { 489 this.setSelected(e, se); 490 }.bind(this)); 491 }, 492 /** 493 * (Dis)Enables all elements. 494 * @param {bool} [enabled] If true, the elements are enabled; they are disabled otherwise. (default: true) 495 */ 496 enableAll: function(enabled) { 497 var en = (Object.isUndefined(enabled) || enabled); 498 this.elements.each(function(e) { 499 this.setEnabled(e, en); 500 }.bind(this)); 501 }, 502 /** 503 * Destroys the activator. Removes all event handlers and extended methods. 504 */ 505 destroy: function() { 506 this.remove(this.elements); 507 this.handlers.each(function(h) { 508 this.stopObserving(h.eventName, h.handler); 509 }.bind(this)); 510 if(this.options.container) { 511 this.options.container.stopObserving("mouseover", this.mouseoverListener); 512 this.options.container.stopObserving("mouseout", this.mouseoutListener); 513 this.options.container.stopObserving("mousedown", this.mousedownListener); 514 this.options.container.stopObserving("mouseup", this.mouseupListener); 515 this.options.container.stopObserving("click", this.clickListener); 516 } 517 this.handlers.length = 0; 518 this.elements.length = 0; 519 } 520 }); 521 /** 522 * Constants 523 */ 524 Object.extend(Widget.Activator, { 525 /** 526 * Template for error message displayed when an element is not a child of the container. 527 */ 528 notContainerChild: "Element #{element} is not a child of the container #{container}.", 529 /** 530 * Name of element attribute for the activator. 531 */ 532 activatorAttribute: "activator", 533 /** 534 * Name of element attribute for selected. 535 */ 536 selectedAttribute: "activatorSelected", 537 /** 538 * Name of element attribute for disabled. 539 */ 540 disabledAttribute: "activatorDisabled", 541 /** 542 * Name of element attribute for getActivator function if elements are extended. 543 */ 544 getActivatorFunction: "getActivator", 545 /** 546 * Name of element attribute for setSelected function if elements are extended. 547 */ 548 setSelectedFunction: "setSelected", 549 /** 550 * Name of element attribute for isSelected function if elements are extended. 551 */ 552 isSelectedFunction: "isSelected", 553 /** 554 * Name of element attribute for setEnabled function if elements are extended. 555 */ 556 setEnabledFunction: "setEnabled", 557 /** 558 * Name of element attribute for isEnabled function if elements are extended. 559 */ 560 isEnabledFunction: "isEnabled", 561 /** 562 * Name of mouseover event fired by the activator. 563 */ 564 mouseoverEvent: "activator:mouseover", 565 /** 566 * Name of mouseout event fired by the activator. 567 */ 568 mouseoutEvent: "activator:mouseout", 569 /** 570 * Name of mousedown event fired by the activator. 571 */ 572 mousedownEvent: "activator:mousedown", 573 /** 574 * Name of mouseup event fired by the activator. 575 */ 576 mouseupEvent: "activator:mouseup", 577 /** 578 * Name of click event fired by the activator. 579 */ 580 clickEvent: "activator:click" 581 });