first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
@@ -0,0 +1,247 @@
/**
* @file js/controllers/AutocompleteHandler.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 AutocompleteHandler
* @ingroup js_controllers
*
* @brief PKP autocomplete handler (extends the functionality of the
* jqueryUI autocomplete)
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $autocompleteField the wrapped HTML input element.
* @param {Object} options options to be passed
* into the jqueryUI autocomplete plugin.
*/
$.pkp.controllers.AutocompleteHandler = function($autocompleteField, options) {
var autocompleteOptions, opt;
this.parent($autocompleteField, options);
// Get the URL passed in
this.sourceUrl_ = options.sourceUrl;
options.sourceUrl = undefined;
this.disableSync_ = options.disableSync;
// Create autocomplete settings.
opt = {};
opt.source = this.callbackWrapper(this.fetchAutocomplete);
// Set the anchor where to append the suggestions menu
opt.appendTo = '#' + $autocompleteField.attr('id');
autocompleteOptions = $.extend({ },
this.self('DEFAULT_PROPERTIES_'), opt, options);
// Get the text input inside of this Div.
this.textInput = $autocompleteField.find(':text');
if (!this.disableSync_) {
this.textInput.keyup(this.callbackWrapper(this.synchronizeFields_));
}
// Create the autocomplete field with the jqueryUI plug-in.
this.textInput.autocomplete(autocompleteOptions);
// Get the text input inside of this Div.
this.hiddenInput_ = $autocompleteField.find('input:hidden');
this.bind('autocompleteselect', this.itemSelected);
this.bind('autocompletefocus', this.itemFocused);
this.textInput.blur(this.callbackWrapper(this.textInputBlurHandler_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.AutocompleteHandler, $.pkp.classes.Handler);
//
// Private static properties
//
/**
* Whether or not to disable syncing the text and hidden field.
* @private
* @type {?string}
*/
$.pkp.controllers.AutocompleteHandler.disableSync_ = null;
/**
* Default options
* @private
* @type {Object}
* @const
*/
$.pkp.controllers.AutocompleteHandler.DEFAULT_PROPERTIES_ = {
// General settings
minLength: 2
};
//
// Private properties
//
/**
* The hidden input inside the autocomplete div that holds the value.
* @private
* @type {jQueryObject}
*/
$.pkp.controllers.AutocompleteHandler.prototype.hiddenInput_ = null;
/**
* The URL to be used for autocomplete
* @private
* @type {?string}
*/
$.pkp.controllers.AutocompleteHandler.prototype.sourceUrl_ = null;
//
// Protected properties
//
/**
* The text input inside the autocomplete div that holds the label.
* @protected
* @type {jQueryObject}
*/
$.pkp.controllers.AutocompleteHandler.prototype.textInput = null;
//
// Public Methods
//
/**
* Handle event triggered by selecting an autocomplete item
*
* @param {HTMLElement} autocompleteElement The element that triggered
* the event.
* @param {Event} event The triggered event.
* @param {Object} ui The tabs ui data.
* @return {boolean} Return value to be passed back
* to jQuery.
*/
$.pkp.controllers.AutocompleteHandler.prototype.itemSelected =
function(autocompleteElement, event, ui) {
var $hiddenInput, $textInput;
$hiddenInput = this.hiddenInput_;
$textInput = this.textInput;
// only update the text field if the item has a value
// this allows us to return a 'no items' label with
// an empty value.
if (ui.item.value !== '') {
$hiddenInput.val(ui.item.value);
$textInput.val(ui.item.label);
}
return false;
};
/**
* Handle event triggered by moving through the autocomplete suggestions.
* The default behaviour is to insert the value in the text field. This
* handler inserts the label instead.
*
* @param {HTMLElement} autocompleteElement The element that triggered
* the event.
* @param {Event} event The triggered event.
* @param {Object} ui The tabs ui data.
* @return {boolean} Return value to be passed back
* to jQuery.
*/
$.pkp.controllers.AutocompleteHandler.prototype.itemFocused =
function(autocompleteElement, event, ui) {
var $textInput;
$textInput = this.textInput;
if (ui.item.value !== '') {
$textInput.val(ui.item.label);
}
return false;
};
/**
* Fetch autocomplete results.
* @param {HTMLElement} callingElement The calling HTML element.
* @param {Object} request The autocomplete search request.
* @param {Function} response The response handler function.
*/
$.pkp.controllers.AutocompleteHandler.prototype.fetchAutocomplete =
function(callingElement, request, response) {
var $textInput;
$textInput = this.textInput;
$textInput.addClass('spinner');
$.post(this.getAutocompleteUrl(), { term: request.term }, function(data) {
$textInput.removeClass('spinner');
response(data.content);
}, 'json');
};
/**
* Get the autocomplete Url
* @return {?string} Autocomplete URL.
*/
$.pkp.controllers.AutocompleteHandler.prototype.getAutocompleteUrl =
function() {
return this.sourceUrl_;
};
/**
* Set the autocomplete url
* @param {string} url Autocomplete URL.
*/
$.pkp.controllers.AutocompleteHandler.prototype.setAutocompleteUrl =
function(url) {
this.sourceUrl_ = url;
};
//
// Private methods.
//
/**
* Text input element blur handler.
* @param {HTMLElement} autocompleteElement The element that triggered
* the event.
* @param {Event} event The blur event.
* @param {Object} ui UI.
* @private
*/
$.pkp.controllers.AutocompleteHandler.prototype.textInputBlurHandler_ =
function(autocompleteElement, event, ui) {
// Make sure we clean the text input if user selected no option
// from the available ones but leaved some text behind. This
// is needed to avoid bad form validation and to make it clear to
// users that they need to select an option.
if (this.hiddenInput_.val() === '') {
this.textInput.val('');
}
};
/**
* Synchronize the input field and the hidden field.
* @private
*/
$.pkp.controllers.AutocompleteHandler.prototype.synchronizeFields_ =
function() {
this.hiddenInput_.val(String(this.textInput.val()));
};
}(jQuery));
@@ -0,0 +1,80 @@
/**
* @file js/controllers/EditorialActionsHandler.js
*
* Copyright (c) 2014-2018 Simon Fraser University
* Copyright (c) 2000-2018 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class EditorialActionsHandler
* @ingroup js_controllers
*
* @brief A handler for the editorial actions button in the workflow
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $element The outer <div> element
* @param {Object} options Handler options.
*/
$.pkp.controllers.EditorialActionsHandler = function($element, options) {
this.parent($element, options);
$element.find('.pkp_workflow_change_decision')
.click(this.callbackWrapper(this.showActions_));
$element.find('[data-decision]')
.click(this.callbackWrapper(this.emitRevisionDecision_));
$element.find('[data-recommendation]')
.click(this.callbackWrapper(this.emitRevisionRecommendation_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.EditorialActionsHandler, $.pkp.classes.Handler);
//
// Private methods
//
/**
* Show the editorial actions
* @private
* @param {HTMLElement} sourceElement The clicked link.
* @param {Event} event The triggered event (click).
*/
$.pkp.controllers.EditorialActionsHandler.prototype.showActions_ =
function(sourceElement, event) {
this.getHtmlElement().find('.pkp_workflow_change_decision').hide();
this.getHtmlElement().find('.pkp_workflow_decisions_options')
.removeClass('pkp_workflow_decisions_options_hidden');
};
/**
* Emit an event when a request revisions decision is initiated
* @private
* @param {HTMLElement} sourceElement The clicked link.
* @param {Event} event The triggered event (click).
*/
$.pkp.controllers.EditorialActionsHandler.prototype.emitRevisionDecision_ =
function(sourceElement, event) {
var $el = $(sourceElement);
pkp.eventBus.$emit('decision:revisions', $el.data('reviewRoundId'));
};
/**
* Emit an event when a request revisions recommendation is initiated
* @private
* @param {HTMLElement} sourceElement The clicked link.
* @param {Event} event The triggered event (click).
*/
$.pkp.controllers.EditorialActionsHandler.prototype.
emitRevisionRecommendation_ = function(sourceElement, event) {
var $el = $(sourceElement);
pkp.eventBus.$emit('recommendation:revisions', $el.data('reviewRoundId'));
};
}(jQuery));
@@ -0,0 +1,118 @@
/**
* @file js/controllers/ExtrasOnDemandHandler.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 ExtrasOnDemandHandler
* @ingroup js_controllers
*
* @brief A basic handler for extras on demand UI pattern.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $widgetWrapper An HTML element that contains the
* widget.
* @param {Object} options Handler options.
*/
$.pkp.controllers.ExtrasOnDemandHandler = function($widgetWrapper, options) {
this.parent($widgetWrapper, options);
$('.toggleExtras', $widgetWrapper).click(
this.callbackWrapper(this.toggleExtras));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.ExtrasOnDemandHandler, $.pkp.classes.Handler);
//
// Public methods
//
/**
* Event handler that is called when toggle extras div is clicked.
*
* @param {HTMLElement} toggleExtras The div that is clicked to toggle extras.
* @param {Event} event The triggering event.
*/
$.pkp.controllers.ExtrasOnDemandHandler.prototype.toggleExtras =
function(toggleExtras, event) {
var $widgetWrapper = this.getHtmlElement(),
$scrollable;
event.preventDefault();
$widgetWrapper.toggleClass('active');
if ($widgetWrapper.hasClass('active')) {
// Identify if there is a scrollable parent.
$scrollable = $widgetWrapper.closest('.scrollable');
if ($scrollable.length > 0) {
// Scroll the parent so that all extra content in
// extras container is visible.
this.scrollToMakeVisible_($widgetWrapper, $scrollable);
}
}
};
//
// Private methods
//
/**
* Scroll a scrollable element to make the
* given content element visible. The content element
* must be a descendant of a scrollable
* element (needs to have class "scrollable").
*
* NB: This method depends on the position() method
* to refer to the same parent element for both the
* content element and the scrollable element.
*
* @private
*
* @param {jQueryObject} $widgetWrapper The element to be made visible.
* @param {Array|jQueryObject} $scrollable The parent scrollable element.
*/
$.pkp.controllers.ExtrasOnDemandHandler.prototype.scrollToMakeVisible_ =
function($widgetWrapper, $scrollable) {
var extrasWidgetTop, scrollingWidgetTop, currentScrollingTop,
hiddenPixels, newScrollingTop;
extrasWidgetTop = $widgetWrapper.position().top;
scrollingWidgetTop = $scrollable.position().top;
currentScrollingTop = parseInt($scrollable.scrollTop(), 10);
// Do we have to scroll down or scroll up?
if (extrasWidgetTop > scrollingWidgetTop) {
// Consider scrolling down...
// Calculate the number of hidden pixels of the child
// element within the scrollable element.
hiddenPixels = Math.ceil(extrasWidgetTop +
$widgetWrapper.height() - $scrollable.height());
// Scroll down if parts or all of this widget are hidden.
if (hiddenPixels > 0) {
$scrollable.scrollTop(currentScrollingTop + hiddenPixels);
}
} else {
// Scroll up...
// Calculate the new scrolling top.
newScrollingTop = Math.max(Math.floor(
currentScrollingTop + extrasWidgetTop - scrollingWidgetTop), 0);
// Set the new scrolling top.
$scrollable.scrollTop(newScrollingTop);
}
};
}(jQuery));
+368
View File
@@ -0,0 +1,368 @@
/**
* @file js/controllers/HelpPanelHandler.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 HelpPanelHandler
* @ingroup js_controllers
*
* @brief A handler for the fly-out contextual help panel.
*
* Listens: pkp.HelpPanel.Open
* Listens: pkp.HelpPanel.Close
* Emits: pkp.HelpPanel.Open
* Emits: pkp.HelpPanel.Close
*
* This handler expects to be be attached to an element which matches the
* following base markup. There should only be one help panel on any page.
*
* <div id="pkpHelpPanel" tabindex="-1">
* <div>
* <!-- This handler should only ever interact with the .content div. -->
* <div class="content"></div>
* <button class="pkpCloseHelpPanel"></button>
* </div>
* </div>
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $element The outer <div> element
* @param {Object} options Handler options.
*/
$.pkp.controllers.HelpPanelHandler = function($element, options) {
this.parent($element, {});
// Let help link events bubble up to the body tag. This way we only
// register one listener once per page load. We won't need to worry
// about content getting swapped out on the page either, as any new
// help links loaded will bubble up to the body tag.
$('body').click(function(e) {
var $self = $(e.target),
options;
if (!$self.hasClass('requestHelpPanel') &&
!$self.parents('.requestHelpPanel').length) {
return;
}
e.preventDefault();
options = $.extend({}, $self.data(), {caller: $self});
$element.trigger('pkp.HelpPanel.Open', options);
});
// Register click handler on close button
$element.find('.pkpCloseHelpPanel').click(function(e) {
e.preventDefault();
$element.trigger('pkp.HelpPanel.Close');
});
// Register click handler on home button
$element.find('.pkpHomeHelpPanel').click(function(e) {
e.preventDefault();
$element.trigger('pkp.HelpPanel.Home');
});
// Handlers for "next" and "previous" buttons
$element.find('.pkpPreviousHelpPanel')
.click(this.callbackWrapper(function(e) {
this.loadHelpContent_(this.previousTopic_, this.helpLocale_);
}));
$element.find('.pkpNextHelpPanel').click(this.callbackWrapper(function(e) {
this.loadHelpContent_(this.nextTopic_, this.helpLocale_);
}));
// Register listeners
$element.on('pkp.HelpPanel.Open', this.callbackWrapper(this.openPanel_))
.on('pkp.HelpPanel.Close', this.callbackWrapper(this.closePanel_))
.on('pkp.HelpPanel.Home', this.callbackWrapper(this.homePanel_));
this.helpUrl_ = options.helpUrl;
this.helpLocale_ = options.helpLocale;
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.HelpPanelHandler, $.pkp.classes.Handler);
//
// Private properties
//
/**
* Calling element. Focus will be returned here when help panel is closed
* @private
* @type {jQueryObject}
*/
$.pkp.controllers.HelpPanelHandler.prototype.caller_ = null;
/**
* Help subsystem's URL. Used to fetch help content for presentation.
* @private
* @type {string?}
*/
$.pkp.controllers.HelpPanelHandler.prototype.helpUrl_ = null;
/**
* Help subsystem's locale. Default: `en`
* @private
* @type {string?}
*/
$.pkp.controllers.HelpPanelHandler.prototype.helpLocale_ = null;
/**
* Current help topic
* @private
* @type {string?}
*/
$.pkp.controllers.HelpPanelHandler.prototype.currentTopic_ = null;
/**
* Previous help topic
* @private
* @type {string?}
*/
$.pkp.controllers.HelpPanelHandler.prototype.previousTopic_ = null;
/**
* Next help topic
* @private
* @type {string?}
*/
$.pkp.controllers.HelpPanelHandler.prototype.nextTopic_ = null;
/**
* Requested help section
* @private
* @type {string?}
*/
$.pkp.controllers.HelpPanelHandler.prototype.requestedSection_ = null;
//
// Private methods
//
/**
* Open the helper panel
* @private
* @param {HTMLElement} context The context in which this function was called
* @param {Event} event The event triggered on this handler
* @param {{
* caller: jQueryObject,
* topic: string,
* section: string
* }} options The options with which to open this handler
*/
$.pkp.controllers.HelpPanelHandler.prototype.openPanel_ =
function(context, event, options) {
var $element = this.getHtmlElement();
// Save the calling element
if (typeof options.caller !== 'undefined') {
this.caller_ = options.caller;
}
// Show the help panel
$element.addClass('is_visible');
$('body').addClass('help_panel_is_visible'); // manage scrollbars
// Listen to close interaction events
$element.on('click.pkp.HelpPanel keyup.pkp.HelpPanel',
this.callbackWrapper(this.handleWrapperEvents));
// Listen to clicks on links
$element.on('click.pkp.HelpPanelContentLink', '.content a',
this.callbackWrapper(this.handleContentLinks_));
// Load the appropriate help content
this.loadHelpContent_(options.topic, this.helpLocale_);
this.requestedSection_ = options.section || '';
// Set focus inside the help panel (delay is required so that element is
// visible when jQuery tries to focus on it)
// @todo This should only happen once content is loaded in
setTimeout(function() {
$element.focus();
}, 300);
};
/**
* Load help content in the panel.
* @param {string?} topic The help context.
* @param {string?} locale The language locale to load the help topic.
* @private
*/
$.pkp.controllers.HelpPanelHandler.prototype.loadHelpContent_ =
function(topic, locale) {
locale = locale || this.helpLocale_;
this.currentTopic_ = topic || '';
var url = this.helpUrl_ + '/index/' + locale + '/';
this.getHtmlElement().addClass('is_loading');
// Don't escape slashes
url += encodeURIComponent(this.currentTopic_).replace(/%2F/g, '/');
$.get(url, null, this.callbackWrapper(this.updateContentHandler_),
'json');
};
/**
* A callback to update the tabs on the interface.
* @private
* @param {Object} ajaxContext The AJAX request context.
* @param {Object} jsonData A parsed JSON response object.
*/
$.pkp.controllers.HelpPanelHandler.prototype.
updateContentHandler_ = function(ajaxContext, jsonData) {
var workingJsonData = this.handleJson(jsonData),
responseObject = workingJsonData.content,
helpPanelHandler = this,
$element = this.getHtmlElement(),
hashIndex = this.currentTopic_.indexOf('#'),
$targetHash,
panel = $element.find('.panel');
this.previousTopic_ = responseObject.previous;
this.nextTopic_ = responseObject.next;
// Place the new content into the DOM
$element.find('.content').replaceWith(
'<div class="content">' + responseObject.content + '</div>');
// If a section was specified, scroll to the named section.
panel.scrollTop(0);
if (this.requestedSection_) {
$targetHash = $element.find(
'a[name="' + this.requestedSection_ + '"]');
if ($targetHash.length) {
panel.scrollTop($targetHash.offset().top - 50);
}
}
this.getHtmlElement().removeClass('is_loading');
};
/**
* A callback to handle clicks on links in the help content
*
* This function will allow external links to open in a new window but take
* control of relative links and try to open the appropriate help topic.
*
* @private
* @param {HTMLElement} target The target element the event was triggered on
* @param {Event} event The event triggered on this handler
* @return {boolean} Event handling status.
*/
$.pkp.controllers.HelpPanelHandler.prototype.
handleContentLinks_ = function(target, event) {
var url = $(target).attr('href'),
urlParts,
topic,
locale,
topicParts;
event.preventDefault();
// External links aren't yet supported in the help docs
// See: https://github.com/pkp/pkp-lib/issues/1032#issuecomment-199342940
if (url.substring(0, 4) == 'http') {
window.open(url);
return false;
}
urlParts = url.split('/');
topic = urlParts.slice(1).join('/');
locale = urlParts[0];
if (topic.indexOf('#') > -1) {
topicParts = topic.split('#');
topic = topicParts[0];
this.requestedSection_ = topicParts[1];
}
this.loadHelpContent_(topic, locale);
return false;
};
/**
* Close the helper panel
* @private
*/
$.pkp.controllers.HelpPanelHandler.prototype.closePanel_ = function() {
// Get a reference to this handler's element as a jQuery object
var $element = this.getHtmlElement();
// Show the help panel
$element.removeClass('is_visible');
$('body').removeClass('help_panel_is_visible'); // manage scrollbars
// Clear the help content
$element.find('.content').empty();
// Set focus back to the calling element
if (this.caller_ !== null) {
this.caller_.focus();
}
// Unbind wrapper events from element and reset vars
$element.off('click.pkp.HelpPanel keyup.pkp.HelpPanel');
$element.off('click.pkp.HelpPanelContentLink', '.content a');
this.caller_ = null;
};
/**
* Home the helper panel
* @private
*/
$.pkp.controllers.HelpPanelHandler.prototype.homePanel_ = function() {
this.loadHelpContent_(null, this.helpLocale_);
};
/**
* Process events that reach the wrapper element.
*
* @param {HTMLElement} context The context in which this function was called
* @param {Event} event The event triggered on this handler
*/
$.pkp.controllers.HelpPanelHandler.prototype.handleWrapperEvents =
function(context, event) {
// Get a reference to this handler's element as a jQuery object
var $element = this.getHtmlElement();
// Close click events directly on modal (background screen)
if (event.type == 'click' && $element.is($(event.target))) {
$element.trigger('pkp.HelpPanel.Close');
return;
}
// Close for ESC keypresses (27)
if (event.type == 'keyup' && event.which == 27) {
$element.trigger('pkp.HelpPanel.Close');
return;
}
};
}(jQuery));
+126
View File
@@ -0,0 +1,126 @@
/**
* @file js/controllers/MenuHandler.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 MenuHandler
* @ingroup js_controllers
*
* @brief A basic handler for a hierarchical list of navigation items.
*
* Attach this handler to a <ul> with nested <li> and <ul> elements.
* <li> elements with submenu should have aria properties:
* <li aria-haspopup="true" aria-expanded="false">
*
* <li> elements wiith a submenu that opens below the parent item should add a
* submenuOpensBelow class to support scrolling in long lists when necessary.
* <li class="submenuOpensBelow"
* aria-haspopup="true" aria-expanded="false"></li>
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $menu The outer <ul> element
* @param {Object} options Handler options.
*/
$.pkp.controllers.MenuHandler = function($menu, options) {
this.parent($menu, options);
// Fix dropdown menus that may go off-screen and recalculate whenever
// the browser window is resized
// 1ms delay allows dom insertion to complete
var self = this;
setTimeout(function() {
self.callbackWrapper( /** @type {Function} */ (
self.setDropdownAlignment()));
}, 1);
$(window).resize(this.callbackWrapper(this.onResize));
// Show/hide dropdown menus using WCAG-compliant aria attributes
this.getHtmlElement().on('focus mouseenter', '[aria-haspopup="true"]',
function(e) {
$(e.currentTarget).attr('aria-expanded', 'true');
});
this.getHtmlElement().on('blur mouseleave', '[aria-haspopup="true"]',
function(e) {
$(e.currentTarget).attr('aria-expanded', 'false');
});
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.MenuHandler, $.pkp.classes.Handler);
//
// Public methods
//
/**
* Check if submenus are straying off-screen and adjust as needed
*/
$.pkp.controllers.MenuHandler.prototype.setDropdownAlignment = function() {
var $this = $(this),
width = Math.max(
document.documentElement.clientWidth, window.innerWidth || 0),
height = Math.max(
document.documentElement.clientHeight, window.innerHeight || 0);
this.getHtmlElement().find('[aria-haspopup="true"]').each(function() {
var $parent = $(this),
$submenus = $parent.children('ul'),
right, pos_top, min_top, pos_btm, offset_top, new_top;
// Width
right = $parent.offset().left + $submenus.outerWidth();
if (right > width) {
$parent.addClass('align_right');
} else {
$parent.removeClass('align_right');
}
// Height
$submenus.attr('style', ''); // reset
pos_top = $parent.offset().top;
min_top = 0;
if ($parent.hasClass('submenuOpensBelow')) {
min_top = pos_top + $parent.outerHeight();
}
pos_btm = pos_top + $submenus.outerHeight();
if (pos_btm > height) {
offset_top = pos_btm - height;
new_top = pos_top - offset_top;
if (new_top < min_top) {
if (min_top > 0) {
offset_top = min_top;
} else {
offset_top = -Math.abs(offset_top) - new_top;
}
$submenus.css('overflow-y', 'scroll');
$submenus.css('bottom',
-Math.abs(height - pos_top - $parent.outerHeight()) + 'px');
}
$submenus.css('top', offset_top + 'px');
}
});
};
/**
* Throttle the dropdown alignment check during resize events. During
* browser resizing this will fire off every single frame, causing lag
* during the resize. So this just throttles the actual alignment check
* function by only firing when resizing has stopped.
*/
$.pkp.controllers.MenuHandler.prototype.onResize = function() {
clearTimeout(this.resize_check);
this.resize_check = setTimeout(
this.callbackWrapper(this.setDropdownAlignment), 1000);
};
}(jQuery));
@@ -0,0 +1,312 @@
/**
* @file js/controllers/NotificationHandler.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 NotificationHandler
* @ingroup js_controllers
*
* @brief Handle in place notifications.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $notificationElement The html notification element.
* @param {Object} options Notification options.
*/
$.pkp.controllers.NotificationHandler =
function($notificationElement, options) {
this.parent($notificationElement, options);
this.options_ = options;
this.bind('notifyUser', this.fetchNotificationHandler_);
// Hide the notification element.
this.getHtmlElement().hide();
// Trigger the notify user event without bubbling up.
this.getHtmlElement().triggerHandler('notifyUser');
if (this.options_.refreshOn) {
this.bindGlobal(this.options_.refreshOn, this.fetchNotificationHandler_);
}
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.NotificationHandler,
$.pkp.classes.Handler);
//
// Private properties.
//
/**
* The options to fetch a notification.
* @private
* @type {Object?}
*/
$.pkp.controllers.NotificationHandler.prototype.options_ = null;
/**
* Time to hide trivial inplace notifications.
* @private
* @type {number?}
*/
$.pkp.controllers.NotificationHandler.prototype.trivialTimer_ = null;
//
// Private methods.
//
/**
* Handler to fetch the notification data.
* @private
*/
$.pkp.controllers.NotificationHandler.prototype.fetchNotificationHandler_ =
function() {
var requestOptions = {};
requestOptions.requestOptions = this.options_.requestOptions;
// Avoid race conditions with other notification controllers.
$.ajax({
type: 'POST',
url: this.options_.fetchNotificationUrl,
data: requestOptions,
success: this.callbackWrapper(this.showNotificationResponseHandler_),
dataType: 'json',
async: false
});
};
/**
* Callback to show the notification data in place.
*
* @param {Object} ajaxContext The AJAX request context.
* @param {Object} jsonData A parsed JSON response object.
* @private
*/
$.pkp.controllers.NotificationHandler.prototype.
showNotificationResponseHandler_ = function(ajaxContext, jsonData) {
// Delete any existing trivial notification timer.
clearTimeout(this.trivialTimer_);
var $notificationElement = this.getHtmlElement(),
workingJsonData = this.handleJson(jsonData),
inPlaceNotificationsData, newNotificationsData,
trivialNotificationsId, notificationId, i;
if (workingJsonData === false) {
return;
}
if (workingJsonData.content.inPlace) {
inPlaceNotificationsData = this.concatenateNotifications_(
workingJsonData.content.inPlace);
newNotificationsData = this.removeAlreadyShownNotifications_(
/** @type {Object} */ (workingJsonData));
this.unbindPartial($notificationElement);
$notificationElement.html(inPlaceNotificationsData);
// We need to show the notification element now so the
// visibility test can be executed.
$notificationElement.show();
trivialNotificationsId = this.getTrivialNotifications_(
workingJsonData.content.inPlace);
if (!(this.visibleWithoutScrolling_()) && newNotificationsData) {
// In place notification is not visible. Let parent widgets
// show the notification data.
$notificationElement.parent().
trigger('notifyUser', [newNotificationsData]);
// Remove in place trivial notifications.
for (i in trivialNotificationsId) {
notificationId = trivialNotificationsId[i];
$notificationElement =
$('#pkp_notification_' + notificationId,
this.getHtmlElement());
this.unbindPartial($notificationElement);
$notificationElement.remove();
}
}
// After visibility test and possible trivial notifications
// removal, we need to test if the in place notification widget
// shows any notification. If not, hide it.
if ($notificationElement.children().length === 0) {
$notificationElement.hide();
} else {
// Add a timer to any trivial notifications
// inside this widget.
this.addTimerToNotifications(trivialNotificationsId);
}
} else {
this.unbindPartial(this.getHtmlElement());
this.getHtmlElement().empty();
this.getHtmlElement().hide();
}
};
/**
* Check if the notification is inside the window are visible.
* @return {boolean} True iff the notification is visible.
* @private
*/
$.pkp.controllers.NotificationHandler.prototype.
visibleWithoutScrolling_ = function() {
var $notificationElement = this.getHtmlElement(),
notificationTop = $notificationElement.offset().top,
notificationMiddle = notificationTop + this.getHtmlElement().height() / 2,
windowScrollTop = $(window).scrollTop(),
windowBottom = windowScrollTop + $(window).height(),
// Consider modals and its own scroll functionality.
$parentModalContentWrapper = $notificationElement
.parents('.ui-dialog-content'),
modalContentTop, modalContentBottom;
if ($parentModalContentWrapper.length > 0) {
modalContentTop = $parentModalContentWrapper.offset().top;
modalContentBottom = modalContentTop +
$parentModalContentWrapper.height();
if (notificationMiddle < modalContentTop ||
notificationMiddle > modalContentBottom) {
// The element is outside of the modal content wrapper area.
return false;
}
}
// Check if the element is inside of the visible window area.
if (notificationMiddle < windowScrollTop ||
notificationMiddle > windowBottom) {
return false;
} else {
return true;
}
};
/**
* Remove notification data from object that is already on page.
* @param {Object} notificationsData The notification data to perform
* the deletion on.
* @return {Object|boolean} Notification data after deletion.
* @private
*/
$.pkp.controllers.NotificationHandler.prototype.
removeAlreadyShownNotifications_ = function(notificationsData) {
var workingNotificationsData = /** @type {{ content: {
inPlace: Array, general: Array} }} */
(notificationsData),
emptyObject = true,
levelId, notificationId, element;
for (levelId in workingNotificationsData.content.inPlace) {
for (notificationId in
workingNotificationsData.content.inPlace[levelId]) {
element = $('#pkp_notification_' + notificationId);
if (element.length > 0) {
delete workingNotificationsData.content.
inPlace[levelId][notificationId];
delete workingNotificationsData.content.
general[levelId][notificationId];
} else {
emptyObject = false;
}
}
}
if (emptyObject) {
return false;
} else {
return workingNotificationsData;
}
};
/**
* Concatenate notification data in a string variable.
* @param {Object} notificationsData The notification data to assemble
* the concatenation from.
* @return {string} The concatenated notification data.
* @private
*/
$.pkp.controllers.NotificationHandler.prototype.
concatenateNotifications_ = function(notificationsData) {
var returner = '', levelId, notificationId;
for (levelId in notificationsData) {
for (notificationId in notificationsData[levelId]) {
// Concatenate all notifications.
returner += notificationsData[levelId][notificationId];
}
}
return returner;
};
/**
* Get all trivial notifications id inside the passed notifications.
* @param {Object} notificationsData The data returned from the fetch
* notification request.
* @return {Array} The trivial notifications id.
* @private
*/
$.pkp.controllers.NotificationHandler.prototype.
getTrivialNotifications_ = function(notificationsData) {
var trivialNotificationsId = [], levelId, notificationId;
for (levelId in notificationsData) {
if (levelId == 1) { // Trivial level.
for (notificationId in notificationsData[levelId]) {
trivialNotificationsId.push(notificationId);
}
}
}
return trivialNotificationsId;
};
/**
* Add a timer for passed notifications to hide them after a time.
* @param {Object} notificationsId Array with the notifications id
* that will receive the timer.
*/
$.pkp.controllers.NotificationHandler.prototype.
addTimerToNotifications = function(notificationsId) {
var self, removeNotification;
self = this; // remember "this" reference for later use in callback
removeNotification = function() {
var $notification = $(this);
self.unbindPartial($notification);
$notification.remove();
};
if (notificationsId.length) {
this.trivialTimer_ = setTimeout(function() {
var notificationId, $notification;
for (notificationId in notificationsId) {
$notification = $('#pkp_notification_' +
notificationsId[notificationId]);
$notification.fadeOut(400, removeNotification);
}
}, 6000);
}
};
}(jQuery));
@@ -0,0 +1,56 @@
/**
* @file js/controllers/RevealMoreHandler.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 RevealMoreHandler
* @ingroup js_controllers
*
* @brief A basic handler for the reveal more UI pattern
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $widgetWrapper An HTML element that contains the
* widget.
* @param {Object} options Handler options.
*/
$.pkp.controllers.RevealMoreHandler = function($widgetWrapper, options) {
this.parent($widgetWrapper, options);
if ($widgetWrapper.outerHeight() > options.height) {
$widgetWrapper.addClass('isHidden').
css('max-height', options.height + 'px');
$('.revealMoreButton', $widgetWrapper).click(
this.callbackWrapper(this.revealMore));
}
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.RevealMoreHandler, $.pkp.classes.Handler);
//
// Public methods
//
/**
* Event handler that is called when the button to reveal more is clicked
*
* @param {HTMLElement} revealMoreButton The button that is clicked to
* toggle extras.
* @param {Event} event The triggering event.
*/
$.pkp.controllers.RevealMoreHandler.prototype.revealMore =
function(revealMoreButton, event) {
this.getHtmlElement().removeClass('isHidden').removeAttr('style');
event.preventDefault();
event.stopPropagation();
};
}(jQuery));
+752
View File
@@ -0,0 +1,752 @@
/**
* @defgroup js_controllers
*/
/**
* @file js/controllers/SiteHandler.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 SiteHandler
* @ingroup js_controllers
*
* @brief Handle the site widget.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $widgetWrapper An HTML element that this handle will
* be attached to.
* @param {{
* fetchNotificationUrl: string,
* requestOptions: Object
* }} options Handler options.
*/
$.pkp.controllers.SiteHandler = function($widgetWrapper, options) {
this.parent($widgetWrapper, options);
this.options_ = options;
this.unsavedFormElements_ = [];
$('.go').button();
this.bind('redirectRequested', this.redirectToUrl);
this.bind('notifyUser', this.fetchNotificationHandler_);
this.bind('reloadTab', this.reloadTabHandler_);
this.bind('callWhenClickOutside', this.callWhenClickOutsideHandler_);
this.bind('mousedown', this.mouseDownHandler_);
// Bind the pageUnloadHandler_ method to the DOM so it is
// called.
$(window).bind('beforeunload', this.pageUnloadHandler_);
// Avoid IE8 caching ajax results. If it does, widgets like
// grids will not refresh correctly.
$.ajaxSetup({cache: false});
// Check if we have notifications to show.
if (options.hasSystemNotifications) {
this.trigger('notifyUser');
}
// bind event handlers for form status change events.
this.bind('formChanged', this.callbackWrapper(
this.registerUnsavedFormElement_));
this.bind('unregisterChangedForm', this.callbackWrapper(
this.unregisterUnsavedFormElement_));
this.bind('unregisterAllForms', this.callbackWrapper(
this.unregisterAllFormElements_));
// React to a modal events
this.bind('pkpModalOpen', this.callbackWrapper(this.openModal_));
this.bind('pkpModalClose', this.callbackWrapper(this.closeModal_));
this.bind('pkpObserveScrolling', this.callbackWrapper(
this.registerScrollingObserver_));
this.bind('pkpRemoveScrollingObserver', this.callbackWrapper(
this.unregisterScrollingObserver_));
this.outsideClickChecks_ = {};
this.initializeTinyMCE();
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.SiteHandler, $.pkp.classes.Handler);
//
// Private properties
//
/**
* Help context.
* @private
* @type {string?}
*/
$.pkp.controllers.SiteHandler.prototype.helpContext_ = null;
/**
* Site handler options.
* @private
* @type {Object}
*/
$.pkp.controllers.SiteHandler.prototype.options_ = null;
/**
* Object with data to be used when checking if user
* clicked outside a site element. See callWhenClickOutsideHandler_()
* to check the expected check options.
* @private
* @type {Object}
*/
$.pkp.controllers.SiteHandler.prototype.outsideClickChecks_ = null;
/**
* A state variable to store the form elements that have unsaved data.
* @private
* @type {Array}
*/
$.pkp.controllers.SiteHandler.prototype.unsavedFormElements_ = null;
//
// Public static methods.
//
/**
* Initialize the tinyMCE plugin
*/
$.pkp.controllers.SiteHandler.prototype.initializeTinyMCE =
function() {
if (typeof tinyMCE !== 'undefined') {
tinyMCE.PluginManager.load('pkpTags', $.pkp.app.baseUrl +
'/plugins/generic/tinymce/plugins/pkpTags/plugin.js');
tinyMCE.PluginManager.load('pkpwordcount', $.pkp.app.baseUrl +
'/plugins/generic/tinymce/plugins/pkpWordcount/plugin.js');
var contentCSS = $.pkp.app.tinyMceContentCSS,
tinymceParams,
tinymceParamDefaults;
tinymceParamDefaults = {
width: '100%',
resize: 'both',
entity_encoding: 'raw',
plugins: 'paste,fullscreen,link,lists,code,' +
'image,-pkpTags,noneditable',
convert_urls: false,
forced_root_block: 'p',
paste_auto_cleanup_on_paste: true,
apply_source_formatting: false,
toolbar: 'copy paste | bold italic underline | link unlink ' +
'code fullscreen | image | pkpTags',
richToolbar: 'copy paste | bold italic underline | bullist numlist | ' +
'superscript subscript | link unlink code fullscreen | ' +
'image | pkpTags',
statusbar: false,
content_css: contentCSS,
browser_spellcheck: true
};
// Support image uploads
if (typeof $.pkp.plugins.generic.tinymceplugin !== 'undefined' &&
typeof $.pkp.plugins.generic.tinymceplugin.uploadUrl !== 'undefined') {
tinymceParamDefaults.paste_data_images = true;
tinymceParamDefaults.relative_urls = false;
tinymceParamDefaults.remove_script_host = false;
// https://www.tiny.cloud/docs/general-configuration-guide/upload-images/
tinymceParamDefaults.images_upload_handler = function(
blobInfo, success, failure) {
/*global FormData: false */
var data = new FormData();
data.append('file', blobInfo.blob(), blobInfo.filename());
$.ajax({
method: 'POST',
url: $.pkp.plugins.generic.tinymceplugin.uploadUrl,
data: data,
processData: false,
contentType: false,
headers: {
'X-Csrf-Token': pkp.currentUser.csrfToken
},
success: function(r) {
success(r.url);
},
error: function(r) {
failure(r.responseJSON.errorMessage);
}
});
};
}
// Allow default params to be overridden
if (typeof $.pkp.plugins.generic.tinymceplugin !== 'undefined' &&
typeof $.pkp.plugins.generic.tinymceplugin.tinymceParams) {
tinymceParams = $.extend({}, tinymceParamDefaults,
$.pkp.plugins.generic.tinymceplugin.tinymceParams);
} else {
tinymceParams = $.extend({}, tinymceParamDefaults);
}
// Don't allow the following settings to be overridden
tinymceParams.init_instance_callback =
$.pkp.controllers.SiteHandler.prototype.triggerTinyMCEInitialized;
tinymceParams.setup =
$.pkp.controllers.SiteHandler.prototype.triggerTinyMCESetup;
tinyMCE.init(tinymceParams);
}
};
/**
* Callback used by the tinyMCE plugin to trigger the tinyMCEInitialized
* event in the DOM.
* @param {Object} tinyMCEObject The tinyMCE object instance being
* initialized.
*/
$.pkp.controllers.SiteHandler.prototype.triggerTinyMCEInitialized =
function(tinyMCEObject) {
var $inputElement = $('#' +
$.pkp.classes.Helper.escapeJQuerySelector(tinyMCEObject.id));
$inputElement.trigger('tinyMCEInitialized', [tinyMCEObject]);
};
/**
* Callback used by the tinyMCE plugin upon setup.
* @param {Object} tinyMCEObject The tinyMCE object instance being
* set up.
*/
$.pkp.controllers.SiteHandler.prototype.triggerTinyMCESetup =
function(tinyMCEObject) {
var target = $('#' +
$.pkp.classes.Helper.escapeJQuerySelector(tinyMCEObject.id)),
height;
// For read-only controls, set up TinyMCE read-only mode.
if (target.attr('readonly')) {
tinyMCEObject.settings.readonly = true;
}
if (target.attr('wordCount') && target.attr('wordCount') > 0) {
tinyMCEObject.settings.plugins =
tinyMCEObject.settings.plugins + ',pkpwordcount';
tinyMCEObject.settings.statusbar = true;
}
if (target.attr('dir')) {
tinyMCEObject.settings.directionality = target.attr('dir');
}
// Set height based on textarea rows
height = target.attr('rows') || 10; // default: 10
height *= 20; // 20 pixels per row
tinyMCEObject.settings.height = height.toString() + 'px';
// Add a fake HTML5 placeholder when the editor is intitialized
tinyMCEObject.on('init', function(tinyMCEObject) {
var $element = $('#' + tinyMCEObject.id),
placeholderText,
$placeholder,
$placeholderParent;
// Don't add anything if we don't have a placeholder
placeholderText = $('#' + tinyMCEObject.id).attr('placeholder');
if (placeholderText === '') {
return;
}
// Create placeholder element
$placeholder = $('<span></span>');
$placeholder.html(/** @type {string} */ (placeholderText));
$placeholder.addClass('mcePlaceholder');
$placeholder.attr('id', 'mcePlaceholder-' + tinyMCEObject.id);
if (tinyMCEObject.target.getContent().length) {
$placeholder.hide();
}
// Create placeholder wrapper
$placeholderParent = $('<div></div>');
$placeholderParent.addClass('mcePlaceholderParent');
$element.wrap($placeholderParent);
$element.parent().append($placeholder);
});
tinyMCEObject.on('activate', function(tinyMCEObject) {
// Hide the placeholder when the editor is activated
$('#mcePlaceholder-' + tinyMCEObject.id).hide();
});
tinyMCEObject.on('deactivate', function(tinyMCEObject) {
// Show the placholder when the editor is deactivated
if (!tinyMCEObject.target.getContent().length) {
$('#mcePlaceholder-' + tinyMCEObject.id).show();
}
});
tinyMCEObject.on('BeforeSetContent', function(e) {
var variablesParsed = $.pkp.classes.TinyMCEHelper.prototype.getVariableMap(
'#' + $.pkp.classes.Helper.escapeJQuerySelector(tinyMCEObject.id));
e.content = e.content.replace(
/\{\$([a-zA-Z]+)\}(?![^<]*>)/g, function(match, contents, offset, s) {
if (variablesParsed[contents] !== undefined) {
return $.pkp.classes.TinyMCEHelper.prototype.getVariableElement(
contents, variablesParsed[contents], '#' + tinyMCEObject.id)
.html();
}
return match;
});
});
// When the field is being saved, replace any tag placeholders
tinyMCEObject.on('GetContent', function(e) {
var $content = $('<div>' + e.content + '</div>');
// Replace tag span elements with the raw tags
$content.find('.pkpTag').replaceWith(function() {
return '{$' + $(this).attr('data-symbolic') + '}';
});
e.content = $content.html();
});
// In fullscreen mode, also present the toolbar.
tinyMCEObject.on('FullscreenStateChanged init', function(e) {
var target = e.target, $container = $(target.editorContainer);
if (target.plugins.fullscreen) {
if (target.plugins.fullscreen.isFullscreen()) {
$container.find('.tox-menubar').show();
} else {
$container.find('.tox-menubar').hide();
}
}
});
};
/**
* Get the current window dimensions.
* @return {Object} The current window dimensions (height and width)
* in pixels.
*/
$.pkp.controllers.SiteHandler.prototype.getWindowDimensions =
function() {
var dimensions = {'height': $(window).height(),
'width': $(window).width()};
return dimensions;
};
//
// Public methods
//
/**
* Callback that is triggered when the page should redirect.
*
* @param {HTMLElement} sourceElement The element that issued the
* "redirectRequested" event.
* @param {Event} event The "redirect requested" event.
* @param {string} url The URL to redirect to.
*/
$.pkp.controllers.SiteHandler.prototype.redirectToUrl =
function(sourceElement, event, url) {
window.location = url;
};
/**
* Handler bound to 'formChanged' events propagated by forms
* that wish to have their form data tracked.
* @private
* @param {HTMLElement} siteHandlerElement The html element
* attached to this handler.
* @param {HTMLElement} sourceElement The element wishes to
* register.
* @param {Event} event The formChanged event.
*/
$.pkp.controllers.SiteHandler.prototype.registerUnsavedFormElement_ =
function(siteHandlerElement, sourceElement, event) {
var $formElement, formId, index;
$formElement = $(event.target.lastElementChild);
formId = $formElement.attr('id');
index = $.inArray(formId, this.unsavedFormElements_);
if (index == -1) {
this.unsavedFormElements_.push(formId);
}
};
/**
* Handler bound to 'unregisterChangedForm' events propagated by forms
* that wish to inform that they no longer wish to be tracked as 'unsaved'.
* @private
* @param {HTMLElement} siteHandlerElement The html element
* attached to this handler.
* @param {HTMLElement} sourceElement The element that wishes to
* unregister.
* @param {Event} event The unregisterChangedForm event.
*/
$.pkp.controllers.SiteHandler.prototype.unregisterUnsavedFormElement_ =
function(siteHandlerElement, sourceElement, event) {
var $formElement, formId, index;
$formElement = $(event.target.lastElementChild);
formId = $formElement.attr('id');
index = $.inArray(formId, this.unsavedFormElements_);
if (index !== -1) {
delete this.unsavedFormElements_[index];
}
};
/**
* Unregister all unsaved form elements.
* @private
*/
$.pkp.controllers.SiteHandler.prototype.unregisterAllFormElements_ =
function() {
this.unsavedFormElements_ = [];
};
//
// Private methods.
//
/**
* Fetch the notification data.
* @private
* @param {HTMLElement} sourceElement The element that issued the
* "fetchNotification" event.
* @param {Event} event The "fetch notification" event.
* @param {Object} jsonData The JSON content representing the
* notification.
*/
$.pkp.controllers.SiteHandler.prototype.fetchNotificationHandler_ =
function(sourceElement, event, jsonData) {
if (jsonData !== undefined) {
// This is an event that came from an inplace notification
// widget that was not visible because of the scrolling.
this.showNotification_(jsonData);
return;
}
// Avoid race conditions with in place notifications.
$.ajax({
url: this.options_.fetchNotificationUrl,
data: this.options_.requestOptions,
success: this.callbackWrapper(this.showNotificationResponseHandler_),
dataType: 'json',
async: false
});
};
/**
* Reload a tab.
* @private
* @param {HTMLElement} sourceElement The element that issued the
* "reloadTab" event.
* @param {Event} event The "reload tab" event.
* @param {Object} jsonData The JSON content representing the
* reload request.
*/
$.pkp.controllers.SiteHandler.prototype.reloadTabHandler_ =
function(sourceElement, event, jsonData) {
$(jsonData.tabsSelector).tabs('load', jsonData.tabSelector);
};
/**
* Call when click outside event handler. Stores the event
* parameters as checks to be used later by mouse down handler so we
* can track if user clicked outside the passed element or not.
* @private
* @param {HTMLElement} sourceElement The element that issued the
* callWhenClickOutside event.
* @param {Event} event The "call when click outside" event.
* @param {{
* container: jQueryObject,
* callback: Function
* }} eventParams The event parameters.
* - container: a jQuery element to be used to test if user click
* outside of it or not.
* - callback: a callback function in case test is true.
*/
$.pkp.controllers.SiteHandler.prototype.callWhenClickOutsideHandler_ =
function(sourceElement, event, eventParams) {
// Check the required parameters.
if (eventParams.container === undefined) {
return;
}
var id = eventParams.container.attr('id');
if (eventParams.clear) {
delete this.outsideClickChecks_[id];
} else if (eventParams.callback !== undefined) {
this.outsideClickChecks_[id] = eventParams;
}
};
/**
* Mouse down event handler attached to the site element.
* @private
* @param {HTMLElement} sourceElement The element that issued the
* click event.
* @param {Event} event The "mousedown" event.
* @return {?boolean} Event handling status.
*/
$.pkp.controllers.SiteHandler.prototype.mouseDownHandler_ =
function(sourceElement, event) {
var $container, callback, id;
if (!$.isEmptyObject(this.outsideClickChecks_)) {
for (id in this.outsideClickChecks_) {
this.processOutsideClickCheck_(
this.outsideClickChecks_[id], event);
}
}
return true;
};
/**
* Check if the passed event target is outside the element
* inside the passed check data. If true and no other check
* option avoids it, use the callback.
* @private
* @param {{
* container: Object,
* callback: Function
* }} checkOptions Object with data to be used to
* check the click.
* @param {Event} event The click event to be checked.
* @return {boolean} Whether the check was processed or not.
*/
$.pkp.controllers.SiteHandler.prototype.processOutsideClickCheck_ =
function(checkOptions, event) {
// Make sure we have a click event.
if (event.type !== 'click' &&
event.type !== 'mousedown' && event.type !== 'mouseup') {
throw new Error('Can not check outside click with the passed event: ' +
event.type + '.');
}
// Get the container element.
var $container = checkOptions.container;
// Doesn't make sense to check an outside click
// with an invisible element, so skip test if
// container is hidden.
if ($container.is(':hidden')) {
return false;
}
// Do the click origin checking.
if ($container.has(event.target).length === 0) {
// Once the check was processed, delete it.
delete this.outsideClickChecks_[$container.attr('id')];
checkOptions.callback();
return true;
}
return false;
};
/**
* Internal callback called upon page unload. If it returns
* anything other than void, a message will be displayed to
* the user.
* @private
* @param {Object} object The window object.
* @param {Event} event The before unload event.
* @return {string|undefined} The warning message string, if needed.
*/
$.pkp.controllers.SiteHandler.prototype.pageUnloadHandler_ =
function(object, event) {
var handler, unsavedElementCount, element;
// any registered and then unregistered forms will exist
// as properties in the unsavedFormElements_ object. They
// will just be undefined. See if there are any that are
// not.
// we need to get the handler this way since this event is bound
// to window, not to SiteHandler.
handler = $.pkp.classes.Handler.getHandler($('body'));
unsavedElementCount = 0;
for (element in handler.unsavedFormElements_) {
if (element) {
unsavedElementCount++;
}
}
if (unsavedElementCount > 0) {
return pkp.localeKeys['form.dataHasChanged'];
}
return undefined;
};
/**
* Method to determine if a form is currently registered as having
* unsaved changes.
*
* @param {string} id the id of the form to check.
* @return {boolean} true if the form is unsaved.
*/
$.pkp.controllers.SiteHandler.prototype.isFormUnsaved =
function(id) {
if (this.unsavedFormElements_ !== null &&
this.unsavedFormElements_[id] !== undefined) {
return true;
}
return false;
};
/**
* Response handler to the notification fetch.
* @private
* @param {Object} ajaxContext The data returned from the server.
* @param {Object} jsonData A parsed JSON response object.
*/
$.pkp.controllers.SiteHandler.prototype.showNotificationResponseHandler_ =
function(ajaxContext, jsonData) {
this.showNotification_(jsonData);
};
/**
* Show the notification content.
* @private
* @param {Object} jsonData The JSON-encoded notification data.
*/
$.pkp.controllers.SiteHandler.prototype.showNotification_ =
function(jsonData) {
var workingJsonData, notificationsData, levelId, notificationId,
addclass, type;
workingJsonData = this.handleJson(jsonData);
if (workingJsonData !== false) {
if (workingJsonData.content.general) {
notificationsData = workingJsonData.content.general;
for (levelId in notificationsData) {
for (notificationId in notificationsData[levelId]) {
addclass = notificationsData[levelId][notificationId].addclass;
type = 'notice';
if (addclass == 'notifySuccess') {
type = 'success';
} else if (addclass == 'notifyWarning' || addclass == 'notifyError' ||
addclass == 'notifyFormError' || addclass == 'notifyForbidden') {
type = 'warning';
}
pkp.eventBus.$emit('notify',
notificationsData[levelId][notificationId].text, type);
}
}
}
}
};
/**
* Reacts to a modal being opened. Adds a class to the body representing
* a modal open state.
* @private
* @param {HTMLElement} handledElement The modal that has been added
* @param {HTMLElement} siteHandlerElement The html element
* attached to this handler.
* @param {HTMLElement} sourceElement The element wishes to
* register.
* @param {Event} event The formChanged event.
*/
$.pkp.controllers.SiteHandler.prototype.openModal_ =
function(handledElement, siteHandlerElement, sourceElement, event) {
this.getHtmlElement().addClass('modal_is_visible');
};
/**
* Reacts to a modal being closed. Removes a class from the body
* representing a modal closed state, after checking if no other modals are
* open.
* @private
* @param {HTMLElement} handledElement The modal that has been added
* @param {HTMLElement} siteHandlerElement The html element
* attached to this handler.
* @param {HTMLElement} sourceElement The element wishes to
* register.
* @param {Event} event The formChanged event.
*/
$.pkp.controllers.SiteHandler.prototype.closeModal_ =
function(handledElement, siteHandlerElement, sourceElement, event) {
var $htmlElement = this.getHtmlElement();
if (!$htmlElement.find('.pkp_modal.is_visible').length) {
$htmlElement.removeClass('modal_is_visible');
}
};
/**
* Register a function to observe the body scrolling event.
* @private
* @param {Object} siteHandler The site handler object.
* @param {HTMLElement} siteHandlerElement The html element
* attached to this handler.
* @param {Object} event The pkpObserveScrolling event object.
* @param {Function} observerFunction The observer function.
* @return {boolean}
*/
$.pkp.controllers.SiteHandler.prototype.registerScrollingObserver_ =
function(siteHandler, siteHandlerElement, event, observerFunction) {
$(document).scroll(observerFunction);
return false;
};
/**
* Unregister a function that was observing the body scrolling event.
* @private
* @param {Object} siteHandler The site handler object.
* @param {HTMLElement} siteHandlerElement The html element
* attached to this handler.
* @param {Object} event The pkpRemoveScrollingObserver event object.
* @param {Function} observerFunction The observer function.
* @return {boolean}
*/
$.pkp.controllers.SiteHandler.prototype.unregisterScrollingObserver_ =
function(siteHandler, siteHandlerElement, event, observerFunction) {
var castObserverFunction = /** @type {function()} */ (observerFunction);
$(document).unbind('scroll', castObserverFunction);
return false;
};
}(jQuery));
+369
View File
@@ -0,0 +1,369 @@
/**
* @file js/controllers/TabHandler.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 TabHandler
* @ingroup js_controllers
*
* @brief A basic handler for a tabbed set of pages.
*
* See <http://jqueryui.com/demos/tabs/> for documentation on JQuery tabs.
* Attach this handler to a div that contains a <ul> with a <li> for each tab
* to be created.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $tabs A wrapped HTML element that
* represents the tabbed interface.
* @param {Object} options Handler options.
*/
$.pkp.controllers.TabHandler = function($tabs, options) {
var pageUrl, pageAnchor, pattern, pageAnchors, tabAnchors, i,
self = this;
this.parent($tabs, options);
// Attach the tabs event handlers.
this.bind('tabsbeforeactivate', this.tabsBeforeActivate);
this.bind('tabsactivate', this.tabsActivate);
this.bind('tabscreate', this.tabsCreate);
this.bind('tabsbeforeload', this.tabsBeforeLoad);
this.bind('tabsload', this.tabsLoad);
this.bind('addTab', this.addTab);
if (options.emptyLastTab) {
this.emptyLastTab_ = options.emptyLastTab;
}
// if the page has been loaded with an #anchor
// determine what tab that is for and set the
// options.selected value to it so it gets used
// when tabs() are initialized.
pageUrl = document.location.toString();
if (pageUrl.match('#')) {
pageAnchor = pageUrl.split('#')[1];
tabAnchors = $tabs.find('li a');
for (i = 0; i < tabAnchors.length; i++) {
if (pageAnchor == tabAnchors[i].getAttribute('name')) {
// Matched on anchor name.
options.selected = i;
}
}
}
// Render the tabs as jQueryUI tabs.
$tabs.tabs({
// Enable AJAX-driven tabs with JSON messages.
beforeLoad: function(event, ui) {
ui.ajaxSettings.dataType = 'json';
ui.jqXHR.setRequestHeader('Accept', 'application/json');
ui.ajaxSettings.dataFilter = self.callbackWrapper(self.dataFilter);
},
disabled: options.disabled,
active: options.selected
});
// Load a tab when the URL hash changes to a named tab
// Original issue: https://github.com/pkp/pkp-lib/issues/1787
// This technique introduced to resolve tab activation errors from #1787.
// See: https://github.com/pkp/pkp-lib/issues/4352
window.addEventListener('hashchange', function(e) {
var parts = e.newURL.split('#'), hash, $tab;
if (parts.length < 2) {
return;
}
hash = parts[1];
$tab = $tabs.find('li > a[name="' + hash + '"]');
if ($tab.length) {
$tab.click();
}
}, false);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.TabHandler, $.pkp.classes.Handler);
//
// Private properties
//
/**
* The current tab.
* @private
* @type {jQueryObject}
*/
$.pkp.controllers.TabHandler.prototype.$currentTab_ = null;
/**
* The current tab index.
* @private
* @type {number}
*/
$.pkp.controllers.TabHandler.prototype.currentTabIndex_ = 0;
//
// Public methods
//
/**
* Event handler that is called when a tab is selected.
*
* @param {HTMLElement} tabsElement The tab element that triggered
* the event.
* @param {Event} event The triggered event.
* @param {jQueryObject} ui The tabs ui data.
* @return {boolean} Should return true to continue tab loading.
*/
$.pkp.controllers.TabHandler.prototype.tabsBeforeActivate =
function(tabsElement, event, ui) {
var unsavedForm = false;
this.$currentTab_.find('form').each(function(index) {
if ($.pkp.classes.Handler.hasHandler($('#' + $(this).attr('id')))) {
var handler = $.pkp.classes.Handler.getHandler(
$('#' + $(this).attr('id')));
if (handler.formChangesTracked) {
unsavedForm = true;
return false; // found an unsaved form, no need to continue with each().
}
}
});
this.$currentTab_.find('.hasDatepicker').datepicker('hide');
if (unsavedForm) {
if (!confirm(pkp.localeKeys['form.dataHasChanged'])) {
return false;
} else {
this.trigger('unregisterAllForms');
}
}
if (this.emptyLastTab_) {
// bind a single (i.e. one()) error event handler to prevent
// propagation if the tab being unloaded no longer exists.
// We cannot simply getHandler() since that in of itself throws
// an Error.
$(window).one('error', function(msg, url, line) { return false; });
if (this.$currentTab_) {
// Unbind global events for handlers embedded in this tab's
// content.
this.unbindPartial(this.$currentTab_);
this.$currentTab_.empty();
}
}
return true;
};
/**
* Event handler that is called when a tab is created.
*
* @param {HTMLElement} tabsElement The tab element that triggered
* the event.
* @param {Event} event The triggered event.
* @param {jQueryObject} ui The tabs ui data.
* @return {boolean} Should return true to continue tab loading.
*/
$.pkp.controllers.TabHandler.prototype.tabsCreate =
function(tabsElement, event, ui) {
// Save the tab index.
this.currentTabIndex_ = ui.tab.index();
// Save a reference to the current panel.
this.$currentTab_ = ui.panel.jquery ? ui.panel : $(ui.panel);
return true;
};
/**
* Event handler that is called when a tab is activated
*
* @param {HTMLElement} tabsElement The tab element that triggered
* the event.
* @param {Event} event The triggered event.
* @param {jQueryObject} ui The tabs ui data.
* @return {boolean} Should return true to continue tab loading.
*/
$.pkp.controllers.TabHandler.prototype.tabsActivate =
function(tabsElement, event, ui) {
// Save the tab index.
this.currentTabIndex_ = ui.newTab.index();
// Save a reference to the current panel.
this.$currentTab_ = ui.newPanel.jquery ? ui.newPanel : $(ui.newPanel);
return true;
};
/**
* Event handler that is called after a remote tab was loaded.
*
* @param {HTMLElement} tabsElement The tab element that triggered
* the event.
* @param {Event} event The triggered event.
* @param {jQueryObject} ui The tabs ui data.
* @return {boolean} Should return true to continue tab loading.
*/
$.pkp.controllers.TabHandler.prototype.tabsLoad =
function(tabsElement, event, ui) {
return true;
};
/**
* Callback that that is triggered before the tab is loaded.
*
* @param {HTMLElement} tabsElement The tab element that triggered
* the event.
* @param {Event} event The triggered event.
* @param {jQueryObject} ui The tabs ui data.
*/
$.pkp.controllers.TabHandler.prototype.tabsBeforeLoad =
function(tabsElement, event, ui) {
// We must unbind global events before the new tab content is loaded.
// This reaches out to the tab content element and unbinds any events
// attached to that element or any embedded handlers before it gets
// destroyed.
this.unbindPartial($('#' + ui.tab.attr('aria-controls')));
// Initialize AJAX settings for loading tab content remotely
ui.ajaxSettings.cache = false;
ui.ajaxSettings.dataFilter = this.callbackWrapper(this.dataFilter);
};
/**
* Callback that processes AJAX data returned by the server before
* it is displayed in a tab.
*
* @param {Object} ajaxOptions The options object from which the
* callback originated.
* @param {string} jsonString Unparsed JSON data returned from the server.
* @return {string} The tab mark-up.
*/
$.pkp.controllers.TabHandler.prototype.dataFilter =
function(ajaxOptions, jsonString) {
var jsonData = this.handleJson($.parseJSON(jsonString));
if (jsonData === false) {
return '';
}
return JSON.stringify(jsonData.content);
};
/**
* Callback that processes data returned by the server when
* an 'addTab' event is received.
*
* This is useful e.g. when the results of a form handler
* should be sent to a different tab in the containing tabset.
*
* @param {HTMLElement} divElement The parent DIV element
* which contains the tabs.
* @param {Event} event The triggered event (addTab).
* @param {{url: string, title: string}} jsonContent The tabs ui data.
*/
$.pkp.controllers.TabHandler.prototype.addTab =
function(divElement, event, jsonContent) {
var $element = this.getHtmlElement(),
numTabs = $element.children('ul').children('li').length + 1,
$anchorElement = $('<a/>')
.text(jsonContent.title)
.attr('href', jsonContent.url),
$closeSpanElement = $('<a/>')
.addClass('close')
.text(pkp.localeKeys['common.close'])
.attr('href', '#'),
$liElement = $('<li/>')
.append($anchorElement)
.append($closeSpanElement);
// Get the "close" button working
$closeSpanElement.click(function() {
var $liElement = $(this).closest('li'),
$divElement = $('#' + $liElement.attr('aria-controls')),
thisTabIndex, unsavedForm;
// Check to see if any unsaved changes need to be confirmed
unsavedForm = false;
$divElement.find('form').each(function() {
var handler = $.pkp.classes.Handler.getHandler($(this));
if (handler.formChangesTracked) {
// Confirm before proceeding
if (!confirm(pkp.localeKeys['form.dataHasChanged'])) {
unsavedForm = true;
return false;
}
}
});
if (!unsavedForm) {
$divElement.find('form').each(function() {
var handler = $.pkp.classes.Handler.getHandler($(this));
if (handler) {
handler.unregisterForm();
}
});
// If the panel being closed is currently selected, move off first.
thisTabIndex = $liElement.eq(0).index();
if ($element.tabs('option', 'active') == thisTabIndex) {
$element.tabs('option', 'active', thisTabIndex - 1);
}
$liElement.remove();
$divElement.remove();
$element.tabs('refresh');
}
});
// Add the new tab element and refresh the tab set.
$element.children('ul').append($liElement);
$element.tabs('refresh');
$element.tabs('option', 'active', numTabs - 1);
};
//
// Protected methods
//
/**
* Get the current tab.
* @protected
* @return {jQueryObject} The current tab.
*/
$.pkp.controllers.TabHandler.prototype.getCurrentTab = function() {
return this.$currentTab_;
};
/**
* Get the current tab index.
* @protected
* @return {number} The current tab index.
*/
$.pkp.controllers.TabHandler.prototype.getCurrentTabIndex = function() {
return this.currentTabIndex_;
};
}(jQuery));
+272
View File
@@ -0,0 +1,272 @@
/**
* @file js/controllers/UploaderHandler.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 UploaderHandler
* @ingroup js_controllers
*
* @brief PKP file uploader widget handler.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $uploader the wrapped HTML uploader element.
* @param {{
* uploadUrl: string,
* baseUrl: string
* }} options options to be passed
* into the validator plug-in.
*/
$.pkp.controllers.UploaderHandler = function($uploader, options) {
this.parent($uploader, options);
// Check whether we really got a div to attach
// our uploader to.
if (!$uploader.is('div')) {
throw new Error(['An uploader widget controller can only be attached',
' to a div!'].join(''));
}
var uploaderOptions,
pluploaderId,
$browseButton,
self;
// Set up options to pass to plupload
uploaderOptions = {
url: options.uploadUrl,
// Flash settings
flash_swf_url: options.baseUrl +
'/lib/pkp/lib/vendor/moxiecode/plupload/js/Moxie.swf',
// Silverlight settings
silverlight_xap_url: options.baseUrl +
'/lib/pkp/lib/vendor/moxiecode/plupload/js/Moxie.xap'
};
if (typeof options.filters) {
uploaderOptions.filters = options.filters;
}
if (typeof options.resize) {
uploaderOptions.resize = options.resize;
}
if (typeof options.browse_button) {
uploaderOptions.browse_button = options.browse_button;
}
if (typeof options.multipart_params) {
uploaderOptions.multipart_params = options.multipart_params;
}
uploaderOptions.drop_element = $uploader.first().find('#' +
$uploader.first().attr('id') +
'-pkpUploaderDropZone').attr('id');
uploaderOptions = $.extend(
{},
this.self('DEFAULT_PROPERTIES_'),
uploaderOptions
);
// Create the uploader with the puploader plug-in.
// Setup the upload widget.
this.pluploader = new plupload.Uploader(uploaderOptions);
this.pluploader.init();
this.updateStatus('waiting');
// Cache re-used DOM references
this.$progress = $uploader.find('.pkpUploaderProgress .percentage');
this.$progressBar = $uploader.find('.pkpUploaderProgressBar');
this.$fileName = $uploader.find('.pkpUploaderFilename');
// Bind to the pluploader for some configuration
this.pluploader.bind('FilesAdded',
this.callbackWrapper(this.startUpload));
this.pluploader.bind('UploadProgress',
this.callbackWrapper(this.updateProgress));
this.pluploader.bind('Error',
this.callbackWrapper(this.handleError));
this.pluploader.bind('FileUploaded',
this.callbackWrapper(this.uploadComplete));
this.pluploader.bind('QueueChanged',
this.callbackWrapper(this.refreshUploader));
// Ensure clicks on the visual button don't attempt to submit the form
$browseButton = $('#' + uploaderOptions.browse_button, this.getHtmlElement());
$browseButton.click(function(e) {
return false;
});
this.pluploader.refresh();
// Fake a focus effect on the visual button when plupload's hidden
// button is focused
self = this;
setTimeout(function() {
self.getHtmlElement().find('.moxie-shim input')
.focus(function(e) {
$browseButton.addClass('in_focus');
})
.blur(function(e) {
$browseButton.removeClass('in_focus');
});
}, 100);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.UploaderHandler, $.pkp.classes.Handler);
//
// Public methods
//
/**
* Initiate upload of a file
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
* @param {Object} file The data of the uploaded file.
*
*/
$.pkp.controllers.UploaderHandler.prototype.
startUpload = function(caller, pluploader, file) {
// Prevent > 1 files from being added.
if (pluploader.files.length > 1) {
pluploader.removeFile(pluploader.files[0]);
}
// Initiate the upload process
this.updateStatus('uploading');
pluploader.start();
};
/**
* Update the progress indicator for a file
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
* @param {Object} file The data of the uploaded file.
*
*/
$.pkp.controllers.UploaderHandler.prototype.
updateProgress = function(caller, pluploader, file) {
this.$progress.html(file.percent);
this.$progressBar.css('width', file.percent + '%');
};
/**
* Indicate the file upload has completed
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
* @param {Object} file The data of the uploaded file.
* @param {{response: string}} response
*/
$.pkp.controllers.UploaderHandler.prototype.
uploadComplete = function(caller, pluploader, file, response) {
var jsonData = $.parseJSON(response.response), filename = file.name;
if (!jsonData.status) {
this.showError(jsonData.content);
return;
}
if (typeof jsonData.uploadedFile !== 'undefined') {
filename = jsonData.uploadedFile.name || jsonData.uploadedFile.fileLabel;
// Store uploaded file data so that it can be referenced during
// other API events. This is used by the submission file wizard
// to delete files that are uploaded then replaced before submission
// is complete.
//
// See: $.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
// prototype.handleRemovedFiles
file.storedData = jsonData.uploadedFile;
}
this.$fileName.html(filename);
this.updateStatus('complete');
this.$progress.html('0');
this.$progressBar.css('width', 0);
};
/**
* Handle error revents from plupload
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
* @param {{message: string}} err An object describing an error condition
*
*/
$.pkp.controllers.UploaderHandler.prototype.
handleError = function(caller, pluploader, err) {
this.showError(err.message);
};
/**
* Display an error if encountered during upload
* @param {string} msg The error message
*
*/
$.pkp.controllers.UploaderHandler.prototype.
showError = function(msg) {
this.$progress.html('0');
this.$progressBar.css('width', 0);
this.updateStatus('error');
this.getHtmlElement().find('.pkpUploaderError').html(msg);
};
/**
* Refresh the uploader interface so buttons work correctly.
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
* @param {Object} file The data of the uploaded file.
*
*/
$.pkp.controllers.UploaderHandler.prototype.
refreshUploader = function(caller, pluploader, file) {
pluploader.refresh();
};
/**
* Update the status of the element in the DOM
* @param {string} status The new status
*
*/
$.pkp.controllers.UploaderHandler.prototype.
updateStatus = function(status) {
this.getHtmlElement().removeClass('loading waiting uploading error complete')
.addClass(status);
};
//
// Private static properties
//
/**
* Default options
* @private
* @type {Object}
* @const
*/
$.pkp.controllers.UploaderHandler.DEFAULT_PROPERTIES_ = {
runtimes: 'html5,flash,silverlight,html4',
max_file_size: $.pkp.cons.UPLOAD_MAX_FILESIZE,
multi_selection: false,
file_data_name: 'uploadedFile',
multipart: true,
headers: {'browser_user_agent': navigator.userAgent},
browse_button: 'pkpUploaderButton'
};
}(jQuery));
+126
View File
@@ -0,0 +1,126 @@
/**
* @file js/controllers/UrlInDivHandler.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 UrlInDivHandler
* @ingroup js_controllers
*
* @brief "URL in div" handler
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $divElement the wrapped div element.
* @param {Object} options options to be passed.
*/
$.pkp.controllers.UrlInDivHandler = function($divElement, options) {
this.parent($divElement, options);
// Store the URL (e.g. for reloads)
this.sourceUrl_ = options.sourceUrl;
// Load the contents.
this.reload();
if (options.refreshOn) {
this.bindGlobal(options.refreshOn, this.reload);
}
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.UrlInDivHandler, $.pkp.classes.Handler);
//
// Private properties
//
/**
* The URL to be used for data loaded into this div
* @private
* @type {?string}
*/
$.pkp.controllers.UrlInDivHandler.sourceUrl_ = null;
//
// Public Methods
//
/**
* Reload the div contents.
*/
$.pkp.controllers.UrlInDivHandler.prototype.reload = function() {
$.get(this.sourceUrl_,
this.callbackWrapper(this.handleLoadedContent_), 'json');
};
/**
* Fetches the progress bar URL.
* @return {?string} the source URL.
*/
$.pkp.controllers.UrlInDivHandler.prototype.getSourceUrl = function() {
return this.sourceUrl_;
};
/**
* Sets the progress bar URL.
* @param {string} sourceUrl the new source URL.
*/
$.pkp.controllers.UrlInDivHandler.prototype.setSourceUrl = function(sourceUrl) {
this.sourceUrl_ = sourceUrl;
};
//
// Private Methods
//
/**
* Handle a callback after a load operation returns.
*
* @param {Object} ajaxContext The AJAX request context.
* @param {Object} jsonData A parsed JSON response object.
* @return {boolean} Message handling result.
* @private
*/
$.pkp.controllers.UrlInDivHandler.prototype.handleLoadedContent_ =
function(ajaxContext, jsonData) {
var handledJsonData = this.handleJson(jsonData), urlInDivHandler = this;
if (handledJsonData.status === true) {
if (handledJsonData.content === undefined) {
// Request successful, but no data returned.
// Hide this div element.
this.getHtmlElement().hide();
} else {
// See bug #8237.
if (! /msie/.test(navigator.userAgent.toLowerCase())) {
this.getHtmlElement().hide();
this.html(handledJsonData.content);
this.getHtmlElement().fadeIn(400);
} else {
this.html(handledJsonData.content);
}
$(function() {
urlInDivHandler.trigger('urlInDivLoaded',
[urlInDivHandler.getHtmlElement().attr('id')]);
});
}
} else {
// Alert that loading failed.
alert(handledJsonData.content);
}
return false;
};
}(jQuery));
@@ -0,0 +1,93 @@
/**
* @defgroup js_controllers_dashboard_form
*/
/**
* @file js/controllers/dashboard/form/DashboardTaskFormHandler.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 DashboardTaskFormHandler
* @ingroup js_controllers_dashboard_form
*
* @brief Handle the styling and actions on the 'start new submission' form
* on the Task tab in the dashboard.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.dashboard =
$.pkp.controllers.dashboard || {form: { } };
/**
* @constructor
*
* @extends $.pkp.controllers.form.FormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {Object} options form options.
*/
$.pkp.controllers.dashboard.form.DashboardTaskFormHandler =
function($form, options) {
this.parent($form, options);
this.singleContextSubmissionUrl_ = options.singleContextSubmissionUrl;
$('#singleContext', $form).click(
this.callbackWrapper(this.startSingleContextSubmission_));
$('#multipleContext', $form).change(
this.callbackWrapper(this.startMultipleContextSubmission_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.dashboard.form.DashboardTaskFormHandler,
$.pkp.controllers.form.FormHandler);
//
// Private properties
//
/**
* The URL to be called to fetch a spotlight item via autocomplete.
* @private
* @type {string?}
*/
$.pkp.controllers.dashboard.form.DashboardTaskFormHandler.
prototype.singleContextSubmissionUrl_ = null;
//
// Private Methods
//
/**
* Redirect to the wizard for single context submissions
* @private
*/
$.pkp.controllers.dashboard.form.DashboardTaskFormHandler.
prototype.startSingleContextSubmission_ = function() {
window.location.href =
/** @type {string} */ (this.singleContextSubmissionUrl_);
};
/**
* Redirect to the wizard for multiple context submissions
* @private
*/
$.pkp.controllers.dashboard.form.DashboardTaskFormHandler.
prototype.startMultipleContextSubmission_ = function() {
var $form = this.getHtmlElement(),
url = $form.find('#multipleContext').val();
if (url != 0) { // not the default
window.location.href = /** @type {string} */ (url);
}
};
}(jQuery));
@@ -0,0 +1,157 @@
/**
* @file js/controllers/form/AjaxFormHandler.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 AjaxFormHandler
* @ingroup js_controllers_form
*
* @brief Form handler that submits the form to the server via AJAX and
* either replaces the form if it is re-rendered by the server or
* triggers the "formSubmitted" event after the server confirmed
* form submission.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.FormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {Object} options options to configure the AJAX form handler.
*/
$.pkp.controllers.form.AjaxFormHandler = function($form, options) {
options.submitHandler = this.submitForm;
this.parent($form, options);
if (typeof options.confirmText !== 'undefined') {
this.confirmText = options.confirmText;
}
this.bind('refreshForm', this.refreshFormHandler_);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.form.AjaxFormHandler,
$.pkp.controllers.form.FormHandler);
/**
* Overridden default from FormHandler -- disable form controls
* on AJAX forms by default.
* @protected
* @type {boolean}
*/
$.pkp.controllers.form.AjaxFormHandler.prototype.
disableControlsOnSubmit = true;
/**
* A confirmation message to display before submitting the form
* @protected
* @type {string}
*/
$.pkp.controllers.form.AjaxFormHandler.prototype.
confirmText = '';
//
// Public methods
//
/**
* Internal callback called after form validation to handle form
* submission.
*
* @param {Object} validator The validator plug-in.
* @param {HTMLElement} formElement The wrapped HTML form.
*/
$.pkp.controllers.form.AjaxFormHandler.prototype.submitForm =
function(validator, formElement) {
// This form implementation will post the form,
// and act depending on the returned JSON message.
var $form = this.getHtmlElement();
this.disableFormControls();
if (!this.confirmText.length || confirm(this.confirmText)) {
$.post($form.attr('action'), $form.serialize(),
this.callbackWrapper(this.handleResponse), 'json');
}
};
/**
* Callback to replace the element's content.
*
* @private
*
* @param {jQueryObject} sourceElement The containing element.
* @param {Event} event The calling event.
* @param {string} content The content to replace with.
*/
$.pkp.controllers.form.AjaxFormHandler.prototype.refreshFormHandler_ =
function(sourceElement, event, content) {
if (content) {
this.replaceWith(content);
}
};
/**
* Internal callback called after form validation to handle the
* response to a form submission.
*
* You can override this handler if you want to do custom handling
* of a form response.
*
* @param {HTMLElement} formElement The wrapped HTML form.
* @param {Object} jsonData The data returned from the server.
* @return {boolean} The response status.
*/
$.pkp.controllers.form.AjaxFormHandler.prototype.handleResponse =
function(formElement, jsonData) {
var $form, formSubmittedEvent, processedJsonData;
processedJsonData = this.handleJson(jsonData);
if (processedJsonData !== false) {
if (processedJsonData.content === '') {
// Notify any nested formWidgets of form submitted event.
formSubmittedEvent = new $.Event('formSubmitted');
$(this.getHtmlElement()).find('.formWidget').trigger(formSubmittedEvent);
// Trigger the "form submitted" event.
this.trigger('formSubmitted');
// Fire off any other optional events.
this.publishChangeEvents();
// re-enable the form control if it was disabled previously.
if (this.disableControlsOnSubmit) {
this.enableFormControls();
}
} else {
// Redisplay the form.
this.replaceWith(processedJsonData.content);
}
} else {
// data was false -- assume errors, re-enable form controls.
this.enableFormControls();
}
// Trigger the notify user event, passing this
// html element as data.
this.trigger('notifyUser');
// Hide the form spinner.
this.hideSpinner();
return processedJsonData.status;
};
}(jQuery));
@@ -0,0 +1,126 @@
/**
* @file js/controllers/form/CancelActionAjaxFormHandler.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 CancelActionAjaxFormHandler
* @ingroup js_controllers_form
*
* @brief A Handler for controlling the Query form
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $handledElement The clickable element
* the modal will be attached to.
* @param {Object} options non-default Dialog options
* to be passed into the dialog widget.
*
* Options are:
* - all options documented for the AjaxModalHandler.
* - cancelUrl: The URL to POST to in case of cancel.
*/
$.pkp.controllers.form.CancelActionAjaxFormHandler =
function($handledElement, options) {
var formHandler = this;
this.parent($handledElement, options);
// Store the options.
this.cancelUrl_ = options.cancelUrl;
this.cancelActionHandler = function() {
formHandler.handleCancelAction();
};
$(window).on('unload', this.cancelActionHandler);
};
$.pkp.classes.Helper.inherits($.pkp.controllers.form.
CancelActionAjaxFormHandler, $.pkp.controllers.form.AjaxFormHandler);
//
// Public properties
//
/**
* Function to handle deregistration of the modal as needed
* @public
* @type {function()?}
*/
$.pkp.controllers.form.CancelActionAjaxFormHandler.
prototype.cancelActionHandler = null;
//
// Private properties
//
/**
* The URL to be called when a cancel event occurs.
* @private
* @type {string?}
*/
$.pkp.controllers.form.CancelActionAjaxFormHandler.
prototype.cancelUrl_ = null;
/**
* True iff the form is complete (i.e. a normal "Save" action is in progress).
* @private
* @type {boolean}
*/
$.pkp.controllers.form.CancelActionAjaxFormHandler.
prototype.isComplete_ = false;
/**
* @inheritDoc
*/
$.pkp.controllers.form.CancelActionAjaxFormHandler.prototype.
containerCloseHandler = function(input, event, informationObject) {
var result = /** @type {boolean} */ (
this.parent('containerCloseHandler', input, event, informationObject));
if (result) {
this.handleCancelAction();
}
};
/**
* @inheritDoc
*/
$.pkp.controllers.form.CancelActionAjaxFormHandler.prototype.
submitForm = function(validator, formElement) {
// Flag the form as complete.
this.isComplete_ = true;
this.parent('submitForm', validator, formElement);
};
/**
* Cancel the form if necessary.
*/
$.pkp.controllers.form.CancelActionAjaxFormHandler.prototype.
handleCancelAction = function() {
// Unregister the window unload listener
if (this.cancelActionHandler !== null) {
$(window).off('unload', this.cancelActionHandler);
this.cancelActionHandler = null;
}
// If the form wasn't completed, post a cancel.
if (!this.isComplete_ && this.cancelUrl_ !== null) {
$.post(this.cancelUrl_);
}
};
}(jQuery));
@@ -0,0 +1,67 @@
/**
* @file js/controllers/form/ClientFormHandler.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 ClientFormHandler
* @ingroup js_controllers_form
*
* @brief Form handler that serializes the form on submission and
* triggers a "formSubmitted" event with the form data. This enables
* other widgets to use forms to request data from users although the
* data is not meant to be sent to the server through the form.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.FormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {Object} options options to be passed
* into the validator plug-in.
*/
$.pkp.controllers.form.ClientFormHandler = function($form, options) {
options.submitHandler = this.submitForm;
this.parent($form, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.form.ClientFormHandler,
$.pkp.controllers.form.FormHandler);
//
// Public methods
//
/**
* Internal callback called after form validation to handle form
* submission.
*
* @param {Object} validator The validator plug-in.
* @param {HTMLElement} formElement The wrapped HTML form.
*/
$.pkp.controllers.form.ClientFormHandler.prototype.submitForm =
function(validator, formElement) {
var $form, formData;
// This form implementation will trigger an event
// with the form data.
$form = this.getHtmlElement();
// Retrieve form data.
formData = $form.serializeArray();
// Inform the server that the form has been submitted.
formData.push({name: 'clientSubmit', value: true});
// Trigger a "form submitted" event with the form
// data as argument.
this.trigger('formSubmitted', [$.param(formData)]);
};
}(jQuery));
@@ -0,0 +1,187 @@
/**
* @file js/controllers/form/FileUploadFormHandler.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 FileUploadFormHandler
* @ingroup js_controllers_form
*
* @brief File upload form handler.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $form The wrapped HTML form element.
* @param {{
* readOnly: boolean,
* resetUploader: boolean,
* $uploader: jQueryObject,
* $preview: jQueryObject,
* uploaderOptions: Object
* }} options Form validation options.
*/
$.pkp.controllers.form.FileUploadFormHandler =
function($form, options) {
this.parent($form, options);
if (options.readOnly === undefined || options.readOnly === null) {
if (options.resetUploader !== undefined) {
this.resetUploader_ = options.resetUploader;
}
// An optional preview container for the file. If this option is passed
// the preview container will be hidden when a new file is uploaded and
// when the `fileDeleted` event is fired.
if (options.$preview !== undefined && options.$preview.length) {
this.$preview = options.$preview;
this.bind('fileDeleted', this.callbackWrapper(this.fileDeleted));
}
// Attach the uploader handler to the uploader HTML element.
this.attachUploader_(options.$uploader, options.uploaderOptions);
this.uploaderSetup(options.$uploader);
}
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.form.FileUploadFormHandler,
$.pkp.controllers.form.AjaxFormHandler);
/**
* Reset the uploader widget flag.
* @private
* @type {boolean}
*/
$.pkp.controllers.form.FileUploadFormHandler.prototype.
resetUploader_ = false;
/**
* The file preview DOM element. A jQuery object when available
* @protected
* @type {boolean|jQueryObject}
*/
$.pkp.controllers.form.FileUploadFormHandler.prototype.
$preview = false;
//
// Extended methods from AjaxFormHandler.
//
/**
* @inheritDoc
*/
$.pkp.controllers.form.FileUploadFormHandler.prototype.handleResponse =
function(formElement, jsonData) {
var fileUploader;
if (this.resetUploader_) {
fileUploader = $('#plupload', this.getHtmlElement())
.plupload('getUploader');
fileUploader.splice();
fileUploader.refresh();
// Reset the temporary file id value.
$('#temporaryFileId', this.getHtmlElement()).val('');
}
return /** @type {boolean} */ (
this.parent('handleResponse', formElement, jsonData));
};
//
// Public methods
//
/**
* The setup callback of the uploader.
* @param {jQueryObject} $uploader Element that contains the plupload object.
*/
$.pkp.controllers.form.FileUploadFormHandler.prototype.
uploaderSetup = function($uploader) {
var uploadHandler = $.pkp.classes.Handler.getHandler($uploader);
// Subscribe to uploader events.
uploadHandler.pluploader.bind('FileUploaded',
this.callbackWrapper(this.handleUploadResponse));
};
/**
* Handle the response of a "file upload" request.
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
* @param {Object} file The data of the uploaded file.
* @param {{response: string}} ret The serialized JSON response.
*/
$.pkp.controllers.form.FileUploadFormHandler.prototype.
handleUploadResponse = function(caller, pluploader, file, ret) {
// Handle the server's JSON response.
var jsonData = /** @type {boolean|{uploadedFile: Object,
temporaryFileId: string, content: string}} */
(this.handleJson($.parseJSON(ret.response))),
$uploadForm, $temporaryFileId;
if (jsonData !== false) {
// Trigger the file uploaded event.
this.trigger('fileUploaded', [jsonData.uploadedFile]);
// Hide preview if one exists
if (this.$preview) {
this.$preview.hide();
}
if (jsonData.content === '') {
// Successful upload to temporary file; save to main form.
$uploadForm = this.getHtmlElement();
$temporaryFileId = $uploadForm.find('#temporaryFileId');
$temporaryFileId.val(jsonData.temporaryFileId);
} else {
// Display the revision confirmation form.
this.replaceWith(jsonData.content);
}
}
};
/**
* Fires when the file has been removed
*/
$.pkp.controllers.form.FileUploadFormHandler.prototype.
fileDeleted = function() {
if (this.$preview) {
this.$preview.hide();
}
};
//
// Private methods
//
/**
* Attach the uploader handler.
* @private
* @param {jQueryObject} $uploader The wrapped HTML uploader element.
* @param {Object} options Uploader options.
*/
$.pkp.controllers.form.FileUploadFormHandler.prototype.
attachUploader_ = function($uploader, options) {
// Attach the uploader handler to the uploader div.
$uploader.pkpHandler('$.pkp.controllers.UploaderHandler', options);
};
}(jQuery));
+640
View File
@@ -0,0 +1,640 @@
/**
* @defgroup js_controllers_form
*/
/**
* @file js/controllers/form/FormHandler.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 FormHandler
* @ingroup js_controllers_form
*
* @brief Abstract form handler.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.form = $.pkp.controllers.form || {};
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {{
* transformButtons: boolean,
* submitHandler: Function,
* cancelRedirectUrl: string,
* disableControlsOnSubmit: boolean,
* trackFormChanges: boolean,
* enableDisablePairs: Object
* }} options options to configure the form handler.
*/
$.pkp.controllers.form.FormHandler = function($form, options) {
var key, validator;
this.parent($form, options);
// Check whether we really got a form.
if (!$form.is('form')) {
throw new Error(['A form handler controller can only be bound',
' to an HTML form element!'].join(''));
}
// Activate and configure the validation plug-in.
if (options.submitHandler) {
this.callerSubmitHandler_ = options.submitHandler;
}
// Handle datepicker
// If we find a .datepicker in tpl the code adds (form/textInput.tpl) a new
// hidden field with the id = <original-element-id>-altField attr:
// data-date-format = date format from config.inc.php translated into JQuery
// Datepicker date format. The <original-element-name> if changed to
// <original-element-name>-removed in order for the hidden field value to
// send to post request altField and altFormat are used in order for the user
// to have its config.inc.php dateFormatShort parameter displayed but
// 'yy-mm-dd' to be send to post request.
// [http://api.jqueryui.com/datepicker/#option-altField]
$form.find('.datepicker').each(function() {
var $this = $(this);
$this.datepicker({
altField: '#' + $this.prop('id') + '-altField',
altFormat: 'yy-mm-dd',
dateFormat: $('#' + $this.prop('id') + '-altField')
.attr('data-date-format')
});
$this.prop('name', $this.prop('name') + '-removed');
});
// Set the redirect-to URL for the cancel button (if there is one).
if (options.cancelRedirectUrl) {
this.cancelRedirectUrl_ = options.cancelRedirectUrl;
}
// specific forms may override the form's default behavior
// to warn about unsaved changes.
if (typeof options.trackFormChanges !== 'undefined') {
this.trackFormChanges = options.trackFormChanges;
}
// disable submission controls on certain forms.
if (options.disableControlsOnSubmit) {
this.disableControlsOnSubmit = options.disableControlsOnSubmit;
}
// Items that should enable or disable each other.
if (options.enableDisablePairs) {
this.enableDisablePairs_ = options.enableDisablePairs;
this.setupEnableDisablePairs();
}
// Update enable disable pairs state.
for (key in this.enableDisablePairs_) {
$form.find("[id^='" + key + "']").trigger('updatePair');
}
validator = $form.validate({
onfocusout: this.callbackWrapper(this.onFocusOutValidation_),
errorClass: 'error',
highlight: function(element, errorClass) {
$(element).parent().parent().addClass(errorClass);
},
unhighlight: function(element, errorClass) {
$(element).parent().parent().removeClass(errorClass);
},
submitHandler: this.callbackWrapper(this.submitHandler_),
showErrors: this.callbackWrapper(this.showErrors),
errorPlacement: function(error, element) {
if (element.is(':checkbox')) {
// place error after checkbox text
element.parent().closest(':not(label)').append(error);
} else {
// default jquery validate placement
error.insertAfter(element);
}
}
});
// Activate the cancel button (if present).
$('[id^=\'cancelFormButton-\']', $form)
.click(this.callbackWrapper(this.cancelForm));
$form.find('.showMore, .showLess').bind('click', this.switchViz);
// Initial form validation.
if (validator.checkForm()) {
this.trigger('formValid');
} else {
this.trigger('formInvalid');
}
this.initializeTinyMCE();
// bind a handler to make sure tinyMCE fields are populated.
$('[id^=\'submitFormButton\']', $form).click(this.callbackWrapper(
this.pushTinyMCEChanges_));
// bind a handler to handle change events on input fields.
// 1. For normal inputs...
$(':input', $form).change(this.callbackWrapper(this.formChange));
// 2. For other kinds of controls like listbuilders
this.bind('formChange', this.callbackWrapper(this.formChange));
// ensure that date picker modals are hidden when clicked away from.
$form.click(this.callbackWrapper(this.hideDatepicker_));
this.publishEvent('tinyMCEInitialized');
this.bind('tinyMCEInitialized', this.tinyMCEInitHandler_);
this.bind('containerClose', this.containerCloseHandler);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.form.FormHandler,
$.pkp.classes.Handler);
//
// Protected properties
//
/**
* If true, the FormHandler will disable the submit button if the form
* successfully validates and is submitted.
* @protected
* @type {boolean}
*/
$.pkp.controllers.form.FormHandler.prototype.disableControlsOnSubmit = false;
/**
* By default, all FormHandler instances and subclasses track changes to
* form data.
* @protected
* @type {boolean}
*/
$.pkp.controllers.form.FormHandler.prototype.trackFormChanges = true;
//
// Private properties
//
/**
* If provided, the caller's submit handler, which will be
* triggered to save the form.
* @private
* @type {Function?}
*/
$.pkp.controllers.form.FormHandler.prototype.callerSubmitHandler_ = null;
/**
* If provided, the URL to redirect to when the cancel button is clicked
* @private
* @type {string?}
*/
$.pkp.controllers.form.FormHandler.prototype.cancelRedirectUrl_ = null;
/**
* Only submit a track event for this form once.
* @type {boolean}
*/
$.pkp.controllers.form.FormHandler.prototype.formChangesTracked = false;
/**
* An object containing items that should enable or disable each other.
* @private
* @type {Object?}
*/
$.pkp.controllers.form.FormHandler.prototype.enableDisablePairs_ = null;
//
// Public methods
//
/**
* Internal callback called whenever the validator has to show form errors.
*
* @param {Object} validator The validator plug-in.
* @param {Object} errorMap An associative list that attributes
* element names to error messages.
* @param {Array} errorList An array with objects that contains
* error messages and the corresponding HTMLElements.
*/
$.pkp.controllers.form.FormHandler.prototype.showErrors =
function(validator, errorMap, errorList) {
// ensure that rich content elements have their
// values stored before validation.
if (typeof tinyMCE !== 'undefined') {
tinyMCE.EditorManager.triggerSave();
}
// Clone the form validator before checking the entire form.
// We need to clone otherwise the internal validator error
// list will be changed when the show errors timeout is
// executed, showing/hiding the wrong errors.
var validatorClone = $.extend(true, {}, validator);
// Show errors generated by the form change.
// Use a timer so we make sure that concurrent triggered events
// are handled before the error messages appear in the UI.
//
// The main issue is a click event in cancel buttons while a non
// valid field is focused. Without the timer, the UI is changed
// before the click action is complete (the mouse up will not occur in
// the cancel link, because it will be moved by the error messages).
setTimeout(this.callbackWrapper(function() {
validatorClone.defaultShowErrors();
validatorClone = null;
}), 250);
// Emit validation events.
if (validator.checkForm()) {
// Trigger a "form valid" event.
this.trigger('formValid');
} else {
// Trigger a "form invalid" event.
this.trigger('formInvalid');
this.enableFormControls();
}
};
/**
* Internal callback called when a form element changes.
*
* @param {HTMLElement} formElement The form element that generated the event.
* @param {Event} event The formChange event.
*/
$.pkp.controllers.form.FormHandler.prototype.formChange =
function(formElement, event) {
if (this.trackFormChanges && !this.formChangesTracked) {
this.formChangesTracked = true;
this.trigger('formChanged');
}
};
//
// Protected methods
//
/**
* Protected method to disable a form's submit control if it is
* desired.
*
* @return {boolean} true.
* @protected
*/
$.pkp.controllers.form.FormHandler.prototype.disableFormControls =
function() {
// We have made it to submission, disable the form control if
// necessary, submit the form.
if (this.disableControlsOnSubmit) {
this.getHtmlElement().find(':submit').attr('disabled', 'disabled').
addClass('ui-state-disabled');
}
return true;
};
/**
* Protected method to reenable a form's submit control if it is
* desired.
*
* @return {boolean} true.
* @protected
*/
$.pkp.controllers.form.FormHandler.prototype.enableFormControls =
function() {
this.getHtmlElement().find(':submit').removeAttr('disabled').
removeClass('ui-state-disabled');
return true;
};
/**
* Internal callback called to cancel the form.
*
* @param {HTMLElement} cancelButton The cancel button.
* @param {Event} event The event that triggered the
* cancel button.
* @return {boolean} false.
*/
$.pkp.controllers.form.FormHandler.prototype.cancelForm =
function(cancelButton, event) {
this.unregisterForm();
this.trigger('formCanceled');
return false;
};
/**
* Unregister form for tracking changed data.
*/
$.pkp.controllers.form.FormHandler.prototype.unregisterForm =
function() {
// Trigger the "form canceled" event and unregister the form.
this.formChangesTracked = false;
this.trigger('unregisterChangedForm');
};
/**
* Configures the enable/disable pair bindings between a checkbox
* and some other form element.
*
* @return {boolean} true.
*/
$.pkp.controllers.form.FormHandler.prototype.setupEnableDisablePairs =
function() {
var formElement = this.getHtmlElement(), key;
for (key in this.enableDisablePairs_) {
$(formElement).find("[id^='" + key + "']").bind(
'click updatePair', this.callbackWrapper(this.toggleDependentElement_));
}
return true;
};
/**
* Internal callback called to submit the form
* without further validation.
*
* @param {Object} validator The validator plug-in.
*/
$.pkp.controllers.form.FormHandler.prototype.submitFormWithoutValidation =
function(validator) {
// NB: When setting a submitHandler in jQuery's validator
// plugin then the submit event will always be canceled and our
// return value will be ignored (see the handle() method in the
// validator plugin). The only way around this seems to be unsetting
// the submit handler before calling the submit method again.
validator.settings.submitHandler = null;
this.disableFormControls();
this.getHtmlElement().submit();
this.formChangesTracked = false;
};
/**
* Add a class to the spinner to indicate it should be hidden
*
* @protected
*/
$.pkp.controllers.form.FormHandler.prototype.hideSpinner =
function() {
this.getHtmlElement()
.find('.formButtons .pkp_spinner').removeClass('is_visible');
};
//
// Private Methods
//
/**
* Internal callback called after form validation to handle form
* submission.
*
* NB: Returning from this method without explicitly submitting
* the form will cancel form submission.
*
* @private
*
* @param {Object} validator The validator plug-in.
* @param {HTMLElement} formElement The wrapped HTML form.
*/
$.pkp.controllers.form.FormHandler.prototype.submitHandler_ =
function(validator, formElement) {
// Notify any nested formWidgets of the submit action.
var defaultPrevented = false;
$(formElement).find('.formWidget').each(function() {
var formSubmitEvent = new $.Event('formSubmitRequested');
if (!defaultPrevented) {
$(this).trigger(formSubmitEvent);
defaultPrevented = formSubmitEvent.isDefaultPrevented();
}
});
// If the default behavior was prevented for any reason, stop.
if (defaultPrevented) {
return;
}
// For datepicker controls, ensure that empty values are respected.
$(formElement).find('.datepicker').each(function() {
if ($(this).prop('value') === '') {
$('#' + $(this).prop('id') + '-altField').prop('value', '');
}
});
this.showSpinner_();
this.trigger('unregisterChangedForm');
if (this.callerSubmitHandler_ !== null) {
this.formChangesTracked = false;
// A form submission handler (e.g. Ajax) was provided. Use it.
this.callbackWrapper(this.callerSubmitHandler_).
call(validator, formElement);
} else {
// No form submission handler was provided. Use the usual method.
this.submitFormWithoutValidation(validator);
}
};
/**
* Internal callback called to push TinyMCE changes back to fields
* so they can be validated.
*
* @param {HTMLElement} submitButton The submit button.
* @param {Event} event The event that triggered the
* submit button.
* @return {boolean} true.
* @private
*/
$.pkp.controllers.form.FormHandler.prototype.pushTinyMCEChanges_ =
function(submitButton, event) {
// ensure that rich content elements have their
// values stored before validation.
if (typeof tinyMCE !== 'undefined') {
tinyMCE.EditorManager.triggerSave();
}
return true;
};
/**
* Enables or disables the item which depends on the state of source of the
* Event.
* @param {HTMLElement} sourceElement The element which generated the event.
* @param {Event} event The event.
* @return {boolean} true.
* @private
*/
$.pkp.controllers.form.FormHandler.prototype.toggleDependentElement_ =
function(sourceElement, event) {
var formElement, elementId, targetElement;
formElement = this.getHtmlElement();
elementId = $(sourceElement).attr('id');
targetElement = $(formElement).find(
"[id^='" + this.enableDisablePairs_[elementId] + "']");
if ($(sourceElement).is(':checked')) {
$(targetElement).prop('disabled', false);
} else {
$(targetElement).prop('disabled', true);
}
return true;
};
/**
* Bind a blur handler on tinyMCE instances inside this form
* to call form validation on form elements that stores the correspondent
* tinyMCE editors.
* @private
* @param {HTMLElement} input The input element that triggered the
* event.
* @param {Event} event The tinyMCE initialized event.
* @param {Object} tinyMCEObject An array containing the tinyMCE object inside
* this multilingual element handler that was initialized.
*/
$.pkp.controllers.form.FormHandler.prototype.tinyMCEInitHandler_ =
function(input, event, tinyMCEObject) {
var editorId = tinyMCEObject.id;
tinyMCEObject.on('blur', this.callbackWrapper(function(tinyMCEObject) {
// Save the current tinyMCE value to the form element.
tinyMCEObject.save();
// Get the form element that stores the tinyMCE data.
var $form = this.getHtmlElement(),
formElement = $('#' +
$.pkp.classes.Helper.escapeJQuerySelector(editorId), $form),
// Validate only this element.
validator = $form.validate();
validator.element(formElement);
}));
};
/**
* Bind a handler for container (e.g. modal) close events to permit forms
* to clean up.blur handler on tinyMCE instances inside this form
* @param {HTMLElement} input The input element that triggered the
* event.
* @param {Event} event The initialized event.
* @param {{closePermitted: boolean}} informationObject
* @return {boolean} Event handling success.
*/
$.pkp.controllers.form.FormHandler.prototype.containerCloseHandler =
function(input, event, informationObject) {
var $form = $(this.getHtmlElement());
// prevent orphaned date pickers that may be still open.
$form.find('.hasDatepicker').datepicker('hide');
if (this.formChangesTracked) {
if (!confirm(pkp.localeKeys['form.dataHasChanged'])) {
if (informationObject) {
informationObject.closePermitted = false;
}
return false;
} else {
this.trigger('unregisterAllForms');
}
}
if (typeof informationObject !== 'undefined') {
informationObject.closePermitted = true;
}
return true;
};
/**
* Blur event handler, attached to all input fields on this form
* by the form validator. It's passed as an option when initializing
* the validator.
*
* It will make sure that fields are always validated on blur. Without this
* users can delete data from a required field and move to another one
* without receiving any validation alert.
* @private
* @param {Object} validator Validator.
* @param {Object} element Element.
* @return {boolean} True so the blur event can still be handled.
*/
$.pkp.controllers.form.FormHandler.prototype.onFocusOutValidation_ =
function(validator, element) {
var $form = this.getHtmlElement();
// Make sure the element is still present in form.
if ($(element).parents('#' + $form.attr('id')).length) {
validator.element(element);
}
return true;
};
/**
* Hide a date picker if a user clicks outside of the element.
* @private
* @param {Object} formElement Element.
* @param {Event} event The event.
*/
$.pkp.controllers.form.FormHandler.prototype.hideDatepicker_ =
function(formElement, event) {
var originalEvent, ele, form;
originalEvent = event.originalEvent;
if (typeof originalEvent == 'undefined') {
return;
}
ele = originalEvent.target;
form = this.getHtmlElement();
if (!$(ele).hasClass('hasDatepicker') &&
!$(ele).hasClass('ui-datepicker') &&
!$(ele).hasClass('ui-icon') &&
!$(ele).hasClass('ui-datepicker-next') &&
!$(ele).hasClass('ui-datepicker-prev') &&
!$(ele).parent().parents('.ui-datepicker').length) {
$(form).find('.hasDatepicker').datepicker('hide');
}
};
/**
* Add a class to the spinner to indicate it should be visible
*
* @private
*/
$.pkp.controllers.form.FormHandler.prototype.showSpinner_ =
function() {
this.getHtmlElement()
.find('.formButtons .pkp_spinner').addClass('is_visible');
};
}(jQuery));
@@ -0,0 +1,264 @@
/**
* @file js/controllers/form/MultilingualInputHandler.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 MultilingualInputHandler
* @ingroup js_controllers_form
*
* @brief Handler for toggling the pop-over on multi lingual inputs (text
* inputs and text areas, mostly).
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $popover the wrapped HTML element.
* @param {Object} options options to be passed
* into the validator plug-in.
*/
$.pkp.controllers.form.MultilingualInputHandler = function($popover, options) {
this.parent($popover, options);
// Bind to the focus of the primary language (the first input)
// open the pop-over
var $popoverNode = null;
if ($popover.hasClass('pkpTagit')) {
$popoverNode = $popover.find(':input').filter(':visible');
} else {
$popoverNode = $popover.find(':input').first();
}
$popoverNode
.focus(this.callbackWrapper(this.focusHandler_));
// Bind to the blur of any of the inputs to check if we should close.
$popover.find(':input').
blur(this.callbackWrapper(this.blurHandler_));
this.publishEvent('tinyMCEInitialized');
this.tinyMCEInitHandler_();
setTimeout(this.callbackWrapper(this.isIncomplete_), 500);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.form.MultilingualInputHandler,
$.pkp.classes.Handler);
//
// Private helper methods.
//
/**
* Focus event handler. This is attached to all primary inputs.
*
* @param {HTMLElement} multilingualInput The primary multilingual
* element.
* @param {Event} event The focus event.
* @private
*/
$.pkp.controllers.form.MultilingualInputHandler.prototype.focusHandler_ =
function(multilingualInput, event) {
this.showPopover_();
};
/**
* Blur event handler. This is attached to all inputs inside this
* popover element.
*
* @param {HTMLElement} multilingualInput The element in the
* multilingual set to hide.
* @param {Event} event The event that triggered the action.
* @return {boolean} Return true to continue the event handling.
* @private
*/
$.pkp.controllers.form.MultilingualInputHandler.prototype.blurHandler_ =
function(multilingualInput, event) {
// Use a timeout to give the other element a chance to acquire the focus.
setTimeout(this.callbackWrapper(function() {
if (!this.hasElementInFocus_()) {
this.hidePopover_();
}
}), 100);
return true;
};
/**
* Hide this popover.
* @private
*/
$.pkp.controllers.form.MultilingualInputHandler.prototype.hidePopover_ =
function() {
var $popover = this.getHtmlElement();
$popover.removeClass('localization_popover_container_focus');
$popover.find('.localization_popover').hide();
this.isIncomplete_();
};
/**
* Show this popover.
* @private
*/
$.pkp.controllers.form.MultilingualInputHandler.prototype.showPopover_ =
function() {
var $popover = this.getHtmlElement();
$popover.addClass('localization_popover_container_focus');
// Hack alert: setting width in JS because they do not line up otherwise.
$popover.find('.localization_popover').width(
/** @type {number} */ ($popover.width()));
// Show the pop over.
$popover.find('.localization_popover').show();
};
/**
* Test if any of the elements inside this popover has focus.
* @return {boolean} True iff an element is in focus.
* @private
*/
$.pkp.controllers.form.MultilingualInputHandler.prototype.hasElementInFocus_ =
function() {
var $popover = this.getHtmlElement();
// Do the test.
if ($popover.has(document.activeElement).length) {
return true;
} else {
return false;
}
};
/**
* Check if the field is missing a localization
*
* Fields are not considered to be missing a localization unless at least
* one of the localizations has an entry.
*
* @private
*/
$.pkp.controllers.form.MultilingualInputHandler.prototype.isIncomplete_ =
function() {
var $popover = this.getHtmlElement(),
$inputs = [],
valuesCount = 0;
if (typeof tinyMCE === 'undefined') {
return;
}
// Track current values in the tinyMCE control
if (this.getHtmlElement().find('.richContent').length) {
$popover.find('textarea').each(function() {
var id = $(this).attr('id'),
tinymce;
$inputs.push($(this));
tinymce = tinyMCE.EditorManager.get(/** @type {string} */(
$(this).attr('id')));
if (tinymce.getContent()) {
valuesCount++;
}
});
} else {
$inputs = $popover.find(':input');
$inputs.each(function() {
if ($(this).val()) {
valuesCount++;
}
});
}
if (valuesCount > 0 && valuesCount < $inputs.length) {
$popover.removeClass('localizationComplete')
.addClass('localizationIncomplete');
} else if (valuesCount === $inputs.length) {
$popover.removeClass('localizationIncomplete')
.addClass('localizationComplete');
} else {
$popover.removeClass('localizationIncomplete localizationComplete');
}
};
/**
* Attach focus and blur event handlers to the tinyMCE window element, which
* show and hide the localization popover
* @private
*/
$.pkp.controllers.form.MultilingualInputHandler.prototype.tinyMCEInitHandler_ =
function() {
if (!this.getHtmlElement().find('.richContent').length) {
return;
}
if (typeof tinyMCE === 'undefined') {
return;
}
var htmlElement = this.getHtmlElement(),
tinyMCEObject = tinyMCE.EditorManager.get(/** @type {string} */(
htmlElement.find('textarea').first().attr('id')));
tinyMCEObject.on('focus', this.callbackWrapper(function() {
// We need also to close the multilingual popover when user clicks
// outside the popover element. The blur event is not enough because
// sometimes (with text selected in editor) Chrome will consider the
// tinyMCE editor as still active and that will avoid the popover to
// close (see the first check of the blur handler, just above).
//
// Firefox will also not completely focus on tinyMCE editors after
// coming back from fullscreen mode (the callback to focus the
// editor when set content will only trigger the focus handler that
// we attach here, but will not move the cursor inside the tinyMCE
// editor). Then, if user clicks outside the popover, it will not
// close because no blur event will be triggered.
this.trigger('callWhenClickOutside', {
container: this.getHtmlElement(),
callback: this.callbackWrapper(this.hidePopover_)
});
this.showPopover_();
}));
tinyMCEObject.on('blur', this.callbackWrapper(function() {
// Check if the active document element is still the tinyMCE
// editor. If true, return false. This will avoid closing the
// popover if user is just inserting an image or editing the
// html source, for example (both actions open a new window).
if ($(tinyMCEObject.getContainer()).find('iframe').attr('id') ==
$(document.activeElement).attr('id')) {
return false;
}
// Use a timeout to give the other element a chance to acquire the
// focus.
setTimeout(this.callbackWrapper(function() {
if (!this.hasElementInFocus_()) {
this.hidePopover_();
}
}), 0);
}));
};
}(jQuery));
@@ -0,0 +1,49 @@
/**
* @file js/controllers/form/ToggleFormHandler.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 ToggleFormHandler
* @ingroup js_controllers_form
*
* @brief Extension to ClientFormHandler that accepts a checkbox click as a
* submit action.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.ClientFormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
*/
$.pkp.controllers.form.ToggleFormHandler =
function($form) {
this.parent($form, {trackFormChanges: false});
$form.change(
this.callbackWrapper(this.toggleHandler_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.form.ToggleFormHandler,
$.pkp.controllers.form.ClientFormHandler);
//
// Private methods
//
/**
* Click handler for the checkbox.
* @private
* @return {boolean} Always returns true.
*/
$.pkp.controllers.form.ToggleFormHandler.
prototype.toggleHandler_ = function() {
this.getHtmlElement().submit();
return true;
};
}(jQuery));
@@ -0,0 +1,149 @@
/**
* @file js/controllers/form/UserFormHandler.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 UserFormHandler
* @ingroup js_controllers_form
*
* @brief Add tools to the AjaxFormHandler facilitating user creation/editing.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {{
* fetchUsernameSuggestionUrl: string,
* usernameSuggestionTextAlert: string,
* hideNonReviewerInterests: boolean
* }} options options to configure the form handler.
*/
$.pkp.controllers.form.UserFormHandler = function($form, options) {
this.parent($form, options);
// Set data for suggesting usernames. Both keys should be present.
if (options.fetchUsernameSuggestionUrl &&
options.usernameSuggestionTextAlert) {
this.fetchUsernameSuggestionUrl_ = options.fetchUsernameSuggestionUrl;
this.usernameSuggestionTextAlert_ = options.usernameSuggestionTextAlert;
}
// Attach handler to suggest username button (if present)
$('[id^="suggestUsernameButton"]', $form).click(
this.callbackWrapper(this.generateUsername));
if (options.hideNonReviewerInterests) {
$('[id^="reviewerGroup-"]', $form).click(
this.callbackWrapper(this.setInterestsVisibility_));
this.setInterestsVisibility_();
}
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.form.UserFormHandler,
$.pkp.controllers.form.AjaxFormHandler);
//
// Private properties
//
/**
* The URL to be called to fetch a username suggestion.
* @private
* @type {string}
*/
$.pkp.controllers.form.UserFormHandler.
prototype.fetchUsernameSuggestionUrl_ = '';
/**
* The message that will be displayed if users click on suggest
* username button with no data in family name.
* @private
* @type {string}
*/
$.pkp.controllers.form.UserFormHandler.
prototype.usernameSuggestionTextAlert_ = '';
//
// Protected methods
//
/**
* Event handler that is called when the suggest username button is clicked.
*
* @param {HTMLElement} el clicked by this event
* @param {Event} e triggered
*/
$.pkp.controllers.form.UserFormHandler.prototype.
generateUsername = function(el, e) {
// Don't submit the form!
e.preventDefault();
var $form = this.getHtmlElement(),
givenName, familyName, fetchUrl, sitePrimaryLocale;
// Fetch entered names
sitePrimaryLocale =
/** @type {string} */ ($('[name="sitePrimaryLocale"]', $form).val());
givenName = /** @type {string} */ ($('[name="givenName[' +
sitePrimaryLocale + ']"]', $form).val());
familyName = /** @type {string} */ ($('[name="familyName[' +
sitePrimaryLocale + ']"]', $form).val());
// Replace dummy values in the URL with entered values
fetchUrl = this.fetchUsernameSuggestionUrl_.
replace('GIVEN_NAME_PLACEHOLDER', givenName).
replace('FAMILY_NAME_PLACEHOLDER', familyName);
$.get(fetchUrl, this.callbackWrapper(this.setUsername), 'json');
};
/**
* Check JSON message and set it to username, back on form.
* @param {HTMLElement} formElement The Form HTML element.
* @param {JSONType} jsonData The jsonData response.
*/
$.pkp.controllers.form.UserFormHandler.prototype.
setUsername = function(formElement, jsonData) {
var processedJsonData = this.handleJson(jsonData),
$form = this.getHtmlElement();
if (processedJsonData === false) {
throw new Error('JSON response must be set to true!');
}
// Re-validate the field
$('[id^="username"]', $form).val(processedJsonData.content)
.trigger('blur');
};
//
// Private methods
//
/**
* Event handler that is called when a reviewer role checkbox is clicked.
* @private
*/
$.pkp.controllers.form.UserFormHandler.prototype.
setInterestsVisibility_ = function() {
var $form = this.getHtmlElement(), $interestsElement = $('#interests', $form);
if ($('[id^="reviewerGroup-"]:checked', $form).size()) {
// At least one checked reviewer role was found; show interests
$interestsElement.show(300);
} else {
// No checked reviewer roles found; hide interests
$interestsElement.hide(300);
}
};
}(jQuery));
@@ -0,0 +1,137 @@
/**
* @defgroup js_controllers_form_reviewer
*/
/**
* @file js/controllers/form/reviewer/ReviewerReviewStep3FormHandler.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 ReviewerReviewStep3FormHandler
* @ingroup js_controllers_form_reviewer
*
* @brief Reviewer step 3 form handler.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.form.reviewer =
$.pkp.controllers.form.reviewer || { };
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $formElement A wrapped HTML element that
* represents the approved proof form interface element.
* @param {Object} options Tabbed modal options.
*/
$.pkp.controllers.form.reviewer.ReviewerReviewStep3FormHandler =
function($formElement, options) {
this.parent($formElement, options);
// bind a handler to make sure we update the required state
// of the comments field.
$formElement.find('[id^=\'submitFormButton-\']').click(this.callbackWrapper(
this.updateCommentsRequired_));
$formElement.find('[type^=\'submit\']').click(this.callbackWrapper(
this.updateRecommendationRequired_));
$formElement.find('[type^=\'submit\']').click(this.callbackWrapper(
this.updateSaveOrSubmit_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.form.reviewer.ReviewerReviewStep3FormHandler,
$.pkp.controllers.form.AjaxFormHandler
);
//
// Private methods.
//
/**
* Internal callback called before form validation to ensure the
* proper "required" state of the Recommendation field
*
* @param {HTMLElement} submitButton The submit button.
* @param {Event} event The event that triggered the
* submit button.
* @return {boolean} true.
* @private
*/
$.pkp.controllers.form.reviewer.ReviewerReviewStep3FormHandler.
prototype.updateRecommendationRequired_ = function(submitButton, event) {
var $formElement = this.getHtmlElement(),
$recommendationElement = $formElement.find('[id^="recommendation"]');
if ($recommendationElement.length) {
if (submitButton.id.includes('submitFormButton-')) {
$recommendationElement.attr('required', '1');
} else {
$recommendationElement.removeAttr('required');
}
}
return true;
};
/**
* Internal callback called before form validation to ensure the
* proper "required" state of the comments field, depending on grid
* contents.
*
* @param {HTMLElement} submitButton The submit button.
* @param {Event} event The event that triggered the
* submit button.
* @return {boolean} true.
* @private
*/
$.pkp.controllers.form.reviewer.ReviewerReviewStep3FormHandler.
prototype.updateCommentsRequired_ = function(submitButton, event) {
var $formElement = this.getHtmlElement(),
$commentsElement = $formElement.find('[id^="comments"]');
if ($('#reviewAttachmentsGridContainer').
find('tbody.empty:visible').length == 1) {
// There's nothing in the files grid; make sure the
// comments field is required.
$commentsElement.attr('required', '1');
} else {
// There's something in the files grid; the comments
// field is optional.
$commentsElement.removeAttr('required');
}
return true;
};
/**
* Internal callback called before form validation to ensure the
* proper handling of the save vs. submit button, using a hidden
* field.
*
* @param {HTMLElement} submitButton The submit button.
* @param {Event} event The event that triggered the
* submit button.
* @return {boolean} true.
* @private
*/
$.pkp.controllers.form.reviewer.ReviewerReviewStep3FormHandler.
prototype.updateSaveOrSubmit_ = function(submitButton, event) {
var $formElement = this.getHtmlElement();
switch ($(submitButton).attr('name')) {
case 'submitFormButton':
$formElement.find('input[name="isSave"]').val('0');
break;
case 'saveFormButton':
$formElement.find('input[name="isSave"]').val('1');
break;
}
return true;
};
}(jQuery));
@@ -0,0 +1,411 @@
/**
* @file js/controllers/grid/CategoryGridHandler.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 CategoryGridHandler
* @ingroup js_controllers_grid
*
* @brief Category grid handler.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.grid.GridHandler
*
* @param {jQueryObject} $grid The grid this handler is
* attached to.
* @param {Object} options Grid handler configuration.
*/
$.pkp.controllers.grid.CategoryGridHandler = function($grid, options) {
this.parent($grid, options);
};
$.pkp.classes.Helper.inherits($.pkp.controllers.grid.CategoryGridHandler,
$.pkp.controllers.grid.GridHandler);
//
// Public methods.
//
/**
* Get category id prefix.
* @return {string} Category id prefix.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getCategoryIdPrefix =
function() {
return this.getGridIdPrefix() + '-category-';
};
/**
* Get categories tbody element.
* @return {jQueryObject} Categories tbody elements.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getCategories =
function() {
return $('.category_grid_body:not(.empty)',
this.getHtmlElement());
};
/**
* Get a category tbody element by category data id.
* @param {string} categoryDataId The category data id.
* @return {jQueryObject} Category tbody element.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getCategoryByDataId =
function(categoryDataId) {
return $('#' + this.getCategoryIdPrefix() + categoryDataId);
};
/**
* Get the category row inside a tbody category element. If none element
* is passed, get all grid category rows.
* @param {jQueryObject} $opt_category Category tbody element.
* @return {jQueryObject} Category rows.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getCategoryRow =
function($opt_category) {
var $context = this.getHtmlElement();
if ($opt_category !== undefined) {
$context = $opt_category;
}
return $('tr.category', $context);
};
/**
* Get rows inside a tbody category element, excluding the category row.
* @param {jQueryObject} $category Category tbody element.
* @return {jQueryObject} Category rows.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getRowsInCategory =
function($category) {
return $('tr.gridRow', $category).not('.category');
};
/**
* Get the category empty placeholder.
* @param {jQueryObject} $category A grid category element.
* @return {jQueryObject} The category empty placeholder.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.
getCategoryEmptyPlaceholder = function($category) {
var selector = '#' + $category.attr('id') + '-emptyPlaceholder';
return $(selector, this.getHtmlElement());
};
/**
* Get the category data id by the passed category element.
* @param {jQueryObject} $category Category element.
* @return {string} Category data id.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getCategoryDataId =
function($category) {
var categoryId = $category.attr('id'),
startExtractPosition = this.getCategoryIdPrefix().length;
return /** @type {string} */ (categoryId.slice(startExtractPosition));
};
/**
* Get the category data id by the passed row element id.
* @param {string} gridRowId Category row element id.
* @return {string} Category data id.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getCategoryDataIdByRowId =
function(gridRowId) {
// Remove the category id prefix to avoid getting wrong data.
gridRowId = gridRowId.replace(this.getCategoryIdPrefix(), ' ');
// Get the category data id.
var categoryDataId = gridRowId.match('(.*)-row');
return $.trim(categoryDataId[1]);
};
/**
* Get the id prefix of the grid row inside a category.
* @return {string}
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getRowIdPrefix =
function() {
return this.getGridIdPrefix() + '-category-';
};
/**
* Get the grid row by the passed data element id.
* @param {number} rowDataId
* @return {jQueryObject}
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getRowByDataId =
function(rowDataId) {
this.parent('getRowByDataId', rowDataId);
return $('#' + this.getRowIdPrefix() + this.currentCategoryId_ +
'-row-' + 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.CategoryGridHandler.prototype.getRowDataId =
function($gridRow) {
var rowDataId;
rowDataId = $gridRow.attr('id').
slice(this.getRowIdPrefix().length);
rowDataId = rowDataId.match('-row-(.*)');
return /** @type {string} */ ($.trim(rowDataId[1]));
};
/**
* Append a category to the end of the list.
* @param {jQueryObject} $category Category to append.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.appendCategory =
function($category) {
var $gridBody = this.getHtmlElement().find(this.bodySelector);
$gridBody.append($category);
};
/**
* Re-sequence all category elements based on the passed sequence map.
* @param {Array} sequenceMap A sequence array with the category
* element id as value.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.resequenceCategories =
function(sequenceMap) {
var categoryId, index, $category;
for (index in sequenceMap) {
categoryId = sequenceMap[index];
$category = $('#' + categoryId);
this.appendCategory($category);
}
this.updateEmptyPlaceholderPosition();
};
/**
* Move all empty category placeholders to their correct position,
* below of each correspondent category element.
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.
updateEmptyPlaceholderPosition = function() {
var $categories = this.getCategories(),
index, limit,
$category, $emptyPlaceholder;
for (index = 0, limit = $categories.length; index < limit; index++) {
$category = $($categories[index]);
$emptyPlaceholder = this.getCategoryEmptyPlaceholder($category);
if ($emptyPlaceholder.length > 0) {
$emptyPlaceholder.insertAfter($category);
}
}
};
//
// Extended methods from GridHandler
//
/**
* @inheritDoc
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.initialize =
function(options) {
// Save the URL to fetch a whole category.
this.fetchCategoryUrl_ = options.fetchCategoryUrl;
this.parent('initialize', options);
};
/**
* @inheritDoc
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getElementsByType =
function($element) {
if ($element.hasClass('category_grid_body')) {
return this.getCategories();
} else {
return /** @type {jQueryObject} */ (
this.parent('getElementsByType', $element));
}
};
/**
* @inheritDoc
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.getEmptyElement =
function($element) {
if ($element.hasClass('category_grid_body')) {
// Return the grid empty element placeholder.
return this.getHtmlElement().find('.empty').not('.category_placeholder');
} else {
return /** @type {jQueryObject} */ (
this.parent('getEmptyElement', $element));
}
};
/**
* @inheritDoc
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.refreshGridHandler =
function(sourceElement, event, opt_elementId) {
var fetchedAlready = false, elementIds,
// Hack to avoid closure compiler warnings on type difference
castElementId = /** @type {{parentElementId: number}} */ (opt_elementId);
if (opt_elementId !== undefined) {
// Check if we want to refresh a row inside a category.
if (castElementId.parentElementId !== undefined) {
elementIds = {rowId: opt_elementId[0],
rowCategoryId: castElementId.parentElementId};
// Store the category id.
this.currentCategoryId_ = castElementId.parentElementId;
// Retrieve a single row from the server.
$.get(this.fetchRowUrl, elementIds,
this.callbackWrapper(
this.replaceElementResponseHandler), 'json');
} else {
// Retrieve the entire category from the server.
$.get(this.fetchCategoryUrl_, {rowId: opt_elementId},
this.callbackWrapper(
this.replaceElementResponseHandler), 'json');
}
fetchedAlready = true;
}
this.parent('refreshGridHandler', sourceElement,
event, opt_elementId, fetchedAlready);
};
/**
* @inheritDoc
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.deleteElement =
function($element) {
var $gridBody, index, limit, $parent, $emptyPlaceholder;
if ($element.length > 1) {
// Category and category row have the same element data id,
// handle this case.
if ($element.length == 2 &&
$element.hasClass('category_grid_body') &&
$element.hasClass('category')) {
// Always delete the entire category.
$element = $element.filter('.category_grid_body');
}
// Sometimes grid rows inside different categories may have
// the same id. Try to find the correct one to delete.
if (this.currentCategoryId_) {
$gridBody = this.getCategoryByDataId(this.currentCategoryId_);
for (index = 0, limit = $element.length; index < limit; index++) {
$parent = $($element[index]).
parents('#' + $gridBody.attr('id'));
if ($parent.length === 1) {
$element = $($element[index]);
break;
}
}
}
}
if ($element.hasClass('category_grid_body')) {
// Need to delete the category empty placeholder.
$emptyPlaceholder = this.getCategoryEmptyPlaceholder($element);
this.unbindPartial($emptyPlaceholder);
$emptyPlaceholder.remove();
}
this.parent('deleteElement', $element);
};
/**
* @inheritDoc
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.addElement =
function($element) {
var $gridBody = null, categoryDataId, $emptyPlaceholder;
if ($element.hasClass('gridRow')) {
// New row must be inside a category.
categoryDataId = /** @type {string} */ (
this.getCategoryDataIdByRowId(
/** @type {string} */ ($element.attr('id'))));
$gridBody = /** @type {jQueryObject} */ (
this.getCategoryByDataId(categoryDataId));
}
// Add the element.
this.parent('addElement', $element, $gridBody);
// Make sure the placeholder is the last grid element.
if ($element.hasClass('category_grid_body')) {
$emptyPlaceholder = this.getEmptyElement($element);
this.getHtmlElement().find(this.bodySelector).append($emptyPlaceholder);
}
};
/**
* @inheritDoc
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.replaceElement =
function($existingElement, $newElement) {
if ($newElement.hasClass('category_grid_body')) {
// Need to delete the category empty placeholder.
var $emptyPlaceholder = this.getCategoryEmptyPlaceholder($existingElement);
this.unbindPartial($emptyPlaceholder);
$emptyPlaceholder.remove();
}
this.parent('replaceElement', $existingElement, $newElement);
};
/**
* @inheritDoc
*/
$.pkp.controllers.grid.CategoryGridHandler.prototype.hasSameNumOfColumns =
function($row) {
var $element = $row,
checkColSpan = false;
if ($row.hasClass('category_grid_body')) {
$element = $row.find('tr');
checkColSpan = true;
}
return /** @type {boolean} */ (
this.parent('hasSameNumOfColumns', $element, checkColSpan));
};
}(jQuery));
+983
View File
@@ -0,0 +1,983 @@
/**
* @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));
@@ -0,0 +1,43 @@
/**
* @file js/controllers/grid/files/review/AuthorReviewRevisionsGridHandler.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 AuthorReviewRevisionsGridHandler
* @ingroup js_controllers_grid
*
* @brief Author revisions grid handler.
*/
(function($) {
/**
* Define the namespace
*/
$.pkp.controllers.grid.files = $.pkp.controllers.grid.files ||
{ review: {} };
/**
* @constructor
*
* @extends $.pkp.controllers.grid.GridHandler
*
* @param {jQueryObject} $grid The grid this handler is
* attached to.
* @param {Object} options Grid handler configuration.
*/
$.pkp.controllers.grid.files.review.AuthorReviewRevisionsGridHandler = function(
$grid, options) {
this.parent($grid, options);
this.bindGlobal('refreshRevisionsGrid', function() {
this.refreshGridHandler();
});
};
$.pkp.classes.Helper.inherits($.pkp.controllers.grid.files.review.
AuthorReviewRevisionsGridHandler, $.pkp.controllers.grid.GridHandler);
}(jQuery));
@@ -0,0 +1,269 @@
/**
* @file js/controllers/grid/navigationMenus/form/NavigationMenuFormHandler.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 NavigationMenuFormHandler
* @ingroup js_controllers_grid_navigationMenus_form
*
* @brief NavigationMenuItems form handler.
*/
(function($) {
/**
* Define the namespace
*/
$.pkp.controllers.grid.navigationMenus =
$.pkp.controllers.grid.navigationMenus ||
{ form: {} };
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $formElement The form element
* @param {Object} options Modal options.
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler =
function($formElement, options) {
this.okButton_ = options.okButton;
this.warningModalTitle_ = options.warningModalTitle;
this.submenuWarning_ = options.submenuWarning;
this.itemTypeConditionalWarnings_ = options.itemTypeConditionalWarnings;
$formElement.on('click', '.btnConditionalDisplay',
this.callbackWrapper(this.showConditionalDisplayWarning));
$formElement.on('click', '.btnSubmenuWarning',
this.callbackWrapper(this.showSubmenuWarning));
this.parent($formElement, options);
this.initSorting();
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler,
$.pkp.controllers.form.AjaxFormHandler);
//
// Private properties
//
/**
* The label for the ok button on the modals displaying submenuWarning and
* conditionalWarnings.
* @private
* @type {?string}
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler
.prototype.okButton_ = null;
/**
* The title of the modals displaying submenuWarning and conditionalWarnings.
* @private
* @type {?string}
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler
.prototype.warningModalTitle_ = null;
/**
* The warning message to display about submenus
* @private
* @type {string|undefined}
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler
.prototype.submenuWarning_ = undefined;
/**
* Warnings about the conditions of display for each item type.
* @private
* @type {?string}
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler
.prototype.itemTypeConditionalWarnings_ = null;
/**
* Initialize the .sortable() lists, limit nesting to one level deep and
* ensure lists are formatted properly the CSS styles
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler.
prototype.initSorting = function() {
var self = this;
// Remove any submenu warning buttons
$('.btnSubmenuWarning', this.getHtmlElement()).remove();
// Limit nesting to one level deep and ensure nested lists are formatted
// properly for appropriate styles
$('#pkpNavAssigned > li').each(
function() {
var $childList = $(this).children('ul'),
$children = $childList.children(),
$grandchildren = $children.find('li');
if (!$childList.length) {
$(this).append('<ul></ul>');
return;
}
if (!$children.length) {
// Ensure it's just an empty ul (with no spaces) so the CSS
// :empty psuedo class can be used
$childList.replaceWith('<ul></ul>');
} else {
// Prevent nesting two levels deep by moving any items
// nested at that level up one level.
if ($grandchildren.length) {
$grandchildren.each(function() {
$(this).appendTo($childList);
});
}
// Add a submenu warning button
if (!$(this).find(
'> .item > .item_buttons .btnSubmenuWarning').length) {
$(this).find('> .item > .item_buttons').prepend(
$('<button></button>')
.addClass('btnSubmenuWarning')
.append(
$('<span></span>')
.addClass('fa fa-exclamation-triangle')
)
.append(
$('<span></span>')
.addClass('-screenReader')
.text(self.submenuWarning_)
)
);
}
}
}
);
// Reset any nesting that's been carried over from the assigned list
$('#pkpNavUnassigned > li').each(
function() {
var $childList = $(this).children('ul');
if ($childList.length) {
$childList.find('li').each(function() {
$(this).appendTo($('#pkpNavUnassigned'));
});
}
$childList.remove();
}
);
// Initialize the sortable controls
$('#pkpNavManagement ul').sortable({
placeholder: 'pkp_nav_item_placeholder',
delay: 250,
connectWith: '#pkpNavManagement ul',
update: this.callbackWrapper(this.updateSorting),
start: function() {
$('#pkpNavAssigned').addClass('pkp_is_sorting');
},
stop: function() {
$('#pkpNavAssigned').removeClass('pkp_is_sorting');
}
});
};
/**
* Re-initialize the .sortable() components and generate the form fields
* whenever the list is re-sorted.
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler.
prototype.updateSorting = function() {
var $navManagement = $('#pkpNavManagement'),
seq = 0,
parent = null,
currentName = '';
// Re-intialize the sortable component after adjusting sort order
this.initSorting();
// Remove existing hidden fields
$('input', $navManagement).remove();
// Generate new hidden form fields
$('#pkpNavAssigned > li').each(function() {
currentName = 'menuTree[' + $(this).data('id') + ']';
$navManagement.append('<input type="hidden" name="' +
currentName + '[seq]" value="' +
seq + '">');
seq++;
var parentId = $(this).data('id');
$(this).find('li').each(function() {
currentName = 'menuTree[' + $(this).data('id') + ']';
$navManagement.append('<input type="hidden" name="' +
currentName + '[seq]" value="' +
seq + '">');
$navManagement.append('<input type="hidden" name="' +
currentName + '[parentId]" value="' +
parentId + '">');
seq++;
});
});
};
/**
* Show the conditional display warning message
* @param {jQueryObject} htmlElement The html element
* @return {boolean}
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler.
prototype.showConditionalDisplayWarning = function(htmlElement) {
var itemType = $(htmlElement).closest('li').data('type'),
opts = {
title: this.warningModalTitle_,
okButton: this.okButton_,
cancelButton: false,
dialogText: this.itemTypeConditionalWarnings_[itemType]
};
if (this.itemTypeConditionalWarnings_[itemType] !== null) {
$('<div id="' + $.pkp.classes.Helper.uuid() + '" ' +
'class="pkp_modal pkpModalWrapper" tabindex="-1"></div>')
.pkpHandler('$.pkp.controllers.modal.ConfirmationModalHandler', opts);
}
return false;
};
/**
* Show the submenu link warning message
* @return {boolean}
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuFormHandler.
prototype.showSubmenuWarning = function() {
var opts = {
title: this.warningModalTitle_,
okButton: this.okButton_,
cancelButton: false,
dialogText: this.submenuWarning_
};
$('<div id="' + $.pkp.classes.Helper.uuid() + '" ' +
'class="pkp_modal pkpModalWrapper" tabindex="-1"></div>')
.pkpHandler('$.pkp.controllers.modal.ConfirmationModalHandler', opts);
return false;
};
}(jQuery));
@@ -0,0 +1,125 @@
/**
* @file js/controllers/grid/navigationMenus/form/NavigationMenuItemsFormHandler.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 NavigationMenuItemsFormHandler
* @ingroup js_controllers_grid_navigationMenus_form
*
* @brief NavigationMenuItems form handler.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $formElement A wrapped HTML element that
* represents the form interface element.
* @param {Object} options javascript form options.
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuItemsFormHandler =
function($formElement, options) {
this.parent($formElement, options);
this.previewUrl_ = options.previewUrl;
this.itemTypeDescriptions_ = options.itemTypeDescriptions;
this.itemTypeConditionalWarnings_ = options.itemTypeConditionalWarnings;
$('#previewButton', $formElement).click(this.callbackWrapper(
this.showPreview_));
$('#menuItemType', $formElement).change(this.callbackWrapper(this.setType));
$('#menuItemType', $formElement).trigger('change');
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuItemsFormHandler,
$.pkp.controllers.form.AjaxFormHandler);
//
// Private properties
//
/**
* The preview url.
* @private
* @type {?string}
*/
$.pkp.controllers.grid.navigationMenus.form.
NavigationMenuItemsFormHandler.prototype.previewUrl_ = null;
/**
* Descriptions for each item type.
* @private
* @type {?Object}
*/
$.pkp.controllers.grid.navigationMenus.form.
NavigationMenuItemsFormHandler.prototype.itemTypeDescriptions_ = null;
/**
* Warnings about the conditions of display for each item type.
* @private
* @type {?Object}
*/
$.pkp.controllers.grid.navigationMenus.form.
NavigationMenuItemsFormHandler.prototype
.itemTypeConditionalWarnings_ = null;
/**
* Callback triggered on clicking the "preview"
* button to open a preview window.
*
* @param {HTMLElement} submitButton The submit button.
* @param {Event} event The event that triggered the
* submit button.
* @return {boolean} true.
* @private
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuItemsFormHandler.
prototype.showPreview_ = function(submitButton, event) {
var $formElement = this.getHtmlElement();
$.post(this.previewUrl_,
$formElement.serialize(),
function(data) {
var win = window.open('about:blank');
win.document.open();
win.document.write(data);
win.document.close();
}
);
return true;
};
/**
* Callback triggered when the type is set
*/
$.pkp.controllers.grid.navigationMenus.form.NavigationMenuItemsFormHandler.
prototype.setType = function() {
var itemType = $('#menuItemType', this.getHtmlElement()).val(),
$descriptionEl = $('#menuItemTypeSection [for="menuItemType"]');
$('.NMI_TYPE_CUSTOM_EDIT', this.getHtmlElement()).hide();
$('#' + itemType).fadeIn();
if (typeof this.itemTypeDescriptions_[itemType] !== 'undefined') {
$descriptionEl.text(this.itemTypeDescriptions_[itemType]);
}
};
}(jQuery));
@@ -0,0 +1,207 @@
/**
* @file js/controllers/grid/notifications/NotificationsGridHandler.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 NotificationsGridHandler
* @ingroup js_controllers_grid
*
* @brief Category grid handler.
*/
(function($) {
// Define the namespace.
$.pkp.controllers.grid.notifications =
$.pkp.controllers.grid.notifications || {};
/**
* @constructor
*
* @extends $.pkp.controllers.grid.GridHandler
*
* @param {jQueryObject} $grid The grid this handler is
* attached to.
* @param {Object} options Grid handler configuration.
*/
$.pkp.controllers.grid.notifications.NotificationsGridHandler =
function($grid, options) {
$grid.find('a[id*="markNew"]').click(
this.callbackWrapper(this.markNewHandler_));
$grid.find('a[id*="markRead"]').click(
this.callbackWrapper(this.markReadHandler_));
$grid.find('a[id*="deleteNotification"]').click(
this.callbackWrapper(this.deleteHandler_));
this.parent($grid, options);
};
$.pkp.classes.Helper.inherits($.pkp.controllers.grid.notifications
.NotificationsGridHandler, $.pkp.controllers.grid.GridHandler);
//
// Private properties
//
/**
* The CSRF token for POST requests.
* @private
* @type {?string}
*/
$.pkp.controllers.grid.notifications.NotificationsGridHandler
.prototype.csrfToken_ = null;
/**
* The "mark notifications as new" URL
* @private
* @type {?string}
*/
$.pkp.controllers.grid.notifications.NotificationsGridHandler
.prototype.markNewUrl_ = null;
/**
* The "mark notifications as read" URL
* @private
* @type {?string}
*/
$.pkp.controllers.grid.notifications.NotificationsGridHandler
.prototype.markReadUrl_ = null;
/**
* The "delete notifications" URL
* @private
* @type {?string}
*/
$.pkp.controllers.grid.notifications.NotificationsGridHandler
.prototype.deleteUrl_ = null;
//
// Extended methods from GridHandler
//
/**
* @inheritDoc
*/
$.pkp.controllers.grid.notifications.NotificationsGridHandler.
prototype.initialize = function(options) {
// Save the URLs to interact with selected sets of notifications
this.markNewUrl_ = options.markNewUrl;
this.markReadUrl_ = options.markReadUrl;
this.deleteUrl_ = options.deleteUrl;
this.csrfToken_ = options.csrfToken;
this.parent('initialize', options);
};
//
// Private methods.
//
/**
* Get the array of selected notifications
* @private
* @return {Array} List of selected notification IDs.
*/
$.pkp.controllers.grid.notifications.NotificationsGridHandler.prototype.
getSelectedNotifications_ = function() {
var selectedElements = [];
this.getHtmlElement().find('input:checkbox:checked').each(function() {
selectedElements.push($(this).val());
});
return selectedElements;
};
/**
* Callback that will be activated when the "mark new" 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.grid.notifications.NotificationsGridHandler.prototype.
markNewHandler_ = function(callingContext, opt_event) {
$.post(this.markNewUrl_, {selectedElements: this.getSelectedNotifications_(),
csrfToken: this.csrfToken_},
this.callbackWrapper(this.responseHandler_, null), 'json');
return false;
};
/**
* Callback that will be activated when the "mark read" 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.grid.notifications.NotificationsGridHandler.prototype.
markReadHandler_ = function(callingContext, opt_event) {
$.post(this.markReadUrl_,
{selectedElements: this.getSelectedNotifications_(),
csrfToken: this.csrfToken_},
this.callbackWrapper(this.responseHandler_, null), 'json');
return false;
};
/**
* Callback that will be activated when the "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.grid.notifications.NotificationsGridHandler.prototype.
deleteHandler_ = function(callingContext, opt_event) {
$.post(this.deleteUrl_, {selectedElements: this.getSelectedNotifications_(),
csrfToken: this.csrfToken_},
this.callbackWrapper(this.responseHandler_, null), 'json');
return false;
};
/**
* Callback after a response returns from the server.
*
* @private
*
* @param {Object} ajaxContext The AJAX request context.
* @param {Object} jsonData A parsed JSON response object.
*/
$.pkp.controllers.grid.notifications.NotificationsGridHandler.prototype.
responseHandler_ = function(ajaxContext, jsonData) {
// Bounce the selected notification IDs back to the server
// so that selections can be maintained
var params = this.getFetchExtraParams();
params.selectedNotificationIds = jsonData.content;
this.setFetchExtraParams(params);
// Pass through the JSON handler to cause the grid to be
// refreshed.
this.handleJson(jsonData);
};
}(jQuery));
@@ -0,0 +1,120 @@
/**
* @defgroup js_controllers_grid_queries
*/
/**
* @file js/controllers/grid/queries/QueryFormHandler.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 ReadQueryHandler
* @ingroup js_controllers_grid_queries
*
* @brief Handler for a query form modal
*
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.queries =
$.pkp.controllers.grid.queries || {};
/**
* @constructor
*
* @extends $.pkp.controllers.form.CancelActionAjaxFormHandler
*
* @param {jQueryObject} $form The query form element
* @param {Object} options non-default Dialog options
* to be passed into the dialog widget.
*
* Options are:
* - all options documented for the CancelActionAjaxFormHandler.
* - templateUrl: The URL to retrieve templates from.
*/
$.pkp.controllers.grid.queries.QueryFormHandler =
function($form, options) {
this.parent($form, options);
// Set the URL to retrieve templates from.
if (options.templateUrl) {
this.templateUrl_ = options.templateUrl;
}
// Attach form elements events.
$form.find('#template').change(
this.callbackWrapper(this.selectTemplateHandler_));
};
$.pkp.classes.Helper.inherits($.pkp.controllers.grid.queries.
QueryFormHandler, $.pkp.controllers.form.CancelActionAjaxFormHandler);
//
// Private properties
//
/**
* The URL to use to retrieve template bodies
* @private
* @type {string?}
*/
$.pkp.controllers.grid.queries.
QueryFormHandler.prototype.templateUrl_ = null;
//
// Private methods
//
/**
* Respond to an "item selected" call by triggering a published event.
*
* @param {HTMLElement} sourceElement The element that
* issued the event.
* @param {Event} event The triggering event.
* @private
*/
$.pkp.controllers.grid.queries.
QueryFormHandler.prototype.selectTemplateHandler_ =
function(sourceElement, event) {
var $form = this.getHtmlElement(),
template = $form.find('[name="template"]');
$.post(this.templateUrl_, template.serialize(),
this.callbackWrapper(this.updateTemplate), 'json');
};
//
// Private methods
//
/**
* Internal callback to replace the textarea with the contents of the
* template body.
*
* @param {HTMLElement} formElement The wrapped HTML form.
* @param {Object} jsonData The data returned from the server.
* @return {boolean} The response status.
*/
$.pkp.controllers.grid.queries.
QueryFormHandler.prototype.updateTemplate =
function(formElement, jsonData) {
var $form = this.getHtmlElement(),
processedJsonData = this.handleJson(jsonData),
jsonDataContent =
/** @type {{variables: Object, body: string}} */ (jsonData.content),
$textarea = $form.find('textarea[name="comment"]'),
editor =
tinyMCE.EditorManager.get(/** @type {string} */ ($textarea.attr('id')));
if (jsonDataContent.variables) {
$textarea.attr('data-variables', JSON.stringify(jsonDataContent.variables));
}
editor.setContent(jsonDataContent.body);
$form.find('[name="subject"]').val(jsonDataContent.subject);
return processedJsonData.status;
};
}(jQuery));
@@ -0,0 +1,167 @@
/**
* @defgroup js_controllers_grid_queries
*/
/**
* @file js/controllers/grid/queries/ReadQueryHandler.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 ReadQueryHandler
* @ingroup js_controllers_grid_queries
*
* @brief Handler for a "read query" modal
*
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.queries =
$.pkp.controllers.grid.queries || { };
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $containerElement The HTML element encapsulating
* the carousel container.
* @param {Object} options Handler options.
*/
$.pkp.controllers.grid.queries.ReadQueryHandler =
function($containerElement, options) {
this.fetchNoteFormUrl_ = options.fetchNoteFormUrl;
this.fetchParticipantsListUrl_ = options.fetchParticipantsListUrl;
$containerElement.find('.openNoteForm a').click(
this.callbackWrapper(this.showNoteFormHandler_));
$containerElement.bind('dataChanged',
this.callbackWrapper(this.reloadParticipantsList_));
$containerElement.bind('user-left-discussion', function() {
$containerElement.parent().trigger('modalFinished');
});
this.loadParticipantsList();
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.queries.ReadQueryHandler,
$.pkp.classes.Handler);
//
// Private properties
//
/**
* The URL to be called to fetch a new note form for a query.
* @private
* @type {string?}
*/
$.pkp.controllers.grid.queries.ReadQueryHandler.
prototype.fetchNoteFormUrl_ = null;
/**
* The URL to be called to fetch a list of participants.
* @private
* @type {string?}
*/
$.pkp.controllers.grid.queries.ReadQueryHandler.
prototype.fetchParticipantsListUrl_ = null;
//
// Public methods
//
/**
* Load the participants list.
*/
$.pkp.controllers.grid.queries.ReadQueryHandler.prototype.
loadParticipantsList = function() {
$.get(this.fetchParticipantsListUrl_,
this.callbackWrapper(this.showFetchedParticipantsList_), 'json');
};
//
// Private methods
//
/**
* Event handler that is called when the "new note" button is clicked.
* @param {HTMLElement} element The checkbox input element.
* @private
*/
$.pkp.controllers.grid.queries.ReadQueryHandler.prototype.
showNoteFormHandler_ = function(element) {
$(element).parents('.queryEditButtons').addClass('is_loading');
$.get(this.fetchNoteFormUrl_,
this.callbackWrapper(this.showFetchedNoteForm_), 'json');
};
/**
* Event handler that is called when the new note form is ready.
* @param {Object} ajaxContext The AJAX request context.
* @param {Object} jsonData A parsed JSON response object.
* @private
*/
$.pkp.controllers.grid.queries.ReadQueryHandler.prototype.
showFetchedNoteForm_ = function(ajaxContext, jsonData) {
var processedJsonData = this.handleJson(jsonData),
$noteFormContainer = $('#newNotePlaceholder', this.getHtmlElement()),
$queryEditButtons = $('.queryEditButtons.is_loading',
this.getHtmlElement());
this.unbindPartial($queryEditButtons);
$queryEditButtons.remove();
this.unbindPartial($noteFormContainer);
$noteFormContainer.html(processedJsonData.content);
};
/**
* Event handler that is called when the participants list fetch is complete.
* @param {Object} ajaxContext The AJAX request context.
* @param {Object} jsonData A parsed JSON response object.
* @private
*/
$.pkp.controllers.grid.queries.ReadQueryHandler.prototype.
showFetchedParticipantsList_ = function(ajaxContext, jsonData) {
var processedJsonData = this.handleJson(jsonData),
$participantsListContainer = $('#participantsListPlaceholder',
this.getHtmlElement()),
$leaveQueryButton = $('.leaveQueryForm', this.getHtmlElement());
if (processedJsonData.showLeaveQueryButton) {
$leaveQueryButton.show();
} else {
$leaveQueryButton.hide();
}
this.unbindPartial($participantsListContainer);
$participantsListContainer.children().remove();
$participantsListContainer.append(processedJsonData.content);
};
/**
* Handler to update the participants list on change.
* @param {HTMLElement} sourceElement The element that issued the
* "dataChanged" event.
* @param {Event} event The "dataChanged" event.
* @param {HTMLElement} triggerElement The element that triggered
* the "dataChanged" event.
* @private
*/
$.pkp.controllers.grid.queries.ReadQueryHandler.prototype.
reloadParticipantsList_ = function(sourceElement, event, triggerElement) {
this.loadParticipantsList();
};
}(jQuery));
@@ -0,0 +1,84 @@
/**
* @defgroup js_controllers_grid_representations_form
*/
/**
* @file js/controllers/grid/representations/form/RepresentationFormHandler.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 RepresentationFormHandler
* @ingroup js_controllers_grid_representations_form
*
* @brief Handle the representations forms.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.representations =
$.pkp.controllers.grid.representations ||
{ form: { } };
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $form the wrapped page element.
* @param {Object} options handler options.
*/
$.pkp.controllers.grid.representations.form.RepresentationFormHandler =
function($form, options) {
this.parent($form, options);
this.remoteRepresentation_ = options.remoteRepresentation;
if (this.remoteRepresentation_) {
$('#remotelyHostedContent').prop('checked', true);
$('#remote').show(20);
$('#urlPathSection').hide();
} else {
$('#remotelyHostedContent').prop('checked', false);
$('#remote').hide();
$('#urlPathSection').show(20);
}
$('#remotelyHostedContent').change(this.callbackWrapper(this.toggleRemote_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.representations.form.RepresentationFormHandler,
$.pkp.controllers.form.AjaxFormHandler);
//
// Private methods.
//
/**
* Internal callback called on checkbox change to show or hide
* remote URL input field.
* @private
* @param {HTMLElement} element The remotely hosted content checkbox.
* @param {Event} event The event that triggered the checkbox.
* @return {boolean} true.
*/
$.pkp.controllers.grid.representations.form.RepresentationFormHandler.
prototype.toggleRemote_ = function(element, event) {
if ($('#remotelyHostedContent').prop('checked')) {
// show the remote URL input field
$('#remote').show(20);
$('#urlPathSection').hide();
$('input[id^="urlPath"]').val('');
} else {
// hide and clear the remote URL input field
$('#remote').hide(20);
$('input[id^="urlRemote"]').val('');
$('#urlPathSection').show(20);
}
return true;
};
}(jQuery));
@@ -0,0 +1,278 @@
/**
* @defgroup js_controllers_grid_roles_form Role form javascript
*/
/**
* @file js/controllers/grid/settings/roles/form/UserGroupFormHandler.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 UserGroupFormHandler
* @ingroup js_controllers_grid_settings_roles_form
*
* @brief Handle the role create/edit form.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.settings.roles =
$.pkp.controllers.grid.settings.roles || { form: { }};
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {{
* selfRegistrationRoleIds: Array,
* roleForbiddenStagesJSON: Object.<string, *>,
* stagesSelector: string
* }} options form options.
*/
$.pkp.controllers.grid.settings.roles.form.UserGroupFormHandler =
function($form, options) {
var $roleId = $('[id^="roleId"]', $form);
this.parent($form, options);
// Set the role IDs for which the self-registration checkbox
// is relevant.
if (options.selfRegistrationRoleIds) {
this.selfRegistrationRoleIds_ = options.selfRegistrationRoleIds;
}
// Set the role IDs for which the recommendOnly checkbox
// is relevant.
if (options.recommendOnlyRoleIds) {
this.recommendOnlyRoleIds_ = options.recommendOnlyRoleIds;
}
// Set the roles that are not able to change
// submission metadata edit perissions
if (options.notChangeMetadataEditPermissionRoles) {
this.notChangeMetadataEditPermissionRoles_ =
options.notChangeMetadataEditPermissionRoles;
}
this.roleForbiddenStages_ = options.roleForbiddenStagesJSON.content;
this.stagesSelector_ = options.stagesSelector;
// Initialize the "permit self register" checkbox disabled
// state based on the form's current selection
this.updatePermitSelfRegistration(
/** @type {string} */ ($roleId.val()));
// Initialize the "permit metadata edit" checkbox disabled
// state based on the form's current selection
this.updatePermitMetadataEdit(
/** @type {string} */ ($roleId.val()), false);
// ...also initialize the stage options, disabling the ones
// that are forbidden for the current role.
this.updateStageOptions(
/** @type {string} */ ($roleId.val()));
// ...also initialize the recommendOnly option, disabling it
// if it is forbidden for the current role.
this.updateRecommendOnly(
/** @type {string} */ ($roleId.val()));
// ...and make sure both it's updated when changing roles.
$roleId.change(this.callbackWrapper(this.changeRoleId));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.settings.roles.form.UserGroupFormHandler,
$.pkp.controllers.form.AjaxFormHandler);
//
// Private properties
//
/**
* The list of self-registration role IDs
* @private
* @type {Object?}
*/
$.pkp.controllers.grid.settings.roles.form.
UserGroupFormHandler.prototype.selfRegistrationRoleIds_ = null;
/**
* A list of role forbidden stages.
* @private
* @type {Object}
*/
$.pkp.controllers.grid.settings.roles.form.
UserGroupFormHandler.prototype.roleForbiddenStages_ = null;
/**
* The stage options selector.
* @private
* @type {?string}
*/
$.pkp.controllers.grid.settings.roles.form.
UserGroupFormHandler.prototype.stagesSelector_ = null;
/**
* The list of not allowed to change submission metadata edit permissions roles
* @private
* @type {Object?}
*/
$.pkp.controllers.grid.settings.roles.form.
UserGroupFormHandler.prototype.notChangeMetadataEditPermissionRoles_ = null;
//
// Private methods.
//
/**
* Event handler that is called when the role ID dropdown is changed.
* @param {string} dropdown The dropdown input element.
*/
$.pkp.controllers.grid.settings.roles.form.UserGroupFormHandler.prototype.
changeRoleId = function(dropdown) {
var dropDownValue = /** @type {string} */ ($(dropdown).val());
this.updatePermitSelfRegistration((dropDownValue));
this.updatePermitMetadataEdit(/** @type {string} */ (dropDownValue), true);
// Also update the stages options.
this.updateStageOptions(/** @type {string} */ (dropDownValue));
this.updateRecommendOnly(/** @type {string} */ (dropDownValue));
};
/**
* Update the enabled/disabled state of the permitSelfRegistration
* checkbox.
* @param {number|string} roleId The role ID to select.
*/
$.pkp.controllers.grid.settings.roles.form.UserGroupFormHandler.prototype.
updatePermitSelfRegistration = function(roleId) {
// JQuerify the element
var $checkbox = $('[id^="permitSelfRegistration"]'),
$form = this.getHtmlElement(),
i,
found = false;
for (i = 0; i < this.selfRegistrationRoleIds_.length; i++) {
if (this.selfRegistrationRoleIds_[i] == roleId) {
found = true;
}
}
if (found) {
$checkbox.removeAttr('disabled');
} else {
$checkbox.attr('disabled', 'disabled');
$checkbox.removeAttr('checked');
}
};
/**
* Update the enabled/disabled state of the PermitMetadataEdit
* checkbox.
* @param {number|string} roleId The role ID to select.
* @param {boolean} roleIdChanged True iff the role ID changed.
*/
$.pkp.controllers.grid.settings.roles.form.UserGroupFormHandler.prototype.
updatePermitMetadataEdit = function(roleId, roleIdChanged) {
var i, $checkbox = $('[id^="permitMetadataEdit"]'),
found = false;
for (i = 0; i < this.notChangeMetadataEditPermissionRoles_.length; i++) {
if (this.notChangeMetadataEditPermissionRoles_[i] == roleId) {
found = true;
}
}
// If found then the check box should be disabled and checked
if (found) {
$checkbox.attr('disabled', 'disabled');
$checkbox.attr('checked', 'checked');
$checkbox.prop('checked', 'checked');
} else {
$checkbox.removeAttr('disabled');
if (roleIdChanged) {
$checkbox.removeAttr('checked');
}
}
};
/**
* Update the stage options.
* @param {number|string} roleId The role ID to select.
*/
$.pkp.controllers.grid.settings.roles.form.UserGroupFormHandler.prototype.
updateStageOptions = function(roleId) {
// JQuerify the element
var $htmlElement = this.getHtmlElement(),
$stageContainer = $htmlElement.find('#userGroupStageContainer'),
$stageOptions = $(this.stagesSelector_, $htmlElement).filter('input'),
i,
stageId = null;
$stageOptions.removeAttr('disabled');
if (this.roleForbiddenStages_[roleId] != undefined) {
for (i = 0; i < this.roleForbiddenStages_[roleId].length; i++) {
stageId = this.roleForbiddenStages_[roleId][i];
$stageOptions.filter('input[value="' + stageId + '"]').
attr('disabled', 'disabled');
}
}
if ($htmlElement.find(
'input[id^=\'assignedStages-\']:enabled').length == 0) {
$stageContainer.hide('slow');
$('#showTitle').attr('disabled', 'disabled');
} else {
$stageContainer.show('slow');
$('#showTitle').removeAttr('disabled');
}
};
/**
* Update the enabled/disabled state of the recommendOnly checkbox.
* @param {number|string} roleId The role ID to select.
*/
$.pkp.controllers.grid.settings.roles.form.UserGroupFormHandler.prototype.
updateRecommendOnly = function(roleId) {
// JQuerify the element
var $checkbox = $('[id^=\'recommendOnly\']', this.getHtmlElement()),
i,
found = false;
for (i = 0; i < this.recommendOnlyRoleIds_.length; i++) {
if (this.recommendOnlyRoleIds_[i] == roleId) {
found = true;
}
}
if (found) {
$checkbox.removeAttr('disabled');
} else {
$checkbox.attr('disabled', 'disabled');
$checkbox.removeAttr('checked');
}
};
}(jQuery));
@@ -0,0 +1,97 @@
/**
* @defgroup js_controllers_grid_users_user_form User form javascript
*/
/**
* @file js/controllers/grid/settings/user/form/UserDetailsFormHandler.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 UserDetailsFormHandler
* @ingroup js_controllers_grid_settings_user_form
*
* @brief Handle the user settings form.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.settings =
$.pkp.controllers.grid.settings || { user: { form: { } }};
/**
* @constructor
*
* @extends $.pkp.controllers.form.UserFormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {Object} options form options.
*/
$.pkp.controllers.grid.settings.user.form.UserDetailsFormHandler =
function($form, options) {
this.parent($form, options);
// Attach form elements events.
$('[id^="generatePassword"]', $form).click(
this.callbackWrapper(this.setGenerateRandom));
// Check the generate password check box.
if ($('[id^="generatePassword"]', $form).attr('checked')) {
this.setGenerateRandom('[id^="generatePassword"]');
}
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.settings.user.form.UserDetailsFormHandler,
$.pkp.controllers.form.UserFormHandler);
//
// Public methods.
//
/**
* @see AjaxFormHandler::submitForm
* @param {Object} validator The validator plug-in.
* @param {HTMLElement} formElement The wrapped HTML form.
*/
$.pkp.controllers.grid.settings.user.form.UserDetailsFormHandler.prototype.
submitForm = function(validator, formElement) {
var $form = this.getHtmlElement();
$(':password', $form).removeAttr('disabled');
this.parent('submitForm', validator, formElement);
};
/**
* Event handler that is called when generate password checkbox is
* clicked.
* @param {string} checkbox The checkbox input element.
*/
$.pkp.controllers.grid.settings.user.form.UserDetailsFormHandler.prototype.
setGenerateRandom = function(checkbox) {
// JQuerify the element
var $checkbox = $(checkbox),
$form = this.getHtmlElement(),
passwordValue = '',
activeAndCheck = 0;
if ($checkbox.prop('checked')) {
passwordValue = '********';
activeAndCheck = 'disabled';
} else {
passwordValue = '';
activeAndCheck = '';
}
$(':password', $form).
prop('disabled', activeAndCheck).val(passwordValue);
$('[id^="sendNotify"]', $form).attr('disabled', activeAndCheck).
prop('checked', activeAndCheck);
};
}(jQuery));
@@ -0,0 +1,51 @@
/**
* @file js/controllers/grid/users/UserGridHandler.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 UserGridHandler
* @ingroup js_controllers_grid
*
* @brief User grid handler. Used to keep user grids in sync, such as when
* merging users.
*/
(function($) {
// Define the namespace.
$.pkp.controllers.grid.users = $.pkp.controllers.grid.users || {};
/**
* @constructor
*
* @extends $.pkp.controllers.grid.GridHandler
*
* @param {jQueryObject} $grid The grid this handler is
* attached to.
* @param {Object} options Grid handler configuration.
*/
$.pkp.controllers.grid.users.UserGridHandler =
function($grid, options) {
this.parent($grid, options);
this.bindGlobal('userMerged', function() {
// Signal to any parent modals that they can close now
this.trigger('modalFinished');
this.refreshGridHandler();
});
// Refresh the grid when a user group has been added/edited
this.bindGlobal('userGroupUpdated', function() {
this.refreshGridHandler();
});
};
$.pkp.classes.Helper.inherits($.pkp.controllers.grid.users.UserGridHandler,
$.pkp.controllers.grid.GridHandler);
}(jQuery));
@@ -0,0 +1,105 @@
/**
* @defgroup js_controllers_grid_users_stageParticipant_form
*/
/**
* @file js/controllers/AdvancedReviewerSearchHandler.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 AdvancedReviewerSearchHandler
* @ingroup js_controllers
*
* @brief Handle the advanced reviewer search tab in the add reviewer modal.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.users.reviewer =
$.pkp.controllers.grid.users.reviewer || {};
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $container the wrapped page element.
* @param {Object} options handler options.
*/
$.pkp.controllers.grid.users.reviewer.AdvancedReviewerSearchHandler =
function($container, options) {
this.parent($container, options);
$container.find('.button').button();
pkp.eventBus.$on('selected:reviewer', function(reviewer) {
$('#reviewerId').val(reviewer.id);
$('[id^="selectedReviewerName"]').text(reviewer.fullName);
$('#searchGridAndButton').hide();
$('#regularReviewerForm').show();
// Set the email message for reviewers depending
// on previous completed assignments
var $textarea = $('#reviewerFormFooter [name="personalMessage"]'),
$templateInput,
$templateOption,
editor,
templateKey;
if ($textarea.val()) {
return; // The message is already set; shouldn't happen
}
// Only 1 template available
$templateInput = $('#reviewerFormFooter input[name="template"]');
// Multiple available templates
$templateOption = $('#reviewerFormFooter select[name="template"]');
editor = tinyMCE.EditorManager.get($textarea.attr('id'));
templateKey = '';
if (options.lastRoundReviewerIds.includes(reviewer.id)) {
templateKey = 'REVIEW_REQUEST_SUBSEQUENT';
editor.setContent(options.reviewerMessages[templateKey]);
$templateInput.val(templateKey);
$templateOption.find('[value="REVIEW_REQUEST"]').remove();
} else {
templateKey = 'REVIEW_REQUEST';
editor.setContent(options.reviewerMessages[templateKey]);
$templateInput.val(templateKey);
$templateOption.find('[value="REVIEW_REQUEST_SUBSEQUENT"]').remove();
}
// Select the right template option to correspond
// the one, which is set in TinyMCE
$templateOption.find('[value="' + templateKey + '"]')
.prop('selected', true);
});
$('#regularReviewerForm').hide();
this.bind('refreshForm', this.handleRefresh_);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.users.reviewer.AdvancedReviewerSearchHandler,
$.pkp.classes.Handler);
//
// Private helper methods.
//
/**
* Handle the form refresh event.
* @private
* @param {HTMLElement} sourceElement The element that issued the event.
* @param {Event} event The triggering event.
* @param {string} content HTML contents to replace element contents.
*/
$.pkp.controllers.grid.users.reviewer.AdvancedReviewerSearchHandler.prototype.
handleRefresh_ = function(sourceElement, event, content) {
if (content) {
this.replaceWith(content);
}
};
}(jQuery));
@@ -0,0 +1,99 @@
/**
* @defgroup js_controllers_grid_users_reviewer
*/
/**
* @file js/controllers/grid/users/reviewer/ReadReviewHandler.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 ReadReviewHandler
* @ingroup js_controllers_grid_users_reviewer
*
* @brief Handle the advanced reviewer search tab in the add reviewer modal.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $form the wrapped page element.
* @param {Object} options handler options.
*/
$.pkp.controllers.grid.users.reviewer.ReadReviewHandler =
function($form, options) {
this.parent($form, options);
this.reviewCompleted_ = options.reviewCompleted;
// bind a handler to make sure that a review file has been uploaded.
$form.find('[id^=\'submitFormButton-\']').click(this.callbackWrapper(
this.reviewFilesRequired_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.users.reviewer.ReadReviewHandler,
$.pkp.controllers.form.AjaxFormHandler);
//
// Private methods.
//
/**
* Is the review completed.
* @private
* @type {boolean}
*/
$.pkp.controllers.grid.users.reviewer.ReadReviewHandler.
prototype.reviewCompleted_ = false;
/**
* Internal callback called on form submit to ensure there are
* some review files uploaded.
* @private
* @param {HTMLElement} submitButton The submit button.
* @param {Event} event The event that triggered the
* submit button.
* @return {boolean} true.
*/
$.pkp.controllers.grid.users.reviewer.ReadReviewHandler.
prototype.reviewFilesRequired_ = function(submitButton, event) {
if (!this.reviewCompleted_ && $('#readReviewAttachmentsGridContainer').
find('tbody.empty:visible').length == 1) {
// There's nothing in the files grid; don't submit the form
this.showWarning_();
return false;
} else {
// There's something in the files grid;
this.hideWarning_();
}
return true;
};
/**
* Hide the "no files" warning.
* @private
*/
$.pkp.controllers.grid.users.reviewer.ReadReviewHandler.
prototype.hideWarning_ = function() {
this.getHtmlElement().find('#noFilesWarning').hide(250);
};
/**
* Show the "no files" warning.
* @private
*/
$.pkp.controllers.grid.users.reviewer.ReadReviewHandler.
prototype.showWarning_ = function() {
this.getHtmlElement().find('#noFilesWarning').show(250);
};
}(jQuery));
@@ -0,0 +1,123 @@
/**
* @file js/controllers/grid/users/reviewer/form/AddReviewerFormHandler.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 AddReviewerFormHandler
* @ingroup js_controllers_grid_users_reviewer_form
*
* @brief Handle the Add Reviewer form (and template for message body).
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.grid.users.reviewer.form.EditReviewFormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {Object} options form options.
*/
$.pkp.controllers.grid.users.reviewer.form.
AddReviewerFormHandler = function($form, options) {
this.parent($form, options);
// Set the URL to retrieve templates from.
if (options.templateUrl) {
this.templateUrl_ = options.templateUrl;
}
// Attach form elements events.
$form.find('#template').change(
this.callbackWrapper(this.selectTemplateHandler_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.users.reviewer.form.
AddReviewerFormHandler,
$.pkp.controllers.grid.users.reviewer.form.
EditReviewFormHandler);
//
// Private properties
//
/**
* The URL to use to retrieve template bodies
* @private
* @type {string?}
*/
$.pkp.controllers.grid.users.reviewer.form.
AddReviewerFormHandler.prototype.templateUrl_ = null;
//
// Protected methods
//
/**
* Show the "no files" warning.
* @protected
*/
$.pkp.controllers.grid.users.reviewer.form.AddReviewerFormHandler.
prototype.showWarning = function() {
// Call the parent showWarning to show the warning
this.parent('showWarning');
// Ask the reviewer form footer handler to expand the file
// list extras-on-demand if it isn't already expanded.
this.getHtmlElement().find('#reviewerFormFooter')
.trigger('expandFileList');
};
//
// Private methods
//
/**
* Respond to an "item selected" call by triggering a published event.
*
* @param {HTMLElement} sourceElement The element that
* issued the event.
* @param {Event} event The triggering event.
* @private
*/
$.pkp.controllers.grid.users.reviewer.form.
AddReviewerFormHandler.prototype.selectTemplateHandler_ =
function(sourceElement, event) {
var $form = this.getHtmlElement();
$.post(this.templateUrl_, $form.find('#template').serialize(),
this.callbackWrapper(this.updateTemplate), 'json');
};
/**
* Internal callback to replace the textarea with the contents of the
* template body.
*
* @param {HTMLElement} formElement The wrapped HTML form.
* @param {Object} jsonData The data returned from the server.
* @return {boolean} The response status.
*/
$.pkp.controllers.grid.users.reviewer.form.
AddReviewerFormHandler.prototype.updateTemplate =
function(formElement, jsonData) {
var $form = this.getHtmlElement(),
processedJsonData = this.handleJson(jsonData),
$textarea = $form.find('textarea[name="personalMessage"]'),
editor =
tinyMCE.EditorManager.get(/** @type {string} */ ($textarea.attr('id')));
if (processedJsonData !== false) {
if (processedJsonData.content !== '') {
editor.setContent(processedJsonData.content);
}
}
return processedJsonData.status;
};
}(jQuery));
@@ -0,0 +1,109 @@
/**
* @defgroup js_controllers_grid_users_reviewer_form
*/
/**
* @file js/controllers/grid/users/reviewer/form/EditReviewFormHandler.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 EditReviewFormHandler
* @ingroup js_controllers_grid_users_reviewer_form
*
* @brief Handle the limit reviewer files form. Also used as a base class
* for the add reviewer form handler.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.users.reviewer.form =
$.pkp.controllers.grid.users.reviewer.form || {};
/**
* @constructor
*
* @extends $.pkp.controllers.form.UserFormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {Object} options form options.
*/
$.pkp.controllers.grid.users.reviewer.form.
EditReviewFormHandler = function($form, options) {
this.parent($form, options);
// When the form changes, check to see if a warning is necessary
// (if all reviewer files are unchecked)
$form.change(this.callbackWrapper(this.handleFormChange));
// When the reviewer files list loads, trigger the above check
this.bind('urlInDivLoaded', this.handleFileListLoad_);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.users.reviewer.form.
EditReviewFormHandler,
$.pkp.controllers.form.UserFormHandler);
//
// Protected methods.
//
/**
* Handle a form change event.
* @protected
*/
$.pkp.controllers.grid.users.reviewer.form.EditReviewFormHandler.
prototype.handleFormChange = function() {
if (this.getHtmlElement()
.find('input[name="selectedFiles[]"]:checked').length) {
this.hideWarning();
} else {
this.showWarning();
}
};
/**
* Hide the "no files" warning.
* @protected
*/
$.pkp.controllers.grid.users.reviewer.form.EditReviewFormHandler.
prototype.hideWarning = function() {
this.getHtmlElement().find('#noFilesWarning').hide(250);
};
/**
* Show the "no files" warning.
* @protected
*/
$.pkp.controllers.grid.users.reviewer.form.EditReviewFormHandler.
prototype.showWarning = function() {
this.getHtmlElement().find('#noFilesWarning').show(250);
};
//
// Private methods.
//
/**
* Handle the loading of the reviewer files list.
* @private
* @param {HTMLElement} sourceElement The element that
* issued the event.
* @param {Event} event The triggering event.
* @param {?string} data additional event data.
*/
$.pkp.controllers.grid.users.reviewer.form.EditReviewFormHandler.
prototype.handleFileListLoad_ =
function(sourceElement, event, data) {
// Trigger a form change event to display the "no files
// selected" warning, if necessary.
this.getHtmlElement().change();
};
}(jQuery));
@@ -0,0 +1,51 @@
/**
* @file js/controllers/grid/users/stageParticipant/StageParticipantGridHandler.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 StageParticipantGridHandler
* @ingroup js_controllers_grid
*
* @brief Stage participant grid handler.
*/
/*global pkp */
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.users.stageParticipant =
$.pkp.controllers.grid.users.stageParticipant || {};
/**
* @constructor
*
* @extends $.pkp.controllers.grid.CategoryGridHandler
*
* @param {jQueryObject} $grid The grid this handler is
* attached to.
* @param {Object} options Grid handler configuration.
*/
$.pkp.controllers.grid.users.stageParticipant.StageParticipantGridHandler =
function($grid, options) {
this.parent($grid, options);
// Reload any editorial actions on the page.
this.bind('dataChanged', function() {
this.refreshGridHandler();
$(['#submissionEditorDecisionsDiv',
'#copyeditingEditorDecisionsDiv',
'[id^=reviewDecisionsDiv]'].join(','))
.each(function() {
$.pkp.classes.Handler.getHandler($(this)).reload();
});
});
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.users.stageParticipant.StageParticipantGridHandler,
$.pkp.controllers.grid.CategoryGridHandler);
}(jQuery));
@@ -0,0 +1,72 @@
/**
* @defgroup js_controllers_grid_users_stageParticipant_form
*/
/**
* @file js/controllers/grid/users/stageParticipant/form/AddParticipantFormHandler.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 AddParticipantFormHandler
* @ingroup js_controllers_grid_users_stageParticipant_form
*
* @brief Handle the search user filter and
* add the value to the hidden userGroupId field.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.form.ClientFormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {Object} options form options.
*/
$.pkp.controllers.grid.users.stageParticipant.form.AddParticipantFormHandler =
function($form, options) {
this.parent($form, options);
$('select[name^=\'filterUserGroupId\']', $form).change(
this.callbackWrapper(this.addUserGroupId));
$form.parent().click(function(e) {
var $target = $(e.target), filterUserIdVal;
if ($target.is('input[name="userId"]')) {
filterUserIdVal = $('input[name=\'userId\']:checked').val();
$('input[name=\'userIdSelected\']').val(
filterUserIdVal).trigger('change');
}
});
// initially populate the input field.
this.addUserGroupId();
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.users.stageParticipant.form.
AddParticipantFormHandler,
$.pkp.controllers.form.ClientFormHandler);
//
// Public methods
//
/**
* Method to add the value to the hidden userGroupId field
*/
$.pkp.controllers.grid.users.stageParticipant.form.AddParticipantFormHandler.
prototype.addUserGroupId = function() {
var $form = this.getHtmlElement(),
$filterUserGroupId = $form.find('select[name^=\'filterUserGroupId\']'),
filterUserGroupIdVal = /** @type {string} */ ($filterUserGroupId.val());
$('input[name=\'userGroupId\']').val(filterUserGroupIdVal).trigger('change');
};
}(jQuery));
@@ -0,0 +1,374 @@
/**
* @defgroup js_controllers_grid_users_stageParticipant_form
*/
/**
* @file js/controllers/grid/users/stageParticipant/form/StageParticipantNotifyHandler.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 StageParticipantNotifyHandler
* @ingroup js_controllers_grid_users_stageParticipant_form
*
* @brief Handle Stage participant notification forms.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.grid.users.stageParticipant.form =
$.pkp.controllers.grid.users.stageParticipant.form || {};
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $form the wrapped HTML form element.
* @param {Object} options form options.
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler = function($form, options) {
this.parent($form, options);
// Set the URL to retrieve templates from.
if (options.templateUrl) {
this.templateUrl_ = options.templateUrl;
}
// Set the user group IDs with the recommendOnly possibility
if (options.possibleRecommendOnlyUserGroupIds) {
this.possibleRecommendOnlyUserGroupIds_ =
options.possibleRecommendOnlyUserGroupIds;
}
// Set the user group IDs with the recommendOnly option set
if (options.recommendOnlyUserGroupIds) {
this.recommendOnlyUserGroupIds_ = options.recommendOnlyUserGroupIds;
}
// Set the user group IDs that are not allowed to change the default
// value of permitMetadataEdit
if (options.notChangeMetadataEditPermissionRoles) {
this.notChangeMetadataEditPermissionRoles_ =
options.notChangeMetadataEditPermissionRoles;
}
// Set the user group IDs that have the permitMetadataEdit flag set to true
if (options.permitMetadataEditUserGroupIds) {
this.permitMetadataEditUserGroupIds_ =
options.permitMetadataEditUserGroupIds;
}
if (options.anonymousReviewerIds) {
this.anonymousReviewerIds_ = options.anonymousReviewerIds;
}
if (options.anonymousReviewerWarning) {
this.anonymousReviewerWarning_ = options.anonymousReviewerWarning;
}
if (options.anonymousReviewerWarningOk) {
this.anonymousReviewerWarningOk_ = options.anonymousReviewerWarningOk;
}
// Update the recommendOnly option display when user group changes
// or user is selected
$('input[name=\'userGroupId\'], input[name=\'userIdSelected\']', $form)
.change(this.callbackWrapper(this.updateRecommendOnly));
// Update the recommendOnly option display when user group changes
// or user is selected
$('input[name=\'userGroupId\'], input[name=\'userIdSelected\']', $form)
.change(this.callbackWrapper(
this.updateSubmissionMetadataEditPermitOption));
// Trigger a warning message if an anonymous reviewer is selected
$('input[name=\'userIdSelected\']', $form)
.change(this.callbackWrapper(this.maybeTriggerReviewerWarning));
// Attach form elements events.
$form.find('#template').change(
this.callbackWrapper(this.selectTemplateHandler_));
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler,
$.pkp.controllers.form.AjaxFormHandler);
//
// Private properties
//
/**
* The URL to use to retrieve template bodies
* @private
* @type {string?}
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.templateUrl_ = null;
/**
* A list of user IDs which are already assigned anonymous reviews for this
* submission.
* @private
* @type {Array}
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.anonymousReviewerIds_ = null;
/**
* A warning message to display when an anonymous reviewer is selected to be
* added as a recipient
* @private
* @type {string?}
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.anonymousReviewerWarning_ = null;
/**
* The OK button language for the anonymous reviewer warning message
* @private
* @type {string?}
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.
anonymousReviewerWarningOk_ = null;
/**
* The list of not allowed to change submission metadata edit permissions roles
* @private
* @type {Object?}
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.
notChangeMetadataEditPermissionRoles_ = null;
/**
* The list of group ids that are allowed to edit metadata
* @private
* @type {Object?}
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.
permitMetadataEditUserGroupIds_ = null;
//
// Private methods
//
/**
* Respond to an "item selected" call by triggering a published event.
*
* @param {HTMLElement} sourceElement The element that
* issued the event.
* @param {Event} event The triggering event.
* @private
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.selectTemplateHandler_ =
function(sourceElement, event) {
var $form = this.getHtmlElement();
$.post(this.templateUrl_, $form.find('#template').serialize(),
this.callbackWrapper(this.updateTemplate), 'json');
};
/**
* Internal callback to replace the textarea with the contents of the
* template body.
*
* @param {HTMLElement} formElement The wrapped HTML form.
* @param {Object} jsonData The data returned from the server.
* @return {boolean} The response status.
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.updateTemplate =
function(formElement, jsonData) {
var $form = this.getHtmlElement(),
processedJsonData = this.handleJson(jsonData),
jsonDataContent =
/** @type {{variables: Object, body: string}} */ (jsonData.content),
$textarea = $form.find('textarea[name="message"]'),
editor =
tinyMCE.EditorManager.get(/** @type {string} */ ($textarea.attr('id')));
if (jsonDataContent.variables) {
$textarea.attr('data-variables', JSON.stringify(jsonDataContent.variables));
}
editor.setContent(jsonDataContent.body);
return processedJsonData.status;
};
/**
* Update the enabled/disabled and checked state of the recommendOnly checkbox.
* @param {HTMLElement} sourceElement The element that
* issued the event.
* @param {Event} event The triggering event.
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.updateRecommendOnly =
function(sourceElement, event) {
var $form = this.getHtmlElement(),
$filterUserGroupId = $form.find('input[name=\'userGroupId\']'),
$checkbox = $form.find('input[id^=\'recommendOnly\']'),
$checkboxDiv = $form.find('.recommendOnlyWrapper'),
i,
found = false,
filterUserGroupIdVal = /** @type {string} */ ($filterUserGroupId.val());
// If user group changes, hide the recommendOnly option
if ($(sourceElement).prop('name') == 'userGroupId') {
$checkbox.attr('disabled', 'disabled');
$checkbox.removeAttr('checked');
$checkboxDiv.hide();
} else if ($(sourceElement).prop('name') == 'userIdSelected' &&
!$checkboxDiv.is(':visible')) {
// Display recommendOnly option if
// an user group with a possible recommendOnly option is selected
for (i = 0; i < this.possibleRecommendOnlyUserGroupIds_.length; i++) {
if (this.possibleRecommendOnlyUserGroupIds_[i] == filterUserGroupIdVal) {
$checkbox.removeAttr('disabled');
$checkboxDiv.show();
// Select the recommendOnly option if
// an user group with a recommendOnly option set is selected
for (i = 0; i < this.recommendOnlyUserGroupIds_.length; i++) {
if (this.recommendOnlyUserGroupIds_[i] == filterUserGroupIdVal) {
$checkbox.prop('checked', true);
break;
}
}
break;
} else {
$checkbox.attr('disabled', 'disabled');
$checkboxDiv.hide();
}
}
}
};
/**
* Update the enabled/disabled and checked state of the recommendOnly checkbox.
* @param {HTMLElement} sourceElement The element that
* issued the event.
* @param {Event} event The triggering event.
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.maybeTriggerReviewerWarning =
function(sourceElement, event) {
var userId = $(sourceElement).val(),
opts;
if (!userId || this.anonymousReviewerIds_.indexOf(userId) < 0) {
return;
}
opts = {
title: '',
okButton: this.anonymousReviewerWarningOk_,
cancelButton: false,
dialogText: this.anonymousReviewerWarning_
};
$('<div id="' + $.pkp.classes.Helper.uuid() + '" ' +
'class="pkp_modal pkpModalWrapper" tabindex="-1"></div>')
.pkpHandler('$.pkp.controllers.modal.ConfirmationModalHandler', opts);
};
/**
* Update the enabled/disabled and checked state of the recommendOnly checkbox.
* @param {HTMLElement} sourceElement The element that
* issued the event.
* @param {Event} event The triggering event.
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.
updateSubmissionMetadataEditPermitOption =
function(sourceElement, event) {
var $form = this.getHtmlElement(),
$filterUserGroupId = $form.find('input[name=\'userGroupId\']'),
$checkbox = $form.find('input[id^=\'canChangeMetadata\']'),
$checkboxDiv = $form.find('.submissionEditMetadataPermit'),
i,
found = false,
filterUserGroupIdVal = /** @type {string} */ ($filterUserGroupId.val());
// If user group changes, hide the canChangeMetadata option
if ($(sourceElement).prop('name') == 'userGroupId') {
$checkbox.attr('disabled', 'disabled');
$checkbox.removeAttr('checked');
$checkboxDiv.hide();
} else if ($(sourceElement).prop('name') == 'userIdSelected' &&
!$checkboxDiv.is(':visible')) {
// Display canChangeMetadata option if
// an user group with a possible canChangeMetadata option is selected
for (i = 0; i < this.notChangeMetadataEditPermissionRoles_.length; i++) {
if (this.notChangeMetadataEditPermissionRoles_[i] ==
filterUserGroupIdVal) {
found = true;
break;
}
}
if (!found) {
$checkbox.removeAttr('disabled');
$checkboxDiv.show();
// Select the recommendOnly option if
// an user group with a recommendOnly option set is selected
for (i = 0; i < this.permitMetadataEditUserGroupIds_.length; i++) {
if (this.permitMetadataEditUserGroupIds_[i] == filterUserGroupIdVal) {
$checkbox.prop('checked', true);
break;
}
}
} else {
$checkbox.attr('disabled', 'disabled');
$checkboxDiv.hide();
}
}
};
/**
* Internal callback called after form validation to handle the
* response to a form submission.
*
* You can override this handler if you want to do custom handling
* of a form response.
*
* @param {HTMLElement} formElement The wrapped HTML form.
* @param {Object} jsonData The data returned from the server.
* @return {boolean} The response status.
*/
$.pkp.controllers.grid.users.stageParticipant.form.
StageParticipantNotifyHandler.prototype.handleResponse =
function(formElement, jsonData) {
// Reload the query grid to show the newly created query.
var $queries = $('#queriesGrid .pkp_controllers_grid');
if ($.pkp.classes.Handler.hasHandler($queries)) {
$.pkp.classes.Handler.getHandler($queries).trigger('dataChanged');
}
return /** @type {boolean} */ (this.parent(
'handleResponse', formElement, jsonData));
};
}(jQuery));
@@ -0,0 +1,69 @@
/**
* @defgroup js_controllers_informationCenter
*/
// Create the modal namespace.
jQuery.pkp.controllers.informationCenter =
jQuery.pkp.controllers.informationCenter || { };
/**
* @file js/controllers/informationCenter/NotesHandler.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 NotesHandler
* @ingroup js_controllers_informationCenter
*
* @brief Information center "notes" tab handler.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $notesDiv A wrapped HTML element that
* represents the "notes" interface element.
* @param {Object} options Tabbed modal options.
*/
$.pkp.controllers.informationCenter.NotesHandler =
function($notesDiv, options) {
this.parent($notesDiv, options);
// Refresh the widget when a note is added or deleted
this.bind('noteAdded', this.handleRefreshNoteList);
this.bind('noteDeleted', this.handleRefreshNoteList);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.informationCenter.NotesHandler,
$.pkp.classes.Handler
);
//
// Public methods
//
/**
* Handle the "note added" event triggered by the
* note form whenever a new note is added.
*
* @param {$.pkp.controllers.form.AjaxFormHandler} callingForm The form
* that triggered the event.
* @param {Event} event The upload event.
* @param {string} html Rendered HTML to refresh the notes handler
*/
$.pkp.controllers.informationCenter.NotesHandler.
prototype.handleRefreshNoteList = function(callingForm, event, html) {
// Scroll back to the top of the notes list, where the note will appear
$('.pkp_modal').first().scrollTop(0);
this.replaceWith(html);
};
}(jQuery));
@@ -0,0 +1,272 @@
/**
* @defgroup js_controllers_linkAction
*/
/**
* @file js/controllers/linkAction/LinkActionHandler.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 LinkActionHandler
* @ingroup js_controllers_linkAction
*
* @brief Link action handler that executes the action's handler when activated
* and delegates the action handler's response to the action's response
* handler.
*/
(function($) {
/** @type {Object} */
jQuery.pkp.controllers.linkAction = jQuery.pkp.controllers.linkAction || { };
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $handledElement The clickable element
* the link action will be attached to.
* @param {{actionRequest, actionRequestOptions}} options
* Configuration of the link action handler. The object must contain the
* following elements:
* - actionRequest: The action to be executed when the link
* action is being activated.
* - actionRequestOptions: Configuration of the action request.
*/
$.pkp.controllers.linkAction.LinkActionHandler =
function($handledElement, options) {
this.parent($handledElement, options);
// We need to know the static part of the element id
// (id attribute will change after refreshing,
// because it uses the uniqId function) for accessing
// the link action element in the DOM.
if (options.staticId) {
this.staticId_ = options.staticId;
} else {
// If none, the link action element id is
// not using the unique function, so we
// can consider it static.
this.staticId_ = /** @type {string} */ ($handledElement.attr('id'));
}
// Instantiate the link action request.
if (!options.actionRequest || !options.actionRequestOptions) {
throw new Error(['The "actionRequest" and "actionRequestOptions"',
'settings are required in a LinkActionHandler'].join(''));
}
// Configure the callback called when the link
// action request finishes.
options.actionRequestOptions.finishCallback =
this.callbackWrapper(this.enableLink);
this.linkActionRequest_ =
/** @type {$.pkp.classes.linkAction.LinkActionRequest} */
($.pkp.classes.Helper.objectFactory(
options.actionRequest,
[$handledElement, options.actionRequestOptions]));
// Bind the link action request to the handled element.
this.bindActionRequest();
// Publish this event so we can handle it and grids still
// can listen to it to refresh themselves.
//
// This needs to happen before the dataChangedHandler_ bound,
// otherwise when the publish event handler try to bubble up the
// dataChanged event, this html element could be already removed
// by the notifyUser event handlers triggered by dataChangedHandler_
this.publishEvent('dataChanged');
// Bind the data changed event, so we know when trigger
// the notify user event.
this.bind('dataChanged', this.dataChangedHandler_);
// Re-enable submit buttons when a modal is closed
this.bind('pkpModalClose', this.removeDisabledAttribute_);
if (options.selfActivate) {
this.trigger('click');
}
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.linkAction.LinkActionHandler,
$.pkp.classes.Handler);
//
// Private properties
//
/**
* The link action request object.
* @private
* @type {$.pkp.classes.linkAction.LinkActionRequest}
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
linkActionRequest_ = null;
/**
* The part of this HTML element id that's static, not
* changing after a refresh.
* @private
* @type {?string}
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
staticId_ = null;
//
// Getter
//
/**
* Get the static id part of the HTML element id.
* @return {?string} Non-unique part of HTML element id.
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
getStaticId = function() {
return this.staticId_;
};
/**
* Get the link url.
* @return {?string} Link url.
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
getUrl = function() {
return this.linkActionRequest_.getUrl();
};
//
// Public methods
//
/**
* Activate the link action request.
*
* @param {HTMLElement} callingElement The element that triggered
* the link action activation event.
* @param {Event} event The event that activated the link action.
* @return {boolean} Should return false to stop event propagation.
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
activateAction = function(callingElement, event) {
// Unbind our click handler to avoid double-execution
// while the link action is executing. We will not disable
// if this link action have a null action request. In that
// case, the action request is handled by some parent widget.
if (this.linkActionRequest_.shouldDebounce()) {
this.disableLink();
}
// Call the link request.
return this.linkActionRequest_.activate.call(this.linkActionRequest_,
callingElement, event);
};
/**
* Bind the link action request.
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
bindActionRequest = function() {
// (Re-)bind our click handler so that the action
// can be executed.
this.bind('click', this.activateAction);
};
/**
* Enable link action.
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
enableLink = function() {
var $linkActionElement, actionRequestUrl;
$linkActionElement = $(this.getHtmlElement());
// only remove the disabled state if it is not a submit button.
// we let FormHandler remove that after a form is submitted.
if (!this.getHtmlElement().is(':submit')) {
this.removeDisabledAttribute_();
}
actionRequestUrl = this.getUrl();
if (this.getHtmlElement().is('a') && actionRequestUrl) {
$linkActionElement.attr('href', actionRequestUrl);
}
this.unbind('click', this.noAction_);
this.bindActionRequest();
};
/**
* Disable link action.
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
disableLink = function() {
var $linkActionElement = $(this.getHtmlElement());
$linkActionElement.attr('disabled', 'disabled');
if (this.getHtmlElement().is('a')) {
$linkActionElement.attr('href', '#');
}
this.unbind('click', this.activateAction);
this.bind('click', this.noAction_);
};
//
// Private methods.
//
/**
* Remove the 'disabled' CSS class for the linkActionElement.
* @private
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
removeDisabledAttribute_ = function() {
var $linkActionElement = $(this.getHtmlElement());
$linkActionElement.removeAttr('disabled');
};
/**
* Handle the changed data event.
* @private
*
* @param {jQueryObject} callingElement The calling html element.
* @param {Event} event The event object (dataChanged).
* @param {Object} eventData Event data.
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
dataChangedHandler_ = function(callingElement, event, eventData) {
if (this.getHtmlElement().parents('.pkp_controllers_grid').length === 0) {
// We might want to redirect this data changed event to a grid.
// Trigger another event so parent widgets can handle this
// redirection.
this.trigger('redirectDataChangedToGrid', [eventData]);
}
this.trigger('notifyUser');
};
/**
* Click event handler used to avoid any action request.
* @return {boolean} Always returns false.
* @private
*/
$.pkp.controllers.linkAction.LinkActionHandler.prototype.
noAction_ = function() {
return false;
};
}(jQuery));
@@ -0,0 +1,856 @@
/**
* @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));
@@ -0,0 +1,101 @@
/**
* @file js/controllers/modal/AjaxModalHandler.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 AjaxModalHandler
* @ingroup js_controllers_modal
*
* @brief A modal that retrieves content from a remote AJAX endpoint.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.modal.ModalHandler
*
* @param {jQueryObject} $handledElement The clickable element
* the modal will be attached to.
* @param {Object} options non-default Dialog options
* to be passed into the dialog widget.
*
* Options are:
* - url string the remote AJAX endpoint that will be used
* to retrieve the content of the modal.
* - all options documented for the jQueryUI dialog widget,
* except for the buttons parameter which is not supported.
*/
$.pkp.controllers.modal.AjaxModalHandler = function($handledElement, options) {
this.parent($handledElement, options);
// We assume that AJAX modals usually contain forms and
// therefore bind to form events by default.
this.bind('formSubmitted', this.formSubmitted);
this.bind('formCanceled', this.modalClose);
this.bind('ajaxHtmlError', this.modalClose);
this.bind('modalFinished', this.modalClose);
};
$.pkp.classes.Helper.inherits($.pkp.controllers.modal.AjaxModalHandler,
$.pkp.controllers.modal.ModalHandler);
//
// Protected methods
//
/** @inheritDoc */
$.pkp.controllers.modal.AjaxModalHandler.prototype.checkOptions =
function(options) {
// Check the mandatory options of the ModalHandler handler.
if (!this.parent('checkOptions', options)) {
return false;
}
// Check for our own mandatory options.
return typeof options.url === 'string';
};
/** @inheritDoc */
$.pkp.controllers.modal.AjaxModalHandler.prototype.mergeOptions =
function(options) {
// Call parent.
return /** @type {Object} */ (this.parent('mergeOptions', options));
};
/**
* Open the modal and fetch content via ajax
* @param {jQueryObject} $handledElement The clickable element
* the modal will be attached to.
* @protected
*/
$.pkp.controllers.modal.AjaxModalHandler.prototype.modalOpen =
function($handledElement) {
this.parent('modalOpen', $handledElement);
// Retrieve remote modal content.
$handledElement.find('.content')
.pkpAjaxHtml(/** @type {{ url: string }} */ (this.options).url);
};
/**
* Close the modal when a form submission is complete
* @param {Object} callingContext The calling element or object.
* @param {Event} event The triggering event (e.g. a click on
* a button.
* @protected
*/
$.pkp.controllers.modal.AjaxModalHandler.prototype.formSubmitted =
function(callingContext, event) {
this.getHtmlElement().parent().trigger('notifyUser');
this.modalClose();
};
}(jQuery));
@@ -0,0 +1,88 @@
/**
* @file js/controllers/modal/ButtonConfirmationModalHandler.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 ButtonConfirmationModalHandler
* @ingroup js_controllers_modal
*
* @brief A confirmation modal that displays a confirmation message before
* actually triggering a click event on the calling element (usually a
* button).
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.modal.ConfirmationModalHandler
*
* @param {jQueryObject} $handledElement The modal.
* @param {Object} options Non-default options to configure
* the modal.
*
* Options are:
* - button jQuery The button to be clicked on success.
* - All options from the ConfirmationModalHandler and ModalHandler
* widgets.
* - All options documented for the jQueryUI dialog widget,
* except for the buttons parameter which is not supported.
*/
$.pkp.controllers.modal.ButtonConfirmationModalHandler =
function($handledElement, options) {
this.parent($handledElement, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.modal.ButtonConfirmationModalHandler,
$.pkp.controllers.modal.ConfirmationModalHandler);
//
// Protected methods
//
/**
* @inheritDoc
*/
$.pkp.controllers.modal.ButtonConfirmationModalHandler.prototype.checkOptions =
function(options) {
// Check inherited options
if (!this.parent('checkOptions', options)) {
return false;
}
return typeof options.$button == 'object' && options.$button.length == 1;
};
/**
* Callback that will be activated when the modal is confirmed
*
* @param {HTMLElement} dialogElement The element the
* dialog was created on.
* @param {Event} event The click event.
*/
$.pkp.controllers.modal.ButtonConfirmationModalHandler.prototype.modalConfirm =
function(dialogElement, event) {
var $button = (/** @type {{ $button: jQueryObject }} */ (this.options))
.$button;
// Close the modal first so that the linkaction is no longer disabled
this.modalClose(dialogElement);
// Trigger the link/button action
if ($button.attr('type') == 'submit') {
$button.trigger('submit');
} else {
$button.click();
}
};
}(jQuery));
@@ -0,0 +1,156 @@
/**
* @file js/controllers/modal/ConfirmationModalHandler.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 ConfirmationModalHandler
* @ingroup js_controllers_modal
*
* @brief A modal that displays a static explanatory text and has cancel and
* confirmation buttons.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.modal.ModalHandler
*
* @param {jQueryObject} $handledElement The clickable element
* the modal will be attached to.
* @param {{
* callback: Function,
* callbackArgs: Object
* }} options Non-default options to configure the modal.
*
* Options are:
* - okButton string the name for the confirmation button.
* - cancelButton string the name for the cancel button
* (or false for no button).
* - dialogText string the text to be displayed in the modal.
* - All options from the ModalHandler widget.
* - callback function A callback function to close when confirmed
* - callbackArgs object Arguments to pass to the callback function
*/
$.pkp.controllers.modal.ConfirmationModalHandler =
function($handledElement, options) {
this.parent($handledElement, options);
this.callback_ = options.callback || null;
this.callbackArgs_ = options.callbackArgs || null;
// Bind to the confirmation button
$handledElement.find('.pkpModalConfirmButton')
.on('click', this.callbackWrapper(this.modalConfirm));
};
$.pkp.classes.Helper.inherits($.pkp.controllers.modal.ConfirmationModalHandler,
$.pkp.controllers.modal.ModalHandler);
//
// Private properties
//
/**
* A callback to fire when confirmed
* @private
* @type {?Function}
*/
$.pkp.controllers.modal.ConfirmationModalHandler.prototype.
callback_ = null;
/**
* Arguments to pass to the callback function
* @private
* @type {?Object}
*/
$.pkp.controllers.modal.ConfirmationModalHandler.prototype.
callbackArgs_ = null;
//
// Protected methods
//
/**
* @inheritDoc
*/
$.pkp.controllers.modal.ConfirmationModalHandler.prototype.checkOptions =
function(options) {
// Check the mandatory options of the ModalHandler handler.
if (!this.parent('checkOptions', options)) {
return false;
}
// Hack to prevent closure compiler type mismatches
var castOptions = /** @type {{okButton: string,
cancelButton: string, dialogText: string}} */ (options);
// Check for our own mandatory options.
return typeof castOptions.okButton === 'string' &&
(/** @type {boolean} */ (castOptions.cancelButton) === false ||
typeof castOptions.cancelButton === 'string') &&
typeof castOptions.dialogText === 'string';
};
//
// Public methods
//
/**
* Add content to modal
*
* @return {jQueryObject} jQuery object representing modal content
*/
$.pkp.controllers.modal.ConfirmationModalHandler.prototype.modalBuild =
function() {
var $modal = this.parent('modalBuild'),
buttons = '<button class="ok pkpModalConfirmButton">' +
(/** @type {{ okButton: string }} */ (this.options)).okButton +
'</button>';
$modal.addClass('pkp_modal_confirmation').find('.content')
.append('<div class="message">' +
(/** @type {{ dialogText: string }} */ (this.options)).dialogText +
'</div>');
if (this.options.cancelButton) {
buttons += '<button class="cancel pkpModalCloseButton">' +
this.options.cancelButton + '</button>';
}
$modal.append('<div class="footer">' + buttons + '</div>');
// Add aria role and label
$modal.attr('role', 'dialog')
.attr('aria-label', this.options.title);
return /** @type {jQueryObject} */ ($modal);
};
/**
* Callback that will be activated when the modal's
* confirm button is clicked.
*
* @param {HTMLElement} dialogElement The element the
* dialog was created on.
* @param {Event} event The click event.
*/
$.pkp.controllers.modal.ConfirmationModalHandler.prototype.modalConfirm =
function(dialogElement, event) {
// The default implementation will simply close the modal.
this.modalClose(dialogElement);
if (this.callback_) {
this.callback_.call(null, this.callbackArgs_);
}
};
}(jQuery));
@@ -0,0 +1,109 @@
/**
* @file js/controllers/modal/JsEventConfirmationModalHandler.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 JsEventConfirmationModalHandler
* @ingroup js_controllers_modal
*
* @brief A confirmation modal that generates a JS event.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.modal.ConfirmationModalHandler
*
* @param {jQueryObject} $handledElement The clickable element
* the modal will be attached to.
* @param {Object} options Non-default options to configure
* the modal.
*
* Options are:
* - remoteUrl string A URL to be redirected to when the confirmation
* button has been clicked.
* - All options from the ConfirmationModalHandler and ModalHandler
* widgets.
* - All options documented for the jQueryUI dialog widget,
* except for the buttons parameter which is not supported.
*/
$.pkp.controllers.modal.JsEventConfirmationModalHandler =
function($handledElement, options) {
this.parent($handledElement, options);
// Configure the event to be generated when
// the modal closes.
this.jsEvent_ = options.jsEvent;
this.extraArguments_ = options.extraArguments;
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.modal.JsEventConfirmationModalHandler,
$.pkp.controllers.modal.ConfirmationModalHandler);
//
// Private properties
//
/**
* An event to be generated when the confirmation button
* has been clicked.
* @private
* @type {?string}
*/
$.pkp.controllers.modal.JsEventConfirmationModalHandler.prototype.
jsEvent_ = null;
/**
* An array of extra information to be passed along with the event.
* @private
* @type {?Array}
*/
$.pkp.controllers.modal.JsEventConfirmationModalHandler.prototype.
extraArguments_ = null;
//
// Protected methods
//
/** @inheritDoc */
$.pkp.controllers.modal.JsEventConfirmationModalHandler.prototype.
checkOptions = function(options) {
// Check the mandatory options of the ModalHandler handler.
if (!this.parent('checkOptions', options)) {
return false;
}
// Check for our own mandatory options.
// The cancel button and event are mandatory.
return typeof options.cancelButton === 'string' &&
typeof options.jsEvent === 'string';
};
//
// Public methods
//
/**
* Callback that will be activated when the modal's
* confirm button is clicked.
*
* @param {HTMLElement} dialogElement The element the
* dialog was created on.
* @param {Event} event The click event.
*/
$.pkp.controllers.modal.JsEventConfirmationModalHandler.prototype.
modalConfirm = function(dialogElement, event) {
this.trigger(/** @type {string} */ (this.jsEvent_),
/** @type {Array} */ (this.extraArguments_));
this.modalClose(dialogElement);
};
}(jQuery));
@@ -0,0 +1,360 @@
/*global pkp */
/**
* @defgroup js_controllers_modal
*/
/**
* @file js/controllers/modal/ModalHandler.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 ModalHandler
* @ingroup js_controllers_modal
*
* @brief Basic modal implementation.
*
* A modal that has only one button and expects a simple message string.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.modal = $.pkp.controllers.modal || { };
/**
* @constructor
*
* @extends $.pkp.classes.Handler
*
* @param {jQueryObject} $handledElement The modal.
* @param {Object.<string, *>} options The modal options.
*/
$.pkp.controllers.modal.ModalHandler = function($handledElement, options) {
this.parent($handledElement, options);
// Check the options.
if (!this.checkOptions(options)) {
throw new Error('Missing or invalid modal options!');
}
// Clone the options object before we manipulate them.
var internalOptions = $.extend(true, {}, options),
canClose;
// Merge user and default options.
this.options = /** @type {{ canClose: boolean, textTitle: string,
title: string, titleIcon: string,
closeCleanVueInstances: Array }} */
(this.mergeOptions(internalOptions));
// Attach content to the modal
$handledElement.html(this.modalBuild()[0].outerHTML);
// Open the modal
this.modalOpen($handledElement);
// Set up close controls
$handledElement.find(
'.pkpModalCloseButton').click(this.callbackWrapper(this.modalClose));
$handledElement.on(
'click keyup', this.callbackWrapper(this.handleWrapperEvents));
// Publish some otherwise private events triggered
// by nested widgets so that they can be handled by
// the element that opened the modal.
this.publishEvent('redirectRequested');
this.publishEvent('dataChanged');
this.publishEvent('updateHeader');
this.publishEvent('gridRefreshRequested');
this.bind('notifyUser', this.redirectNotifyUserEventHandler_);
this.bindGlobal('form-success', this.onFormSuccess_);
};
$.pkp.classes.Helper.inherits($.pkp.controllers.modal.ModalHandler,
$.pkp.classes.Handler);
//
// Private static properties
//
/**
* Default options
* @private
* @type {Object}
* @const
*/
$.pkp.controllers.modal.ModalHandler.DEFAULT_OPTIONS_ = {
autoOpen: true,
width: 710,
modal: true,
draggable: false,
resizable: false,
position: {my: 'center', at: 'center center-10%', of: window},
canClose: true,
closeCallback: false,
// Vue components to destroy when when modal is closed
closeCleanVueInstances: []
};
//
// Public properties
//
/**
* Current options
*
* After passed options are merged with defaults.
*
* @type {Object}
*/
$.pkp.controllers.modal.ModalHandler.options = null;
//
// Protected methods
//
/**
* Check whether the correct options have been
* given for this modal.
* @protected
* @param {Object.<string, *>} options Modal options.
* @return {boolean} True if options are ok.
*/
$.pkp.controllers.modal.ModalHandler.prototype.checkOptions =
function(options) {
// Check for basic configuration requirements.
return typeof options === 'object' &&
(/** @type {{ buttons: Object }} */ (options)).buttons === undefined;
};
/**
* Determine the options based on
* default options.
* @protected
* @param {Object.<string, *>} options Non-default modal options.
* @return {Object.<string, *>} The default options merged
* with the non-default options.
*/
$.pkp.controllers.modal.ModalHandler.prototype.mergeOptions =
function(options) {
// Merge the user options into the default options.
var mergedOptions = $.extend(true, { },
this.self('DEFAULT_OPTIONS_'), options);
return mergedOptions;
};
//
// Public methods
//
/**
* Build the markup for a modal container, including the header, close
* button and a container for the content to be placed in.
* TODO: This kind of markup probably shouldn't be embedded within the JS...
*
* @protected
* @return {Object} jQuery object representing modal content
*/
$.pkp.controllers.modal.ModalHandler.prototype.modalBuild =
function() {
var $titleDiv, $modal = $('<div class="pkp_modal_panel"></div>');
// Title bar
if (typeof(this.options.textTitle) !== 'undefined') {
$titleDiv = $('<div class="header"/>').text(this.options.textTitle);
$modal.append($titleDiv);
} else if (typeof(this.options.title) !== 'undefined') {
$modal.append('<div class="header">' + this.options.title + '</div>');
} else {
$modal.append('<div class="header">' + '</div>');
}
// Close button
if (this.options.canClose) {
$modal.append(
'<a href="#" class="close pkpModalCloseButton">' +
'<span :aria-hidden="true">×</span>' +
'<span class="pkp_screen_reader">' +
(/** @type {{ closeButtonText: string }} */ (this.options))
.closeButtonText + '</span></a>');
}
// Content
$modal.append('<div class="content"></div>');
// Add aria role and label
$modal.attr('role', 'dialog')
.attr('aria-label', this.options.title);
return $modal;
};
/**
* Attach a modal to the dom and make it visible
* @param {jQueryObject} $handledElement The modal.
*/
$.pkp.controllers.modal.ModalHandler.prototype.modalOpen =
function($handledElement) {
// The $handledElement must be attached to the DOM before events will
// bubble up to SiteHandler
var $body = $('body');
$body.append($handledElement);
// Trigger visibility state change on the next tick, so that CSS
// transform animations will run
setTimeout(function() {
$handledElement.addClass('is_visible');
},10);
// Set focus to the modal. Leave a sizeable delay here so that the
// element can be added to the dom first
setTimeout(function() {
$handledElement.focus();
}, 300);
// Trigger events
$handledElement.trigger('pkpModalOpen', [$handledElement]);
};
/**
* Close the modal. Typically invoked via an event of some kind, such as
* a `click` or `keyup`
*
* @param {Object=} opt_callingContext The calling element or object.
* @param {Event=} opt_event The triggering event (e.g. a click on
* a close button. Not set if called via callback.
* @return {boolean} Should return false to stop event processing.
*/
$.pkp.controllers.modal.ModalHandler.prototype.modalClose =
function(opt_callingContext, opt_event) {
var modalHandler = this,
$modalElement = this.getHtmlElement(),
$form = $modalElement.find('form').first(),
handler, informationObject;
// Unregister a form if attached to this modalElement
// modalClose is called on both 'cancel' and 'close' events. With
// callbacks both callingContext and event are undefined. So,
// unregister this form with SiteHandler.
if ($form.length == 1) {
informationObject = {closePermitted: true};
$form.trigger('containerClose', [informationObject]);
if (!informationObject.closePermitted) {
return false;
}
}
// Hide the modal, clean up any mounted vue instances, remove it from the
// DOM and remove the handler once the CSS animation is complete
$modalElement.removeClass('is_visible');
this.trigger('pkpModalClose');
setTimeout(function() {
var vueInstances = modalHandler.options.closeCleanVueInstances,
instance,
i,
id;
if (vueInstances.length) {
for (i = 0; i < vueInstances.length; i++) {
id = vueInstances[i];
if (typeof pkp.registry._instances[id] !== 'undefined') {
instance = /** @type {{ $destroy: Function }} */
(pkp.registry._instances[id]);
instance.$destroy();
}
}
}
modalHandler.unbindPartial($modalElement);
$modalElement.empty();
modalHandler.remove();
// Fire a callback function if one has been passed with options
if (typeof modalHandler.options.closeCallback === 'function') {
modalHandler.options.closeCallback.call();
}
}, 300);
return false;
};
/**
* Process events that reach the wrapper element.
* Should NOT block other events from bubbling up. Doing so
* can disable submit buttons in nested forms.
*
* @param {Object=} opt_callingContext The calling element or object.
* @param {Event=} opt_event The triggering event (e.g. a click on
* a close button. Not set if called via callback.
*/
$.pkp.controllers.modal.ModalHandler.prototype.handleWrapperEvents =
function(opt_callingContext, opt_event) {
// Close click events directly on modal (background screen)
if (opt_event.type == 'click' && opt_callingContext == opt_event.target) {
$.pkp.classes.Handler.getHandler($(opt_callingContext))
.modalClose();
return;
}
// Close for ESC keypresses (27) that have bubbled up
if (opt_event.type == 'keyup' && opt_event.which == 27) {
$.pkp.classes.Handler.getHandler($(opt_callingContext))
.modalClose();
return;
}
};
//
// Private methods
//
/**
* Handler to redirect to the correct notification widget the
* notify user event.
* @param {HTMLElement} sourceElement The element that issued the
* "notifyUser" event.
* @param {Event} event The "notify user" event.
* @param {HTMLElement} triggerElement The element that triggered
* the "notifyUser" event.
* @private
*/
$.pkp.controllers.modal.ModalHandler.prototype.redirectNotifyUserEventHandler_ =
function(sourceElement, event, triggerElement) {
// Use the notification helper to redirect the notify user event.
$.pkp.classes.notification.NotificationHelper.
redirectNotifyUserEvent(this, triggerElement);
};
/**
* Handler to listen to global form success events, and close when an event
* from a child form has been fired, and this form matches the config id
*
* @param {Object} source The Vue.js component which fired the event
* @param {Object} formId The form component's id prop
* @private
*/
$.pkp.controllers.modal.ModalHandler.prototype.onFormSuccess_ =
function(source, formId) {
if (this.options.closeOnFormSuccessId &&
this.options.closeOnFormSuccessId === formId) {
var self = this;
setTimeout(function() {
self.modalClose();
}, 1500);
}
};
}(jQuery));
@@ -0,0 +1,96 @@
/**
* @file js/controllers/modal/RedirectConfirmationModalHandler.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 RedirectConfirmationModalHandler
* @ingroup js_controllers_modal
*
* @brief A confirmation modal that redirects to a URL upon confirmation.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.modal.ConfirmationModalHandler
*
* @param {jQueryObject} $handledElement The clickable element
* the modal will be attached to.
* @param {Object} options Non-default options to configure
* the modal.
*
* Options are:
* - remoteUrl string A URL to be redirected to when the confirmation
* button has been clicked.
* - All options from the ConfirmationModalHandler and ModalHandler
* widgets.
* - All options documented for the jQueryUI dialog widget,
* except for the buttons parameter which is not supported.
*/
$.pkp.controllers.modal.RedirectConfirmationModalHandler =
function($handledElement, options) {
this.parent($handledElement, options);
// Configure the redirect URL to be called when
// the modal closes.
this.remoteUrl_ = options.remoteUrl;
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.modal.RedirectConfirmationModalHandler,
$.pkp.controllers.modal.ConfirmationModalHandler);
//
// Private properties
//
/**
* A URL to be redirected to when the confirmation button
* has been clicked.
* @private
* @type {?string}
*/
$.pkp.controllers.modal.RedirectConfirmationModalHandler.prototype.
remoteUrl_ = null;
//
// Protected methods
//
/** @inheritDoc */
$.pkp.controllers.modal.RedirectConfirmationModalHandler.prototype.
checkOptions = function(options) {
// Check the mandatory options of the ModalHandler handler.
if (!this.parent('checkOptions', options)) {
return false;
}
// Check for our own mandatory options.
// The cancel button is mandatory for redirect confirmation modals.
return typeof options.cancelButton === 'string' &&
typeof options.remoteUrl === 'string';
};
//
// Public methods
//
/**
* Callback that will be activated when the modal's
* confirm button is clicked.
*
* @param {HTMLElement} dialogElement The element the
* dialog was created on.
* @param {Event} event The click event.
*/
$.pkp.controllers.modal.RedirectConfirmationModalHandler.prototype.
modalConfirm = function(dialogElement, event) {
document.location = this.remoteUrl_;
};
}(jQuery));
@@ -0,0 +1,138 @@
/**
* @file js/controllers/modal/RemoteActionConfirmationModalHandler.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 RemoteActionConfirmationModalHandler
* @ingroup js_controllers_modal
*
* @brief A confirmation modal that executes a remote action on confirmation.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.modal.ConfirmationModalHandler
*
* @param {jQueryObject} $handledElement The clickable element
* the modal will be attached to.
* @param {{
* remoteAction: string,
* postData: Object,
* csrfToken: string
* }} options Non-default options to configure the modal.
*
* Options are:
* - remoteAction string An action to be executed when the confirmation
* button has been clicked.
* - All options from the ConfirmationModalHandler and ModalHandler
* widgets.
* - All options documented for the jQueryUI dialog widget,
* except for the buttons parameter which is not supported.
*/
$.pkp.controllers.modal.RemoteActionConfirmationModalHandler =
function($handledElement, options) {
this.parent($handledElement, options);
// Configure the remote action (URL) to be called when
// the modal closes.
this.remoteAction_ = options.remoteAction;
// Store the data to send with the post request
this.postData_ = options.postData || {};
// Add the CSRF token to the post data
this.postData_.csrfToken = options.csrfToken;
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.modal.RemoteActionConfirmationModalHandler,
$.pkp.controllers.modal.ConfirmationModalHandler);
//
// Private properties
//
/**
* A remote action to be executed when the confirmation button
* has been clicked.
* @private
* @type {?string}
*/
$.pkp.controllers.modal.RemoteActionConfirmationModalHandler.prototype.
remoteAction_ = null;
/**
* Data params to send with the post request
* @private
* @type {?Object}
*/
$.pkp.controllers.modal.RemoteActionConfirmationModalHandler.prototype.
postData_ = null;
//
// Protected methods
//
/**
* @inheritDoc
*/
$.pkp.controllers.modal.RemoteActionConfirmationModalHandler.prototype.
checkOptions = function(options) {
// Check the mandatory options of the ModalHandler handler.
if (!this.parent('checkOptions', options)) {
return false;
}
// Check for our own mandatory options.
// The cancel button is mandatory for remote action confirmation modals.
return typeof options.cancelButton === 'string' &&
typeof options.remoteAction === 'string';
};
//
// Public methods
//
/**
* Callback that will be activated when the modal's
* confirm button is clicked.
*
* @param {HTMLElement} dialogElement The element the
* dialog was created on.
* @param {Event} event The click event.
*/
$.pkp.controllers.modal.RemoteActionConfirmationModalHandler.prototype.
modalConfirm = function(dialogElement, event) {
event.preventDefault();
$.post(this.remoteAction_,
this.postData_,
this.callbackWrapper(this.remoteResponse), 'json');
};
//
// Protected methods
//
/**
* @inheritDoc
*/
$.pkp.controllers.modal.RemoteActionConfirmationModalHandler.prototype.
remoteResponse = function(ajaxOptions, jsonData) {
var processedJsonData = this.parent('remoteResponse', ajaxOptions, jsonData);
if (processedJsonData !== false) {
this.modalClose(ajaxOptions);
}
return false;
};
}(jQuery));
@@ -0,0 +1,91 @@
/**
* @file js/controllers/modal/WizardModalHandler.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 WizardModalHandler
* @ingroup js_controllers_modal
*
* @brief A modal that contains a wizard and handles its events.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.controllers.modal.AjaxModalHandler
*
* @param {jQueryObject} $handledElement The clickable element
* the modal will be attached to.
* @param {Object} options non-default Dialog options
* to be passed into the dialog widget.
*
* Options are:
* - all options documented for the AjaxModalHandler.
*/
$.pkp.controllers.modal.WizardModalHandler =
function($handledElement, options) {
this.parent($handledElement, options);
// Subscribe the modal to wizard events.
this.bind('wizardClose', this.wizardClose);
this.bind('wizardCancel', this.wizardClose);
};
$.pkp.classes.Helper.inherits($.pkp.controllers.modal.WizardModalHandler,
$.pkp.controllers.modal.AjaxModalHandler);
/**
* Overridden version of the modal close button handler acting
* as a wizard cancel button.
*
* @protected
* @param {Object=} opt_callingElement The close button.
* @param {Event=} opt_event The close button click event.
* @param {boolean=} opt_closeWithoutCancel Set to true to immediately
* close the modal.
* @return {boolean} Should return false to stop event processing.
*/
$.pkp.controllers.modal.WizardModalHandler.prototype.modalClose =
function(opt_callingElement, opt_event, opt_closeWithoutCancel) {
if (opt_closeWithoutCancel) {
this.parent('modalClose', opt_callingElement, opt_event);
} else {
// Trigger a cancel event on the wizard.
var wizardCancelRequestedEvent = new $.Event('wizardCancelRequested'),
$wizard;
wizardCancelRequestedEvent.stopPropagation();
$wizard = this.getHtmlElement().children().first();
$wizard.trigger(wizardCancelRequestedEvent);
// Only close the modal if the wizard didn't prevent this.
if (!wizardCancelRequestedEvent.isDefaultPrevented()) {
this.parent('modalClose', opt_callingElement, opt_event);
}
}
return false;
};
/**
* Handle the wizard close event.
*
* @param {HTMLElement} wizardElement The calling
* wizard.
* @param {Event} event The triggered event.
*/
$.pkp.controllers.modal.WizardModalHandler.prototype.wizardClose =
function(wizardElement, event) {
this.modalClose(wizardElement, event, true);
};
}(jQuery));
@@ -0,0 +1,62 @@
/**
* @defgroup js_controllers_tab_workflow
*/
/**
* @file js/controllers/tab/workflow/WorkflowTabHandler.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 WorkflowTabHandler
* @ingroup js_controllers_tab_workflow
*
* @brief A subclass of TabHandler for handling requests to load stages
* of the workflow.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.tab =
$.pkp.controllers.tab || {};
/** @type {Object} */
$.pkp.controllers.tab.workflow =
$.pkp.controllers.tab.workflow || {};
/**
* @constructor
*
* @extends $.pkp.controllers.TabHandler
*
* @param {jQueryObject} $tabs A wrapped HTML element that
* represents the tabbed interface.
* @param {Object} options Handler options.
*/
$.pkp.controllers.tab.workflow.WorkflowTabHandler =
function($tabs, options) {
var pageUrl, stage, pattern, i, tabAnchors, matches;
this.parent($tabs, options);
pageUrl = document.location.toString();
matches = pageUrl.match('workflow/([^/]+)/');
if (matches) {
stage = matches[1];
tabAnchors = $tabs.find('li a');
for (i = 0; i < tabAnchors.length; i++) {
pattern = new RegExp(stage);
if (tabAnchors[i].getAttribute('class').match(pattern)) {
options.selected = i;
}
}
}
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.tab.workflow.WorkflowTabHandler,
$.pkp.controllers.TabHandler);
}(jQuery));
@@ -0,0 +1,632 @@
/**
* @defgroup js_controllers_wizard
*/
/**
* @file js/controllers/wizard/WizardHandler.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 WizardHandler
* @ingroup js_controllers_wizard
*
* @brief Basic wizard handler.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.wizard = $.pkp.controllers.wizard || { };
/**
* @constructor
*
* @extends $.pkp.controllers.TabHandler
*
* @param {jQueryObject} $wizard A wrapped HTML element that
* represents the wizard.
* @param {{
* enforceLinear: boolean,
* cancelButtonText: string,
* continueButtonTest: string,
* finishButtonText: string
* }} options options to configure the form handler.
*/
$.pkp.controllers.wizard.WizardHandler = function($wizard, options) {
this.parent($wizard, options);
// Add the wizard buttons
this.addWizardButtons_($wizard, options);
this.enforceLinear_ = options.hasOwnProperty('enforceLinear') ?
options.enforceLinear : true;
// Start the wizard.
this.startWizard();
// Bind the wizard events to handlers.
this.bindWizardEvents();
// Assume that we usually have forms in the wizard
// tabs and bind to form events.
this.bind('formValid', this.formValid);
this.bind('formInvalid', this.formInvalid);
this.bind('formSubmitted', this.formSubmitted);
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.wizard.WizardHandler, $.pkp.controllers.TabHandler);
//
// Private properties
//
/**
* The continue button.
* @private
* @type {jQueryObject?}
*/
$.pkp.controllers.wizard.WizardHandler.prototype.$continueButton_ = null;
/**
* The progress indicator.
* @private
* @type {jQueryObject?}
*/
$.pkp.controllers.wizard.WizardHandler.prototype.$progressIndicator_ = null;
/**
* The continue button label.
* @private
* @type {?string}
*/
$.pkp.controllers.wizard.WizardHandler.prototype.continueButtonText_ = null;
/**
* The finish button label.
* @private
* @type {?string}
*/
$.pkp.controllers.wizard.WizardHandler.prototype.finishButtonText_ = null;
/**
* Whether or not to enforce linear progress through the wizard.
* @private
* @type {?boolean}
*/
$.pkp.controllers.wizard.WizardHandler.prototype.enforceLinear_ = null;
//
// Private methods
//
/**
* Show the loading spinner
*
* @private
*/
$.pkp.controllers.wizard.WizardHandler.prototype.showProgressIndicator_ =
function() {
this.getProgressIndicator().css('opacity', 1);
};
/**
* Hide the loading spinner
*
* @private
*/
$.pkp.controllers.wizard.WizardHandler.prototype.hideProgressIndicator_ =
function() {
this.getProgressIndicator().css('opacity', 0);
};
//
// Public methods
//
/**
* Handle the user's request to advance the wizard.
*
* NB: Please do not override this method. This is an internal event
* handler. Override the wizardAdvanceRequested() and wizardAdvance()
* event handlers instead if you want to provide custom behavior.
*
* @param {HTMLElement} buttonElement The button that triggered the event.
* @param {Event} event The triggered event.
* @return {boolean} Should return false to stop event propagation.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.continueRequest =
function(buttonElement, event) {
// Trigger the "advance requested" event on the current
// tab's children to give it a chance to veto the advance
// request.
var advanceRequestedEvent = new $.Event('wizardAdvanceRequested');
this.getCurrentTab().children().first().trigger(advanceRequestedEvent);
// Advance the wizard if the advanceRequestEvent handler didn't
// prevent it.
if (!advanceRequestedEvent.isDefaultPrevented()) {
this.advanceOrClose_();
}
return false;
};
/**
* Handle "form valid" events that may be triggered by forms in the
* wizard tab.
*
* @param {HTMLElement} formElement The form that triggered the event.
* @param {Event} event The triggered event.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.formValid =
function(formElement, event) {
// The default implementation enables the continue button
// as soon as the form validates.
this.enableContinueButton();
};
/**
* Handle "form invalid" events that may be triggered by forms in the
* wizard tab.
*
* @param {HTMLElement} formElement The form that triggered the event.
* @param {Event} event The triggered event.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.formInvalid =
function(formElement, event) {
// The default implementation disables the continue button
// as if the form no longer validates.
this.disableContinueButton();
};
/**
* Handle "form submitted" events that may be triggered by forms in the
* wizard tab.
*
* @param {HTMLElement} formElement The form that triggered the event.
* @param {Event} event The triggered event.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.formSubmitted =
function(formElement, event) {
// The default implementation advances the wizard.
this.advanceOrClose_();
};
/**
* Handle the user's request to cancel the wizard.
*
* NB: Please do not override this method. This is an internal event
* handler. Override the wizardCancel() event handler instead if you
* want to provide custom behavior.
*
* @param {HTMLElement} buttonElement The button that triggered the event.
* @param {Event} event The triggered event.
* @return {boolean} Should return false to stop event propagation.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.cancelRequest =
function(buttonElement, event) {
// This is a 'cancel' click, so unregister forms without prompting.
this.checkForm_(false);
// Trigger the "cancel requested" event on the current
// tab's children to give it a chance to veto the cancel
// request.
var cancelRequestedEvent = new $.Event('wizardCancelRequested');
this.getCurrentTab().children().first().trigger(cancelRequestedEvent);
// Trigger the wizardCancel event if the
// cancelRequestEvent handler didn't prevent it.
if (!cancelRequestedEvent.isDefaultPrevented()) {
this.trigger('wizardCancel');
}
return false;
};
/**
* Handle the wizard "cancel requested" event.
*
* Please override this method to clean up before the wizard is
* being canceled. You can execute event.preventDefault() if you
* don't want the wizard to cancel.
*
* NB: This is a fallback handler that will be called if no other
* event handler calls the event.stopPropagation() method.
*
* @param {HTMLElement} wizardElement The wizard's HTMLElement on
* which the event was triggered.
* @param {Event} event The triggered event.
* @return {boolean} Return false if not overridden and if check form
* returns true.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.wizardCancelRequested =
function(wizardElement, event) {
if (this.checkForm_(true)) {
// User doesn't wants to leave. Return false to stop cancel request.
return false;
}
// User wants to leave or there is no form inside this wizard.
return true;
};
/**
* Handle the wizard "advance requested" event.
*
* Please override this method to make custom validation checks or
* place server requests before you let the wizard advance to the next
* step. You can execute event.preventDefault() if you don't want
* the wizard to advance because you encountered errors during
* validation.
*
* NB: This is a fallback handler that will be called if no other
* event handler calls the event.stopPropagation() method.
*
* @param {HTMLElement} wizardElement The wizard's HTMLElement on
* which the event was triggered.
* @param {Event} event The triggered event.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.wizardAdvanceRequested =
function(wizardElement, event) {
// If we find a form then submit it.
var $form = this.getForm_();
if ($form) {
// Try to submit the form.
if ($form.submit()) {
this.disableContinueButton();
this.showProgressIndicator_();
}
// Prevent default event handling so that the form
// can do its validation checks first.
event.preventDefault();
}
};
/**
* Handle the "wizard advance" event. The default implementation
* advances the wizard to the next step and disables the previous step.
*
* In most cases you probably don't want to override this method unless
* you want to provide a different navigation experience. Form validation
* and submission or similar tasks should be done in the
* wizardAdvanceRequested() event handler.
*
* @param {HTMLElement} wizardElement The wizard's HTMLElement on
* which the event was triggered.
* @param {Event} event The triggered event.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.wizardAdvance =
function(wizardElement, event) {
// The wizard can only be advanced one step at a time.
// The step cannot be greater than the number of wizard
// tabs and not less than 1.
var currentStep = this.getCurrentStep(),
lastStep = this.getNumberOfSteps() - 1,
targetStep = currentStep + 1,
$wizard, $continueButton;
// Do not advance beyond the last step.
if (targetStep > lastStep) {
throw new Error('Trying to set an invalid wizard step!');
}
// Enable the target step.
$wizard = this.getHtmlElement();
$wizard.tabs('enable', targetStep);
// Advance to the target step.
$wizard.tabs('option', 'active', targetStep);
if (this.enforceLinear_) {
// Disable the previous step.
$wizard.tabs('disable', currentStep);
}
// If this is the last step then change the text on the
// continue button to finish.
$continueButton = this.getContinueButton();
if (targetStep === lastStep) {
$continueButton.text(
/** @type {string} */ (this.getFinishButtonText()));
}
this.hideProgressIndicator_();
this.enableContinueButton();
};
//
// Protected methods
//
/**
* (Re-)Start the wizard.
* @protected
*/
$.pkp.controllers.wizard.WizardHandler.prototype.startWizard = function() {
// Retrieve the wizard element.
var $wizard = this.getHtmlElement(),
$continueButton, disabledSteps, i;
// Do we re-start the wizard?
if (this.getCurrentStep() !== 0) {
// Make sure that the first step is enabled, otherwise
// we cannot select it.
$wizard.tabs('enable', 0);
// Go to the first step.
$wizard.tabs('option', 'active', 0);
// Reset the continue button label.
$continueButton = this.getContinueButton();
$continueButton.text(
/** @type {string} */ (this.getContinueButtonText()));
}
if (this.enforceLinear_) {
// Disable all but the first step.
disabledSteps = [];
for (i = 1; i < this.getNumberOfSteps(); i++) {
disabledSteps.push(i);
}
$wizard.tabs('option', 'disabled', disabledSteps);
}
};
/**
* Bind wizard events to default event handlers.
* @protected
*/
$.pkp.controllers.wizard.WizardHandler.prototype.bindWizardEvents = function() {
this.bind('wizardCancelRequested', this.wizardCancelRequested);
this.bind('wizardAdvanceRequested', this.wizardAdvanceRequested);
this.bind('wizardAdvance', this.wizardAdvance);
};
/**
* Get the current wizard step.
* @protected
* @return {number} The current wizard step.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.
getCurrentStep = function() {
return this.getCurrentTabIndex();
};
/**
* Get the continue button.
* @protected
* @return {jQueryObject} The continue button.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.
getContinueButton = function() {
return this.$continueButton_;
};
/**
* Get the progress indicator.
* @protected
* @return {jQueryObject} The progress indicator.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.
getProgressIndicator = function() {
return this.$progressIndicator_;
};
/**
* Get the continue button label.
* @protected
* @return {?string} The text to display on the continue button.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.
getContinueButtonText = function() {
return this.continueButtonText_;
};
/**
* Get the finish button label.
* @protected
* @return {?string} The text to display on the continue button
* in the last wizard step.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.
getFinishButtonText = function() {
return this.finishButtonText_;
};
/**
* Count the wizard steps.
* @return {number} The current number of wizard steps.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.
getNumberOfSteps = function() {
var $wizard = this.getHtmlElement();
return $wizard.find('ul').first().children().length;
};
//
// Private methods
//
/**
* Return the current form (if any).
*
* @private
* @return {jQueryObject?} The form (if any).
*/
$.pkp.controllers.wizard.WizardHandler.prototype.getForm_ = function() {
var i, $element, $tabContent;
// If we find a form in the current tab then return it.
$tabContent = this.getCurrentTab().children();
for (i = 0; i < $tabContent.length; i++) {
$element = $($tabContent[i]);
if ($element.is('form')) {
return $element;
}
}
return null;
};
/**
* Continue to the next step or, if this is the last step,
* then close the wizard.
*
* @private
*/
$.pkp.controllers.wizard.WizardHandler.prototype.advanceOrClose_ =
function() {
var currentStep = this.getCurrentStep(),
lastStep = this.getNumberOfSteps() - 1;
if (currentStep < lastStep) {
this.trigger('wizardAdvance');
} else {
this.trigger('wizardClose');
}
};
/**
* Helper method to look for changed forms.
*
* @param {boolean} prompt Whether or not to prompt.
* @return {boolean} Whether or not they wish to cancel. If no form is
* available in wizard, also return false.
* @private
*/
$.pkp.controllers.wizard.WizardHandler.prototype.checkForm_ =
function(prompt) {
var $form = this.getForm_(),
handler;
if ($form !== null) {
handler = $.pkp.classes.Handler.getHandler($('#' + $form.attr('id')));
if (prompt) {
if (handler.formChangesTracked) {
if (!confirm(pkp.localeKeys['form.dataHasChanged'])) {
return true; // the user has clicked cancel, they wish to stay.
} else {
handler.unregisterForm();
}
}
} else {
handler.unregisterForm();
}
}
return false;
};
/**
* Add wizard buttons to the wizard.
*
* @private
* @param {jQueryObject} $wizard The wizard element.
* @param {Object} options The wizard options.
*/
$.pkp.controllers.wizard.WizardHandler.prototype.addWizardButtons_ =
function($wizard, options) {
// Add space before wizard buttons.
var $wizardButtons =
$('<div id="wizardButtons" class="modal_buttons"></div>'),
$cancelButton, $continueButton, $progressIndicator;
if (options.continueButtonText) {
// Add continue/finish button.
$continueButton = $(
'<button id="continueButton" class="pkp_button"></button>')
.text(options.continueButtonText);
$wizardButtons.append($continueButton);
$progressIndicator = $(
'<span class="pkp_spinner"></span>');
$wizardButtons.append($progressIndicator);
$continueButton.
// Attach the continue request handler.
bind('click',
this.callbackWrapper(this.continueRequest));
this.$continueButton_ = /** @type {jQueryObject} */ ($continueButton);
this.$progressIndicator_ = $progressIndicator;
// Remember the button labels.
this.continueButtonText_ = options.continueButtonText;
if (options.finishButtonText) {
this.finishButtonText_ = options.finishButtonText;
} else {
this.finishButtonText_ = options.continueButtonText;
}
}
if (options.cancelButtonText) {
// Add cancel button.
$cancelButton = $('<a id="cancelButton" class="cancel" href="#"></a>')
.text(options.cancelButtonText);
$wizardButtons.append($cancelButton);
// Attach the cancel request handler.
$cancelButton.bind('click',
this.callbackWrapper(this.cancelRequest));
}
// Insert wizard buttons.
$wizard.after($wizardButtons);
};
/**
* Disable the continue button
*/
$.pkp.controllers.wizard.WizardHandler.prototype.disableContinueButton =
function() {
this.getContinueButton().attr('disabled', 'disabled');
};
/**
* Enable the continue button
*/
$.pkp.controllers.wizard.WizardHandler.prototype.enableContinueButton =
function() {
this.getContinueButton().removeAttr('disabled');
};
}(jQuery));
@@ -0,0 +1,384 @@
/**
* @defgroup controllers_wizard_fileUpload
*/
/**
* @file js/controllers/wizard/fileUpload/FileUploadWizardHandler.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 FileUploadWizardHandler
* @ingroup controllers_wizard_fileUpload
*
* @brief File uploader wizard handler.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.wizard.fileUpload =
$.pkp.controllers.wizard.fileUpload || { };
/**
* @constructor
*
* @extends $.pkp.controllers.wizard.WizardHandler
*
* @param {jQueryObject} $wizard The wrapped HTML form element.
* @param {Object} options Wizard options.
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler =
function($wizard, options) {
this.parent($wizard, options);
// Save action urls.
this.csrfToken_ = options.csrfToken;
this.deleteUrl_ = options.deleteUrl;
this.metadataUrl_ = options.metadataUrl;
this.finishUrl_ = options.finishUrl;
this.cancelUrl_ = options.cancelUrl;
// Bind events of the nested upload forms.
this.bind('fileUploaded', this.handleFileUploaded);
this.bind('filesRemoved', this.handleRemovedFiles);
// Initially disable the continue button.
this.disableContinueButton();
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler,
$.pkp.controllers.wizard.WizardHandler);
//
// Private properties
//
/**
* The CSRF token to use with a cancel event.
* @private
* @type {string}
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.csrfToken_ = '';
/**
* The URL to be called when a delete event occurs.
* @private
* @type {string}
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.deleteUrl_ = '';
/**
* The URL from which to load the meta-data form.
* @private
* @type {string}
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.metadataUrl_ = '';
/**
* The URL from which to load the finish form.
* @private
* @type {string}
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.finishUrl_ = '';
/**
* The URL to be called when a cancel event occurs.
* @private
* @type {string}
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.cancelUrl_ = '';
/**
* Information about the uploaded file (once there is one).
* @private
* @type {{fileId: number}?}
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.uploadedFile_ = null;
/**
* Information about the file being revised.
* @private
* @type {{fileId: number, name: string, uploaderUserId: number}}
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.originalFile_ = null;
//
// Public methods
//
/**
* @inheritDoc
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.tabsBeforeActivate = function(tabsElement, event, ui) {
// The last two tabs require a file to be uploaded.
if (ui.newTab.index() > 0) {
if (!this.uploadedFile_) {
throw new Error('Uploaded file missing!');
}
// Set the correct URLs.
var $wizard = this.getHtmlElement(), newUrl = '';
switch (ui.newTab.index()) {
case 1:
newUrl = this.metadataUrl_;
break;
case 2:
newUrl = this.finishUrl_;
break;
default:
throw new Error('Unsupported tab index.');
}
newUrl = newUrl + '&submissionFileId=' + this.uploadedFile_.id;
ui.newTab.find('.ui-tabs-anchor').attr('href', newUrl);
}
return /** @type {boolean} */ (
this.parent('tabsBeforeActivate', tabsElement, event, ui));
};
/**
* Overridden version of WizardHandler's wizardAdvance handler.
* This version allows a user to return to all tabs but the very
* first one (the actual file upload).
*
* @param {HTMLElement} wizardElement The wizard's HTMLElement on
* which the event was triggered.
* @param {Event} event The triggered event.
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.wizardAdvance = function(wizardElement, event) {
// The wizard can only be advanced one step at a time.
// The step cannot be greater than the number of wizard
// tabs and not less than 1.
var currentStep = this.getCurrentStep(),
lastStep = this.getNumberOfSteps() - 1,
targetStep = currentStep + 1,
$wizard = this.getHtmlElement(),
$continueButton;
// Do not advance beyond the last step.
if (targetStep > lastStep) {
throw new Error('Trying to set an invalid wizard step!');
}
// Enable the target step.
$wizard.tabs('enable', targetStep);
// Advance to the target step.
$wizard.tabs('option', 'active', targetStep);
// Disable the previous step if it is the first one.
if (currentStep === 0) {
$wizard.tabs('disable', currentStep);
}
// If this is the last step then change the text on the
// continue button to finish.
if (targetStep === lastStep) {
$continueButton = this.getContinueButton();
$continueButton.text(
/** @type {string} */ (this.getFinishButtonText()));
this.enableContinueButton();
}
};
/**
* @inheritDoc
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.tabsLoad = function(tabsElement, event, ui) {
var $wizard = this.getHtmlElement(),
$newFileButton,
$progressIndicator = this.getProgressIndicator();
// In the last step: Bind click a event to the button that re-starts
// the upload process.
if (ui.tab.index() === 2) {
$newFileButton = $('#newFile', $wizard);
// In some cases only a single file can be uploaded and no new
// file button appears
if ($newFileButton.length) {
$newFileButton.bind('click', this.callbackWrapper(this.startWizard));
}
}
$progressIndicator.hide();
return /** @type {boolean} */ (
this.parent('tabsLoad', tabsElement, event, ui));
};
/**
* @inheritDoc
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.formValid = function(formElement, event) {
// Ignore form validation events for the upload form.
if (this.getCurrentStep() === 0 &&
this.getHtmlElement().find('#uploadConfirmationForm').length === 0 &&
!this.uploadedFile_) {
return;
}
this.parent('formValid', formElement, event);
};
/**
* @inheritDoc
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.wizardCancelRequested = function(wizardElement, event) {
if (this.parent('wizardCancelRequested', wizardElement, event)) {
// If the user presses cancel after uploading a file then delete the file.
if (this.uploadedFile_) {
this.uploadedFile_.csrfToken = this.csrfToken_;
// Authorization policy expects to find the submissionFileId para
this.uploadedFile_.submissionFileId = this.uploadedFile_.id;
this.uploadedFile_.originalFile = this.originalFile_;
$.post(this.cancelUrl_, this.uploadedFile_,
$.pkp.classes.Helper.curry(this.wizardCancelSuccess, this,
wizardElement, event), 'json');
// The uploaded file is being dealt with; reset.
this.uploadedFile_ = null;
// Do not cancel immediately.
event.preventDefault();
} else {
return true;
}
return false;
} else {
// Stop the cancel request.
return false;
}
};
/**
* Callback triggered when the deletion of a file after clicking
* the cancel button was successful.
*
* @param {HTMLElement} wizardElement The wizard's HTMLElement on
* which the event was triggered.
* @param {Event} event The original event.
* @param {Object} jsonData The JSON data returned by the server on
* file deletion.
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.wizardCancelSuccess = function(wizardElement, event, jsonData) {
var processedJsonData = this.handleJson(jsonData);
if (processedJsonData !== false) {
// Cancel the wizard.
this.trigger('wizardCancel');
}
};
/**
* Handle the "file uploaded" event triggered by the
* file upload/revision confirmation forms whenever the
* uploaded file changed.
*
* @param {$.pkp.controllers.form.AjaxFormHandler} callingForm The form
* that triggered the event.
* @param {Event} event The upload event.
* @param {{fileId: number}} uploadedFile Information about the uploaded
* file.
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.handleFileUploaded = function(callingForm, event, uploadedFile) {
// Keep the original file data to restore if the wizard is canceled
if (this.originalFile_ === null) {
this.originalFile_ = uploadedFile.originalFile;
}
delete uploadedFile.originalFile;
// Save the uploaded file information
this.uploadedFile_ = uploadedFile;
};
/**
* Handle the filesRemoved event triggered by the associated form. The
* original event is triggered by plupload and passed via
* FileUploadFormHandler.
*
* See the TODO note under FileUPloadFormHandler::handleFilesRemoved
*
* @param {$.pkp.controllers.form.AjaxFormHandler} callingForm The form
* that triggered the event.
* @param {Event} event The upload event.
* @param {Object} pluploader plupload component that fired the original
* event.
* @param {Array} file Array of files removed
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.handleRemovedFiles =
function(callingForm, event, pluploader, file) {
var i;
if (typeof file === 'undefined' || !file.length) {
return;
}
// There's no error handling done for the response because we don't
// really have an elegant way to handle or display a failed deletion
for (i in file) {
if (typeof file[i].storedData === 'undefined') {
return;
}
file[i].storedData.csrfToken = this.csrfToken_;
$.post(this.deleteUrl_, file[i].storedData);
}
};
//
// Protected methods
//
/**
* @inheritDoc
*/
$.pkp.controllers.wizard.fileUpload.FileUploadWizardHandler.
prototype.startWizard = function() {
// Reset the uploaded and original file.
this.uploadedFile_ = this.originalFile_ = null;
this.parent('startWizard');
};
}(jQuery));
@@ -0,0 +1,354 @@
/**
* @defgroup js_controllers_wizard_fileUpload_form
*/
/**
* @file js/controllers/wizard/fileUpload/form/FileUploadFormHandler.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 FileUploadFormHandler
* @ingroup js_controllers_wizard_fileUpload_form
*
* @brief File upload tab handler.
*/
(function($) {
/** @type {Object} */
$.pkp.controllers.wizard.fileUpload.form =
$.pkp.controllers.wizard.fileUpload.form || { };
/**
* @constructor
*
* @extends $.pkp.controllers.form.AjaxFormHandler
*
* @param {jQueryObject} $form The wrapped HTML form element.
* @param {Object} options Form validation options.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler =
function($form, options) {
this.parent($form, options);
// Set internal state properties.
this.hasFileSelector_ = options.hasFileSelector;
this.hasGenreSelector_ = options.hasGenreSelector;
if (options.presetRevisedFileId) {
this.presetRevisedFileId_ = options.presetRevisedFileId;
}
this.fileGenres_ = options.fileGenres;
this.$uploader_ = options.$uploader;
// Attach the uploader handler to the uploader HTML element.
this.attachUploader_(this.$uploader_, options.uploaderOptions);
this.uploaderSetup(options.$uploader);
// Enable/disable the uploader and genre selection based on selection
this.$revisedFileSelector_ = $form.find('#revisedFileId')
.change(this.callbackWrapper(this.revisedFileChange));
if (this.hasGenreSelector_) {
this.$genreSelector = $form.find('#genreId')
.change(this.callbackWrapper(this.genreChange));
}
this.setUploaderVisibility_();
};
$.pkp.classes.Helper.inherits(
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler,
$.pkp.controllers.form.AjaxFormHandler);
//
// Private properties
//
/**
* Whether the file upload form has a file selector.
* @private
* @type {boolean}
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler
.hasFileSelector_ = false;
/**
* The file upload form's file selector if available.
* @private
* @type {boolean?}
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler
.$revisedFileSelector_ = null;
/**
* Whether the file upload form has a genre selector.
* @private
* @type {boolean}
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler
.hasGenreSelector_ = false;
/**
* The file upload form's genre selector if available.
* @private
* @type {boolean?}
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler
.$genreSelector_ = null;
/**
* A preset revised file id (if any).
* @private
* @type {?string}
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler
.presetRevisedFileId_ = null;
/**
* All currently available file genres.
* @private
* @type {Object}
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler
.fileGenres_ = null;
/**
* A jQuery object referencing the DOM element plupload is attached to
* @private
* @type {Object}
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler
.$uploader_ = null;
//
// Public methods
//
/**
* The setup callback of the uploader.
* @param {jQueryObject} $uploader Element that contains the plupload object.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
uploaderSetup = function($uploader) {
var uploadHandler = $.pkp.classes.Handler.getHandler($uploader);
// Subscribe to uploader events.
uploadHandler.pluploader.bind('BeforeUpload',
this.callbackWrapper(this.prepareFileUploadRequest));
uploadHandler.pluploader.bind('FileUploaded',
this.callbackWrapper(this.handleUploadResponse));
uploadHandler.pluploader.bind('FilesRemoved',
this.callbackWrapper(this.handleRemovedFiles));
};
/**
* Prepare the request parameters for the file upload request.
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
prepareFileUploadRequest = function(caller, pluploader) {
var $uploadForm = this.getHtmlElement(),
multipartParams = {};
// Add the revised file to the upload message.
if (this.hasFileSelector_) {
this.$revisedFileSelector_.attr('disabled', 'disabled');
multipartParams.revisedFileId = this.$revisedFileSelector_.val();
} else {
if (this.presetRevisedFileId_ !== null) {
multipartParams.revisedFileId = this.presetRevisedFileId_;
} else {
multipartParams.revisedFileId = 0;
}
}
// Add the file genre to the upload message.
if (this.hasGenreSelector_) {
this.$genreSelector.attr('disabled', 'disabled');
multipartParams.genreId = this.$genreSelector.val();
} else {
multipartParams.genreId = '';
}
// Add the upload message parameters to the uploader.
pluploader.settings.multipart_params = multipartParams;
};
/**
* Handle the response of a "file upload" request.
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
* @param {Object} file The data of the uploaded file.
* @param {{response: string}} ret The serialized JSON response.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
handleUploadResponse = function(caller, pluploader, file, ret) {
// Handle the server's JSON response.
var jsonData = this.handleJson($.parseJSON(ret.response)),
$uploadForm = this.getHtmlElement();
if (jsonData !== false) {
// Trigger the file uploaded event.
this.trigger('fileUploaded', jsonData.uploadedFile);
// Display the revision confirmation form.
if (jsonData.content !== '') {
this.replaceWith(jsonData.content);
}
}
// Trigger validation on the form. This doesn't happen automatically
// until `blur` is triggered on the file input field, requiring the
// user to click before any disabled form functions become available.
this.getHtmlElement().valid();
};
/**
* Pass the `FilesRemoved` event from plupload on to FileUploadWizardHandler
* so it can delete the file.
*
* TODO this is necessary because only the FileUploadWizardHandler knows
* the delete URL. But other file upload utilities could benefit from this
* feature, so it would be best to internalize this functionality in the
* UploadHandler by passing in a deleteURL option. This is a task that
* should be handled when the file upload process is rewritten to support
* a multi-file upload workflow.
* @param {Object} caller The original context in which the callback was called.
* @param {Object} pluploader The pluploader object.
* @param {Object} file The data of the uploaded file.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
handleRemovedFiles = function(caller, pluploader, file) {
this.trigger('filesRemoved', [pluploader, file]);
};
/**
* Internal callback to handle form submission.
*
* @param {Object} validator The validator plug-in.
* @param {HTMLElement} formElement The wrapped HTML form.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
submitForm = function(validator, formElement) {
// There is no form to submit (file already uploaded).
// Trigger event to signal that user requests the form to be submitted.
this.trigger('formSubmitted');
};
/**
* Handle the "change" event of the revised file selector.
* @param {HTMLElement} revisedFileElement The original context in
* which the event was triggered.
* @param {Event} event The change event.
* @return {boolean} Event handling status.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
revisedFileChange = function(revisedFileElement, event) {
// Enable/disable the genre field when a revision is selected
if (!this.$revisedFileSelector_.val()) {
this.$genreSelector.removeAttr('disabled');
} else {
this.$genreSelector.val(this.fileGenres_[this.$revisedFileSelector_.val()]);
this.$genreSelector.attr('disabled', 'disabled');
}
this.setUploaderVisibility_();
return false;
};
/**
* Handle the "change" event of the genre selector, if it exists.
* @param {HTMLElement} genreElement The original context in
* which the event was triggered.
* @param {Event} event The change event.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
genreChange = function(genreElement, event) {
this.setUploaderVisibility_();
};
//
// Private methods
//
/**
* Attach the uploader handler.
* @private
* @param {jQueryObject} $uploader The wrapped HTML uploader element.
* @param {Object} options Uploader options.
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
attachUploader_ = function($uploader, options) {
// Attach the uploader handler to the uploader div.
$uploader.pkpHandler('$.pkp.controllers.UploaderHandler', options);
};
/**
* Adjust the display of the plupload component depending on required
* settings
* @private
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
setUploaderVisibility_ = function() {
if ((this.hasGenreSelector_ && this.$genreSelector.val()) ||
this.$revisedFileSelector_.val()) {
this.showUploader_();
} else if (!this.hasGenreSelector_ && !this.hasFileSelector_) {
this.showUploader_();
} else {
this.hideUploader_();
}
};
/**
* Hide the plupload component
* @private
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
hideUploader_ = function() {
this.$uploader_.addClass('pkp_screen_reader');
};
/**
* Show the the plupload component
* @private
*/
$.pkp.controllers.wizard.fileUpload.form.FileUploadFormHandler.prototype.
showUploader_ = function() {
this.$uploader_.removeClass('pkp_screen_reader');
// Reset the button position
$.pkp.classes.Handler.getHandler(this.$uploader_)
.pluploader.refresh();
};
}(jQuery));