PageSourceSearch

https://safpj.co.za/lib/pkp/js/controllers/grid/GridHandler.js

js safpj.co.za collected 2026-10-02 10:26:19 UTC 21,462 bytes, 827 lines download raw bytes

1/**
2 * @defgroup js_controllers_grid
3 */
4/**
5 * @file js/controllers/grid/GridHandler.js
6 *
7 * Copyright (c) 2013-2019 Simon Fraser University
8 * Copyright (c) 2000-2019 John Willinsky
9 * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING.
10 *
11 * @class GridHandler
12 * @ingroup js_controllers_grid
13 *
14 * @brief Grid row handler.
15 */
16(function($) {
17
18	// Define the namespace.
19	$.pkp.controllers.grid = $.pkp.controllers.grid || {};
20
21
22
23	/**
24	 * @constructor
25	 *
26	 * @extends $.pkp.classes.Handler
27	 *
28	 * @param {jQueryObject} $grid The grid this handler is
29	 *  attached to.
30	 * @param {{features}} options Grid handler configuration.
31	 */
32	$.pkp.controllers.grid.GridHandler = function($grid, options) {
33		this.parent($grid, options);
34
35		// We give a chance for this handler to initialize
36		// before we initialize its features.
37		this.initialize(options);
38
39		this.initFeatures_(options.features);
40	};
41	$.pkp.classes.Helper.inherits($.pkp.controllers.grid.GridHandler,
42			$.pkp.classes.Handler);
43
44
45	//
46	// Private properties
47	//
48	/**
49	 * The id of the grid.
50	 * @private
51	 * @type {?string}
52	 */
53	$.pkp.controllers.grid.GridHandler.prototype.gridId_ = null;
54
55
56	/**
57	 * The URL to fetch a grid row.
58	 * @private
59	 * @type {?string}
60	 */
61	$.pkp.controllers.grid.GridHandler.prototype.fetchRowUrl_ = null;
62
63
64	/**
65	 * The URL to fetch the entire grid.
66	 * @private
67	 * @type {?string}
68	 */
69	$.pkp.controllers.grid.GridHandler.prototype.fetchGridUrl_ = null;
70
71
72	/**
73	 * The selector for the grid body.
74	 * @private
75	 * @type {?string}
76	 */
77	$.pkp.controllers.grid.GridHandler.prototype.bodySelector_ = null;
78
79
80	/**
81	 * This grid features.
82	 * @private
83	 * @type {Object}
84	 */
85	$.pkp.controllers.grid.GridHandler.prototype.features_ = null;
86
87
88	//
89	// Public methods
90	//
91	/**
92	 * Get the fetch row URL.
93	 * @return {?string} URL to the "fetch row" operation handler.
94	 */
95	$.pkp.controllers.grid.GridHandler.prototype.getFetchRowUrl =
96			function() {
97
98		return this.fetchRowUrl_;
99	};
100
101
102	/**
103	 * Get all grid rows.
104	 *
105	 * @return {jQueryObject} The rows as a JQuery object.
106	 */
107	$.pkp.controllers.grid.GridHandler.prototype.getRows =
108			function() {
109		return $('.gridRow', this.getHtmlElement());
110	};
111
112
113	/**
114	 * Get the id prefix of this grid.
115	 * @return {string} The ID prefix of this grid.
116	 */
117	$.pkp.controllers.grid.GridHandler.prototype.getGridIdPrefix =
118			function() {
119		return 'component-' + this.gridId_;
120	};
121
122
123	/**
124	 * Get the id prefix of this grid row.
125	 * @return {string} The id prefix of this grid row.
126	 */
127	$.pkp.controllers.grid.GridHandler.prototype.getRowIdPrefix =
128			function() {
129		return this.getGridIdPrefix() + '-row-';
130	};
131
132
133	/**
134	 * Get the data element id of the passed grid row.
135	 * @param {jQueryObject} $gridRow The grid row JQuery object.
136	 * @return {string|undefined} The data element id of the passed grid row.
137	 */
138	$.pkp.controllers.grid.GridHandler.prototype.getRowDataId =
139			function($gridRow) {
140
141		var gridRowHtmlClasses, rowDataIdPrefix, index, rowDataId,
142				startExtractPosition;
143
144		gridRowHtmlClasses = $gridRow.attr('class').split(' ');
145		rowDataIdPrefix = 'element';
146		for (index in gridRowHtmlClasses) {
147			startExtractPosition = gridRowHtmlClasses[index]
148					.indexOf(rowDataIdPrefix);
149			if (startExtractPosition != -1) {
150				rowDataId = gridRowHtmlClasses[index].slice(rowDataIdPrefix.length);
151				break;
152			}
153		}
154
155		return rowDataId;
156	};
157
158
159	/**
160	 * Get the parent grid row of the passed element, if any.
161	 * @param {jQueryObject} $element The element that is inside the row.
162	 * @return {jQueryObject} The element parent grid row.
163	 */
164	$.pkp.controllers.grid.GridHandler.prototype.getParentRow =
165			function($element) {
166		return $element.parents('.gridRow:first');
167	};
168
169
170	/**
171	 * Get the same type elements of the passed element.
172	 * @param {jQueryObject} $element The element to get the type from.
173	 * @return {jQueryObject} The grid elements with the same type
174	 * of the passed element.
175	 */
176	$.pkp.controllers.grid.GridHandler.prototype.getElementsByType =
177			function($element) {
178		if ($element.hasClass('gridRow')) {
179			var $container = $element.parents('tbody:first');
180			return $('.gridRow', $container);
181		}
182
183		return null;
184	};
185
186
187	/**
188	 * Get the empty element based on the type of the passed element.
189	 * @param {jQueryObject} $element The element to get the type from.
190	 * @return {jQueryObject} The empty element.
191	 */
192	$.pkp.controllers.grid.GridHandler.prototype.getEmptyElement =
193			function($element) {
194		if ($element.hasClass('gridRow')) {
195			// Return the rows empty element placeholder.
196			var $container = $element.parents('tbody:first');
197			return $container.next('.empty');
198		}
199
200		return null;
201	};
202
203
204	/**
205	 * Show/hide row actions.
206	 *
207	 * @param {HTMLElement} sourceElement The element that
208	 *  issued the event.
209	 */
210	$.pkp.controllers.grid.GridHandler.prototype.toggleRowActions =
211			function(sourceElement) {
212
213		// Toggle the row actions.
214		var $controlRow = $(sourceElement).parents('tr').next('.row_controls');
215		this.applyToggleRowActionEffect_($controlRow);
216	};
217
218
219	/**
220	 * Hide all visible row controls.
221	 */
222	$.pkp.controllers.grid.GridHandler.prototype.hideAllVisibleRowActions =
223			function() {
224		var $visibleControlRows, index, limit;
225
226		$visibleControlRows = $('.row_controls:visible', this.getHtmlElement());
227		for (index = 0, limit = $visibleControlRows.length; index < limit; index++) {
228			this.applyToggleRowActionEffect_($($visibleControlRows[index]));
229		}
230	};
231
232
233	/**
234	 * Hide row actions div container.
235	 */
236	$.pkp.controllers.grid.GridHandler.prototype.hideRowActionsDiv =
237			function() {
238		var $rowActionDivs, index, limit, $div;
239
240		$rowActionDivs = $('.gridRow div.row_actions', this.getHtmlElement());
241		$rowActionDivs.hide();
242
243		// FIXME: Hack to correctly align the first column cell content after
244		// hiding the row actions div.
245		for (index = 0, limit = $rowActionDivs.length; index < limit; index++) {
246			$div = $($rowActionDivs[index]);
247			$div.parents('.row_container:first').
248					attr('style', 'padding-left:0px !important');
249		}
250	};
251
252
253	/**
254	 * Show row actions div container.
255	 */
256	$.pkp.controllers.grid.GridHandler.prototype.showRowActionsDiv =
257			function() {
258		var $rowActionDivs, index, limit, $div;
259
260		$rowActionDivs = $('.gridRow div.row_actions', this.getHtmlElement());
261		$rowActionDivs.show();
262
263		// FIXME: This is a hack. It removes the inline style that grid handler
264		// inserts in the row container when it hides the row actions div.
265		// See $.pkp.controllers.grid.GridHandler.prototype.hideRowActionsDiv
266		for (index = 0, limit = $rowActionDivs.length; index < limit; index++) {
267			$div = $($rowActionDivs[index]);
268			$div.parents('.row_container:first').removeAttr('style');
269		}
270	};
271
272
273	/**
274	 * Enable/disable all link actions inside this grid.
275	 * @param {boolean} enable Enable/disable flag.
276	 * @param {jQueryObject} $linkElements Link elements JQuery object.
277	 */
278	$.pkp.controllers.grid.GridHandler.prototype.changeLinkActionsState =
279			function(enable, $linkElements) {
280		if ($linkElements === undefined) {
281			$linkElements = $('.pkp_controllers_linkAction', this.getHtmlElement());
282		}
283		$linkElements.each(function() {
284			/** {$.pkp.controllers.LinkActionHandler} */
285			var linkHandler;
286			linkHandler = $.pkp.classes.Handler.getHandler($(this));
287			if (enable) {
288				linkHandler.enableLink();
289			} else {
290				linkHandler.disableLink();
291			}
292		});
293	};
294
295
296	/**
297	 * Re-sequence all grid rows based on the passed sequence map.
298	 * @param {Array} sequenceMap A sequence array with the row id as value.
299	 */
300	$.pkp.controllers.grid.GridHandler.prototype.resequenceRows =
301			function(sequenceMap) {
302		var rowId, index, $row;
303		for (index in sequenceMap) {
304			rowId = sequenceMap[index];
305			$row = $('#' + rowId);
306			this.appendElement($row);
307		}
308		this.updateControlRowsPosition();
309	};
310
311
312	/**
313	 * Move all grid control rows to their correct position,
314	 * below of each correspondent data grid row.
315	 */
316	$.pkp.controllers.grid.GridHandler.prototype.updateControlRowsPosition =
317			function() {
318		var $rows, index, limit, $row, $controlRow;
319
320		$rows = this.getRows();
321		for (index = 0, limit = $rows.length; index < limit; index++) {
322			$row = $($rows[index]);
323			$controlRow = this.getControlRowByGridRow($row);
324			if ($controlRow.length > 0) {
325				$controlRow.insertAfter($row);
326			}
327		}
328	};
329
330
331	//
332	// Protected methods
333	//
334	/**
335	 * Set data and execute operations to initialize.
336	 *
337	 * @protected
338	 *
339	 * @param {Object} options Grid options.
340	 */
341	$.pkp.controllers.grid.GridHandler.prototype.initialize =
342			function(options) {
343		// Bind the handler for the "elements changed" event.
344		this.bind('dataChanged', this.refreshGridHandler);
345
346		// Bind the handler for the "add new row" event.
347		this.bind('addRow', this.addRowHandler_);
348
349		// Handle grid filter events.
350		this.bind('formSubmitted', this.refreshGridWithFilterHandler_);
351
352		// Save the ID of this grid.
353		this.gridId_ = options.gridId;
354
355		// Save the URL to fetch a row.
356		this.fetchRowUrl_ = options.fetchRowUrl;
357
358		// Save the URL to fetch the entire grid
359		this.fetchGridUrl_ = options.fetchGridUrl;
360
361		// Save the selector for the grid body.
362		this.bodySelector_ = options.bodySelector;
363
364		// Show/hide row action feature.
365		this.activateRowActions_();
366
367		this.trigger('gridInitialized');
368	};
369
370
371	/**
372	 * Call features hooks.
373	 *
374	 * @protected
375	 *
376	 * @param {string} hookName The name of the hook.
377	 * @param {Array|jQueryObject} args The arguments array.
378	 */
379	$.pkp.controllers.grid.GridHandler.prototype.callFeaturesHook =
380			function(hookName, args) {
381		var featureName;
382		if (!$.isArray(args)) {
383			args = [args];
384		}
385		for (featureName in this.features_) {
386			this.features_[featureName][hookName].
387					apply(this.features_[featureName], args);
388		}
389	};
390
391
392	/**
393	 * Refresh either a single row of the grid or the whole grid.
394	 *
395	 * @protected
396	 *
397	 * @param {HTMLElement} sourceElement The element that
398	 *  issued the event.
399	 * @param {Event} event The triggering event.
400	 * @param {number=} opt_elementId The id of a data element that was
401	 *  updated, added or deleted. If not given then the whole grid
402	 *  will be refreshed.
403	 *  @param {Boolean=} opt_fetchedAlready Flag that subclasses can send
404	 *  telling that a fetch operation was already handled there.
405	 */
406	/*jslint unparam: true*/
407	$.pkp.controllers.grid.GridHandler.prototype.refreshGridHandler =
408			function(sourceElement, event, opt_elementId, opt_fetchedAlready) {
409
410		// Check if subclasses already handled the fetch of new elements.
411		if (!opt_fetchedAlready) {
412			if (opt_elementId) {
413				// Retrieve a single row from the server.
414				$.get(this.fetchRowUrl_, {rowId: opt_elementId},
415						this.callbackWrapper(this.replaceElementResponseHandler_), 'json');
416			} else {
417				// Retrieve the whole grid from the server.
418				$.get(this.fetchGridUrl_, null,
419						this.callbackWrapper(this.replaceGridResponseHandler_), 'json');
420			}
421		}
422
423		// Let the calling context (page?) know that the grids are being redrawn.
424		this.trigger('gridRefreshRequested');
425		this.publishChangeEvents();
426	};
427	/*jslint unparam: false*/
428
429
430	/**
431	 * Delete a grid element.
432	 *
433	 * @protected
434	 *
435	 * @param {jQueryObject} $element The element to be deleted.
436	 */
437	$.pkp.controllers.grid.GridHandler.prototype.deleteElement =
438			function($element) {
439		var lastElement, $emptyElement;
440
441		// Check whether we really only match one element.
442		if ($element.length !== 1) {
443			throw new Error('There were ' + $element.length +
444					' rather than 1 element to delete!');
445		}
446
447		// Check whether this is the last row.
448		lastElement = false;
449		if (this.getElementsByType($element).length == 1) {
450			lastElement = true;
451		}
452
453		// Remove the controls row, if any.
454		if ($element.hasClass('gridRow')) {
455			this.deleteControlsRow_($element);
456		}
457
458		$emptyElement = this.getEmptyElement($element);
459		$element.fadeOut(500, function() {
460			$(this).remove();
461			if (lastElement) {
462				$emptyElement.fadeIn(500);
463			}
464		});
465	};
466
467
468	/**
469	 * Append a new row to the end of the list.
470	 *
471	 * @protected
472	 *
473	 * @param {jQueryObject} $newRow The new row to append.
474	 * @param {jQueryObject=} opt_$gridBody The tbody container element.
475	 */
476	$.pkp.controllers.grid.GridHandler.prototype.appendElement =
477			function($newRow, opt_$gridBody) {
478
479		if (opt_$gridBody === undefined) {
480			opt_$gridBody = this.getHtmlElement().find(this.bodySelector_);
481		}
482
483		// Add the new element.
484		opt_$gridBody.append($newRow);
485
486		// Hide the empty placeholder.
487		var $emptyElement = this.getEmptyElement($newRow);
488		$emptyElement.hide();
489
490		this.callFeaturesHook('appendElement', $newRow);
491	};
492
493
494	/**
495	 * Update an existing element using the passed new element content.
496	 *
497	 * @protected
498	 *
499	 * @param {jQueryObject} $existingElement The element that is already
500	 *  in grid.
501	 * @param {jQueryObject} $newElement The element with new content.
502	 */
503	$.pkp.controllers.grid.GridHandler.prototype.replaceElement =
504			function($existingElement, $newElement) {
505
506		if ($newElement.hasClass('gridRow')) {
507			this.deleteControlsRow_($existingElement);
508		}
509
510		$existingElement.replaceWith($newElement);
511		this.callFeaturesHook('replaceElement', $newElement);
512	};
513
514
515	/**
516	 * Does the passed row have a different number of columns than the
517	 * existing grid?
518	 *
519	 * @protected
520	 *
521	 * @param {jQueryObject} $row The row to be checked against grid columns.
522	 * @param {Boolean=} opt_checkColSpan Will get the number of row columns
523	 * by column span.
524	 * @return {boolean} Whether it has the same number of grid columns
525	 * or not.
526	 */
527	$.pkp.controllers.grid.GridHandler.prototype.hasSameNumOfColumns =
528			function($row, opt_checkColSpan) {
529		var $grid, numColumns, $tdElements, numCellsInNewRow;
530
531		$grid = this.getHtmlElement();
532		numColumns = $grid.find('th').length;
533		$tdElements = $row.first().find('td');
534		if (opt_checkColSpan) {
535			numCellsInNewRow = $tdElements.attr('colspan');
536		} else {
537			numCellsInNewRow = $tdElements.length;
538		}
539
540		return (numColumns == numCellsInNewRow);
541	};
542
543
544	//
545	// Private methods
546	//
547	/**
548	 * Refresh the grid after its filter has changed.
549	 *
550	 * @private
551	 *
552	 * @param {$.pkp.controllers.form.ClientFormHandler} filterForm
553	 *  The filter form.
554	 * @param {Event} event A "formSubmitted" event.
555	 * @param {string} filterData Serialized filter data.
556	 */
557	/*jslint unparam: true*/
558	$.pkp.controllers.grid.GridHandler.prototype.refreshGridWithFilterHandler_ =
559			function(filterForm, event, filterData) {
560
561		// Retrieve the grid from the server and add the
562		// filter data as form data.
563		$.post(this.fetchGridUrl_, filterData,
564				this.callbackWrapper(this.replaceGridResponseHandler_), 'json');
565	};
566	/*jslint unparam: false*/
567
568
569	/**
570	 * Add a new row to the grid.
571	 *
572	 * @private
573	 *
574	 * @param {HTMLElement} sourceElement The element that
575	 *  issued the event.
576	 * @param {Event} event The triggering event.
577	 * @param {Object} params The request parameters to use to generate
578	 *  the new row.
579	 */
580	/*jslint unparam: true*/
581	$.pkp.controllers.grid.GridHandler.prototype.addRowHandler_ =
582			function(sourceElement, event, params) {
583
584		// Retrieve a single new row from the server.
585		$.get(this.fetchRowUrl_, params,
586				this.callbackWrapper(this.replaceElementResponseHandler_), 'json');
587	};
588	/*jslint unparam: false*/
589
590
591	/**
592	 * Callback to insert, remove or replace a row after an
593	 * element has been inserted, update or deleted.
594	 *
595	 * @private
596	 *
597	 * @param {Object} ajaxContext The AJAX request context.
598	 * @param {Object} jsonData A parsed JSON response object.
599	 * @return {boolean|undefined} Return false when no replace action is taken.
600	 */
601	/*jslint unparam: true*/
602	$.pkp.controllers.grid.GridHandler.prototype.replaceElementResponseHandler_ =
603			function(ajaxContext, jsonData) {
604		var elementId, $element, handledJsonData;
605
606		handledJsonData = this.handleJson(jsonData);
607		if (handledJsonData !== false) {
608			if (handledJsonData.elementNotFound) {
609				// The server reported that this element no
610				// longer exists in the database so let's
611				// delete it.
612				elementId = handledJsonData.elementNotFound;
613				$element = this.getHtmlElement().
614						find('.element' + elementId);
615
616				// Sometimes we get a delete event before the
617				// element has actually been inserted (e.g. when deleting
618				// elements due to a cancel action or similar).
619				if ($element.length === 0) {
620					return false;
621				}
622
623				this.deleteElement($element);
624			} else {
625				// The server returned mark-up to replace
626				// or insert the row.
627				this.insertOrReplaceElement_(handledJsonData.content);
628
629				// Refresh row action event binding.
630				this.activateRowActions_();
631			}
632		}
633	};
634	/*jslint unparam: false*/
635
636
637	/**
638	 * Callback to replace a grid's content.
639	 *
640	 * @private
641	 *
642	 * @param {Object} ajaxContext The AJAX request context.
643	 * @param {Object} jsonData A parsed JSON response object.
644	 */
645	/*jslint unparam: true*/
646	$.pkp.controllers.grid.GridHandler.prototype.replaceGridResponseHandler_ =
647			function(ajaxContext, jsonData) {
648		var handledJsonData, $grid;
649
650		handledJsonData = this.handleJson(jsonData);
651		if (handledJsonData !== false) {
652			// Get the grid that we're updating
653			$grid = this.getHtmlElement();
654
655			// Replace the grid content
656			$grid.replaceWith(handledJsonData.content);
657
658			// Refresh row action event binding.
659			this.activateRowActions_();
660		}
661	};
662	/*jslint unparam: false*/
663
664
665	/**
666	 * Helper that inserts or replaces an element.
667	 *
668	 * @private
669	 *
670	 * @param {string} elementContent The new mark-up of the element.
671	 */
672	$.pkp.controllers.grid.GridHandler.prototype.insertOrReplaceElement_ =
673			function(elementContent) {
674		var $newElement, newElementId, $grid, $existingElement;
675
676		// Parse the HTML returned from the server.
677		$newElement = $(elementContent);
678		newElementId = $newElement.attr('id');
679
680		// Does the element exist already?
681		$grid = this.getHtmlElement();
682		$existingElement = newElementId ? $grid.find('#' + newElementId) : null;
683
684		if ($existingElement !== null && $existingElement.length > 1) {
685			throw new Error('There were ' + $existingElement.length +
686					' rather than 0 or 1 elements to be replaced!');
687		}
688
689		if (!this.hasSameNumOfColumns($newElement)) {
690			// Redraw the whole grid so new columns
691			// get added/removed to match element.
692			$.get(this.fetchGridUrl_, null,
693					this.callbackWrapper(this.replaceGridResponseHandler_), 'json');
694		} else {
695			if ($existingElement !== null && $existingElement.length === 1) {
696				// Update element.
697				this.replaceElement($existingElement, $newElement);
698			} else {
699				// Insert row.
700				this.appendElement($newElement);
701			}
702		}
703	};
704
705
706	/**
707	 * Helper that deletes the row of controls (if present).
708	 *
709	 * @private
710	 *
711	 * @param {jQueryObject} $row The row whose matching control row should be
712	 *  deleted.
713	 */
714	$.pkp.controllers.grid.GridHandler.prototype.deleteControlsRow_ =
715			function($row) {
716		var $controlRow = $('#' + $row.attr('id') + '-control-row',
717				this.getHtmlElement());
718
719		if ($controlRow.is('tr') && $controlRow.hasClass('row_controls')) {
720			$controlRow.remove();
721		}
722	};
723
724
725	/**
726	 * Get the control row for the passed the grid row.
727	 *
728	 * @param {jQueryObject} $gridRow The grid row JQuery object.
729	 * @return {jQueryObject} The control row JQuery object.
730	 */
731	$.pkp.controllers.grid.GridHandler.prototype.getControlRowByGridRow =
732			function($gridRow) {
733		var rowId, controlRowId;
734
735		rowId = $gridRow.attr('id');
736		controlRowId = rowId + '-control-row';
737		return $('#' + controlRowId);
738	};
739
740
741	/**
742	 * Helper that attaches click events to row actions.
743	 *
744	 * @private
745	 */
746	$.pkp.controllers.grid.GridHandler.prototype.activateRowActions_ =
747			function() {
748
749		var $grid = this.getHtmlElement();
750		$grid.find('a.settings').unbind('click').bind('click',
751				this.callbackWrapper(this.toggleRowActions));
752	};
753
754
755	/**
756	 * Apply the effect for hide/show row actions.
757	 *
758	 * @private
759	 *
760	 * @param {jQueryObject} $controlRow The control row JQuery object.
761	 */
762	$.pkp.controllers.grid.GridHandler.prototype.applyToggleRowActionEffect_ =
763			function($controlRow) {
764		var delay, $row;
765
766		// FIXME #7582: IE8 and Safari don't work well with delay to show
767		// or hide the control grid rows.
768		delay = 0;
769
770		$row = $controlRow.prev().find('td:not(.indent_row)');
771		$row = $row.add($controlRow.prev());
772		if ($controlRow.is(':visible')) {
773			setTimeout(function() {
774				$row.removeClass('no_border');
775			}, delay);
776			$controlRow.hide(delay);
777		} else {
778			$row.addClass('no_border');
779			$controlRow.show(delay);
780		}
781	};
782
783
784	/**
785	 * Add a grid feature.
786	 *
787	 * @private
788	 *
789	 * @param {string} id Feature id.
790	 * @param {$.pkp.classes.features.Feature} $feature The grid
791	 * feature to be added.
792	 */
793	$.pkp.controllers.grid.GridHandler.prototype.addFeature_ =
794			function(id, $feature) {
795		if (!this.features_) {
796			this.features_ = [];
797		}
798		this.features_[id] = $feature;
799	};
800
801
802	/**
803	 * Add grid features.
804	 *
805	 * @private
806	 *
807	 * @param {Array.<{JSClass, options}>} features The features options array.
808	 */
809	$.pkp.controllers.grid.GridHandler.prototype.initFeatures_ =
810			function(features) {
811
812		var id, $feature;
813		for (id in features) {
814			$feature =
815					/** @type {$.pkp.classes.features.Feature} */
816					($.pkp.classes.Helper.objectFactory(
817							features[id].JSClass,
818							[this, features[id].options]));
819
820			this.addFeature_(id, $feature);
821			this.features_[id].init();
822		}
823	};
824
825
826/** @param {jQuery} $ jQuery closure. */
827}(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.