1/** 2 * @file js/classes/Handler.js 3 * 4 * Copyright (c) 2013-2015 Simon Fraser University Library 5 * Copyright (c) 2000-2015 John Willinsky 6 * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING. 7 * 8 * @class Handler 9 * @ingroup js_classes 10 * 11 * @brief Base class for handlers bound to a DOM HTML element. 12 */ 13(function($) { 14 15 16 /** 17 * @constructor 18 * 19 * @extends $.pkp.classes.ObjectProxy 20 * 21 * @param {jQueryObject} $element A DOM element to which 22 * this handler is bound. 23 * @param {Object} options Handler options. 24 */ 25 $.pkp.classes.Handler = function($element, options) { 26 27 // Check whether a single element was passed in. 28 if ($element.length > 1) { 29 throw new Error('jQuery selector contained more than one handler!'); 30 } 31 32 // Save a pointer to the bound element in the handler. 33 this.$htmlElement_ = $element; 34 35 // Check whether a handler has already been bound 36 // to the element. 37 if (this.data('handler') !== undefined) { 38 throw new Error(['The handler "', this.getObjectName(), 39 '" has already been bound to the selected element!'].join('')); 40 } 41 42 // Initialize object properties. 43 this.eventBindings_ = { }; 44 this.dataItems_ = { }; 45 this.publishedEvents_ = { }; 46 47 if (options.$eventBridge) { 48 // Configure the event bridge. 49 this.$eventBridge_ = options.$eventBridge; 50 } 51 52 // The "publishChangeEvents" option can be used to specify 53 // a list of event names that will also be published upon 54 // content change. 55 if (options.publishChangeEvents) { 56 this.publishChangeEvents_ = options.publishChangeEvents; 57 var i; 58 for (i = 0; i < this.publishChangeEvents_.length; i++) { 59 this.publishEvent(this.publishChangeEvents_[i]); 60 } 61 } else { 62 this.publishChangeEvents_ = []; 63 } 64 65 // Bind the handler to the DOM element. 66 this.data('handler', this); 67 }; 68 69 70 // 71 // Private properties 72 // 73 /** 74 * Optional list of publication events. 75 * @private 76 * @type {Array} 77 */ 78 $.pkp.classes.Handler.prototype.publishChangeEvents_ = null; 79 80 81 /** 82 * The HTML element this handler is bound to. 83 * @private 84 * @type {jQueryObject} 85 */ 86 $.pkp.classes.Handler.prototype.$htmlElement_ = null; 87 88 89 /** 90 * A list of event bindings for this handler. 91 * @private 92 * @type {Object.<string, Array>} 93 */ 94 $.pkp.classes.Handler.prototype.eventBindings_ = null; 95 96 97 /** 98 * A list of data items bound to the DOM element 99 * managed by this handler. 100 * @private 101 * @type {Object.<string, boolean>} 102 */ 103 $.pkp.classes.Handler.prototype.dataItems_ = null; 104 105 106 /** 107 * A list of published events. 108 * @private 109 * @type {Object.<string, boolean>} 110 */ 111 $.pkp.classes.Handler.prototype.publishedEvents_ = null; 112 113 114 /** 115 * An HTML element id to which we'll forward all handler events. 116 * @private 117 * @type {?string} 118 */ 119 $.pkp.classes.Handler.prototype.$eventBridge_ = null; 120 121 122 // 123 // Public static methods 124 // 125 /** 126 * Retrieve the bound handler from the jQuery element. 127 * @param {jQueryObject} $element The element to which the 128 * handler was attached. 129 * @return {Object} The retrieved handler. 130 */ 131 $.pkp.classes.Handler.getHandler = function($element) { 132 // Retrieve the handler. We cannot do this with our own 133 // data() method because this method cannot be called 134 // in the context of the handler if it's purpose is to 135 // retrieve the handler. This should be the only place 136 // at all where we have to do access element data 137 // directly. 138 var handler = $element.data('pkp.handler'); 139 140 // Check whether the handler exists. 141 if (!(handler instanceof $.pkp.classes.Handler)) { 142 throw new Error('There is no handler bound to this element!'); 143 } 144 145 return handler; 146 }; 147 148 149 // 150 // Public methods 151 // 152 /** 153 * Returns the HTML element this handler is bound to. 154 * 155 * @return {jQueryObject} The element this handler is bound to. 156 */ 157 $.pkp.classes.Handler.prototype.getHtmlElement = function() { 158 $.pkp.classes.Handler.checkContext_(this); 159 160 // Return the HTML element. 161 return this.$htmlElement_; 162 }; 163 164 165 /** 166 * Publish change events. (See options.publishChangeEvents.) 167 */ 168 $.pkp.classes.Handler.prototype.publishChangeEvents = function() { 169 var i; 170 for (i = 0; i < this.publishChangeEvents_.length; i++) { 171 this.trigger(this.publishChangeEvents_[i]); 172 } 173 }; 174 175 176 /** 177 * A generic event dispatcher that will be bound to 178 * all handler events. See bind() above. 179 * 180 * @this {HTMLElement} 181 * @param {jQuery.Event} event The jQuery event object. 182 * @return {boolean} Return value to be passed back 183 * to jQuery. 184 */ 185 $.pkp.classes.Handler.prototype.handleEvent = function(event) { 186 var $callingElement, handler, boundEvents, args, returnValue, i, l; 187 188 // This handler is always called out of the 189 // handler context. 190 $callingElement = $(this); 191 192 // Identify the targeted handler. 193 handler = $.pkp.classes.Handler.getHandler($callingElement); 194 195 // Make sure that we really got the right element. 196 if ($callingElement[0] !== handler.getHtmlElement()[0]) { 197 throw new Error(['An invalid handler is bound to the calling ', 198 'element of an event!'].join('')); 199 } 200 201 // Retrieve the event handlers for the given event type. 202 boundEvents = handler.eventBindings_[event.type]; 203 if (boundEvents === undefined) { 204 // We have no handler for this event but we also 205 // don't allow bubbling of events outside of the 206 // GUI widget! 207 return false;
208 } 209 210 // Call all event handlers. 211 args = $.makeArray(arguments); 212 returnValue = true; 213 args.unshift(this); 214 for (i = 0, l = boundEvents.length; i < l; i++) { 215 // Invoke the event handler in the context 216 // of the handler object. 217 if (boundEvents[i].apply(handler, args) === false) { 218 // False overrides true. 219 returnValue = false; 220 } 221 222 // Stop immediately if one of the handlers requests this. 223 if (event.isImmediatePropagationStopped()) { 224 break; 225 } 226 } 227 228 // We do not allow bubbling of events outside of the GUI widget! 229 event.stopPropagation(); 230 231 // Return the event handler status. 232 return returnValue; 233 }; 234 235 236 /** 237 * Create a closure that calls the callback in the 238 * context of the handler object. 239 * 240 * NB: Always make sure that the callback is properly 241 * unbound and freed for garbage collection. Otherwise 242 * you might create a memory leak. If you want to bind 243 * an event to the HTMLElement handled by this handler 244 * then always use the above bind() method instead which 245 * is safer. 246 * 247 * @param {Function} callback The callback to be wrapped. 248 * @param {Object=} opt_context Specifies the object which 249 * |this| should point to when the function is run. 250 * If the value is not given, the context will default 251 * to the handler object. 252 * @return {Function} The wrapped callback. 253 */ 254 $.pkp.classes.Handler.prototype.callbackWrapper = 255 function(callback, opt_context) { 256 257 $.pkp.classes.Handler.checkContext_(this); 258 259 // Create a closure that calls the event handler 260 // in the right context. 261 if (!opt_context) { 262 opt_context = this; 263 } 264 return function() { 265 var args; 266 args = $.makeArray(arguments); 267 args.unshift(this); 268 return callback.apply(opt_context, args); 269 }; 270 }; 271 272 273 /** 274 * This callback can be used to handle simple remote server requests. 275 * 276 * @param {Object} ajaxOptions AJAX options. 277 * @param {Object} jsonData A JSON object. 278 * @return {Object|boolean} The parsed JSON data if no error occurred, 279 * otherwise false. 280 */ 281 /*jslint unparam: true*/ 282 $.pkp.classes.Handler.prototype.remoteResponse = 283 function(ajaxOptions, jsonData) { 284 return this.handleJson(jsonData); 285 }; 286 /*jslint unparam: false*/ 287 288 289 /** 290 * Completely remove all traces of the handler from the 291 * HTML element to which it is bound and leave the element in 292 * it's previous state. 293 * 294 * Subclasses should override this method if necessary but 295 * should always call this implementation. 296 */ 297 $.pkp.classes.Handler.prototype.remove = function() { 298 $.pkp.classes.Handler.checkContext_(this); 299 var $element, key; 300 301 // Remove all event handlers in our namespace. 302 $element = this.getHtmlElement(); 303 $element.unbind('.pkpHandler'); 304 305 // Remove all our data items except for the 306 // handler itself. 307 for (key in this.dataItems_) { 308 if (key !== 'pkp.handler') { 309 $element.removeData(key); 310 } 311 } 312 313 // Trigger the remove event, then delete it. 314 $element.trigger('pkpRemoveHandler'); 315 $element.unbind('.pkpHandlerRemove'); 316 317 // Delete the handler. 318 $element.removeData('pkp.handler'); 319 }; 320 321 322 // 323 // Protected methods 324 // 325 /** 326 * Bind an event to a handler operation. 327 * 328 * This will be done with a generic event handler 329 * to make sure that we get a chance to re-set 330 * 'this' to the handler before we call the actual 331 * handler method. 332 * 333 * @protected 334 * @param {string} eventName The name of the event 335 * to be bound. See jQuery.bind() for event names. 336 * @param {Function} handler The event handler to 337 * be called when the even is triggered. 338 */ 339 $.pkp.classes.Handler.prototype.bind = function(eventName, handler) { 340 $.pkp.classes.Handler.checkContext_(this); 341 342 if (!this.eventBindings_[eventName]) { 343 // Initialize the event store for this event. 344 this.eventBindings_[eventName] = []; 345 346 // Determine the event namespace. 347 var eventNamespace; 348 eventNamespace = '.pkpHandler'; 349 if (eventName === 'pkpRemoveHandler') { 350 // We have a special namespace for the remove event 351 // because it needs to be triggered when all other 352 // events have already been removed. 353 eventNamespace = '.pkpHandlerRemove'; 354 } 355 356 // Bind the generic event handler to the event within our namespace. 357 this.getHtmlElement().bind(eventName + eventNamespace, this.handleEvent); 358 } 359 360 // Store the event binding internally 361 this.eventBindings_[eventName].push(handler); 362 }; 363 364 365 /** 366 * Unbind an event from a handler operation. 367 * 368 * @protected 369 * @param {string} eventName The name of the event 370 * to be bound. See jQuery.bind() for event names. 371 * @param {Function} handler The event handler to 372 * be called when the even is triggered. 373 * @return {boolean} True, if a handler was found and 374 * removed, otherwise false. 375 */ 376 $.pkp.classes.Handler.prototype.unbind = function(eventName, handler) { 377 $.pkp.classes.Handler.checkContext_(this); 378 379 // Remove the event from the internal event cache. 380 if (!this.eventBindings_[eventName]) { 381 return false;
382 } 383 384 var i, length; 385 for (i = 0, length = this.eventBindings_[eventName].length; i < length; i++) { 386 if (this.eventBindings_[eventName][i] === handler) { 387 this.eventBindings_[eventName].splice([i], 1); 388 break; 389 } 390 } 391 392 if (this.eventBindings_[eventName].length === 0) { 393 // If this was the last event then unbind the generic event handler. 394 delete this.eventBindings_[eventName]; 395 this.getHtmlElement().unbind(eventName, this.handleEvent); 396 } 397 398 return true; 399 }; 400 401 402 /** 403 * Add or retrieve a data item to/from the DOM element 404 * this handler is managing. 405 * 406 * Always use this method if you want to store data 407 * items. It makes sure that your items will be properly 408 * namespaced and it also guarantees correct garbage 409 * collection of your items once the handler is removed. 410 * 411 * @protected 412 * @param {string} key The name of the item to be stored 413 * or retrieved. 414 * @param {Object=} opt_value The data item to be stored. If no item 415 * is given then the existing value for the given key 416 * will be returned. 417 * @return {Object} The cached data item. 418 */ 419 $.pkp.classes.Handler.prototype.data = function(key, opt_value) { 420 $.pkp.classes.Handler.checkContext_(this); 421 422 // Namespace the key. 423 key = 'pkp.' + key; 424 425 if (opt_value !== undefined) { 426 // Add the key to the list of data items 427 // that need to be garbage collected. 428 this.dataItems_[key] = true; 429 } 430 431 // Add/retrieve the data to/from the 432 // element's data cache. 433 return this.getHtmlElement().data(key, opt_value); 434 }; 435 436 437 /** 438 * This function should be used to pre-process a JSON response 439 * from the server. 440 * 441 * @protected 442 * @param {Object} jsonData The returned server response data. 443 * @return {Object|boolean} The returned server response data or 444 * false if an error occurred. 445 */ 446 $.pkp.classes.Handler.prototype.handleJson = function(jsonData) { 447 if (!jsonData) { 448 throw new Error('Server error: Server returned no or invalid data!'); 449 } 450 451 if (jsonData.status === true) { 452 // Did the server respond with an event to be triggered? 453 if (jsonData.event) { 454 if (jsonData.event.data) { 455 this.trigger(jsonData.event.name, 456 jsonData.event.data); 457 } else { 458 this.trigger(jsonData.event.name); 459 } 460 } 461 return jsonData; 462 } 463 // If we got an error message then display it. 464 if (jsonData.content) { 465 alert(jsonData.content); 466 } 467 return false; 468 }; 469 470 471 /** 472 * This function should be used to let the element emit events 473 * that bubble outside the widget and are published over the 474 * event bridge. 475 * 476 * @protected 477 * @param {string} eventName The event to be triggered. 478 * @param {Array=} opt_data Additional event data. 479 */ 480 $.pkp.classes.Handler.prototype.trigger = 481 function(eventName, opt_data) { 482 483 if (opt_data === undefined) { 484 opt_data = null; 485 } 486 487 // Trigger the event on the handled element. 488 var $handledElement = this.getHtmlElement(); 489 $handledElement.triggerHandler(eventName, opt_data); 490 491 // Trigger the event publicly if it's not 492 // published anyway. 493 if (!this.publishedEvents_[eventName]) { 494 this.triggerPublicEvent_(eventName, opt_data); 495 } 496 }; 497 498 499 /** 500 * Publish an event triggered by a nested widget. This event 501 * will bubble outside the widget and will also be published 502 * over the event bridge. 503 * 504 * @param {string} eventName The event name. 505 */ 506 $.pkp.classes.Handler.prototype.publishEvent = function(eventName) { 507 // If the event has been published before then do nothing. 508 if (this.publishedEvents_[eventName]) { 509 return; 510 } 511 512 // Add the event to the published event list. 513 this.publishedEvents_[eventName] = true; 514 515 /*jslint unparam: true*/ 516 this.bind(eventName, function(context, privateEvent, var_args) { 517 // Retrieve additional event data. 518 var eventData = null; 519 if (arguments.length > 2) { 520 eventData = Array.prototype.slice.call(arguments, 2); 521 } 522 523 // Re-trigger the private event publicly. 524 this.triggerPublicEvent_(eventName, eventData); 525 }); 526 /*jslint unparam: false*/ 527 }; 528 529 530 /** 531 * Handle the "show more" and "show less" clicks triggered by the 532 * links in longer text items. 533 * 534 * @param {Event} event The event. 535 */ 536 $.pkp.classes.Handler.prototype.switchViz = function(event) { 537 var eventElement = event.currentTarget; 538 $(eventElement).parent().parent().find('span').toggle(); 539 }; 540 541 542 // 543 // Private methods 544 // 545 /** 546 * Trigger a public event. 547 * 548 * Public events will bubble outside the widget and will 549 * also be forwarded through the event bridge if one has 550 * been configured. 551 * 552 * @private 553 * @param {string} eventName The event to be triggered. 554 * @param {Array=} opt_data Additional event data. 555 */ 556 $.pkp.classes.Handler.prototype.triggerPublicEvent_ = 557 function(eventName, opt_data) { 558 559 // Publish the event. 560 var $handledElement = this.getHtmlElement(); 561 $handledElement.parent().trigger(eventName, opt_data); 562 563 // If we have an event bridge configured then re-trigger 564 // the event on the target object. 565 if (this.$eventBridge_) { 566 $('[id^="' + this.$eventBridge_ + '"]').trigger(eventName, opt_data); 567 } 568 }; 569 570 571 // 572 // Private static methods 573 // 574 /** 575 * Check the context of a method invocation. 576 * 577 * @private 578 * @param {Object} context The function context 579 * to be tested. 580 */ 581 $.pkp.classes.Handler.checkContext_ = function(context) { 582 if (!(context instanceof $.pkp.classes.Handler)) { 583 throw new Error('Trying to call handler method in non-handler context!'); 584 } 585 }; 586 587 588/** @param {jQuery} $ jQuery closure. */ 589}(jQuery));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.