KONTOLODON
/
var
/
www
/
ojs-3.3.0-13
/
lib
/
pkp
/
js
/
controllers
/
listbuilder
/
Nama File / Folder
Size
Action
ListbuilderHandler.js
23.934KB
Hapus
Edit
Rename
/** * @defgroup js_controllers_listbuilder */ /** * @file js/controllers/listbuilder/ListbuilderHandler.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 ListbuilderHandler * @ingroup js_controllers_listbuilder * * @brief Listbuilder row handler. */ (function($) { /** @type {Object} */ $.pkp.controllers.listbuilder = $.pkp.controllers.listbuilder || {}; /** * @constructor * * @extends $.pkp.controllers.grid.GridHandler * * @param {jQueryObject} $listbuilder The listbuilder this handler is * attached to. * @param {Object} options Listbuilder handler configuration. */ $.pkp.controllers.listbuilder.ListbuilderHandler = function($listbuilder, options) { this.parent($listbuilder, options); }; $.pkp.classes.Helper.inherits($.pkp.controllers.listbuilder.ListbuilderHandler, $.pkp.controllers.grid.GridHandler); // // Private properties // /** * The source type (LISTBUILDER_SOURCE_TYPE_...) of the listbuilder. * @private * @type {?number} */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. sourceType_ = null; /** * The "save" URL of the listbuilder (for * LISTBUILDER_SAVE_TYPE_INTERNAL). * @private * @type {?string} */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. saveUrl_ = null; /** * The "save" field name of the listbuilder (for * LISTBUILDER_SAVE_TYPE_EXTERNAL). * @private * @type {?string} */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. saveFieldName_ = null; /** * The "fetch options" URL of the listbuilder (for "select" source type). * @private * @type {?string} */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. fetchOptionsUrl_ = null; /** * Stores the calling context of the edit item click event. * @private * @type {HTMLElement} */ $.pkp.controllers.listbuilder.ListbuilderHandler. prototype.editItemCallingContext_ = null; /** * Flag whether there's still available options to be selected or not. * @private * @type {boolean} */ $.pkp.controllers.listbuilder.ListbuilderHandler. prototype.availableOptions_ = false; // // Protected methods // /** * @inheritDoc */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.initialize = function(options) { this.parent('initialize', options); // Save listbuilder options this.sourceType_ = options.sourceType; this.saveUrl_ = options.saveUrl; this.saveFieldName_ = options.saveFieldName; this.fetchOptionsUrl_ = options.fetchOptionsUrl; this.availableOptions_ = options.availableOptions; // Attach the button handlers var $listbuilder = this.getHtmlElement(); // Use mousedown to avoid two events being triggered at the same time // (click event was being triggered together with blur event from inputs. // That and a syncronous ajax call triggered by those events // handlers, was leading to an error in IE8 and it was freezing // Firefox 13.0). $listbuilder.find('.actions .pkp_linkaction_addItem').mousedown( this.callbackWrapper(this.addItemHandler_)); // Attach the content manipulation handlers this.attachContentHandlers_($listbuilder); // Sign up for notification of form submission. this.bind('formSubmitRequested', this.formSubmitHandler_); // Sign up for notification of form submitted. this.bind('formSubmitted', this.formSubmittedHandler_); }; /** * Get the "save" URL for LISTBUILDER_SAVE_TYPE_INTERNAL. * @private * @return {?string} URL to the "save listbuilder" handler operation. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.getSaveUrl_ = function() { return this.saveUrl_; }; /** * Get the "save" field name for LISTBUILDER_SAVE_TYPE_EXTERNAL. * @private * @return {string} Name of the field to transmit LB contents in. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.getSaveFieldName_ = function() { return /** @type {string} */ (this.saveFieldName_); }; /** * "Save" and close any editing rows in the listbuilder. * @protected */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.closeEdits = function() { var $editedRow = this.getHtmlElement().find('.gridRowEdit:visible'); if ($editedRow.length !== 0) { this.saveRow($editedRow); $editedRow.removeClass('gridRowEdit'); } }; /** * Save the listbuilder. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.save = function() { // Get deletions var deletions = this.getHtmlElement().find('input.deletions').val(), // Get insertions and modifications changes = [], numberOfRows, stringifiedData, saveUrl, saveFieldName, $e, handler = this; this.getHtmlElement().find('.gridRow input.isModified[value="1"]') .each(function(index, v) { var $row = $(v).parents('.gridRow'), params = handler.buildParamsFromInputs_($row.find(':input')); changes.push(params); }); // The listbuilder form validator needs to know if this listbuilder contains // rows or not, so we pass the items number. numberOfRows = this.getRows().length; // Assemble and send to the server stringifiedData = JSON.stringify( {deletions: deletions, changes: changes, numberOfRows: numberOfRows}); saveUrl = this.getSaveUrl_(); if (saveUrl) { // Post the changes to the server using the internal // save handler. $.post(saveUrl, {data: stringifiedData}, this.callbackWrapper(this.saveResponseHandler_, null), 'json'); } else { // Supply the data to an external save handler (e.g. // a form handler) using a hidden field. saveFieldName = this.getSaveFieldName_(); // Try to find and reuse an existing element (if // e.g. a previous attempt was aborted) $e = this.getHtmlElement() .find(':input[type=hidden]') .filter( function() {return $(this).attr('name') == saveFieldName;}) .first(); // If we couldn't find one, create one. if ($e.length === 0) { $e = $('<input type="hidden" />'); $e.attr('name', saveFieldName); this.getHtmlElement().append($e); } // Set the value of the hidden element. $e.attr('value', stringifiedData); } }; /** * Function that will be called to save an edited row. * @param {Object} $row The DOM element representing the row to save. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. saveRow = function($row) { // Retrieve a single new row from the server. // (Avoid IE closure leak using this flag rather than passing // around a DOM element in a closure.) $row.addClass('saveRowResponsePlaceholder'); var params = this.buildParamsFromInputs_($row.find(':input')); params.modify = true; // Flag the row for modification // Use a blocking request to avoid race conditions sometimes // duplicating items, i.e. when editing an existing item after // adding a new one. this.disableControls(); $.ajax({ url: this.getFetchRowUrl(), data: params, success: this.callbackWrapper(this.saveRowResponseHandler_, null), dataType: 'json', async: false }); }; // // Extended methods from GridHandler. // /** * @inheritDoc */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.getEmptyElement = function($element) { // Listbuilders have only one empty element placeholder. return this.getHtmlElement().find('.empty'); }; // // Private Methods // /** * Callback that will be activated when the "add item" icon is clicked * * @private * * @param {Object} callingContext The calling element or object. * @param {Event=} opt_event The triggering event (e.g. a click on * a button. * @return {boolean} Should return false to stop event processing. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.addItemHandler_ = function(callingContext, opt_event) { if (this.availableOptions_) { // Make sure this event will be handled after any other next triggered one, // like blur event that comes from inputs. setTimeout(this.callbackWrapper(function() { // Close any existing edits if necessary this.closeEdits(); this.disableControls(); $.get(this.getFetchRowUrl(), {modify: true}, this.callbackWrapper(this.appendRowResponseHandler_, null), 'json'); }), 0); } return false; }; /** * Callback that will be activated when a delete icon is clicked * * @private * * @param {Object} callingContext The calling element or object. * @param {Event=} opt_event The triggering event (e.g. a click on * a button. * @return {boolean} Should return false to stop event processing. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.deleteItemHandler_ = function(callingContext, opt_event) { // Close any existing edits if necessary this.closeEdits(); var $callingContext = $(callingContext), $targetRow = $callingContext.closest('.gridRow'), $deletions = $callingContext.closest('.pkp_controllers_listbuilder') .find('.deletions'), rowId = $targetRow.find('input[name="rowId"]').val(); // Append the row ID to the deletions list. if (rowId !== undefined) { $deletions.val($deletions.val() + ' ' + rowId); // Notify containing form (if any) about a change this.getHtmlElement().trigger('formChange'); } this.deleteElement(/** @type {jQueryObject} */ ($targetRow)); this.availableOptions_ = true; return false; }; /** * Callback that will be activated when a request for row appending * returns. * * @private * * @param {Object} ajaxContext The AJAX request context. * @param {Object} jsonData A parsed JSON response object. * @return {boolean} Should return false to stop event processing. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. appendRowResponseHandler_ = function(ajaxContext, jsonData) { var processedJsonData = this.handleJson(jsonData), $newRow; if (processedJsonData !== false) { // Show the new input row; hide the "empty" row $newRow = $(processedJsonData.content); this.getHtmlElement().find('.empty').hide().before($newRow); // Attach content handlers and focus this.attachContentHandlers_($newRow); $newRow.addClass('gridRowEdit'); $newRow.find(':input').not('[type="hidden"]').first().focus(); // If this is a select menu listbuilder, load the options if (this.sourceType_ == $.pkp.cons.LISTBUILDER_SOURCE_TYPE_SELECT) { this.disableControls(); $.get(this.fetchOptionsUrl_, {}, this.callbackWrapper(this.fetchOptionsResponseHandler_, null), 'json'); } else { this.enableControls(); } this.callFeaturesHook('addElement', $newRow); } return false; }; /** * Callback that will be activated when a set of options is returned * from the server for a new select control. * * @private * * @param {Object} ajaxContext The AJAX request context. * @param {Object} jsonData A parsed JSON response object. * @return {boolean} Should return false to stop event processing. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. fetchOptionsResponseHandler_ = function(ajaxContext, jsonData) { // Find the currently editable select menu and fill var pjd = this.handleJson(jsonData), $listbuilder = this.getHtmlElement(), selectedValues = [], $selectInput, i, limit, $pulldown, $container, optionsCount, j, $option, label, $optgroup, k, optionsInsideGroup, $lastElement; if (pjd !== false) { // Get the list of already-selected options, to ensure // that we don't offer duplicates. $listbuilder.find('.gridCellDisplay :input').each(function(i, selected) { selectedValues[i] = $(selected).val(); }); // Get the currently available input row's elements $selectInput = $listbuilder.find( '.gridRowEdit:visible .selectMenu:input' ); // For each pulldown (generally 1), add options. for (i = 0, limit = $selectInput.length; i < limit; i++) { // Fetch some useful properties $pulldown = $($selectInput[i]); $container = $pulldown.parents('.gridCellContainer'); // Add the options, noting the currently selected index optionsCount = 0; $pulldown.children().empty(); j = null; for (j in pjd.content[i]) { // Ignore optgroup labels. if (j == $.pkp.cons.LISTBUILDER_OPTGROUP_LABEL) { continue; } if (typeof(pjd.content[i][j]) == 'object') { // Options must go inside an optgroup. // Check if we have optgroup label data. if ( pjd. content[i][$.pkp.cons.LISTBUILDER_OPTGROUP_LABEL] === undefined) { continue; } if (typeof( pjd.content[i][$.pkp.cons.LISTBUILDER_OPTGROUP_LABEL] ) != 'object') { continue; } label = pjd.content[i][$.pkp.cons.LISTBUILDER_OPTGROUP_LABEL][j]; if (!label) { continue; } $optgroup = $('<optgroup></optgroup>'); $optgroup.attr('label', label); $pulldown.append($optgroup); k = null; optionsInsideGroup = 0; for (k in pjd.content[i][j]) { // Populate the optgroup. $option = this.populatePulldown_($optgroup, selectedValues, pjd.content[i][j][k], k); if ($option) { optionsCount++; optionsInsideGroup++; } } // Avoid inserting optgroups that have no option. if (optionsInsideGroup === 0) { $optgroup.remove(); } } else { // Just insert the current option. $option = this.populatePulldown_($pulldown, selectedValues, pjd.content[i][j], j); if ($option) { optionsCount++; } } } $lastElement = $option; // If only one element is available, select it. if (optionsCount === 1 && $lastElement) { $lastElement.attr('selected', 'selected'); this.availableOptions_ = false; } // If no options are available for this select menu, // hide the input to prevent empty dropdown. if (optionsCount === 0) { $container.find('.gridCellDisplay').show(); $container.find('.gridCellEdit').hide(); } } } this.enableControls(); return false; }; /** * Populate the pulldown with options. * @private * @param {jQueryObject} $element The element to be populated. * Can be a pulldown or an optgroup inside the pulldonw. * @param {Object} selectedValues Current listbuilder * selected values. * @param {string} optionText The text to populate the pulldown with. * @param {string} optionValue The key to populate the pulldown with. * @return {Object|boolean} Return the inserted option or false. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. populatePulldown_ = function( $element, selectedValues, optionText, optionValue) { var $container = $element.parents('.gridCellContainer'), currentIndex = $container.find('.gridCellDisplay :input').val(), isDuplicate = false, k, $option; // Check to see if this option is already in the LB. if (optionValue != currentIndex) { // If it's the current row, don't consider it a duplicate for (k = 0; k < selectedValues.length; k++) { if (selectedValues[k] == optionValue) { isDuplicate = true; } } } if (!isDuplicate) { // Create and populate the option node $option = $('<option/>'); $option.attr('value', optionValue); $option.text(optionText); if (optionValue == currentIndex) { $option.attr('selected', 'selected'); } $element.append($option); return $option; } else { return false; } }; /** * Callback that will be activated when a row is clicked for editing * * @private * * @param {HTMLElement} callingContext The calling element or object. * @param {Event=} opt_event The triggering event (e.g. a click on * a button. * @return {boolean} Should return false to stop event processing. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype.editItemHandler_ = function(callingContext, opt_event) { // Close any existing edits if necessary this.closeEdits(); this.editItemCallingContext_ = callingContext; // Show inputs; hide display. IE8 is slow, and it will execute // this before the timeout setted in inputBlurHandler_. Insert this // code inside a timeout too to avoid closing inputs that are not // meant to. setTimeout(this.callbackWrapper(function() { var $targetRow = $(this.editItemCallingContext_).closest('.gridRow'); $targetRow.addClass('gridRowEdit'); $targetRow.find(':input').not('[type="hidden"]').first().focus(); // If this is a select menu listbuilder, load the options if (this.sourceType_ == $.pkp.cons.LISTBUILDER_SOURCE_TYPE_SELECT) { this.disableControls(); $.get(this.fetchOptionsUrl_, {}, this.callbackWrapper(this.fetchOptionsResponseHandler_, null), 'json'); } }), 0); return false; }; /** * Helper function to turn a row into an array of parameters used * to generate the DOM representation of that row when bounced * off the server. * * @private * * @param {Object} $inputs The grid inputs to mine for parameters. * @return {Object} A name: value association of relevant parameters. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. buildParamsFromInputs_ = function($inputs) { var params = {}; $.each($inputs.serializeArray(), function(k, v) { var name = v.name, value = v.value; params[name] = params[name] === undefined ? value : $.isArray(params[name]) ? params[name].concat(value) : [params[name], value]; }); return params; }; /** * Callback that will be activated upon keystroke in a new input field * to check for a <cr> (acts as tab-to-next, or if no next, submit). * * @private * * @param {HTMLElement} callingContext The calling element or object. * @param {Event=} opt_event The triggering event. * @return {boolean} Should return false to stop event processing. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. inputKeystrokeHandler_ = function(callingContext, opt_event) { var CR_KEY = 13, TAB_KEY = 9, $target, $row, $inputs, i; if (opt_event.which == CR_KEY) { $target = $(callingContext); $row = $target.parents('.gridRow'); $inputs = $row.find(':input:visible'); i = $inputs.index($target); if ($inputs.length == i + 1) { this.saveRow($row); return false; // Prevent default } else { // Not the last field. Tab to the next. $inputs[i + 1].focus(); return false; // Prevent default } } return true; }; /** * Callback that will be activated when a new/modifying row's input * field is blurred to check whether or not to save the row. * * @private * * @param {HTMLElement} callingContext The calling element or object. * @param {Event=} opt_event The triggering event. * @return {boolean} Should return false to stop event processing. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. inputBlurHandler_ = function(callingContext, opt_event) { // Flag currently selected input using a CSS class. (Don't // want to pass it into the closure because of the IE memory // leak bug.) $(callingContext).closest('.gridRow').addClass('editingRowPlaceholder'); // Check to see whether the row has lost focus after this event has // been processed. setTimeout(this.callbackWrapper(function() { var $editingRow = $('.editingRowPlaceholder'), found = false; $editingRow.find(':input').each(function(index, elem) { if (elem === document.activeElement) { found = true; } }); // Clean up extra placeholder class. $editingRow.removeClass('editingRowPlaceholder'); // If the focused element isn't within the current row, save. if (!found) { this.closeEdits(); } }), 0); return true; }; /** * Callback to replace a grid row's content. * * @private * * @param {Object} ajaxContext The AJAX request context. * @param {Object} jsonData A parsed JSON response object. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. saveRowResponseHandler_ = function(ajaxContext, jsonData) { var processedJsonData = this.handleJson(jsonData), $newContent, rowId; if (processedJsonData !== false) { // Unfortunately we can't use a closure to get this from // the calling context. Use a class flag "saveRowResponsePlaceholder". // (Risks IE closure/DOM element memory leak.) $newContent = $(processedJsonData.content); // Store current row id. rowId = /** @type {string} */ (this.getHtmlElement() .find('.saveRowResponsePlaceholder').attr('id')); // Add to the DOM this.getHtmlElement().find('.saveRowResponsePlaceholder'). replaceWith($newContent); // Make sure row id won't change. $newContent.attr('id', rowId); // Attach handlers for content manipulation this.attachContentHandlers_($newContent); this.callFeaturesHook('replaceElement', $newContent); } // Ensure that containing forms are notified of the changed data this.getHtmlElement().trigger('formChange'); this.enableControls(); }; /** * Callback after a save response returns from the server. * * @private * * @param {Object} ajaxContext The AJAX request context. * @param {Object} jsonData A parsed JSON response object. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. saveResponseHandler_ = function(ajaxContext, jsonData) { // Noop }; /** * Attach content handlers to all "click-to-edit" content within * the provided context. * * @private * * @param {Object} $context The JQuery object to search for attachables. */ $.pkp.controllers.listbuilder.ListbuilderHandler.prototype. attachContentHandlers_ = function($context) { // Attach click handler for text fields and select menus $context.find('.gridCellDisplay').click( this.callbackWrapper(this.editItemHandler_)); // Attach keypress handler for text fields $context.find(':input') .keypress(this.callbackWrapper(this.inputKeystrokeHandler_)) .blur(this.callbackWrapper(this.inputBlurHandler_)); // Attach deletion handler $context.find('.pkp_linkaction_delete').click( this.callbackWrapper(this.deleteItemHandler_)); }; /** * Save the Listbuilder's contents upon a "form submitted" event. * @private * * @param {$.pkp.controllers.form.AjaxFormHandler} callingForm The form * that triggered the event. * @param {Event} event The event. * @return {boolean} False if the form submission should abort. */ $.pkp.controllers.listbuilder.ListbuilderHandler. prototype.formSubmitHandler_ = function(callingForm, event) { // Save the contents this.save(); // Prevent the submission of LB elements to the parent form // (except potentially for :input[name='getSaveFieldName()']) this.getHtmlElement().find('.gridRow :input').attr('disabled', 'disabled'); // Continue the default (form submit) behavior return true; }; /** * Enable deactivated inputs. * @private * * @param {$.pkp.controllers.form.AjaxFormHandler} callingForm The form * that triggered the event. * @param {Event} event The event. */ $.pkp.controllers.listbuilder.ListbuilderHandler. prototype.formSubmittedHandler_ = function(callingForm, event) { this.getHtmlElement().find('.gridRow :input').removeAttr('disabled'); }; /** * Disable the add_* links and show the spinner before making AJAX calls. */ $.pkp.controllers.listbuilder.ListbuilderHandler. prototype.disableControls = function() { this.getHtmlElement(). find('span[class="options"] > a[id*="addItem"]').unbind('mousedown'); this.getHtmlElement().find('span[class="options"] > a[id*="addItem"]') .mousedown(function() {return false;}); this.getHtmlElement().find('.h3').addClass('spinner'); }; /** * Re-enable the add_* links and hide the spinner. */ $.pkp.controllers.listbuilder.ListbuilderHandler. prototype.enableControls = function() { // rebind our 'click' handler so we can add another item // if needed this.getHtmlElement().find('span[class="options"] > a[id*="addItem"]'). mousedown(this.callbackWrapper(this.addItemHandler_)); this.getHtmlElement().find('.h3').removeClass('spinner'); }; }(jQuery));