/**
 * @defgroup js_controllers_grid
 */
/**
 * @file js/controllers/grid/GridHandler.js
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class GridHandler
 * @ingroup js_controllers_grid
 *
 * @brief Grid row handler.
 */
(function($) {

	// Define the namespace.
	$.pkp.controllers.grid = $.pkp.controllers.grid || {};



	/**
	 * @constructor
	 *
	 * @extends $.pkp.classes.Handler
	 *
	 * @param {jQueryObject} $grid The grid this handler is
	 *  attached to.
	 * @param {{features}} options Grid handler configuration.
	 */
	$.pkp.controllers.grid.GridHandler = function($grid, options) {
		this.parent($grid, options);

		// We give a chance for this handler to initialize
		// before we initialize its features.
		this.initialize(options);

		this.initFeatures_(options.features);
	};
	$.pkp.classes.Helper.inherits($.pkp.controllers.grid.GridHandler,
			$.pkp.classes.Handler);


	//
	// Constants.
	//
	/**
	 * Flag to be used to fetch all curent page grid rows.
	 * @public
	 * @type {Object}
	 */
	$.pkp.controllers.grid.GridHandler.FETCH_ALL_ROWS_ID = {};


	//
	// Protected properties
	//
	/**
	 * The selector for the grid body.
	 * @protected
	 * @type {?string}
	 */
	$.pkp.controllers.grid.GridHandler.prototype.bodySelector = null;


	/**
	 * The URL to fetch a grid row.
	 * @protected
	 * @type {?string}
	 */
	$.pkp.controllers.grid.GridHandler.prototype.fetchRowUrl = null;


	/**
	 * The URL to fetch all loaded grid rows.
	 * @protected
	 * @type {?string}
	 */
	$.pkp.controllers.grid.GridHandler.prototype.fetchRowsUrl = null;


	//
	// Private properties
	//
	/**
	 * The id of the grid.
	 * @private
	 * @type {?string}
	 */
	$.pkp.controllers.grid.GridHandler.prototype.gridId_ = null;


	/**
	 * The URL to fetch the entire grid.
	 * @private
	 * @type {?string}
	 */
	$.pkp.controllers.grid.GridHandler.prototype.fetchGridUrl_ = null;


	/**
	 * This grid features.
	 * @private
	 * @type {Object}
	 */
	$.pkp.controllers.grid.GridHandler.prototype.features_ = null;


	/**
	 * Fetch elements extra request parameters.
	 * @private
	 * @type {Object}
	 */
	$.pkp.controllers.grid.GridHandler.prototype.fetchExtraParams_ = null;


	//
	// Public methods
	//
	/**
	 * Get fetch element extra request parameters.
	 * @return {Object} Extra request parameters.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getFetchExtraParams =
			function() {
		return this.fetchExtraParams_;
	};


	/**
	 * Set fetch element extra request parameters.
	 * @param {Object} extraParams Extra request parameters.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.setFetchExtraParams =
			function(extraParams) {
		this.fetchExtraParams_ = extraParams;
	};


	/**
	 * Get the fetch row URL.
	 * @return {?string} URL to the "fetch row" operation handler.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getFetchRowUrl =
			function() {

		return this.fetchRowUrl;
	};


	/**
	 * Get the fetch rows URL.
	 * @return {?string} URL to the "fetch rows" operation handler.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getFetchRowsUrl =
			function() {

		return this.fetchRowsUrl;
	};


	/**
	 * Get all grid rows.
	 *
	 * @return {jQueryObject} The rows as a JQuery object.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getRows =
			function() {
		return $('.gridRow', this.getHtmlElement()).not('.gridRowDeleted');
	};


	/**
	 * Get the id prefix of this grid.
	 * @return {string} The ID prefix of this grid.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getGridIdPrefix =
			function() {
		return 'component-' + this.gridId_;
	};


	/**
	 * Get the id prefix of this grid row.
	 * @return {string} The id prefix of this grid row.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getRowIdPrefix =
			function() {
		return this.getGridIdPrefix() + '-row-';
	};


	/**
	 * Get the grid row by the passed data element id.
	 * @param {number} rowDataId
	 * @param {number=} opt_parentElementId
	 * @return {jQueryObject}
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getRowByDataId =
			function(rowDataId, opt_parentElementId) {
		return $('#' +
				this.getRowIdPrefix() +
				$.pkp.classes.Helper.escapeJQuerySelector(String(rowDataId)),
				this.getHtmlElement());
	};


	/**
	 * Get the data element id of the passed grid row.
	 * @param {jQueryObject} $gridRow The grid row JQuery object.
	 * @return {string} The data element id of the passed grid row.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getRowDataId =
			function($gridRow) {
		var rowDataId;
		rowDataId = /** @type {string} */ ($gridRow.attr('id').
				slice(this.getRowIdPrefix().length));
		return rowDataId;
	};


	/**
	 * Get the parent grid row of the passed element, if any.
	 * @param {jQueryObject} $element The element that is inside the row.
	 * @return {jQueryObject} The element parent grid row.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getParentRow =
			function($element) {
		return $element.parents('.gridRow:first');
	};


	/**
	 * Get the same type elements of the passed element.
	 * @param {jQueryObject} $element The element to get the type from.
	 * @return {jQueryObject} The grid elements with the same type
	 * of the passed element.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getElementsByType =
			function($element) {
		if ($element.hasClass('gridRow')) {
			var $container = $element.parents('tbody:first');
			return $('.gridRow', $container);
		} else {
			return null;
		}
	};


	/**
	 * Get the empty element based on the type of the passed element.
	 * @param {jQueryObject} $element The element to get the type from.
	 * @return {jQueryObject} The empty element.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getEmptyElement =
			function($element) {
		if ($element.hasClass('gridRow')) {
			// Return the rows empty element placeholder.
			var $container = $element.parents('tbody:first');
			return $container.next('.empty');
		} else {
			return null;
		}
	};


	/**
	 * Show/hide row actions.
	 *
	 * @param {HTMLElement} sourceElement The element that
	 *  issued the event.
	 * @param {Event} event The triggering event.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.toggleRowActions =
			function(sourceElement, event) {

		// Don't follow the link
		event.preventDefault();

		// Toggle the extras link class.
		$(sourceElement).toggleClass('show_extras');
		$(sourceElement).toggleClass('hide_extras');

		// Toggle the row actions.
		var $controlRow = $(sourceElement).parents('tr').next('.row_controls');
		this.applyToggleRowActionEffect_($controlRow);
	};


	/**
	 * Hide all visible row controls.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.hideAllVisibleRowActions =
			function() {
		this.getHtmlElement().find('a.hide_extras').click();
	};


	/**
	 * Hide row actions div container.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.hideRowActionsDiv =
			function() {
		var $rowActionDivs, index, limit, $div;

		$rowActionDivs = $('.gridRow div.row_actions', this.getHtmlElement());
		$rowActionDivs.hide();

		// FIXME: Hack to correctly align the first column cell content after
		// hiding the row actions div.
		for (index = 0, limit = $rowActionDivs.length; index < limit; index++) {
			$div = $($rowActionDivs[index]);
		}
	};


	/**
	 * Show row actions div container.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.showRowActionsDiv =
			function() {
		var $rowActionDivs, index, limit, $div;

		$rowActionDivs = $('.gridRow div.row_actions', this.getHtmlElement());
		$rowActionDivs.show();
	};


	/**
	 * Enable/disable all link actions inside this grid.
	 * @param {boolean} enable Enable/disable flag.
	 * @param {jQueryObject} $linkElements Link elements JQuery object.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.changeLinkActionsState =
			function(enable, $linkElements) {
		if ($linkElements === undefined) {
			$linkElements = $('.pkp_controllers_linkAction', this.getHtmlElement());
		}
		$linkElements.each(function() {
			/** {$.pkp.controllers.LinkActionHandler} */
			var linkHandler;
			linkHandler = $.pkp.classes.Handler.getHandler($(this));
			if (enable) {
				linkHandler.enableLink();
			} else {
				linkHandler.disableLink();
			}
		});
	};


	/**
	 * Re-sequence all grid rows based on the passed sequence map.
	 * @param {Array} sequenceMap A sequence array with the row id or
	 * row data id as value.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.resequenceRows =
			function(sequenceMap) {
		var id, index, $row;
		if (!sequenceMap) {
			return;
		}
		for (index in sequenceMap) {
			id = sequenceMap[index];
			$row = $('#' + $.pkp.classes.Helper.escapeJQuerySelector(String(id)));
			if ($row.length == 0) {
				$row = this.getRowByDataId(id);
			}
			if ($row.length == 0) {
				throw new Error('Row with id ' + id + ' not found!');
			}
			this.addElement($row);
		}
		this.updateControlRowsPosition();

		this.callFeaturesHook('resequenceRows', sequenceMap);
	};


	/**
	 * Move all grid control rows to their correct position,
	 * below of each correspondent data grid row.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.updateControlRowsPosition =
			function() {
		var $rows, index, limit, $row, $controlRow;

		$rows = this.getRows();
		for (index = 0, limit = $rows.length; index < limit; index++) {
			$row = $($rows[index]);
			$controlRow = this.getControlRowByGridRow($row);
			if ($controlRow.length > 0) {
				$controlRow.insertAfter($row);
			}
		}
	};


	/**
	 * Inserts or replaces a grid element.
	 * @param {string|jQueryObject} elementContent The new mark-up of the element.
	 * @param {boolean=} opt_prepend Prepend the new row instead of append it?
	 */
	$.pkp.controllers.grid.GridHandler.prototype.insertOrReplaceElement =
			function(elementContent, opt_prepend) {
		var $newElement, newElementId, $grid, $existingElement;
		// Parse the HTML returned from the server.
		$newElement = $(elementContent);
		newElementId = $newElement.attr('id');

		// Does the element exist already?
		$grid = this.getHtmlElement();
		$existingElement = newElementId ?
				$grid.find('#' +
				$.pkp.classes.Helper.escapeJQuerySelector(
				/** @type {string} */ (newElementId))
				) :
				null;

		if ($existingElement !== null && $existingElement.length > 1) {
			throw new Error('There were ' + $existingElement.length +
					' rather than 0 or 1 elements to be replaced!');
		}

		if (!this.hasSameNumOfColumns($newElement)) {
			// Redraw the whole grid so new columns
			// get added/removed to match element.
			$.get(this.fetchGridUrl_, null,
					this.callbackWrapper(this.replaceGridResponseHandler_), 'json');
		} else {
			if ($existingElement !== null && $existingElement.length === 1) {
				// Update element.
				this.replaceElement($existingElement, $newElement);
			} else {
				// Insert row.
				this.addElement($newElement, null, opt_prepend);
			}

			// Refresh row action event binding.
			this.activateRowActions_();
		}
	};


	/**
	 * Delete a grid element.
	 * @param {jQueryObject} $element The element to be deleted.
	 * @param {boolean=} opt_noFadeOut Whether the item deletion
	 * will use the fade out effect or not.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.deleteElement =
			function($element, opt_noFadeOut) {
		var lastElement, $emptyElement, deleteFunction, self;

		// Check whether we really only match one element.
		if ($element.length !== 1) {
			throw new Error('There were ' + $element.length +
					' rather than 1 element to delete!');
		}

		// Flag this element as deleted, so getRows()
		// will return only existing rows from now on.
		$element.addClass('gridRowDeleted');

		// Check whether this is the last row.
		lastElement = false;
		if (this.getElementsByType($element).length == 1) {
			lastElement = true;
		}

		// Remove the controls row, if any.
		if ($element.hasClass('gridRow')) {
			this.deleteControlsRow_($element);
		}

		$emptyElement = this.getEmptyElement($element);
		self = this;
		deleteFunction = function() {
			self.unbindPartial($element);
			$element.remove();
			if (lastElement) {
				$emptyElement.fadeIn(100);
			}
		};

		if (opt_noFadeOut != undefined && opt_noFadeOut) {
			deleteFunction();
		} else {
			$element.fadeOut(500, deleteFunction);
		}
	};


	//
	// Protected methods
	//
	/**
	 * Set data and execute operations to initialize.
	 *
	 * @protected
	 *
	 * @param {Object} options Grid options.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.initialize =
			function(options) {
		var $searchLink;

		// Bind the handler for the "elements changed" event.
		this.bind('dataChanged', this.refreshGridHandler);

		// Bind the handler for the "add new row" event.
		this.bind('addRow', this.addRowHandler_);

		// Handle grid filter events.
		this.bind('formSubmitted', this.refreshGridWithFilterHandler_);

		// Save the ID of this grid.
		this.gridId_ = options.gridId;

		// Save the URL to fetch a row.
		this.fetchRowUrl = options.fetchRowUrl;

		// Save the URL to fetch all rows.
		this.fetchRowsUrl = options.fetchRowsUrl;

		// Save the URL to fetch the entire grid
		this.fetchGridUrl_ = options.fetchGridUrl;

		// Save the selector for the grid body.
		if ($('div.scrollable', this.getHtmlElement()).length > 0) {
			this.bodySelector = 'div.scrollable table';
		} else {
			this.bodySelector = options.bodySelector;
		}

		// Show/hide row action feature.
		this.activateRowActions_();

		this.setFetchExtraParams({});

		// Search control.
		this.getHtmlElement().find('.pkp_form').hide();
		$searchLink = this.getHtmlElement().
				find('.pkp_linkaction_search');
		if ($searchLink.length !== 0) {
			$searchLink.click(
					this.callbackWrapper(function() {
						this.getHtmlElement().find('.pkp_form').toggle();
						$searchLink.toggleClass('is_open');
					}));
		} else {
			// This grid doesn't have an expand/collapse control. If there is
			// a form, expand it.
			this.getHtmlElement().find('.pkp_form').toggle();
		}

		this.trigger('gridInitialized');
	};


	/**
	 * Call features hooks.
	 *
	 * @protected
	 *
	 * @param {string} hookName The name of the hook.
	 * @param {Array|jQueryObject|Object|number|boolean=} opt_args
	 * The arguments array.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.callFeaturesHook =
			function(hookName, opt_args) {
		var featureName;
		if (!$.isArray(opt_args)) {
			opt_args = [opt_args];
		}
		for (featureName in this.features_) {
			this.features_[featureName][hookName].
					apply(this.features_[featureName], opt_args);
		}
	};


	/**
	 * Refresh either a single row of the grid or the whole grid.
	 *
	 * @protected
	 *
	 * @param {HTMLElement} sourceElement The element that
	 *  issued the event.
	 * @param {Event} event The triggering event.
	 * @param {number|Object=} opt_elementId The id of a data element that was
	 *  updated, added or deleted. If not given then the whole grid
	 *  will be refreshed.
	 *  @param {Boolean=} opt_fetchedAlready Flag that subclasses can send
	 *  telling that a fetch operation was already handled there.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.refreshGridHandler =
			function(sourceElement, event, opt_elementId, opt_fetchedAlready) {
		var params;

		this.callFeaturesHook('refreshGrid', opt_elementId);

		params = this.getFetchExtraParams();


		// Check if subclasses already handled the fetch of new elements.
		if (!opt_fetchedAlready) {
			if (opt_elementId) {
				if (opt_elementId ==
						$.pkp.controllers.grid.GridHandler.FETCH_ALL_ROWS_ID) {
					$.get(this.fetchRowsUrl, params,
							this.callbackWrapper(this.replaceElementResponseHandler), 'json');
				} else {
					params.rowId = opt_elementId;
					// Retrieve a single row from the server.
					$.get(this.fetchRowUrl, params,
							this.callbackWrapper(this.replaceElementResponseHandler), 'json');
				}
			} else {
				// Retrieve the whole grid from the server.
				$.get(this.fetchGridUrl_, params,
						this.callbackWrapper(this.replaceGridResponseHandler_), 'json');
			}
		}

		// Let the calling context (page?) know that the grids are being redrawn.
		this.trigger('gridRefreshRequested');
		this.publishChangeEvents();
	};


	/**
	 * Add a new row to the grid.
	 *
	 * @protected
	 *
	 * @param {jQueryObject} $newRow The new row to append.
	 * @param {jQueryObject=} opt_$gridBody The tbody container element.
	 * @param {boolean=} opt_prepend Prepend the element instead of append it?
	 */
	$.pkp.controllers.grid.GridHandler.prototype.addElement =
			function($newRow, opt_$gridBody, opt_prepend) {

		if (opt_$gridBody === undefined || opt_$gridBody === null) {
			opt_$gridBody = this.getHtmlElement().find(this.bodySelector);
		}

		// Add the new element.
		if (opt_prepend != undefined && opt_prepend) {
			opt_$gridBody.prepend($newRow);
		} else {
			opt_$gridBody.append($newRow);
		}


		// Hide the empty placeholder.
		var $emptyElement = this.getEmptyElement($newRow);
		if ($emptyElement) {
			$emptyElement.hide();
		}

		this.callFeaturesHook('addElement', $newRow);
	};


	/**
	 * Update an existing element using the passed new element content.
	 *
	 * @protected
	 *
	 * @param {jQueryObject} $existingElement The element that is already
	 *  in grid.
	 * @param {jQueryObject} $newElement The element with new content.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.replaceElement =
			function($existingElement, $newElement) {

		if ($newElement.hasClass('gridRow')) {
			this.deleteControlsRow_($existingElement);
		}

		this.replacePartialWith($newElement, $existingElement);
		this.callFeaturesHook('replaceElement', $newElement);
	};


	/**
	 * Does the passed row have a different number of columns than the
	 * existing grid?
	 *
	 * @protected
	 *
	 * @param {jQueryObject} $row The row to be checked against grid columns.
	 * @param {Boolean=} opt_checkColSpan Will get the number of row columns
	 * by column span.
	 * @return {boolean} Whether it has the same number of grid columns
	 * or not.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.hasSameNumOfColumns =
			function($row, opt_checkColSpan) {
		var $grid, numColumns, $tdElements, numCellsInNewRow;

		$grid = this.getHtmlElement();
		numColumns = $grid.find('th').length;
		$tdElements = $row.first().find('td');
		if (opt_checkColSpan) {
			numCellsInNewRow = $tdElements.attr('colspan');
		} else {
			numCellsInNewRow = $tdElements.length;
		}

		return (numColumns == numCellsInNewRow);
	};


	/**
	 * Callback to insert, remove or replace a row after an
	 * element has been inserted, update or deleted.
	 *
	 * @protected
	 *
	 * @param {Object} ajaxContext The AJAX request context.
	 * @param {Object} jsonData A parsed JSON response object.
	 * @return {boolean|undefined} Return false when no replace action is taken.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.replaceElementResponseHandler =
			function(ajaxContext, jsonData) {
		var elementId, $element, handledJsonData, castJsonData, $responseElement,
				$responseRow, $responseControlRow, $responseRows, $responseRowsControls,
				index, limit;

		handledJsonData = this.handleJson(jsonData);
		if (handledJsonData !== false) {
			if (handledJsonData.elementNotFound) {
				// The server reported that this element no
				// longer exists in the database so let's
				// delete it.
				elementId = handledJsonData.elementNotFound;
				$element = this.getRowByDataId(elementId);

				// Sometimes we get a delete event before the
				// element has actually been inserted (e.g. when deleting
				// elements due to a cancel action or similar).
				if ($element.length > 0) {
					this.deleteElement($element);
				}
			} else {
				// The server returned mark-up to replace
				// or insert the row.
				$responseElement = $(handledJsonData.content);
				if ($responseElement.filter("tr:not('.row_controls')").length > 1) {
					$responseRows = $responseElement.filter('tr.gridRow');
					$responseRowsControls = $responseElement.filter('tr.row_controls');
					for (index = 0, limit = $responseRows.length; index < limit; index++) {
						$responseRow = $($responseRows[index]);
						$responseControlRow = this.getControlRowByGridRow($responseRow,
								$responseRowsControls);
						this.insertOrReplaceElement($responseRow.add($responseControlRow));
					}
				} else {
					this.insertOrReplaceElement(handledJsonData.content);
				}

				castJsonData = /** @type {{sequenceMap: Array}} */ (handledJsonData);
				this.resequenceRows(castJsonData.sequenceMap);
			}
		}

		this.callFeaturesHook('replaceElementResponseHandler', handledJsonData);
	};


	//
	// Private methods
	//
	/**
	 * Refresh the grid after its filter has changed.
	 *
	 * @private
	 *
	 * @param {$.pkp.controllers.form.ClientFormHandler} filterForm
	 *  The filter form.
	 * @param {Event} event A "formSubmitted" event.
	 * @param {string} filterData Serialized filter data.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.refreshGridWithFilterHandler_ =
			function(filterForm, event, filterData) {

		// Retrieve the grid from the server and add the
		// filter data as form data.
		$.post(this.fetchGridUrl_, filterData,
				this.callbackWrapper(this.replaceGridResponseHandler_), 'json');
	};


	/**
	 * Add a new row to the grid.
	 *
	 * @private
	 *
	 * @param {HTMLElement} sourceElement The element that
	 *  issued the event.
	 * @param {Event} event The triggering event.
	 * @param {Object} params The request parameters to use to generate
	 *  the new row.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.addRowHandler_ =
			function(sourceElement, event, params) {

		// Retrieve a single new row from the server.
		$.get(this.fetchRowUrl, params,
				this.callbackWrapper(this.replaceElementResponseHandler), 'json');
	};


	/**
	 * Callback to replace a grid's content.
	 *
	 * @private
	 *
	 * @param {Object} ajaxContext The AJAX request context.
	 * @param {Object} jsonData A parsed JSON response object.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.replaceGridResponseHandler_ =
			function(ajaxContext, jsonData) {
		var handledJsonData, $grid, $gridParent, $newGrid,
				isFilterVisible;

		handledJsonData = this.handleJson(jsonData);
		if (handledJsonData !== false) {
			// Get the grid that we're updating
			$grid = this.getHtmlElement();
			$gridParent = $grid.parent();

			isFilterVisible = $grid.find('.filter').is(':visible');

			// Replace the grid content
			this.replaceWith(handledJsonData.content);

			// Update the html element of this handler.
			$newGrid = $('div[id^="' + this.getGridIdPrefix() + '"]', $gridParent);
			this.setHtmlElement($newGrid);

			// Refresh row action event binding.
			this.activateRowActions_();

			if (isFilterVisible) {
				// Open search control again.
				$newGrid.find('.pkp_linkaction_search').click();
			}
		}
	};


	/**
	 * Helper that deletes the row of controls (if present).
	 *
	 * @private
	 *
	 * @param {jQueryObject} $row The row whose matching control row should be
	 *  deleted.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.deleteControlsRow_ =
			function($row) {
		var $controlRow = $('#' + $.pkp.classes.Helper.escapeJQuerySelector(
				/** @type {string} */ ($row.attr('id'))) + '-control-row',
				this.getHtmlElement());

		if ($controlRow.is('tr') && $controlRow.hasClass('row_controls')) {
			this.unbindPartial($controlRow);
			$controlRow.remove();
		}
	};


	/**
	 * Get the control row for the passed the grid row.
	 *
	 * @param {jQueryObject} $gridRow The grid row JQuery object.
	 * @param {jQueryObject=} opt_$context Optional context to get
	 * the control row from.
	 * @return {jQueryObject} The control row JQuery object.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.getControlRowByGridRow =
			function($gridRow, opt_$context) {
		var rowId, controlRowId, $context;

		if (opt_$context === undefined || opt_$context === null) {
			$context = this.getHtmlElement().find('tr');
		} else {
			$context = opt_$context;
		}

		rowId = $gridRow.attr('id');
		controlRowId = rowId + '-control-row';
		return $context.filter('#' +
				$.pkp.classes.Helper.escapeJQuerySelector(controlRowId));
	};


	/**
	 * Helper that attaches any action handlers related to rows.
	 *
	 * @private
	 */
	$.pkp.controllers.grid.GridHandler.prototype.activateRowActions_ =
			function() {

		var $grid = this.getHtmlElement(),
						$gridRows = this.getHtmlElement().find('tr.gridRow').not('.category');

		$grid.find('a.show_extras').unbind('click').bind('click',
				this.callbackWrapper(this.toggleRowActions));
	};


	/**
	 * Apply the effect for hide/show row actions.
	 *
	 * @private
	 *
	 * @param {jQueryObject} $controlRow The control row JQuery object.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.applyToggleRowActionEffect_ =
			function($controlRow) {
		var $row;

		$row = $controlRow.prev().find('td:not(.indent_row)');
		$row = $row.add($controlRow.prev());
		$controlRow.toggle();
	};


	/**
	 * Add a grid feature.
	 *
	 * @private
	 *
	 * @param {string} id Feature id.
	 * @param {$.pkp.classes.features.Feature} $feature The grid
	 * feature to be added.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.addFeature_ =
			function(id, $feature) {
		if (!this.features_) {
			this.features_ = [];
		}
		this.features_[id] = $feature;
	};


	/**
	 * Add grid features.
	 *
	 * @private
	 *
	 * @param {Array.<{JSClass, options}>} features The features options array.
	 */
	$.pkp.controllers.grid.GridHandler.prototype.initFeatures_ =
			function(features) {

		var id, $feature, jsClass;

		for (id in features) {
			// Only initiate features that have a js handler.
			jsClass = features[id].JSClass;
			if (jsClass === null) {
				continue;
			}

			$feature =
					/** @type {$.pkp.classes.features.Feature} */
					($.pkp.classes.Helper.objectFactory(
							jsClass, [this, features[id].options]));

			this.addFeature_(id, $feature);
			this.features_[id].init();
		}
	};


}(jQuery));
