/** * @file js/classes/features/OrderItemsFeature.js * * Copyright (c) 2014-2018 Simon Fraser University * Copyright (c) 2000-2018 John Willinsky * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING. * * @class OrderItemsFeature * @ingroup js_classes_features * * @brief Base feature class for ordering grid items. */ (function($) { /** * @constructor * * @param {jQueryObject} gridHandler The handler of * the grid element that this feature is attached to. * @param {Object} options Configuration of this feature. * @extends $.pkp.classes.features.Feature */ $.pkp.classes.features.OrderItemsFeature = function(gridHandler, options) { this.parent(gridHandler, options); this.$orderButton_ = $('.pkp_linkaction_orderItems', this.getGridHtmlElement()); this.$finishControl_ = $('.order_finish_controls', this.getGridHtmlElement()); if (this.$orderButton_.length === 0) { // No order button, it will always stay in ordering mode. this.isOrdering = true; } this.itemsOrder = []; }; $.pkp.classes.Helper.inherits( $.pkp.classes.features.OrderItemsFeature, $.pkp.classes.features.Feature); // // Protected properties // /** * Item sequence. * @protected * @type {Array} */ $.pkp.classes.features.OrderItemsFeature.prototype.itemsOrder = null; /** * Flag to control if user is ordering items. * @protected * @type {boolean} */ $.pkp.classes.features.OrderItemsFeature.prototype.isOrdering = false; // // Private properties. // /** * Initiate ordering state button. * @private * @type {jQueryObject} */ $.pkp.classes.features.OrderItemsFeature.prototype.$orderButton_ = null; /** * Cancel ordering state button. * @private * @type {jQueryObject} */ $.pkp.classes.features.OrderItemsFeature.prototype.$cancelButton_ = null; /** * Save ordering state button. * @private * @type {jQueryObject} */ $.pkp.classes.features.OrderItemsFeature.prototype.$saveButton_ = null; /** * Ordering finish control. * @private * @type {jQueryObject} */ $.pkp.classes.features.OrderItemsFeature.prototype.$finishControl_ = null; // // Getters and setters. // /** * Get the order button. * @return {jQueryObject} The order button JQuery object. */ $.pkp.classes.features.OrderItemsFeature.prototype.getOrderButton = function() { return this.$orderButton_; }; /** * Get the finish control. * @return {jQueryObject} The JQuery "finish" control. */ $.pkp.classes.features.OrderItemsFeature.prototype.getFinishControl = function() { return this.$finishControl_; }; /** * Get save order button. * * @return {jQueryObject} The "save order" JQuery object. */ $.pkp.classes.features.OrderItemsFeature.prototype.getSaveOrderButton = function() { return this.getFinishControl().find('.saveButton'); }; /** * Get cancel order link. * * @return {jQueryObject} The "cancel order" JQuery control. */ $.pkp.classes.features.OrderItemsFeature.prototype.getCancelOrderButton = function() { return this.getFinishControl().find('.cancelFormButton'); }; /** * Get the move item row action element selector. * @return {string} Return the element selector. */ $.pkp.classes.features.OrderItemsFeature.prototype. getMoveItemRowActionSelector = function() { return '.orderable .pkp_linkaction_moveItem'; }; /** * Get the css classes used to stylize the ordering items. * @return {string} CSS classes. */ $.pkp.classes.features.OrderItemsFeature.prototype.getMoveItemClasses = function() { return 'pkp_helpers_moveicon ordering'; }; // // Public template methods. // /** * Called every time user start dragging an item. * @param {jQueryObject} contextElement The element this event occurred for. * @param {Event} event The drag/drop event. * @param {Object} ui Object with data related to the event elements. */ $.pkp.classes.features.OrderItemsFeature.prototype.dragStartCallback = function(contextElement, event, ui) { // The default implementation does nothing. }; /** * Called every time user stop dragging an item. * @param {jQueryObject} contextElement The element this event occurred for. * @param {Event} event The drag/drop event. * @param {Object} ui Object with data related to the event elements. */ $.pkp.classes.features.OrderItemsFeature.prototype.dragStopCallback = function(contextElement, event, ui) { // The default implementation does nothing. }; /** * Called every time sequence is changed. * @param {jQueryObject} contextElement The element this event occurred for. * @param {Event} event The drag/drop event. * @param {Object} ui Object with data related to the event elements. */ $.pkp.classes.features.OrderItemsFeature.prototype.updateOrderCallback = function(contextElement, event, ui) { // The default implementation does nothing. }; // // Extended methods from Feature // /** * @inheritDoc */ $.pkp.classes.features.OrderItemsFeature.prototype.init = function() { this.addOrderingClassToRows(); this.toggleMoveItemRowAction(this.isOrdering); this.getGridHtmlElement().find('div.order_message').hide(); this.toggleOrderLink_(); if (this.isOrdering) { this.setupSortablePlugin(); } }; /** * @inheritDoc */ $.pkp.classes.features.OrderItemsFeature.prototype.addFeatureHtml = function($gridElement, options) { var castOptions = /** @type {{orderFinishControls: string?, orderMessage: string?}} */ (options), $orderFinishControls, orderMessageHtml, $gridRows; if (castOptions.orderFinishControls !== undefined) { $orderFinishControls = $(castOptions.orderFinishControls); $gridElement.find('table').last().after($orderFinishControls); $orderFinishControls.hide(); } if (castOptions.orderMessage !== undefined) { orderMessageHtml = castOptions.orderMessage; $gridRows = $gridElement.find('.gridRow').filter(function(index, element) { return !Boolean($(this).find('a.pkp_linkaction_moveItem').length); }); $gridRows.find('td:first-child').prepend(orderMessageHtml); } this.updateOrderLinkVisibility_(); }; // // Protected template methods. // /** * Add orderable class to grid rows. */ $.pkp.classes.features.OrderItemsFeature.prototype.addOrderingClassToRows = function() { // Add ordering class to grid rows. var $gridRows = this.gridHandler.getRows().filter(function(index, element) { return $(this).find('a.pkp_linkaction_moveItem').length; }); $gridRows.addClass('orderable'); }; /** * Setup the sortable plugin. Must be implemented in subclasses. */ $.pkp.classes.features.OrderItemsFeature.prototype.setupSortablePlugin = function() { // Default implementation does nothing. }; /** * Called every time storeOrder is called. This is a chance to subclasses * execute operations with each row that has their sequence being saved. * @param {number} index The current row index position inside the rows * jQuery object. * @param {jQueryObject} $row Row for which to store the sequence. */ $.pkp.classes.features.OrderItemsFeature.prototype.storeRowOrder = function(index, $row) { // The default implementation does nothing. }; // // Protected methods. // /** * Initiate ordering button click event handler. * @return {boolean} Always returns false. */ $.pkp.classes.features.OrderItemsFeature.prototype.clickOrderHandler = function() { this.gridHandler.hideAllVisibleRowActions(); this.storeOrder(this.gridHandler.getRows()); this.toggleState(true); return false; }; /** * Save order handler. * @return {boolean} Return false to stop click event processing. */ $.pkp.classes.features.OrderItemsFeature.prototype.saveOrderHandler = function() { var $rows; this.gridHandler.updateControlRowsPosition(); this.unbindOrderFinishControlsHandlers_(); $rows = this.gridHandler.getRows(); this.storeOrder($rows); return false; }; /** * Cancel ordering action click event handler. * @return {boolean} Always returns false. */ $.pkp.classes.features.OrderItemsFeature.prototype.cancelOrderHandler = function() { this.gridHandler.resequenceRows(this.itemsOrder); this.toggleState(false); return false; }; /** * Execute all operations necessary to change the state of the * ordering process (enabled or disabled). * @param {boolean} isOrdering Is ordering process active? */ $.pkp.classes.features.OrderItemsFeature.prototype.toggleState = function(isOrdering) { this.isOrdering = isOrdering; this.toggleGridLinkActions_(); this.toggleOrderLink_(); this.toggleFinishControl_(); this.toggleItemsDragMode(); this.setupSortablePlugin(); this.setupNonOrderableMessage_(); }; /** * Set rows sequence store, using * the sequence of the passed items. * * @param {jQueryObject} $rows The rows to be used to get the sequence * information. */ $.pkp.classes.features.OrderItemsFeature.prototype.storeOrder = function($rows) { var index, limit, $row, elementId; this.itemsOrder = []; for (index = 0, limit = $rows.length; index < limit; index++) { $row = $($rows[index]); elementId = $row.attr('id'); this.itemsOrder.push(elementId); // Give a chance to subclasses do extra operations to store // the current row order. this.storeRowOrder(index, $row); } }; /** * Enable/disable the items drag mode. */ $.pkp.classes.features.OrderItemsFeature.prototype.toggleItemsDragMode = function() { var isOrdering = this.isOrdering, $rows = this.gridHandler.getRows(), $orderableRows = $rows.filter('.orderable'), moveClasses = this.getMoveItemClasses(); if (isOrdering) { $orderableRows.addClass(moveClasses); } else { $orderableRows.removeClass(moveClasses); } this.toggleMoveItemRowAction(isOrdering); }; /** * Apply (disabled or enabled) the sortable plugin on passed elements. * @param {jQueryObject} $container The element that contain all the orderable * items. * @param {string} itemsSelector The jQuery selector for orderable items. * @param {Object?} extraParams Optional set of extra parameters for sortable. */ $.pkp.classes.features.OrderItemsFeature.prototype.applySortPlgOnElements = function($container, itemsSelector, extraParams) { var isOrdering = this.isOrdering, dragStartCallback = this.gridHandler.callbackWrapper( this.dragStartCallback, this), dragStopCallback = this.gridHandler.callbackWrapper( this.dragStopCallback, this), orderItemCallback = this.gridHandler.callbackWrapper( this.updateOrderCallback, this), config = { disabled: !isOrdering, items: itemsSelector, activate: dragStartCallback, deactivate: dragStopCallback, update: orderItemCallback, tolerance: 'pointer'}; if (typeof extraParams === 'object') { config = $.extend(true, config, extraParams); } $container.sortable(config); }; /** * Get the data element id of all rows inside the passed * container, in the current order. * @param {jQueryObject} $rowsContainer The element that contains the rows * that will be used to retrieve the id. * @return {Array} A sequence array with data element ids as values. */ $.pkp.classes.features.OrderItemsFeature.prototype.getRowsDataId = function($rowsContainer) { var index, rowDataIds = [], $row, rowDataId; for (index in this.itemsOrder) { $row = $('#' + this.itemsOrder[index], $rowsContainer); if ($row.length < 1) { continue; } rowDataId = this.gridHandler.getRowDataId($row); rowDataIds.push(rowDataId); } return rowDataIds; }; /** * Show/hide the move item row action (position left). * @param {boolean} enable New enable state. */ $.pkp.classes.features.OrderItemsFeature.prototype.toggleMoveItemRowAction = function(enable) { var $grid = this.getGridHtmlElement(), $actionsContainer = $('div.row_actions', $grid), allLinksButMoveItemSelector = 'a:not(' + this.getMoveItemRowActionSelector() + ')', $actions = $actionsContainer.find(allLinksButMoveItemSelector), $moveItemRowAction = $(this.getMoveItemRowActionSelector(), $grid), $rowActionsContainer, $rowActions; if (enable) { $actions.addClass('pkp_helpers_display_none'); $moveItemRowAction.show(); // Make sure row actions div is visible. this.gridHandler.showRowActionsDiv(); } else { $actions.removeClass('pkp_helpers_display_none'); $rowActionsContainer = $('.gridRow div.row_actions', $grid); $rowActions = $rowActionsContainer. find(allLinksButMoveItemSelector); if ($rowActions.length === 0) { // No link action to show, hide row actions div. this.gridHandler.hideRowActionsDiv(); } $moveItemRowAction.hide(); } }; // // Hooks implementation. // /** * @inheritDoc */ $.pkp.classes.features.OrderItemsFeature.prototype.addElement = function($element) { this.addOrderingClassToRows(); this.toggleItemsDragMode(); return false; }; /** * @inheritDoc */ $.pkp.classes.features.OrderItemsFeature.prototype.replaceElement = function($content) { this.addOrderingClassToRows(); this.toggleItemsDragMode(); return false; }; /** * @inheritDoc */ $.pkp.classes.features.OrderItemsFeature.prototype. replaceElementResponseHandler = function(handledJsonData) { this.updateOrderLinkVisibility_(); this.setupNonOrderableMessage_(); return false; }; // // Private helper methods. // /** * Make sure that the order action visibility state is correct, * based on the grid rows number. * @private */ $.pkp.classes.features.OrderItemsFeature.prototype. updateOrderLinkVisibility_ = function() { var $orderLink = $('.pkp_linkaction_orderItems', this.getGridHtmlElement()); if (this.gridHandler.getRows().length <= 1) { $orderLink.hide(); } else { $orderLink.show(); } }; /** * Set the state of the grid link actions, based on current ordering state. * @private */ $.pkp.classes.features.OrderItemsFeature.prototype.toggleGridLinkActions_ = function() { var isOrdering = this.isOrdering, // We want to enable/disable all link actions, except this // features controls. $gridLinkActions = $('.pkp_controllers_linkAction', this.getGridHtmlElement()).not( this.getMoveItemRowActionSelector()).not( this.getOrderButton()).not( this.getFinishControl().find('*')); this.gridHandler.changeLinkActionsState(!isOrdering, $gridLinkActions); }; /** * Enable/disable the order link action. * @private */ $.pkp.classes.features.OrderItemsFeature.prototype.toggleOrderLink_ = function() { if (this.isOrdering) { this.$orderButton_.unbind('click'); this.$orderButton_.attr('disabled', 'disabled'); } else { var clickHandler = this.gridHandler.callbackWrapper( this.clickOrderHandler, this); this.$orderButton_.click(clickHandler); this.$orderButton_.removeAttr('disabled'); } }; /** * Show/hide the ordering process finish control, based * on the current ordering state. * @private */ $.pkp.classes.features.OrderItemsFeature.prototype.toggleFinishControl_ = function() { if (this.isOrdering) { this.bindOrderFinishControlsHandlers_(); this.getFinishControl().slideDown(300); } else { this.unbindOrderFinishControlsHandlers_(); this.getFinishControl().slideUp(300); } }; /** * Bind event handlers to the controls that finish the * ordering action (save and cancel). * @private */ $.pkp.classes.features.OrderItemsFeature.prototype. bindOrderFinishControlsHandlers_ = function() { var $saveButton = this.getSaveOrderButton(), $cancelLink = this.getCancelOrderButton(), cancelLinkHandler = this.gridHandler.callbackWrapper( this.cancelOrderHandler, this), saveButtonHandler = this.gridHandler.callbackWrapper( this.saveOrderHandler, this); $saveButton.click(saveButtonHandler); $cancelLink.click(cancelLinkHandler); }; /** * Unbind event handlers from the controls that finish the * ordering action (save and cancel). * @private */ $.pkp.classes.features.OrderItemsFeature.prototype. unbindOrderFinishControlsHandlers_ = function() { this.getSaveOrderButton().unbind('click'); this.getCancelOrderButton().unbind('click'); }; /** * Toggle hover action to show message for non orderable * grid rows. * @private */ $.pkp.classes.features.OrderItemsFeature.prototype. setupNonOrderableMessage_ = function() { if (this.isOrdering) { this.gridHandler.getRows().hover(function() { $(this).find('div.order_message').toggle(); }); } else { this.gridHandler.getRows().unbind('mouseenter mouseleave'); } }; /** @param {jQuery} $ jQuery closure. */ }(jQuery));