PageSourceSearch

https://forikes-ejournal.com/lib/pkp/js/classes/Handler.js

js forikes-ejournal.com collected 2026-10-07 15:30:14 UTC 15,884 bytes, 589 lines download raw bytes

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.