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
+880
View File
@@ -0,0 +1,880 @@
/**
* @file js/classes/Handler.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 Handler
* @ingroup js_classes
*
* @brief Base class for handlers bound to a DOM HTML element.
*/
/*global _, pkp */
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.ObjectProxy
*
* @param {jQueryObject} $element A DOM element to which
* this handler is bound.
* @param {Object} options Handler options.
*/
$.pkp.classes.Handler = function($element, options) {
var $parents, self, i;
// Check whether a single element was passed in.
if ($element.length > 1) {
throw new Error('jQuery selector contained more than one handler!');
}
// Save a pointer to the bound element in the handler.
this.$htmlElement_ = $element;
// Check whether a handler has already been bound
// to the element.
if (this.data('handler') !== undefined) {
throw new Error(['The handler "', this.getObjectName(),
'" has already been bound to the selected element!'].join(''));
}
// Initialize object properties.
this.eventBindings_ = { };
this.dataItems_ = { };
this.publishedEvents_ = { };
this.handlerChildren_ = [];
this.globalEventListeners_ = { };
// Register this handler with a parent handler if one is found. This
// allows global events to be de-registered when a parent handler is
// refreshed.
$parents = this.$htmlElement_.parents();
self = this;
$parents.each(function(i) {
if ($.pkp.classes.Handler.hasHandler($($parents[i]))) {
$.pkp.classes.Handler.getHandler($($parents[i]))
.handlerChildren_.push(self);
return; // only attach to the closest parent handler
}
});
if (options.eventBridge) {
// Configure the event bridge.
this.eventBridge_ = options.eventBridge;
}
// The "publishChangeEvents" option can be used to specify
// a list of event names that will also be published upon
// content change.
if (options.publishChangeEvents) {
this.publishChangeEvents_ = options.publishChangeEvents;
for (i = 0; i < this.publishChangeEvents_.length; i++) {
this.publishEvent(this.publishChangeEvents_[i]);
}
} else {
this.publishChangeEvents_ = [];
}
// Bind the handler to the DOM element.
this.data('handler', this);
};
//
// Private properties
//
/**
* Optional list of publication events.
* @private
* @type {Array}
*/
$.pkp.classes.Handler.prototype.publishChangeEvents_ = null;
/**
* The HTML element this handler is bound to.
* @private
* @type {jQueryObject}
*/
$.pkp.classes.Handler.prototype.$htmlElement_ = null;
/**
* A list of event bindings for this handler.
* @private
* @type {Object.<string, Array>}
*/
$.pkp.classes.Handler.prototype.eventBindings_ = null;
/**
* A list of data items bound to the DOM element
* managed by this handler.
* @private
* @type {Object.<string, boolean>}
*/
$.pkp.classes.Handler.prototype.dataItems_ = null;
/**
* A list of published events.
* @private
* @type {Object.<string, boolean>}
*/
$.pkp.classes.Handler.prototype.publishedEvents_ = null;
/**
* An HTML element id to which we'll forward all handler events.
* @private
* @type {?string}
*/
$.pkp.classes.Handler.prototype.eventBridge_ = null;
/**
* Global event bindings. These are tracked so they can be deregistered when
* the handler is destroyed.
* @private
* @type {Object}
*/
$.pkp.classes.Handler.prototype.globalEventListeners_ = null;
//
// Public static methods
//
/**
* Retrieve the bound handler from the jQuery element.
* @param {jQueryObject} $element The element to which the
* handler was attached.
* @return {Object} The retrieved handler.
*/
$.pkp.classes.Handler.getHandler = function($element) {
// Retrieve the handler. We cannot do this with our own
// data() method because this method cannot be called
// in the context of the handler if it's purpose is to
// retrieve the handler. This should be the only place
// at all where we have to do access element data
// directly.
var handler = $element.data('pkp.handler');
// Check whether the handler exists.
if (!(handler instanceof $.pkp.classes.Handler)) {
throw new Error('There is no handler bound to this element!');
}
return handler;
};
/**
* Check if a jQuery element has a handler bound to it
*
* @param {jQueryObject} $element The element to check for a handler
* @return {boolean}
*/
$.pkp.classes.Handler.hasHandler = function($element) {
return $element.data('pkp.handler') instanceof $.pkp.classes.Handler;
};
//
// Public methods
//
/**
* Returns the HTML element this handler is bound to.
*
* @return {jQueryObject} The element this handler is bound to.
*/
$.pkp.classes.Handler.prototype.getHtmlElement = function() {
$.pkp.classes.Handler.checkContext_(this);
// Return the HTML element.
return this.$htmlElement_;
};
/**
* Publish change events. (See options.publishChangeEvents.)
*/
$.pkp.classes.Handler.prototype.publishChangeEvents = function() {
var i;
for (i = 0; i < this.publishChangeEvents_.length; i++) {
this.trigger(this.publishChangeEvents_[i]);
}
};
/**
* A generic event dispatcher that will be bound to
* all handler events. See bind() above.
*
* @this {HTMLElement}
* @param {jQuery.Event} event The jQuery event object.
* @return {boolean} Return value to be passed back
* to jQuery.
*/
$.pkp.classes.Handler.prototype.handleEvent = function(event) {
var $callingElement, handler, boundEvents, args, returnValue, i, l;
// This handler is always called out of the
// handler context.
$callingElement = $(this);
// Identify the targeted handler.
handler = $.pkp.classes.Handler.getHandler($callingElement);
// Make sure that we really got the right element.
if ($callingElement[0] !== handler.getHtmlElement.call(handler)[0]) {
throw new Error(['An invalid handler is bound to the calling ',
'element of an event!'].join(''));
}
// Retrieve the event handlers for the given event type.
boundEvents = handler.eventBindings_[event.type];
if (boundEvents === undefined) {
// We have no handler for this event but we also
// don't allow bubbling of events outside of the
// GUI widget!
return false;
}
// Call all event handlers.
args = $.makeArray(arguments);
returnValue = true;
args.unshift(this);
for (i = 0, l = boundEvents.length; i < l; i++) {
// Invoke the event handler in the context
// of the handler object.
if (boundEvents[i].apply(handler, args) === false) {
// False overrides true.
returnValue = false;
}
// Stop immediately if one of the handlers requests this.
if (event.isImmediatePropagationStopped()) {
break;
}
}
// We do not allow bubbling of events outside of the GUI widget!
event.stopPropagation();
// Return the event handler status.
return returnValue;
};
/**
* Create a closure that calls the callback in the
* context of the handler object.
*
* NB: Always make sure that the callback is properly
* unbound and freed for garbage collection. Otherwise
* you might create a memory leak. If you want to bind
* an event to the HTMLElement handled by this handler
* then always use the above bind() method instead which
* is safer.
*
* @param {Function} callback The callback to be wrapped.
* @param {Object=} opt_context Specifies the object which
* |this| should point to when the function is run.
* If the value is not given, the context will default
* to the handler object.
* @return {Function} The wrapped callback.
*/
$.pkp.classes.Handler.prototype.callbackWrapper =
function(callback, opt_context) {
$.pkp.classes.Handler.checkContext_(this);
// Create a closure that calls the event handler
// in the right context.
if (!opt_context) {
opt_context = this;
}
return function() {
var args;
args = $.makeArray(arguments);
args.unshift(this);
return callback.apply(opt_context, args);
};
};
/**
* This callback can be used to handle simple remote server requests.
*
* @param {Object} ajaxOptions AJAX options.
* @param {Object} jsonData A JSON object.
* @return {Object|boolean} The parsed JSON data if no error occurred,
* otherwise false.
*/
$.pkp.classes.Handler.prototype.remoteResponse =
function(ajaxOptions, jsonData) {
return this.handleJson(jsonData);
};
/**
* Completely remove all traces of the handler from the
* HTML element to which it is bound and leave the element in
* its previous state.
*
* Subclasses should override this method if necessary but
* should always call this implementation.
*/
$.pkp.classes.Handler.prototype.remove = function() {
$.pkp.classes.Handler.checkContext_(this);
var $element, key;
// Remove all event handlers in our namespace.
$element = this.getHtmlElement();
$element.unbind('.pkpHandler');
// Remove all our data items except for the
// handler itself.
for (key in this.dataItems_) {
if (key !== 'pkp.handler') {
$element.removeData(key);
}
}
// Trigger the remove event, then delete it.
$element.trigger('pkpRemoveHandler');
$element.unbind('.pkpHandlerRemove');
// Delete the handler.
$element.removeData('pkp.handler');
};
/**
* This function should be used to pre-process a JSON response
* from the server.
*
* @param {Object} jsonData The returned server response data.
* @return {Object|boolean} The returned server response data or
* false if an error occurred.
*/
$.pkp.classes.Handler.prototype.handleJson = function(jsonData) {
var key, eventData;
if (!jsonData) {
throw new Error('Server error: Server returned no or invalid data!');
}
if (jsonData.status === true) {
// Trigger events passed from the server
for (key in jsonData.events) {
eventData = jsonData.events[key].hasOwnProperty('data') ?
jsonData.events[key].data : null;
if (eventData !== null && eventData.isGlobalEvent) {
eventData.handler = this;
pkp.eventBus.$emit(jsonData.events[key].name, eventData);
} else {
this.trigger(jsonData.events[key].name, eventData);
}
}
return jsonData;
} else {
// If we got an error message then display it.
if (jsonData.content) {
alert(jsonData.content);
}
return false;
}
};
//
// Protected methods
//
/**
* Sets the HTML element this handler is bound to.
*
* @protected
* @param {jQueryObject} $htmlElement The element this handler should be bound
* to.
* @return {jQueryObject} Passes through the supplied parameter.
*/
$.pkp.classes.Handler.prototype.setHtmlElement = function($htmlElement) {
$.pkp.classes.Handler.checkContext_(this);
// Return the HTML element.
this.$htmlElement_ = $htmlElement;
return $htmlElement;
};
/**
* Bind an event to a handler operation.
*
* This will be done with a generic event handler
* to make sure that we get a chance to re-set
* 'this' to the handler before we call the actual
* handler method.
*
* @protected
* @param {string} eventName The name of the event
* to be bound. See jQuery.bind() for event names.
* @param {Function} handler The event handler to
* be called when the even is triggered.
*/
$.pkp.classes.Handler.prototype.bind = function(eventName, handler) {
$.pkp.classes.Handler.checkContext_(this);
if (!this.eventBindings_[eventName]) {
// Initialize the event store for this event.
this.eventBindings_[eventName] = [];
// Determine the event namespace.
var eventNamespace;
eventNamespace = '.pkpHandler';
if (eventName === 'pkpRemoveHandler') {
// We have a special namespace for the remove event
// because it needs to be triggered when all other
// events have already been removed.
eventNamespace = '.pkpHandlerRemove';
}
// Bind the generic event handler to the event within our namespace.
this.getHtmlElement().bind(eventName + eventNamespace, this.handleEvent);
}
// Store the event binding internally
this.eventBindings_[eventName].push(handler);
};
/**
* Unbind an event from a handler operation.
*
* @protected
* @param {string} eventName The name of the event
* to be bound. See jQuery.bind() for event names.
* @param {Function} handler The event handler to
* be called when the even is triggered.
* @return {boolean} True, if a handler was found and
* removed, otherwise false.
*/
$.pkp.classes.Handler.prototype.unbind = function(eventName, handler) {
$.pkp.classes.Handler.checkContext_(this);
// Remove the event from the internal event cache.
if (!this.eventBindings_[eventName]) {
return false;
}
var i, length;
for (i = 0, length = this.eventBindings_[eventName].length; i < length; i++) {
if (this.eventBindings_[eventName][i] === handler) {
this.eventBindings_[eventName].splice([i], 1);
break;
}
}
if (this.eventBindings_[eventName].length === 0) {
// If this was the last event then unbind the generic event handler.
delete this.eventBindings_[eventName];
this.getHtmlElement().unbind(eventName, this.handleEvent);
}
return true;
};
/**
* Bind a global event to a handler operation.
*
* Binds a callback function to fire when a global event is triggered on
* the global event router.
*
* @param {string} eventName The name of the event to bind to.
* @param {Function} callback The function to firewhen the event is triggered
*/
$.pkp.classes.Handler.prototype.bindGlobal = function(eventName, callback) {
if (typeof this.globalEventListeners_[eventName] === 'undefined') {
this.globalEventListeners_[eventName] = [];
}
var wrapper = this.callbackWrapper(callback);
this.globalEventListeners_[eventName].push(wrapper);
pkp.eventBus.$on(eventName, wrapper);
};
/**
* Unbind a global event from a handler operation.
*
* If passing a `null` callback, all callbacks bound to eventName by this
* handler will be unbound. See: http://backbonejs.org/#Events-off
*
* @see $.pkp.classes.Handler.prototype.bindGlobal()
* @param {string} eventName The name of the event to bind to
* @param {Function} callback The function to fire when event is triggered
*/
$.pkp.classes.Handler.prototype.unbindGlobal = function(eventName, callback) {
var wrapper = this.callbackWrapper(callback),
globalEventListeners = [];
if (typeof this.globalEventListeners_[eventName] !== 'undefined') {
this.globalEventListeners.forEach(function(callback) {
if (callback !== wrapper) {
globalEventListeners.push(callback);
}
});
this.globalEventListeners = globalEventListeners;
}
pkp.eventBus.$off(eventName, wrapper);
};
/**
* Unbind all global event listeners on this handler and any child handlers
*/
$.pkp.classes.Handler.prototype.unbindGlobalAll = function() {
var event, callback;
if (typeof this.globalEventListeners_ !== 'undefined') {
for (event in this.globalEventListeners_) {
for (callback in this.globalEventListeners_[event]) {
pkp.eventBus.$off(event, this.globalEventListeners_[event][callback]);
}
}
}
this.globalEventListeners = null;
this.unbindGlobalChildren();
};
/**
* Unbind all global event listeners on child handlers
*/
$.pkp.classes.Handler.prototype.unbindGlobalChildren = function() {
this.handlerChildren_.forEach(function(childHandler) {
// Handler in legacy JS framework
if (typeof childHandler.unbindGlobalAll !== 'undefined') {
childHandler.unbindGlobalAll();
// Handler in new Vue.js framework
} else if (typeof childHandler.$destroy !== 'undefined') {
delete pkp.registry._instances[childHandler.id];
childHandler.$destroy();
}
});
};
/**
* Add or retrieve a data item to/from the DOM element
* this handler is managing.
*
* Always use this method if you want to store data
* items. It makes sure that your items will be properly
* namespaced and it also guarantees correct garbage
* collection of your items once the handler is removed.
*
* @protected
* @param {string} key The name of the item to be stored
* or retrieved.
* @param {Object=} opt_value The data item to be stored. If no item
* is given then the existing value for the given key
* will be returned.
* @return {Object} The cached data item.
*/
$.pkp.classes.Handler.prototype.data = function(key, opt_value) {
$.pkp.classes.Handler.checkContext_(this);
// Namespace the key.
key = 'pkp.' + key;
if (opt_value !== undefined) {
// Add the key to the list of data items
// that need to be garbage collected.
this.dataItems_[key] = true;
}
// Add/retrieve the data to/from the
// element's data cache.
if (arguments.length > 1) {
return this.getHtmlElement().data(key, opt_value);
} else {
return this.getHtmlElement().data(key);
}
};
/**
* This function should be used to let the element emit events
* that bubble outside the widget and are published over the
* event bridge.
*
* @protected
* @param {string} eventName The event to be triggered.
* @param {Array=} opt_data Additional event data.
*/
$.pkp.classes.Handler.prototype.trigger =
function(eventName, opt_data) {
if (opt_data === undefined) {
opt_data = null;
}
// Trigger the event on the handled element.
var $handledElement = this.getHtmlElement();
$handledElement.triggerHandler(eventName, opt_data);
// Trigger the event publicly if it's not
// published anyway.
if (!this.publishedEvents_[eventName]) {
this.triggerPublicEvent_(eventName, opt_data);
}
};
/**
* Publish an event triggered by a nested widget. This event
* will bubble outside the widget and will also be published
* over the event bridge.
*
* @param {string} eventName The event name.
*/
$.pkp.classes.Handler.prototype.publishEvent = function(eventName) {
// If the event has been published before then do nothing.
if (this.publishedEvents_[eventName]) {
return;
}
// Add the event to the published event list.
this.publishedEvents_[eventName] = true;
this.bind(eventName, function(context, privateEvent, var_args) {
// Retrieve additional event data.
var eventData = null;
if (arguments.length > 2) {
eventData = Array.prototype.slice.call(arguments, 2);
}
// Re-trigger the private event publicly.
this.triggerPublicEvent_(eventName, eventData);
});
};
/**
* Handle the "show more" and "show less" clicks triggered by the
* links in longer text items.
*
* @param {Event} event The event.
*/
$.pkp.classes.Handler.prototype.switchViz = function(event) {
var eventElement = event.currentTarget;
$(eventElement).parent().parent().find('span').toggle();
};
/**
* Initialize TinyMCE instances.
*
* There are instances where TinyMCE is not initialized with the call to
* init(). These occur when content is loaded after the fact (via AJAX).
*
* In these cases, search for richContent fields and initialize them.
*/
$.pkp.classes.Handler.prototype.initializeTinyMCE =
function() {
if (typeof tinyMCE !== 'undefined') {
var $element = this.getHtmlElement(),
elementId = $element.attr('id'),
settings = tinyMCE.EditorManager.settings;
settings.defaultToolbar = settings.toolbar;
$('#' + elementId).find('.richContent').each(function() {
var id = /** @type {string} */ ($(this).attr('id')),
icon = $('<div></div>'),
iconParent = $('<div></div>'),
classes, i, editor,
settings = tinyMCE.EditorManager.settings;
// Set the extended toolbar, if requested
if ($(this).hasClass('extendedRichContent')) {
settings.toolbar = settings.richToolbar;
} else {
settings.toolbar = settings.defaultToolbar;
}
editor = tinyMCE.EditorManager.createEditor(id, settings).render();
// For localizable text fields add globe and flag icons
if ($(this).hasClass('localizable') || $(this).hasClass('flag')) {
icon.addClass('mceLocalizationIcon localizable');
icon.attr('id', 'mceLocalizationIcon-' + id);
$(this).wrap(iconParent);
$(this).parent().append(icon);
if ($(this).hasClass('localizable')) {
// Add a globe icon to localizable TinyMCE textareas
icon.addClass('mceGlobe');
} else if ($(this).hasClass('flag')) {
// Add country flag icon to localizable TinyMCE textareas
classes = $(this).attr('class').split(' ');
if (classes.length) {
for (i = 0; i < classes.length; i++) {
if (classes[i].match(/^flag_[a-z]{2}_[A-Z]{2}$/)) {
icon.addClass(classes[i]);
break;
}
}
}
}
}
});
}
};
//
// Private methods
//
/**
* Trigger a public event.
*
* Public events will bubble outside the widget and will
* also be forwarded through the event bridge if one has
* been configured.
*
* @private
* @param {string} eventName The event to be triggered.
* @param {Array=} opt_data Additional event data.
*/
$.pkp.classes.Handler.prototype.triggerPublicEvent_ =
function(eventName, opt_data) {
// Publish the event.
var $handledElement = this.getHtmlElement();
$handledElement.parent().trigger(eventName, opt_data);
// If we have an event bridge configured then re-trigger
// the event on the target object.
if (this.eventBridge_) {
$('[id^="' + this.eventBridge_ + '"]').trigger(eventName, opt_data);
}
};
/**
* Wrapper for the jQuery .replaceWith() function.
*
* This unbinds all global events before replacing the HTML content, to
* ensure there are no orphaned event listeners lingering from handlers
* which may have been destroyed when the HTML was replaced.
*
* This function can only be used when the entire handler is replaced. For
* replacing parts of a handler, see replacePartialWith().
*
* @param {string|jQueryObject} html The HTML content to replace the
* current element with
*/
$.pkp.classes.Handler.prototype.replaceWith = function(html) {
this.unbindGlobalAll();
this.getHtmlElement().replaceWith(html);
};
/**
* Wrapper for the jQuery .replaceWith() function.
*
* This function works like the .replaceWith() wrapper above, except it
* allows you to pass a specific dom element to replace within the Handler.
*
* This function loops over any handlers found within the $partial dom
* element, unbinding global events to ensure there are no orphaned event
* listeners when the HTML element is replaced.
*
* The .replaceWith() function is preferred in most cases. This should only
* been used when you _need_ to replace part of a Handler's HTML content.
* Full handler refreshes are preferred to keep things simple. Also, this
* function isn't very performant, because it requires looping over every
* child DOM element.
*
* @param {string|jQueryObject} html The HTML content to inject into
* the $partial
* @param {jQueryObject} $partial The HTML element to unbind
*/
$.pkp.classes.Handler.prototype.replacePartialWith =
function(html, $partial) {
// Check if the $partial already has a handler bound to it on which
// we can call .unbindGlobalAll() instead
if ($.pkp.classes.Handler.hasHandler($partial)) {
$.pkp.classes.Handler.getHandler($partial).replaceWith(html);
return;
}
this.unbindPartial($partial);
$partial.replaceWith(html);
};
/**
* Wrapper for the jQuery .html() function.
*
* This unbinds all global events before replacing the inner HTML content.
* It differs from the .replaceWith() wrapper function in that the handler's
* element is not removed. This means the handler isn't re-initialized, and
* so only child handler events need to be unbound.
*
* @param {string} html The HTML content to inject into the $partial
*/
$.pkp.classes.Handler.prototype.html = function(html) {
this.unbindGlobalChildren();
this.getHtmlElement().html(html);
};
/**
* This function loops over any handlers found within the $partial dom
* element, unbinding global events to ensure there are no orphaned event
* listeners when the HTML element is replaced.
*
* This function isn't very performant. It requires looping over every
* element in scope, which could potentially be hundreds or thousands.
* This should only be used as a last resort for some handlers which need
* to empty out partial content, such as tabs and grids.
*
* @param {jQueryObject} $partial The HTML element to unbind
*/
$.pkp.classes.Handler.prototype.unbindPartial =
function($partial) {
$('*', $partial).each(function() {
if ($.pkp.classes.Handler.hasHandler($(this))) {
var handler = $.pkp.classes.Handler.getHandler($(this));
handler.callbackWrapper(handler.unbindGlobalAll());
}
});
};
//
// Private static methods
//
/**
* Check the context of a method invocation.
*
* @private
* @param {Object} context The function context
* to be tested.
*/
$.pkp.classes.Handler.checkContext_ = function(context) {
if (!(context instanceof $.pkp.classes.Handler)) {
throw new Error('Trying to call handler method in non-handler context!');
}
};
}(jQuery));
+367
View File
@@ -0,0 +1,367 @@
/**
* @defgroup js_classes
*/
/**
* @file js/classes/Helper.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 Helper
* @ingroup js_controllers
*
* @brief PKP helper methods
*/
(function($) {
// Create PKP namespaces.
/** @type {Object} */
$.pkp = $.pkp || { };
/** @type {Object} */
$.pkp.classes = $.pkp.classes || { };
/** @type {Object} */
$.pkp.controllers = $.pkp.controllers || { };
/** @type {Object} */
$.pkp.plugins = $.pkp.plugins || {};
/** @type {Object} */
$.pkp.plugins.blocks = $.pkp.plugins.blocks || {};
/** @type {Object} */
$.pkp.plugins.generic = $.pkp.plugins.generic || {};
/** @type {Object} */
$.pkp.plugins.pubIds = $.pkp.plugins.pubIds || {};
/** @type {Object} */
$.pkp.plugins.importexport = $.pkp.plugins.importexport || {};
/**
* Helper singleton
* @constructor
*
* @extends $.pkp.classes.ObjectProxy
*/
$.pkp.classes.Helper = function() {
throw new Error('Trying to instantiate the Helper singleton!');
};
//
// Private class constants
//
/**
* Characters available for UUID generation.
* @const
* @private
* @type {Array}
*/
$.pkp.classes.Helper.CHARS_ = ['0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'abcdefghijklmnopqrstuvwxyz'].join('').split('');
//
// Public static helper methods
//
/**
* Generate a random UUID.
*
* Original code thanks to Robert Kieffer <robert@broofa.com>,
* http://www.broofa.com, adapted by PKP.
*
* Copyright (c) 2010 Robert Kieffer
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2010-2021 John Willinsky
* Distributed under the GNU GPL v3 and MIT licenses. For full
* terms see the file docs/COPYING.
*
* See discussion of randomness versus uniqueness:
* http://www.broofa.com/2008/09/javascript-uuid-function/
*
* @return {string} an RFC4122v4 compliant UUID.
*/
$.pkp.classes.Helper.uuid = function() {
var chars = $.pkp.classes.Helper.CHARS_, uuid = new Array(36), rnd = 0, r, i;
for (i = 0; i < 36; i++) {
if (i == 8 || i == 13 || i == 18 || i == 23) {
uuid[i] = '-';
} else if (i == 14) {
uuid[i] = '4';
} else {
/*jslint bitwise: true*/
if (rnd <= 0x02) {
rnd = 0x2000000 + (Math.random() * 0x1000000) | 0;
}
r = rnd & 0xf;
rnd = rnd >> 4;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
/*jslint bitwise: false*/
}
}
return uuid.join('');
};
/**
* Let one object inherit from another.
*
* Example:
* $.pkp.classes.Parent = function() {...};
* $.pkp.classes.Child = function() {...};
* $.pkp.classes.Helper.inherits($.pkp.classes.Child, $.pkp.classes.Parent);
*
* @param {Function} Child Constructor of the child object.
* @param {Function} Parent Constructor of the parent object.
*/
$.pkp.classes.Helper.inherits = function(Child, Parent) {
// Use an empty temporary object to avoid
// calling a potentially costly constructor
// on the parent object which also may have
// undesired side effects. Also avoids instantiating
// a potentially big object.
/** @constructor */ var Temp = function() {};
Temp.prototype = Parent.prototype;
// Provide a way to reach the parent's
// method implementations even after
// overriding them in the child object.
Child.parent_ = Parent.prototype;
// Let the child object inherit from
// the parent object.
Child.prototype = new Temp();
// Need to fix the child constructor because
// it get's lost when setting the prototype
// to an object instance.
Child.prototype.constructor = Child;
// Make sure that we can always call the parent object's
// constructor without coupling the child constructor
// to it. This should work even when the parent inherits
// directly from an Object instance (i.e. the parent's
// prototype was set like this: Parent.prototype = {...})
// which wipes out the original constructor.
if (Parent.prototype.constructor == Object.prototype.constructor) {
Parent.prototype.constructor = Parent;
}
};
/**
* Introduce a central object factory that maintains some
* level of indirection so that we can enrich objects, e.g.
* with aspects, provide different runtime-implementations
* of objects, distinguish between singletons and prototypes
* or even implement dependency injection if we want to.
*
* The standard implementation has a 'convention over
* configuration' approach that assumes that an object's
* name corresponds to the name of a constructor within
* the global jQuery namespace ($).
*
* The factory also helps us to avoid the common pitfall to
* use a constructor without the 'new' keyword.
*
* @param {string} objectName The name of an object.
* @param {Array} args The arguments to be passed
* into the object's constructor.
* @return {$.pkp.classes.ObjectProxy} the instantiated object.
*/
$.pkp.classes.Helper.objectFactory = function(objectName, args) {
var ObjectConstructor, ObjectProxyInstance, objectInstance;
// Resolve the object name.
ObjectConstructor = $.pkp.classes.Helper.resolveObjectName(objectName);
// Create a new proxy constructor instance.
ObjectProxyInstance = $.pkp.classes.Helper.getObjectProxyInstance();
// Copy static members over from the object proxy. (This may
// overwrite the proxy constructor's prototype in some
// browsers but we don't care because we'll replace the prototype
// anyway when we inherit.)
$.extend(true, ObjectProxyInstance, $.pkp.classes.ObjectProxy);
// Let the proxy inherit from the proxied object.
$.pkp.classes.Helper.inherits(ObjectProxyInstance, ObjectConstructor);
// Enrich the new proxy constructor prototype with proxy object
// prototype members.
$.extend(true, ObjectProxyInstance.prototype,
$.pkp.classes.ObjectProxy.prototype);
// Instantiate the proxy with the proxied object.
objectInstance = new ObjectProxyInstance(objectName, args);
return objectInstance;
};
/**
* Resolves the given object name to an object implementation
* (or better to it's constructor).
* @param {string} objectName The object name to resolve.
* @return {Function} The constructor of the object.
*/
$.pkp.classes.Helper.resolveObjectName = function(objectName) {
var objectNameParts, i, functionName, ObjectConstructor;
// Currently only objects in the $ namespace are
// supported.
objectNameParts = objectName.split('.');
if (objectNameParts.shift() != '$') {
throw new Error(['Namespace "', objectNameParts[0], '" for object "',
objectName, '" is currently not supported!'].join(''));
}
// Make sure that we actually have a constructor name
// (starts with an upper case letter).
functionName = objectNameParts[objectNameParts.length - 1];
if (functionName.charAt(0).toUpperCase() !== functionName.charAt(0)) {
throw new Error(['The name "', objectName, '" does not point to a',
'constructor which must always be upper case!'].join(''));
}
// Run through the namespace and identify the constructor.
ObjectConstructor = $;
for (i in objectNameParts) {
ObjectConstructor = ObjectConstructor[objectNameParts[i]];
if (ObjectConstructor === undefined) {
throw new Error(['Constructor for object "', objectName, '" not found!']
.join(''));
}
}
// Check that the constructor actually is a function.
if (!$.isFunction(ObjectConstructor)) {
throw new Error(['The name "', objectName, '" does not point to a',
'constructor which must always be a function!'].join());
}
return ObjectConstructor;
};
/**
* Create a new instance of a proxy constructor.
*
* NB: We do this in a separate closure to avoid
* memory leaks.
*
* @return {Function} a new proxy instance.
*/
$.pkp.classes.Helper.getObjectProxyInstance = function() {
// Create a new proxy constructor so that proxies
// do not interfere with each other.
/**
* @constructor
*
* @param {string} objectName The name of the proxied
* object.
* @param {Array} args The arguments to be passed to
* the constructor of the proxied object.
*/
var proxyConstructor = function(objectName, args) {
// Set the internal object name.
this.objectName_ = objectName;
// Call the constructor of the proxied object.
this.parent.apply(this, args);
};
// Declare properties/methods used in the constructor for the
// closure compiler. These will later be overwritten by the
// true implementation.
/**
* @private
* @type {string} The object name of this object.
*/
proxyConstructor.objectName_ = '';
/**
* @param {*=} opt_methodName The name of the method to
* be found. Do not set when calling this method from a
* constructor!
* @param {...*} var_args Arguments to be passed to the
* parent method.
* @return {*} The return value of the parent method.
*/
proxyConstructor.prototype.parent = function(opt_methodName, var_args) {};
return proxyConstructor;
};
/**
* Inject (mix in) an interface into an object.
* @param {Function} Constructor The target object's constructor.
* @param {string} mixinObjectName The object name of interface
* that can be resolved to an interface implementation by the
* object factory.
*/
$.pkp.classes.Helper.injectMixin = function(Constructor, mixinObjectName) {
// Retrieve an instance of the mix-in interface implementation.
var mixin = $.pkp.classes.Helper.objectFactory(mixinObjectName, []);
// Inject the mix-in into the target constructor.
$.extend(true, Constructor, mixin);
};
/**
* A function currying implementation borrowed from Google Closure.
* @param {Function} fn A function to partially apply.
* @param {Object} context Specifies the object which |this| should
* point to when the function is run. If the value is null or undefined, it
* will default to the global object.
* @param {...*} var_args Additional arguments that are partially
* applied to the function.
* @return {!Function} A partially-applied form of the function bind() was
* invoked as a method of.
*/
$.pkp.classes.Helper.curry = function(fn, context, var_args) {
if (arguments.length > 2) {
var boundArgs, newArgs;
boundArgs = Array.prototype.slice.call(arguments, 2);
return function() {
// Prepend the bound arguments to the current arguments.
newArgs = Array.prototype.slice.call(arguments);
Array.prototype.unshift.apply(newArgs, boundArgs);
return fn.apply(context, newArgs);
};
} else {
return function() {
return fn.apply(context, arguments);
};
}
};
/**
* A function that takes care of escaping @ character which could be interpreted
* as CSS notation. This is due to the fact that jQuery uses CSS syntax for
* selecting elements. These characters must be escaped by placing two
* backslashes in front of them.
* @param {string} elementId jQuery element selector
* @return {string}
*/
$.pkp.classes.Helper.escapeJQuerySelector = function(elementId) {
return elementId.replace('@', '\\@');
};
}(jQuery));
+154
View File
@@ -0,0 +1,154 @@
/**
* @file js/classes/ObjectProxy.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 ObjectProxy
* @ingroup js_classes
*
* @brief Proxy that will be added to every object before
* instantiation.
*
* This proxy allows us to use a generic object factory. It'll
* also be a good place to intercept objects and implement
* cross-cutting concerns if required.
*/
(function($) {
/**
* @constructor
* The constructor must remain empty because it will
* be replaced on instantiation of the proxy.
*/
$.pkp.classes.ObjectProxy = function() {};
//
// Private instance variables
//
/**
* @private
* @type {string} The object name of this object.
*/
$.pkp.classes.ObjectProxy.prototype.objectName_ = '';
//
// Protected methods
//
/**
* Find a static property in the constructor hierarchy.
*
* NB: If the property is a function then it will be executed
* in the current context with the additional arguments given.
* If it is any other type then the property will be returned.
*
* @param {string} propertyName The name of the static
* property to be found.
* @param {...*} var_args Arguments to be passed to the
* static method (if any).
* @return {*} The property or undefined if the property
* was not found.
*/
$.pkp.classes.ObjectProxy.prototype.self =
function(propertyName, var_args) {
var ctor, foundProperty, args;
// Loop through the inheritance hierarchy to find the property.
for (ctor = this.constructor; ctor;
ctor = ctor.parent_ && ctor.parent_.constructor) {
// Try to find the property in the current constructor.
if (ctor.hasOwnProperty(propertyName)) {
foundProperty = ctor[propertyName];
if ($.isFunction(foundProperty)) {
// If the property is a function then call it.
args = Array.prototype.slice.call(arguments, 1);
return foundProperty.apply(this, args);
} else {
// Return the property itself.
return foundProperty;
}
}
}
// The property was not found on any of the functions
// in the constructor hierarchy.
throw new Error(['Static property "', propertyName, '" not found!'].join(''));
};
/**
* Find the parent constructor or method in the prototype
* hierarchy.
*
* NB: If the method is found then it will be executed in the
* context of the me parameter with the given arguments.
*
* @param {*=} opt_methodName The name of the method to
* be found. Do not set when calling this method from a
* constructor!
* @param {...*} var_args Arguments to be passed to the
* parent method.
* @return {*} The return value of the parent method.
*/
$.pkp.classes.ObjectProxy.prototype.parent =
function(opt_methodName, var_args) {
var caller, args, foundCaller, ctor;
// Retrieve a reference to the function that called us.
caller = $.pkp.classes.ObjectProxy.prototype.parent.caller;
// 1) Check whether the caller is a constructor.
if (caller.parent_) {
// We were called from within a constructor and
// therefore the methodName parameter is not set.
args = Array.prototype.slice.call(arguments);
// Call the constructor.
return caller.parent_.constructor.apply(this, args);
}
// Assume that we were called from within a method and that
// therefore the methodName parameter is set.
args = Array.prototype.slice.call(arguments, 1);
// 2) Look for the caller in the top-level instance methods.
if (this.hasOwnProperty(opt_methodName) && this[opt_methodName] === caller) {
return this.constructor.parent_[opt_methodName].apply(this, args);
}
// 3) Look for the caller in the prototype chain.
foundCaller = false;
for (ctor = this.constructor; ctor;
ctor = ctor.parent_ && ctor.parent_.constructor) {
if (ctor.prototype.hasOwnProperty(opt_methodName) &&
ctor.prototype[opt_methodName] === caller) {
foundCaller = true;
} else if (foundCaller) {
return ctor.prototype[opt_methodName].apply(this, args);
}
}
// 4) This method was not called by the right caller.
throw new Error(['Trying to call parent from a method of one name ',
'to a method of a different name'].join(''));
};
//
// Public methods
//
/**
* Return the object name of this object
* @return {string} The object name of this object.
*/
$.pkp.classes.ObjectProxy.prototype.getObjectName = function() {
return this.objectName_;
};
}(jQuery));
+101
View File
@@ -0,0 +1,101 @@
/**
* @file js/classes/TinyMCEHelper.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 TinyMCEHelper
* @ingroup js_classes
*
* @brief TinyMCE helper methods
*/
(function($) {
/**
* Helper singleton
* @constructor
*
* @extends $.pkp.classes.ObjectProxy
*/
$.pkp.classes.TinyMCEHelper = function() {
throw new Error('Trying to instantiate the TinyMCEHelper singleton!');
};
//
// Public static methods.
//
/**
* Get the list of variables and their descriptions for a specified field.
* @param {string} selector The textarea field's selector.
* @return {?Object} Map of variableName: variableDisplayName entries.
*/
$.pkp.classes.TinyMCEHelper.prototype.getVariableMap =
function(selector) {
var variablesEncoded = $(selector).attr('data-variables'),
variablesParsed;
// If we found the data attribute, decode and return it.
if (variablesEncoded !== undefined) {
return $.parseJSON(decodeURIComponent(
/** @type {string} */ (variablesEncoded)));
}
// If we could not find the data attribute, return an empty list.
return [];
};
/**
* Get the list of variables and their types for a specified field.
* @param {string} selector The textarea field's selector.
* @return {?Object} Map of variableName: variableType entries.
*/
$.pkp.classes.TinyMCEHelper.prototype.getVariableTypesMap =
function(selector) {
var variablesTypeEncoded = $(selector).attr('data-variablesType');
// If we found the data attribute, decode and return it.
if (variablesTypeEncoded !== undefined) {
return $.parseJSON(decodeURIComponent(
/** @type {string} */(variablesTypeEncoded)));
}
// If we could not find the data attribute, return an empty list.
return [];
};
/**
* Generate an element to represent a PKP variable (e.g. primary contact name
* in setup) within the TinyMCE editor.
* @param {string} variableSymbolic The variable symbolic name.
* @param {string} variableName The human-readable name for the variable.
* @param {string} selector The selector to use for the element.
* @return {jQueryObject} JQuery DOM representing the PKP variable.
*/
$.pkp.classes.TinyMCEHelper.prototype.getVariableElement =
function(variableSymbolic, variableName, selector) {
var variableType, variableTypes =
$.pkp.classes.TinyMCEHelper.prototype.getVariableTypesMap(selector);
// Check if there is a variable type that should be treated otherwise
if (variableTypes[variableSymbolic] != undefined) {
variableType = variableTypes[variableSymbolic];
if (variableType == $.pkp.cons.INSERT_TAG_VARIABLE_TYPE_PLAIN_TEXT) {
return $('<div/>').append($('<span/>').text(variableName));
}
}
return $('<div/>').append($('<span/>')
.addClass('pkpTag mceNonEditable')
.attr('data-symbolic', variableSymbolic)
.text(variableName));
};
}(jQuery));
+62
View File
@@ -0,0 +1,62 @@
/**
* @file js/classes/VueRegistry.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 VueRegistry
* @ingroup js_classes
*
* @brief Registry and initialization class for Vue.js handlers
*/
export default {
/**
* Registry of all active vue instances
*/
_instances: {},
/**
* Initialize a Vue controller
*
* This method is often called directly from a <script> tag in a template
* file to spin up a Vue controller on-demand. This allows the Vue component
* lifecycle to be compatible with the legacy JS framework.
*
* @param string id Element ID to attach this controller to
* @param string type The type of controller to initialize
* @param object The data object to pass to the controller. Can include
* configuration parameters, translatable strings and initial data.
*/
init: function (id, type, data) {
if (pkp.controllers[type] === undefined) {
return;
}
var baseData = {};
if (typeof pkp.controllers[type].data === 'function') {
baseData = pkp.controllers[type].data();
}
var args = $.extend(true, {}, pkp.controllers[type], {
el: '#' + id,
data: $.extend(true, {}, baseData, data, {id: id}),
});
pkp.registry._instances[id] = new pkp.Vue(args);
pkp.eventBus.$emit('root:mounted', id, pkp.registry._instances[id]);
// Register with a parent handler from the legacy JS framework, so that
// those componments can destroy a Vue instance when removing HTML code
var $parents = $(pkp.registry._instances[id].$el).parents();
$parents.each(function (i) {
if ($.pkp.classes.Handler.hasHandler($($parents[i]))) {
$.pkp.classes.Handler.getHandler($($parents[i])).handlerChildren_.push(
pkp.registry._instances[id]
);
return false; // only attach to the closest parent handler
}
});
},
};
@@ -0,0 +1,91 @@
/**
* @file js/classes/features/CollapsibleGridFeature.js
*
* Copyright (c) 2016-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 CollapsibleGridFeature
* @ingroup js_classes_features
*
* @brief Adds collapse/expand functionality to grids.
*/
(function($) {
/**
* @constructor
* @inheritDoc
* @extends $.pkp.classes.features.Feature
*/
$.pkp.classes.features.CollapsibleGridFeature =
function(gridHandler, options) {
this.parent(gridHandler, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.features.CollapsibleGridFeature,
$.pkp.classes.features.Feature);
//
// Getter and setters.
//
/**
* Get the collapse/expand control link selector.
* @return {string}
*/
$.pkp.classes.features.CollapsibleGridFeature.prototype.getControlSelector =
function() {
return "a[id^='collapsibleGridControl-expandGridControlLink-button-']";
};
/**
* @inheritDoc
*/
$.pkp.classes.features.CollapsibleGridFeature.prototype.init =
function() {
$(this.getControlSelector(), this.getGridHtmlElement()).
click(this.callbackWrapper(this.toggleGridClickHandler_, this));
};
/**
* @inheritDoc
*/
$.pkp.classes.features.CollapsibleGridFeature.prototype.
addFeatureHtml = function($gridElement, options) {
var castOptions = /** @type {{collapsibleLink: string?}} */ (options);
$gridElement.find('div.grid_header_bar').prepend(castOptions.collapsibleLink);
};
//
// Private helper methods.
//
/**
* Collapse/expand grid.
* @private
* @param {Object} callingContext The calling element or object.
* @param {Event=} opt_event The triggering event.
* @return {boolean} Should return false to stop event processing.
*/
$.pkp.classes.features.CollapsibleGridFeature.prototype.
toggleGridClickHandler_ = function(callingContext, opt_event) {
var $control = this.getGridHtmlElement().find(this.getControlSelector());
this.getGridHtmlElement().find('div.grid_header').siblings().toggle();
$control.toggleClass('expand_all').toggleClass('collapse_all');
// Hide the search controls, if they are visible.
this.getGridHtmlElement().
find('div.grid_header_bar .search_extras_collapse').click();
// Toggle all grid actions.
this.getGridHtmlElement().find('div.grid_header span.options').toggle();
return false;
};
}(jQuery));
+200
View File
@@ -0,0 +1,200 @@
/**
* @defgroup js_classes_features
*/
/**
* @file js/classes/features/Feature.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 Feature
* @ingroup js_classes_features
*
* @brief Base grid feature class.
* @see lib/pkp/classes/controllers/grid/feature/GridFeature.php
*
* We use the features concept of the ext js framework:
* http://docs.sencha.com/ext-js/4-0/#!/api/Ext.grid.feature.Feature
*/
(function($) {
/** @type {Object} */
$.pkp.classes.features = $.pkp.classes.features || {};
/**
* @constructor
* @extends $.pkp.classes.ObjectProxy
* @param {$.pkp.controllers.grid.GridHandler} gridHandler The grid
* handler object.
* @param {Array} options Associated options.
*/
$.pkp.classes.features.Feature =
function(gridHandler, options) {
this.gridHandler = gridHandler;
this.options_ = options;
this.addFeatureHtml(this.getGridHtmlElement(), options);
};
//
// Protected properties.
//
/**
* The grid that this feature is attached to.
* @protected
* @type {$.pkp.controllers.grid.GridHandler}
*/
$.pkp.classes.features.Feature.prototype.gridHandler = null;
//
// Private properties.
//
/**
* This feature configuration options.
* @private
* @type {Object}
*/
$.pkp.classes.features.Feature.prototype.options_ = null;
//
// Setters and getters.
//
/**
* @param {Object} options The feature options.
*/
$.pkp.classes.features.Feature.prototype.setOptions =
function(options) {
this.options_ = options;
};
/**
* @return {Object} The feature options.
*/
$.pkp.classes.features.Feature.prototype.getOptions =
function() {
return this.options_;
};
//
// Public template methods.
//
/**
* Initialize this feature. Needs to be extended to implement
* specific initialization. This method will always be called
* by the components that this feature is attached to, in the
* moment of the attachment.
*/
$.pkp.classes.features.Feature.prototype.init =
function() {
throw new Error('Abstract method!');
};
//
// Template methods (hooks into grid widgets).
//
/**
* Hook into the add new element grid functionality.
* @param {jQueryObject} $newElement The new element to be added.
* @return {boolean} Always returns false.
*/
$.pkp.classes.features.Feature.prototype.addElement =
function($newElement) {
return false;
};
/**
* Hook into the replace element content grid functionality.
* @param {jQueryObject} $newContent The element new content to be shown.
* @return {boolean} Always returns false.
*/
$.pkp.classes.features.Feature.prototype.replaceElement =
function($newContent) {
return false;
};
/**
* Hook into the resequence rows grid functionality.
* @param {Object} sequenceMap The grid rows sequence.
* @return {boolean} Always returns false.
*/
$.pkp.classes.features.Feature.prototype.resequenceRows =
function(sequenceMap) {
return false;
};
/**
* Hook into the refresh grid functionality. Called just before
* the fetch (grid or row) call is done.
* @param {number|Object=} opt_elementId
* @return {boolean} Always returns false.
*/
$.pkp.classes.features.Feature.prototype.refreshGrid =
function(opt_elementId) {
return false;
};
/**
* Hook into the replace element response handler. Called after the
* response is handled.
* @param {Object} handledJsonData Object with the response content handled
* by the grid.
* @return {boolean} Always returns false.
*/
$.pkp.classes.features.Feature.prototype.replaceElementResponseHandler =
function(handledJsonData) {
return false;
};
//
// Protected methods.
//
/**
* Use the grid handler object and call the
* callback wrapper method there.
* @see $.pkp.classes.Handler.callbackWrapper()
* @return {Function} Callback function.
*/
$.pkp.classes.features.Feature.prototype.callbackWrapper =
function(callback, opt_context) {
return this.gridHandler.callbackWrapper(callback, opt_context);
};
/**
* Extend to add extra html elements in the component
* that this feature is attached to.
* @param {jQueryObject} $gridElement Grid element to add elements to.
* @param {Object} options Feature options.
*/
$.pkp.classes.features.Feature.prototype.addFeatureHtml =
function($gridElement, options) {
// Default implementation does nothing.
};
/**
* Get the html element of the grid that this feature
* is attached to.
*
* @return {jQueryObject} Return the grid's HTML element.
*/
$.pkp.classes.features.Feature.prototype.getGridHtmlElement =
function() {
return this.gridHandler.getHtmlElement();
};
}(jQuery));
@@ -0,0 +1,90 @@
/**
* @file js/classes/features/GeneralPagingFeature.js
*
* Copyright (c) 2016-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 GeneralPagingFeature
* @ingroup js_classes_features
*
* @brief Base class that implements general functionalities for features
* that handles paging on grids.
*/
(function($) {
/**
* @constructor
* @inheritDoc
* @extends $.pkp.classes.features.Feature
*/
$.pkp.classes.features.GeneralPagingFeature =
function(gridHandler, options) {
options.defaultItemsPerPage = parseInt(options.defaultItemsPerPage, 10);
options.currentItemsPerPage = parseInt(options.currentItemsPerPage, 10);
if (!options.itemsTotal) {
options.itemsTotal = 0;
} else {
options.itemsTotal = parseInt(options.itemsTotal, 10);
}
options.currentPage = parseInt(options.currentPage, 10);
this.parent(gridHandler, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.features.GeneralPagingFeature,
$.pkp.classes.features.Feature);
//
// Getters and setters.
//
/**
* @return {{itemsPerPageParamName: string,
* defaultItemsPerPage: number,
* currentItemsPerPage: number,
* itemsTotal: number,
* pageParamName: string,
* currentPage: number,
filter: string,
* pagingMarkup: string }}
* @override
*/
$.pkp.classes.features.GeneralPagingFeature.prototype.getOptions =
function() {
var castOptions = /** @type {{itemsPerPageParamName: string,
defaultItemsPerPage: number,
currentItemsPerPage: number,
itemsTotal: number,
pageParamName: string,
currentPage: number,
filter: string,
pagingMarkup: string }} */
(this.parent('getOptions'));
return castOptions;
};
//
// Protected methods.
//
/**
* Set grid requests extra parameters.
* @param {Object} params
*/
$.pkp.classes.features.GeneralPagingFeature.prototype.setGridParams =
function(params) {
var options = this.getOptions(), filter;
// Add the filter data, if any.
if (options.hasOwnProperty('filter')) {
filter = $.parseJSON(options.filter);
$.extend(true, params, filter);
}
this.gridHandler.setFetchExtraParams(params);
};
}(jQuery));
@@ -0,0 +1,339 @@
/**
* @file js/classes/features/InfiniteScrollingFeature.js
*
* Copyright (c) 2016-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 InfiniteScrollingFeature
* @ingroup js_classes_features
*
* @brief Feature that implements infinite scrolling on grids.
* It doesn't support category grids.
*/
(function($) {
/**
* @constructor
* @inheritDoc
* @extends $.pkp.classes.features.GeneralPagingFeature
*/
$.pkp.classes.features.InfiniteScrollingFeature =
function(gridHandler, options) {
this.parent(gridHandler, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.features.InfiniteScrollingFeature,
$.pkp.classes.features.GeneralPagingFeature);
//
// Private properties
//
/**
* The scrollable element.
* @private
* @type {jQueryObject}
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.
$scrollableElement_ = $();
/**
* The scrolling observer callback function.
* @private
* @type {Function}
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.
observeScrollCallback_ = function() {};
//
// Extended methods from GeneralPagingFeature
//
/**
* @inheritDoc
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.init =
function() {
var $scrollableElement = $('div.scrollable', this.getGridHtmlElement());
if (!$scrollableElement.length) {
this.gridHandler.publishEvent('pkpObserveScrolling');
this.gridHandler.publishEvent('pkpRemoveScrollingObserver');
}
this.$scrollableElement_ = $scrollableElement;
this.observeScrollCallback_ = this.gridHandler.callbackWrapper(
this.observeScroll_, this);
this.addScrollHandler_();
this.fixGridHeight_();
this.addPagingDataToRows_();
};
/**
* @inheritDoc
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.addFeatureHtml =
function($gridElement, options) {
var castOptions = /** @type {{pagingMarkup: string?,
loadingContainer: string?}} */ (options);
$gridElement.append(castOptions.pagingMarkup);
$gridElement.find('.pkp_linkaction_moreItems')
.click(this.gridHandler.callbackWrapper(this.loadMoreItems_, this));
};
//
// Hooks implementation.
//
/**
* @inheritDoc
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.refreshGrid =
function(opt_elementId) {
var options = this.getOptions(), params, $firstRow, $lastRow, page, $gridRow,
elementId;
params = this.gridHandler.getFetchExtraParams();
params[options.pageParamName] = options.currentPage;
if (opt_elementId && opt_elementId !==
$.pkp.controllers.grid.GridHandler.FETCH_ALL_ROWS_ID) {
// We need to make sure we pass the right page for the element.
elementId = (/** @type {number} */ (opt_elementId));
$gridRow = this.gridHandler.getRowByDataId(elementId);
if ($gridRow.length == 1) {
params[options.pageParamName] = Number($gridRow.attr('data-paging'));
}
}
params[options.itemsPerPageParamName] = options.currentItemsPerPage;
this.setGridParams(params);
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.
replaceElementResponseHandler = function(handledJsonData) {
var pagingInfo, options, castJsonData, rowMarkup;
options = this.getOptions();
castJsonData = /** @type {{pagingInfo: Object,
deletedRowReplacement: string}} */
(handledJsonData);
if (castJsonData.deletedRowReplacement != undefined) {
rowMarkup = handledJsonData.deletedRowReplacement;
this.gridHandler.insertOrReplaceElement(rowMarkup);
this.updatePagingDataInAllRows_();
}
this.addScrollHandler_();
if (castJsonData.pagingInfo != undefined) {
pagingInfo = handledJsonData.pagingInfo;
this.setOptions(pagingInfo);
if (pagingInfo.pagingMarkup != undefined) {
$('div.gridPagingScrolling', this.getGridHtmlElement()).
replaceWith(pagingInfo.pagingMarkup);
}
}
this.addPagingDataToRows_();
this.toggleLoadingContainer_();
this.getGridHtmlElement().find('.pkp_linkaction_moreItems').
click(this.gridHandler.callbackWrapper(this.loadMoreItems_, this));
return false;
};
//
// Private helper methods.
//
/**
* Scroll handler to detect when it's time to request more rows.
*
* @private
*
* @param {HTMLElement} sourceElement
* @param {Event} event
* @return {boolean}
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.observeScroll_ =
function(sourceElement, event) {
var options = this.getOptions(), sourceElementHeight,
bottomLimit, windowDimensions;
if (options.itemsTotal == this.gridHandler.getRows().length) {
return false;
}
if (!this.getGridHtmlElement().is(':visible')) {
return false;
}
if ($(sourceElement).hasClass('scrollable')) {
sourceElementHeight = $(sourceElement).height();
bottomLimit = sourceElement.scrollHeight;
} else {
windowDimensions = $.pkp.controllers.SiteHandler.
prototype.getWindowDimensions();
sourceElementHeight = windowDimensions.height;
bottomLimit = this.getGridHtmlElement().offset().top +
this.getGridHtmlElement().height();
}
if (sourceElementHeight + $(sourceElement).scrollTop() >= bottomLimit) {
// Avoid multiple rows requests.
if (this.$scrollableElement_.length) {
this.$scrollableElement_.unbind('scroll');
} else {
this.getGridHtmlElement().trigger('pkpRemoveScrollingObserver',
[this.observeScrollCallback_]);
}
this.loadMoreItems_();
}
return false;
};
/**
* Fix the grid height to acomodate the number of initial visible rows.
*
* @private
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.fixGridHeight_ =
function() {
var $scrollableDivs = $('div.scrollable', this.getGridHtmlElement()),
index, limit, $div, timer, length;
if ($scrollableDivs.length > 0) {
timer = setInterval(function() {
if ($scrollableDivs.is(':visible')) {
clearInterval(timer);
length = $scrollableDivs.length;
for (index = 0, limit = length; index < limit; index++) {
$div = $($scrollableDivs[index]);
if ($div.get(0).scrollHeight > $div.height()) {
$div.css('max-height', $div.get(0).scrollHeight - 10);
}
}
}
},300);
}
};
/**
* Add paging data to the respective rows.
*
* @private
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.addPagingDataToRows_ =
function() {
var $rows, options = this.getOptions();
$rows = this.gridHandler.getRows().filter('tr:not([data-paging])');
$rows.attr('data-paging', options.currentPage);
};
/**
* Update paging data in all grid rows.
*
* @private
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.
updatePagingDataInAllRows_ = function() {
var $rows, options = this.getOptions(), index, limit, page = 1,
itemsCount = 1;
$rows = this.gridHandler.getRows();
$rows.removeAttr('data-paging');
for (index = 0, limit = $rows.length; index < limit; index++) {
$($rows[index]).attr('data-paging', page);
itemsCount++;
if (itemsCount > options.currentItemsPerPage) {
itemsCount = 1;
page++;
}
}
};
/**
* Add scroll handler to the grid element.
*
* @private
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.addScrollHandler_ =
function() {
var $scrollableElement = this.$scrollableElement_;
if ($scrollableElement.length) {
$scrollableElement.
scroll(this.observeScrollCallback_);
} else {
this.getGridHtmlElement().trigger('pkpObserveScrolling',
[this.observeScrollCallback_]);
}
};
/**
* Toggle the scrolling loading element.
*
* @private
*
* @param {boolean=} opt_show
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.
toggleLoadingContainer_ = function(opt_show) {
var $loadingElement =
this.getGridHtmlElement().find('div.gridPagingScrolling div.pkp_loading'),
$scrollableElement = this.$scrollableElement_,
scrollTop,
loadingHeight = $loadingElement.height(),
scrollTarget;
if (opt_show) {
this.getGridHtmlElement().addClass('loading');
scrollTop = $scrollableElement.scrollTop();
scrollTarget = /** @type {number} */ (scrollTop + loadingHeight);
$scrollableElement.scrollTop(scrollTarget);
} else {
this.getGridHtmlElement().removeClass('loading');
}
};
/**
* Trigger necessary actions for the grid to
* load next page items.
*
* @private
*
*/
$.pkp.classes.features.InfiniteScrollingFeature.prototype.
loadMoreItems_ = function() {
var options = this.getOptions();
// Show the loading icon.
this.toggleLoadingContainer_(true);
options.currentPage = Number($('tr.gridRow',
this.getGridHtmlElement()).last().attr('data-paging')) + 1;
this.getGridHtmlElement().trigger('dataChanged',
[$.pkp.controllers.grid.GridHandler.FETCH_ALL_ROWS_ID]);
};
}(jQuery));
@@ -0,0 +1,201 @@
/**
* @file js/classes/features/OrderCategoryGridItemsFeature.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 OrderCategoryGridItemsFeature
* @ingroup js_classes_features
*
* @brief Feature for ordering category grid items.
*/
(function($) {
/**
* @constructor
* @inheritDoc
* @extends $.pkp.classes.features.OrderGridItemsFeature
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature =
function(gridHandler, options) {
this.parent(gridHandler, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.features.OrderCategoryGridItemsFeature,
$.pkp.classes.features.OrderGridItemsFeature);
//
// Extended methods from OrderItemsFeature.
//
/**
* Setup the sortable plugin.
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature.prototype.
setupSortablePlugin = function() {
var $categories, index, limit, $category, userAgent;
this.applySortPlgOnElements(
this.getGridHtmlElement(), 'tbody.orderable', null);
// FIXME *7610*: IE8 can't handle well ordering in both categories and
// category rows.
userAgent = navigator.userAgent.toLowerCase();
if (/msie/.test(userAgent) &&
parseInt(userAgent.substr(userAgent.indexOf('msie') + 5, 1), 10) <= 8) {
return;
}
$categories = this.gridHandler.getCategories();
for (index = 0, limit = $categories.length; index < limit; index++) {
$category = $($categories[index]);
this.applySortPlgOnElements($category, 'tr.orderable', null);
}
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature.prototype.
saveOrderHandler = function() {
this.gridHandler.updateEmptyPlaceholderPosition();
this.parent('saveOrderHandler');
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature.prototype.
cancelOrderHandler = function() {
var categorySequence = this.getCategorySequence_(this.itemsOrder);
this.parent('cancelOrderHandler');
this.gridHandler.resequenceCategories(categorySequence);
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature.prototype.
toggleItemsDragMode = function() {
this.parent('toggleItemsDragMode');
var isOrdering = this.isOrdering,
$categories = this.gridHandler.getCategories(),
index, limit, $category;
for (index = 0, limit = $categories.length; index < limit; index++) {
$category = $($categories[index]);
this.toggleCategoryDragMode_($category);
}
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature.prototype.
addOrderingClassToRows = function() {
var options = this.getOptions(),
type = parseInt(options.type, 10), $categories;
if (type == $.pkp.cons.ORDER_CATEGORY_GRID_CATEGORIES_ONLY ||
type == $.pkp.cons.ORDER_CATEGORY_GRID_CATEGORIES_AND_ROWS) {
$categories = this.gridHandler.getCategories();
$categories.addClass('orderable');
}
if (type == $.pkp.cons.ORDER_CATEGORY_GRID_CATEGORIES_ROWS_ONLY ||
type == $.pkp.cons.ORDER_CATEGORY_GRID_CATEGORIES_AND_ROWS) {
this.parent('addOrderingClassToRows');
}
// We don't want to order category rows tr elements, so
// remove any style that might be added by calling parent.
this.gridHandler.getCategoryRow().removeClass('orderable');
};
//
// Overriden method from OrderGridItemsFeature
//
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature.prototype.getItemsDataId =
function() {
var categoriesSeq = this.getCategorySequence_(this.itemsOrder),
itemsDataId = [],
index, limit,
$category, categoryRowsDataId, categoryDataId;
for (index = 0, limit = categoriesSeq.length; index < limit; index++) {
$category = $('#' + categoriesSeq[index]);
categoryRowsDataId = this.getRowsDataId($category);
categoryDataId = this.gridHandler.getCategoryDataId($category);
itemsDataId.push(
{'categoryId': categoryDataId, 'rowsId': categoryRowsDataId });
}
return itemsDataId;
};
//
// Private helper methods.
//
/**
* Enable/disable category drag mode.
* @param {jQueryObject} $category Category to set mode on.
* @private
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature.prototype.
toggleCategoryDragMode_ = function($category) {
var isOrdering = this.isOrdering,
$categoryRow = this.gridHandler.getCategoryRow($category),
$categoryRowColumn = $('td:first', $categoryRow),
moveClasses = this.getMoveItemClasses();
if (isOrdering) {
$categoryRowColumn.addClass(moveClasses);
} else {
$categoryRowColumn.removeClass(moveClasses);
}
};
/**
* Get the categories sequence, based on the passed items order.
* @param {Array} itemsOrder Items order.
* @return {Array} A sequence array with the category data id as values.
* @private
*/
$.pkp.classes.features.OrderCategoryGridItemsFeature.prototype.
getCategorySequence_ = function(itemsOrder) {
var index, limit, categorySequence = [], categoryDataId, categoryId;
for (index = 0, limit = itemsOrder.length; index < limit; index++) {
categoryDataId = this.gridHandler
.getCategoryDataIdByRowId(itemsOrder[index]);
categoryId = this.gridHandler.getCategoryIdPrefix() + categoryDataId;
if ($.inArray(categoryId, categorySequence) > -1) {
continue;
}
categorySequence.push(categoryId);
}
return categorySequence;
};
}(jQuery));
@@ -0,0 +1,97 @@
/**
* @file js/classes/features/OrderGridItemsFeature.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 OrderGridItemsFeature
* @ingroup js_classes_features
*
* @brief Feature for ordering grid items.
*/
(function($) {
/**
* @constructor
* @inheritDoc
* @extends $.pkp.classes.features.OrderItemsFeature
*/
$.pkp.classes.features.OrderGridItemsFeature =
function(gridHandler, options) {
this.parent(gridHandler, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.features.OrderGridItemsFeature,
$.pkp.classes.features.OrderItemsFeature);
//
// Extended methods from OrderItemsFeature.
//
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderGridItemsFeature.prototype.setupSortablePlugin =
function() {
this.applySortPlgOnElements(
this.getGridHtmlElement(), 'tr.orderable', null);
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderGridItemsFeature.prototype.saveOrderHandler =
function() {
var stringifiedData, saveOrderCallback,
options = /** @type {{saveItemsSequenceUrl: string}} */
(this.getOptions()),
returner;
this.parent('saveOrderHandler');
stringifiedData = JSON.stringify(this.getItemsDataId());
saveOrderCallback = this.callbackWrapper(
this.saveOrderResponseHandler_, this);
$.post(options.saveItemsSequenceUrl,
{data: stringifiedData, csrfToken: options.csrfToken},
saveOrderCallback, 'json');
return false;
};
//
// Protected methods to be overriden by subclasses
//
/**
* Get all items data id in a sequence array.
* @return {Array} List of all items data.
*/
$.pkp.classes.features.OrderGridItemsFeature.prototype.getItemsDataId =
function() {
return this.getRowsDataId(this.getGridHtmlElement());
};
//
// Private helper methods.
//
/**
* Save order response handler.
* @private
*
* @param {Object} ajaxContext The AJAX request context.
* @param {Object} jsonData A parsed JSON response object.
*/
$.pkp.classes.features.OrderGridItemsFeature.prototype.
saveOrderResponseHandler_ = function(ajaxContext, jsonData) {
var processedJsonData = this.gridHandler.handleJson(jsonData);
this.toggleState(false);
};
}(jQuery));
@@ -0,0 +1,639 @@
/**
* @file js/classes/features/OrderItemsFeature.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 OrderItemsFeature
* @ingroup js_classes_features
*
* @brief Base feature class for ordering grid items.
*/
(function($) {
/**
* @constructor
*
* @param {jQueryObject} gridHandler The handler of
* the grid element that this feature is attached to.
* @param {Object} options Configuration of this feature.
* @extends $.pkp.classes.features.Feature
*/
$.pkp.classes.features.OrderItemsFeature =
function(gridHandler, options) {
this.parent(gridHandler, options);
this.$orderButton_ = $('.pkp_linkaction_orderItems',
this.getGridHtmlElement());
this.$finishControl_ = $('.order_finish_controls', this.getGridHtmlElement());
if (this.$orderButton_.length === 0) {
// No order button, it will always stay in ordering mode.
this.isOrdering = true;
}
this.itemsOrder = [];
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.features.OrderItemsFeature,
$.pkp.classes.features.Feature);
//
// Protected properties
//
/**
* Item sequence.
* @protected
* @type {Array}
*/
$.pkp.classes.features.OrderItemsFeature.prototype.itemsOrder = null;
/**
* Flag to control if user is ordering items.
* @protected
* @type {boolean}
*/
$.pkp.classes.features.OrderItemsFeature.prototype.isOrdering = false;
//
// Private properties.
//
/**
* Initiate ordering state button.
* @private
* @type {jQueryObject}
*/
$.pkp.classes.features.OrderItemsFeature.prototype.$orderButton_ = null;
/**
* Cancel ordering state button.
* @private
* @type {jQueryObject}
*/
$.pkp.classes.features.OrderItemsFeature.prototype.$cancelButton_ = null;
/**
* Save ordering state button.
* @private
* @type {jQueryObject}
*/
$.pkp.classes.features.OrderItemsFeature.prototype.$saveButton_ = null;
/**
* Ordering finish control.
* @private
* @type {jQueryObject}
*/
$.pkp.classes.features.OrderItemsFeature.prototype.$finishControl_ = null;
//
// Getters and setters.
//
/**
* Get the order button.
* @return {jQueryObject} The order button JQuery object.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.getOrderButton =
function() {
return this.$orderButton_;
};
/**
* Get the finish control.
* @return {jQueryObject} The JQuery "finish" control.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.getFinishControl =
function() {
return this.$finishControl_;
};
/**
* Get save order button.
*
* @return {jQueryObject} The "save order" JQuery object.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.getSaveOrderButton =
function() {
return this.getFinishControl().find('.saveButton');
};
/**
* Get cancel order link.
*
* @return {jQueryObject} The "cancel order" JQuery control.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.getCancelOrderButton =
function() {
return this.getFinishControl().find('.cancelFormButton');
};
/**
* Get the move item row action element selector.
* @return {string} Return the element selector.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.
getMoveItemRowActionSelector = function() {
return '.orderable .pkp_linkaction_moveItem';
};
/**
* Get the css classes used to stylize the ordering items.
* @return {string} CSS classes.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.getMoveItemClasses =
function() {
return 'pkp_helpers_moveicon ordering';
};
//
// Public template methods.
//
/**
* Called every time user start dragging an item.
* @param {jQueryObject} contextElement The element this event occurred for.
* @param {Event} event The drag/drop event.
* @param {Object} ui Object with data related to the event elements.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.dragStartCallback =
function(contextElement, event, ui) {
// The default implementation does nothing.
};
/**
* Called every time user stop dragging an item.
* @param {jQueryObject} contextElement The element this event occurred for.
* @param {Event} event The drag/drop event.
* @param {Object} ui Object with data related to the event elements.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.dragStopCallback =
function(contextElement, event, ui) {
// The default implementation does nothing.
};
/**
* Called every time sequence is changed.
* @param {jQueryObject} contextElement The element this event occurred for.
* @param {Event} event The drag/drop event.
* @param {Object} ui Object with data related to the event elements.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.updateOrderCallback =
function(contextElement, event, ui) {
// The default implementation does nothing.
};
//
// Extended methods from Feature
//
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderItemsFeature.prototype.init =
function() {
this.addOrderingClassToRows();
this.toggleMoveItemRowAction(this.isOrdering);
this.getGridHtmlElement().find('div.order_message').hide();
this.toggleOrderLink_();
if (this.isOrdering) {
this.setupSortablePlugin();
}
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderItemsFeature.prototype.addFeatureHtml =
function($gridElement, options) {
var castOptions = /** @type {{orderFinishControls: string?,
orderMessage: string?}} */ (options),
$orderFinishControls, orderMessageHtml, $gridRows;
if (castOptions.orderFinishControls !== undefined) {
$orderFinishControls = $(castOptions.orderFinishControls);
$gridElement.find('table').last().after($orderFinishControls);
$orderFinishControls.hide();
}
if (castOptions.orderMessage !== undefined) {
orderMessageHtml = castOptions.orderMessage;
$gridRows = $gridElement.find('.gridRow').filter(function(index, element) {
return !Boolean($(this).find('a.pkp_linkaction_moveItem').length);
});
$gridRows.find('td:first-child').prepend(orderMessageHtml);
}
this.updateOrderLinkVisibility_();
};
//
// Protected template methods.
//
/**
* Add orderable class to grid rows.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.addOrderingClassToRows =
function() {
// Add ordering class to grid rows.
var $gridRows = this.gridHandler.getRows().filter(function(index, element) {
return $(this).find('a.pkp_linkaction_moveItem').length;
});
$gridRows.addClass('orderable');
};
/**
* Setup the sortable plugin. Must be implemented in subclasses.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.setupSortablePlugin =
function() {
// Default implementation does nothing.
};
/**
* Called every time storeOrder is called. This is a chance to subclasses
* execute operations with each row that has their sequence being saved.
* @param {number} index The current row index position inside the rows
* jQuery object.
* @param {jQueryObject} $row Row for which to store the sequence.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.storeRowOrder =
function(index, $row) {
// The default implementation does nothing.
};
//
// Protected methods.
//
/**
* Initiate ordering button click event handler.
* @return {boolean} Always returns false.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.clickOrderHandler =
function() {
this.gridHandler.hideAllVisibleRowActions();
this.storeOrder(this.gridHandler.getRows());
this.toggleState(true);
return false;
};
/**
* Save order handler.
* @return {boolean} Return false to stop click event processing.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.saveOrderHandler =
function() {
var $rows;
this.gridHandler.updateControlRowsPosition();
this.unbindOrderFinishControlsHandlers_();
$rows = this.gridHandler.getRows();
this.storeOrder($rows);
return false;
};
/**
* Cancel ordering action click event handler.
* @return {boolean} Always returns false.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.cancelOrderHandler =
function() {
this.gridHandler.resequenceRows(this.itemsOrder);
this.toggleState(false);
return false;
};
/**
* Execute all operations necessary to change the state of the
* ordering process (enabled or disabled).
* @param {boolean} isOrdering Is ordering process active?
*/
$.pkp.classes.features.OrderItemsFeature.prototype.toggleState =
function(isOrdering) {
this.isOrdering = isOrdering;
this.toggleGridLinkActions_();
this.toggleOrderLink_();
this.toggleFinishControl_();
this.toggleItemsDragMode();
this.setupSortablePlugin();
this.setupNonOrderableMessage_();
};
/**
* Set rows sequence store, using
* the sequence of the passed items.
*
* @param {jQueryObject} $rows The rows to be used to get the sequence
* information.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.storeOrder =
function($rows) {
var index, limit, $row, elementId;
this.itemsOrder = [];
for (index = 0, limit = $rows.length; index < limit; index++) {
$row = $($rows[index]);
elementId = $row.attr('id');
this.itemsOrder.push(elementId);
// Give a chance to subclasses do extra operations to store
// the current row order.
this.storeRowOrder(index, $row);
}
};
/**
* Enable/disable the items drag mode.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.toggleItemsDragMode =
function() {
var isOrdering = this.isOrdering,
$rows = this.gridHandler.getRows(),
$orderableRows = $rows.filter('.orderable'),
moveClasses = this.getMoveItemClasses();
if (isOrdering) {
$orderableRows.addClass(moveClasses);
} else {
$orderableRows.removeClass(moveClasses);
}
this.toggleMoveItemRowAction(isOrdering);
};
/**
* Apply (disabled or enabled) the sortable plugin on passed elements.
* @param {jQueryObject} $container The element that contain all the orderable
* items.
* @param {string} itemsSelector The jQuery selector for orderable items.
* @param {Object?} extraParams Optional set of extra parameters for sortable.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.applySortPlgOnElements =
function($container, itemsSelector, extraParams) {
var isOrdering = this.isOrdering,
dragStartCallback = this.gridHandler.callbackWrapper(
this.dragStartCallback, this),
dragStopCallback = this.gridHandler.callbackWrapper(
this.dragStopCallback, this),
orderItemCallback = this.gridHandler.callbackWrapper(
this.updateOrderCallback, this),
config = {
disabled: !isOrdering,
items: itemsSelector,
activate: dragStartCallback,
deactivate: dragStopCallback,
update: orderItemCallback,
tolerance: 'pointer'};
if (typeof extraParams === 'object') {
config = $.extend(true, config, extraParams);
}
$container.sortable(config);
};
/**
* Get the data element id of all rows inside the passed
* container, in the current order.
* @param {jQueryObject} $rowsContainer The element that contains the rows
* that will be used to retrieve the id.
* @return {Array} A sequence array with data element ids as values.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.getRowsDataId =
function($rowsContainer) {
var index, rowDataIds = [], $row, rowDataId;
for (index in this.itemsOrder) {
$row = $('#' + this.itemsOrder[index], $rowsContainer);
if ($row.length < 1) {
continue;
}
rowDataId = this.gridHandler.getRowDataId($row);
rowDataIds.push(rowDataId);
}
return rowDataIds;
};
/**
* Show/hide the move item row action (position left).
* @param {boolean} enable New enable state.
*/
$.pkp.classes.features.OrderItemsFeature.prototype.toggleMoveItemRowAction =
function(enable) {
var $grid = this.getGridHtmlElement(),
$actionsContainer = $('div.row_actions', $grid),
allLinksButMoveItemSelector = 'a:not(' +
this.getMoveItemRowActionSelector() + ')',
$actions = $actionsContainer.find(allLinksButMoveItemSelector),
$moveItemRowAction = $(this.getMoveItemRowActionSelector(), $grid),
$rowActionsContainer, $rowActions;
if (enable) {
$actions.addClass('pkp_helpers_display_none');
$moveItemRowAction.show();
// Make sure row actions div is visible.
this.gridHandler.showRowActionsDiv();
} else {
$actions.removeClass('pkp_helpers_display_none');
$rowActionsContainer = $('.gridRow div.row_actions', $grid);
$rowActions = $rowActionsContainer.
find(allLinksButMoveItemSelector);
if ($rowActions.length === 0) {
// No link action to show, hide row actions div.
this.gridHandler.hideRowActionsDiv();
}
$moveItemRowAction.hide();
}
};
//
// Hooks implementation.
//
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderItemsFeature.prototype.addElement =
function($element) {
this.addOrderingClassToRows();
this.toggleItemsDragMode();
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderItemsFeature.prototype.replaceElement =
function($content) {
this.addOrderingClassToRows();
this.toggleItemsDragMode();
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderItemsFeature.prototype.
replaceElementResponseHandler = function(handledJsonData) {
this.updateOrderLinkVisibility_();
this.setupNonOrderableMessage_();
return false;
};
//
// Private helper methods.
//
/**
* Make sure that the order action visibility state is correct,
* based on the grid rows number.
* @private
*/
$.pkp.classes.features.OrderItemsFeature.prototype.
updateOrderLinkVisibility_ = function() {
var $orderLink = $('.pkp_linkaction_orderItems', this.getGridHtmlElement());
if (this.gridHandler.getRows().length <= 1) {
$orderLink.hide();
} else {
$orderLink.show();
}
};
/**
* Set the state of the grid link actions, based on current ordering state.
* @private
*/
$.pkp.classes.features.OrderItemsFeature.prototype.toggleGridLinkActions_ =
function() {
var isOrdering = this.isOrdering,
// We want to enable/disable all link actions, except this
// features controls.
$gridLinkActions = $('.pkp_controllers_linkAction',
this.getGridHtmlElement()).not(
this.getMoveItemRowActionSelector()).not(
this.getOrderButton()).not(
this.getFinishControl().find('*'));
this.gridHandler.changeLinkActionsState(!isOrdering, $gridLinkActions);
};
/**
* Enable/disable the order link action.
* @private
*/
$.pkp.classes.features.OrderItemsFeature.prototype.toggleOrderLink_ =
function() {
if (this.isOrdering) {
this.$orderButton_.unbind('click');
this.$orderButton_.attr('disabled', 'disabled');
} else {
var clickHandler = this.gridHandler.callbackWrapper(
this.clickOrderHandler, this);
this.$orderButton_.click(clickHandler);
this.$orderButton_.removeAttr('disabled');
}
};
/**
* Show/hide the ordering process finish control, based
* on the current ordering state.
* @private
*/
$.pkp.classes.features.OrderItemsFeature.prototype.toggleFinishControl_ =
function() {
if (this.isOrdering) {
this.bindOrderFinishControlsHandlers_();
this.getFinishControl().slideDown(300);
} else {
this.unbindOrderFinishControlsHandlers_();
this.getFinishControl().slideUp(300);
}
};
/**
* Bind event handlers to the controls that finish the
* ordering action (save and cancel).
* @private
*/
$.pkp.classes.features.OrderItemsFeature.prototype.
bindOrderFinishControlsHandlers_ = function() {
var $saveButton = this.getSaveOrderButton(),
$cancelLink = this.getCancelOrderButton(),
cancelLinkHandler = this.gridHandler.callbackWrapper(
this.cancelOrderHandler, this),
saveButtonHandler = this.gridHandler.callbackWrapper(
this.saveOrderHandler, this);
$saveButton.click(saveButtonHandler);
$cancelLink.click(cancelLinkHandler);
};
/**
* Unbind event handlers from the controls that finish the
* ordering action (save and cancel).
* @private
*/
$.pkp.classes.features.OrderItemsFeature.prototype.
unbindOrderFinishControlsHandlers_ = function() {
this.getSaveOrderButton().unbind('click');
this.getCancelOrderButton().unbind('click');
};
/**
* Toggle hover action to show message for non orderable
* grid rows.
* @private
*/
$.pkp.classes.features.OrderItemsFeature.prototype.
setupNonOrderableMessage_ = function() {
if (this.isOrdering) {
this.gridHandler.getRows().hover(function() {
$(this).find('div.order_message').toggle();
});
} else {
this.gridHandler.getRows().unbind('mouseenter mouseleave');
}
};
}(jQuery));
@@ -0,0 +1,220 @@
/**
* @file js/classes/features/OrderListbuilderItemsFeature.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 OrderListbuilderItemsFeature
* @ingroup js_classes_features
*
* @brief Feature for ordering grid items.
*/
(function($) {
/**
* @constructor
* @inheritDoc
* @extends $.pkp.classes.features.OrderItemsFeature
*/
$.pkp.classes.features.OrderListbuilderItemsFeature =
function(gridHandler, options) {
this.parent(gridHandler, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.features.OrderListbuilderItemsFeature,
$.pkp.classes.features.OrderItemsFeature);
//
// Extended methods from OrderItemsFeature.
//
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.addFeatureHtml =
function($gridElement, options) {
var $itemSequenceInput, $gridRows, index, limit, $gridRow,
$itemSequenceInputClone;
this.parent('addFeatureHtml', $gridElement, options);
$itemSequenceInput = this.getSequenceInput_();
$gridRows = this.gridHandler.getRows();
for (index = 0, limit = $gridRows.length; index < limit; index++) {
$gridRow = $($gridRows[index]);
$itemSequenceInputClone = $itemSequenceInput.clone();
$('td.first_column', $gridRow).append($itemSequenceInputClone);
}
};
/**
* Set up the sortable plugin.
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.
setupSortablePlugin = function() {
this.applySortPlgOnElements(
this.getGridHtmlElement(), 'tr.orderable', null);
};
//
// Extended methods from ToggleableOrderItemsFeature.
//
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.init =
function() {
this.parent('init');
this.toggleItemsDragMode();
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.toggleState =
function(isOrdering) {
this.parent('toggleState', isOrdering);
this.toggleContentHandlers_();
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.storeRowOrder =
function(index, $row) {
var seq = index + 1,
$orderableInput = $row.find('.itemSequence'),
$modifiedInput;
$orderableInput.attr('value', seq);
$modifiedInput = $row.find('.isModified');
$modifiedInput.attr('value', 1);
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.saveOrderHandler =
function() {
this.parent('saveOrderHandler');
this.toggleState(false);
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.
updateOrderCallback = function(contextElement, event, ui) {
var $rows;
this.parent('updateOrderCallback');
$rows = this.gridHandler.getRows();
this.storeOrder($rows);
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.
clickOrderHandler = function() {
var $selects = $('select:visible', this.getGridHtmlElement()),
index, limit;
if ($selects.length > 0) {
for (index = 0, limit = $selects.length; index < limit; index++) {
this.gridHandler.saveRow($($selects[index]).parents('.gridRow'));
}
}
return /** @type {boolean} */ (this.parent('clickOrderHandler'));
};
//
// Implemented Feature template hook methods.
//
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.addElement =
function($newElement) {
this.parent('addElement', $newElement);
this.formatAndStoreNewRow_($newElement);
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.replaceElement =
function($newContent) {
this.parent('replaceElement', $newContent);
this.formatAndStoreNewRow_($newContent);
return false;
};
//
// Private helper methods.
//
/**
* Get the sequence input html element.
* @private
* @return {jQueryObject} Sequence input.
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.
getSequenceInput_ = function() {
return $('<input type="hidden" name="newRowId[sequence]" ' +
'class="itemSequence" />');
};
/**
* Enable/disable row content handlers.
* @private
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.
toggleContentHandlers_ = function() {
var $rows = this.gridHandler.getRows(),
index, limit, $row;
for (index = 0, limit = $rows.length; index < limit; index++) {
$row = $($rows[index]);
if (this.isOrdering) {
$row.find('.gridCellDisplay').unbind('click');
} else {
this.gridHandler.attachContentHandlers_($row);
}
}
};
/**
* Format and store new row.
* @private
* @param {jQueryObject} $row The new row element.
*/
$.pkp.classes.features.OrderListbuilderItemsFeature.prototype.
formatAndStoreNewRow_ = function($row) {
var $rows;
$row.children().after(this.getSequenceInput_());
$rows = this.gridHandler.getRows();
this.storeOrder($rows);
};
}(jQuery));
@@ -0,0 +1,247 @@
/**
* @file js/classes/features/PagingFeature.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 PagingFeature
* @ingroup js_classes_features
*
* @brief Feature that implements paging on grids.
*/
(function($) {
/**
* @constructor
* @inheritDoc
* @extends $.pkp.classes.features.GeneralPagingFeature
*/
$.pkp.classes.features.PagingFeature =
function(gridHandler, options) {
this.parent(gridHandler, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.features.PagingFeature,
$.pkp.classes.features.GeneralPagingFeature);
/**
* @inheritDoc
*/
$.pkp.classes.features.PagingFeature.prototype.init =
function() {
this.configPagingLinks_();
this.configItemsPerPageElement_();
};
/**
* @inheritDoc
*/
$.pkp.classes.features.PagingFeature.prototype.addFeatureHtml =
function($gridElement, options) {
$gridElement.append(options.pagingMarkup);
};
//
// Hooks implementation.
//
/**
* @inheritDoc
*/
$.pkp.classes.features.PagingFeature.prototype.resequenceRows =
function(sequenceMap) {
var $rows = this.gridHandler.getRows(),
extraRowsNum, index,
options = this.getOptions();
// Clean any extra rows that might still be visible from old range data.
extraRowsNum = $rows.length - options.currentItemsPerPage;
if (extraRowsNum > 0) {
for (index = 0; index < extraRowsNum; index++) {
this.gridHandler.deleteElement($rows.first(), true);
}
}
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.PagingFeature.prototype.refreshGrid =
function(opt_elementId) {
var options = this.getOptions(), params, $firstRow, $lastRow;
params = this.gridHandler.getFetchExtraParams();
params[options.pageParamName] = options.currentPage;
params[options.itemsPerPageParamName] = options.currentItemsPerPage;
$firstRow = this.gridHandler.getRows().first();
$lastRow = this.gridHandler.getRows().last();
if ($firstRow.length == 0) {
params.topLimitRowId = 0;
} else {
params.topLimitRowId = this.gridHandler.getRowDataId($firstRow);
}
if ($lastRow.length == 0) {
params.bottomLimitRowId = 0;
} else {
params.bottomLimitRowId = this.gridHandler.getRowDataId($lastRow);
}
this.setGridParams(params);
return false;
};
/**
* @inheritDoc
*/
$.pkp.classes.features.PagingFeature.prototype.replaceElementResponseHandler =
function(handledJsonData) {
var rowMarkup, rowDataId, pagingInfo, options, $rows, castJsonData;
options = this.getOptions();
castJsonData = /** @type {{deletedRowReplacement: string,
pagingInfo: string,
loadLastPage: boolean,
newTopRow: string}} */
(handledJsonData);
if (castJsonData.deletedRowReplacement != undefined) {
rowMarkup = handledJsonData.deletedRowReplacement;
this.gridHandler.insertOrReplaceElement(rowMarkup);
}
if (castJsonData.pagingInfo != undefined) {
pagingInfo = handledJsonData.pagingInfo;
this.setOptions(pagingInfo);
this.gridHandler.replacePartialWith(pagingInfo.pagingMarkup,
$('div.gridPaging', this.getGridHtmlElement()));
this.init();
}
if (castJsonData.loadLastPage) {
this.getGridHtmlElement().trigger('dataChanged');
}
if (castJsonData.newTopRow != undefined) {
// Check if we need to remove one row from the bottom
// to keep the same range info count value.
$rows = this.gridHandler.getRows();
if (options.currentItemsPerPage == $rows.length) {
this.gridHandler.deleteElement($rows.last(), true);
}
rowMarkup = handledJsonData.newTopRow;
this.gridHandler.insertOrReplaceElement(rowMarkup, true);
}
return false;
};
//
// Private helper methods.
//
/**
* Configure paging links.
*
* @private
*/
$.pkp.classes.features.PagingFeature.prototype.configPagingLinks_ =
function() {
var options, $pagingDiv, $links, index, limit, $link, regex, match,
clickPagesCallback;
options = this.getOptions();
$pagingDiv = $('div.gridPaging', this.getGridHtmlElement());
if ($pagingDiv) {
clickPagesCallback = this.callbackWrapper(
function(sourceElement, event) {
regex = new RegExp('[?&]' + options.pageParamName +
'(?:=([^&]*))?', 'i');
match = regex.exec($(event.target).attr('href'));
if (match != null) {
options.currentPage = parseInt(match[1], 10);
this.getGridHtmlElement().trigger('dataChanged');
}
// Stop event handling.
return false;
}, this);
$links = $pagingDiv.find('a').
not('.showMoreItems').not('.showLessItems');
for (index = 0, limit = $links.length; index < limit; index++) {
$link = $($links[index]);
$link.click(clickPagesCallback);
}
}
};
/**
* Configure items per page element.
*
* @private
*/
$.pkp.classes.features.PagingFeature.prototype.configItemsPerPageElement_ =
function() {
var options, $pagingDiv, index, limit, $select, itemsPerPageValues,
changeItemsPerPageCallback;
options = this.getOptions();
$pagingDiv = $('div.gridPaging', this.getGridHtmlElement());
if ($pagingDiv) {
changeItemsPerPageCallback = this.callbackWrapper(
function(sourceElement, event) {
options.currentItemsPerPage = parseInt($('option',
event.target).filter(':selected').attr('value'), 10);
// Reset to first page.
options.currentPage = 1;
this.getGridHtmlElement().trigger('dataChanged');
// Stop event handling.
return false;
}, this);
$select = $pagingDiv.find('select.itemsPerPage');
itemsPerPageValues = [10, 25, 50, 75, 100];
if ($.inArray(options.defaultItemsPerPage,
itemsPerPageValues) < 0) {
itemsPerPageValues.push(options.defaultItemsPerPage);
}
itemsPerPageValues.sort(function(a, b) { return a - b; });
if (options.itemsTotal <= itemsPerPageValues[0]) {
$('div.gridItemsPerPage', $pagingDiv).hide();
} else {
limit = itemsPerPageValues.length - 1;
for (index = 0; index <= limit; index++) {
$select.append($('<option value="' + itemsPerPageValues[index] +
'">' + itemsPerPageValues[index] + '</option>'));
}
$select.val(options.currentItemsPerPage.toString());
$select.change(changeItemsPerPageCallback);
}
}
};
}(jQuery));
@@ -0,0 +1,79 @@
/**
* @file js/classes/linkAction/AjaxRequest.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 AjaxRequest
* @ingroup js_classes_linkAction
*
* @brief AJAX link action request.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.linkAction.LinkActionRequest
*
* @param {jQueryObject} $linkActionElement The element the link
* action was attached to.
* @param {{
* requestType: string,
* data: PlainObject
* }} options Configuration of the link action
* request.
*/
$.pkp.classes.linkAction.AjaxRequest =
function($linkActionElement, options) {
this.parent($linkActionElement, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.linkAction.AjaxRequest,
$.pkp.classes.linkAction.LinkActionRequest);
//
// Public methods
//
/**
* @inheritDoc
*/
$.pkp.classes.linkAction.AjaxRequest.prototype.activate =
function(element, event) {
var returnValue = /** @type {boolean} */ (
this.parent('activate', element, event)),
options = this.getOptions(),
responseHandler = $.pkp.classes.Helper.curry(
this.handleResponse, this);
switch (options.requestType) {
case 'get':
$.getJSON(options.url, options.data, responseHandler);
break;
case 'post':
$.post(options.url, options.data, responseHandler, 'json');
break;
}
return returnValue;
};
/**
* Handle the AJAX response.
* @param {Object} jsonData The data returned by the server.
*/
$.pkp.classes.linkAction.AjaxRequest.prototype.handleResponse =
function(jsonData) {
var $linkActionHandler = this.getLinkActionElement().data('pkp.handler');
$linkActionHandler.handleJson(jsonData);
this.finish();
};
}(jQuery));
@@ -0,0 +1,57 @@
/**
* @file js/classes/linkAction/EventAction.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 EventAction
* @ingroup js_classes_linkAction
*
* @brief A simple action request that triggers a Javascript event.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.linkAction.LinkActionRequest
*
* @param {jQueryObject} $linkActionElement The element the link
* action was attached to.
* @param {Object} options Configuration of the link action
* request.
*/
$.pkp.classes.linkAction.EventAction =
function($linkActionElement, options) {
this.parent($linkActionElement, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.linkAction.EventAction,
$.pkp.classes.linkAction.LinkActionRequest);
//
// Public methods
//
/**
* @inheritDoc
*/
$.pkp.classes.linkAction.EventAction.prototype.activate =
function(element, event) {
$(this.options.target).trigger(this.options.event,
/** @type {Array} */ (this.options));
return /** @type {boolean} */ (this.parent('activate', element, event));
};
/**
* Determine whether or not the link action should be debounced.
* @return {boolean} Whether or not to debounce the link action.
*/
$.pkp.classes.linkAction.EventAction.prototype.shouldDebounce =
function() {
return false;
};
}(jQuery));
@@ -0,0 +1,157 @@
/**
* @defgroup js_classes_linkAction
*/
/**
* @file js/classes/linkAction/LinkActionRequest.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 LinkActionRequest
* @ingroup js_classes_linkAction
*
* @brief Base class for all link action requests.
*/
(function($) {
/** @type {Object} */
$.pkp.classes.linkAction = $.pkp.classes.linkAction || {};
/**
* @constructor
*
* @extends $.pkp.classes.ObjectProxy
*
* @param {jQueryObject} $linkActionElement The element the link
* action was attached to.
* @param {Object} options Configuration of the link action
* request.
*/
$.pkp.classes.linkAction.LinkActionRequest =
function($linkActionElement, options) {
// Save the reference to the link action element.
this.$linkActionElement = $linkActionElement;
// Save the link action request options.
this.options = options;
// If the link action element is an actual link
// and we find a URL in the options then set the
// link of the link action for better documentation
// and easier debugging in the DOM and for other
// JS to easily access the target if required.
if ($linkActionElement.is('a') && options.url) {
$linkActionElement.attr('href', options.url);
}
};
//
// Protected properties
//
/**
* The element the link action was attached to.
* @protected
* @type {jQueryObject}
*/
$.pkp.classes.linkAction.LinkActionRequest.prototype.
$linkActionElement = null;
/**
* The link action request options.
* @protected
* @type {Object}
*/
$.pkp.classes.linkAction.LinkActionRequest.prototype.options = null;
//
// Public methods
//
/**
* Callback that will be bound to the link action element.
* @param {HTMLElement} element 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.classes.linkAction.LinkActionRequest.prototype.activate =
function(element, event) {
this.getLinkActionElement().trigger('actionStart');
return false;
};
/**
* Callback that will be bound to the 'action finished' event of the
* link action.
*
* @return {boolean} Should return false to stop event propagation.
*/
$.pkp.classes.linkAction.LinkActionRequest.prototype.finish =
function() {
// Execute the finish callback if there is one.
if (this.options.finishCallback) {
this.options.finishCallback();
}
this.getLinkActionElement().trigger('actionStop');
return false;
};
/**
* Get the link action request url.
* @return {?string} The link action request url.
*/
$.pkp.classes.linkAction.LinkActionRequest.prototype.getUrl =
function() {
if (this.options.url) {
return this.options.url;
} else {
return null;
}
};
//
// Protected methods
//
/**
* Retrieve the link action request options.
* @return {Object} The link action request options.
*/
$.pkp.classes.linkAction.LinkActionRequest.prototype.getOptions = function() {
return this.options;
};
/**
* Retrieve the element the link action was attached to.
* @return {jQueryObject} The element the link action was attached to.
*/
$.pkp.classes.linkAction.LinkActionRequest.prototype.
getLinkActionElement = function() {
return this.$linkActionElement;
};
/**
* Determine whether or not the link action should be debounced.
* @return {boolean} Whether or not to debounce the link action.
*/
$.pkp.classes.linkAction.LinkActionRequest.prototype.
shouldDebounce = function() {
return true;
};
}(jQuery));
@@ -0,0 +1,132 @@
/**
* @file js/classes/linkAction/ModalRequest.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 ModalRequest
* @ingroup js_classes_linkAction
*
* @brief Modal link action request.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.linkAction.LinkActionRequest
*
* @param {jQueryObject} $linkActionElement The element the link
* action was attached to.
* @param {{
* modalHandler: Object
* }} options Configuration of the link action
* request.
*/
$.pkp.classes.linkAction.ModalRequest =
function($linkActionElement, options) {
this.parent($linkActionElement, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.linkAction.ModalRequest,
$.pkp.classes.linkAction.LinkActionRequest);
//
// Private properties
//
/**
* A pointer to the modal HTML element.
* @private
* @type {jQueryObject}
*/
$.pkp.classes.linkAction.ModalRequest.prototype.$modal_ = null;
//
// Public methods
//
/**
* @inheritDoc
*/
$.pkp.classes.linkAction.ModalRequest.prototype.activate =
function(element, event) {
// If there is no title then try to retrieve a title
// from the calling element's text.
var modalOptions = this.getOptions(),
$handledElement = this.getLinkActionElement(),
title = $handledElement.text(),
uuid,
$linkActionElement,
linkActionHandler,
handlerOptions,
modalHandler;
if (modalOptions.title === undefined) {
if (title === '') {
// Try to retrieve a title from the link action element's
// title attribute.
title = $handledElement.attr('title');
}
modalOptions.title = title;
}
// Generate a unique ID.
uuid = $.pkp.classes.Helper.uuid();
// Instantiate the modal.
if (!modalOptions.modalHandler) {
throw new Error(['The "modalHandler" setting is required ',
'in a ModalRequest'].join(''));
}
// Make sure that all events triggered on the modal will be
// forwarded to the link action. This is necessary because the
// modal has to be created outside the regular DOM.
$linkActionElement = /** @type {jQueryObject} */ (
this.getLinkActionElement());
linkActionHandler = $.pkp.classes.Handler.getHandler($linkActionElement);
handlerOptions = $.extend(true,
{eventBridge: linkActionHandler.getStaticId()}, modalOptions);
this.$modal_ = $(
'<div id="' + uuid + '" ' +
'class="pkp_modal pkpModalWrapper" tabindex="-1"></div>')
.pkpHandler(modalOptions.modalHandler, handlerOptions);
// Subscribe to the modal handler's 'removed' event so that
// we can clean up.
modalHandler = $.pkp.classes.Handler.getHandler(this.$modal_);
modalHandler.bind('pkpRemoveHandler',
$.pkp.classes.Helper.curry(this.finish, this));
return /** @type {boolean} */ (this.parent('activate', element, event));
};
/**
* @inheritDoc
*/
$.pkp.classes.linkAction.ModalRequest.prototype.finish =
function() {
// A workaround for a bug in IE9-11 (and maybe others), whereby restoring
// the focus to the New Review Round tab causes the modal to be opened
// again. This hack effects accessibility and should be removed if/when we
// move away from jQueryUI tabs.
// See: https://github.com/pkp/pkp-lib/issues/2703
if (this.$linkActionElement.attr('id')
.indexOf('newRoundTabContainer') !== 0) {
// Put the focus back on the linkAction which launched the modal
this.$linkActionElement.focus();
}
this.$modal_.remove();
return /** @type {boolean} */ (this.parent('finish'));
};
}(jQuery));
@@ -0,0 +1,59 @@
/**
* @file js/classes/linkAction/NullAction.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 NullAction
* @ingroup js_classes_linkAction
*
* @brief A simple action request that doesn't.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.linkAction.LinkActionRequest
*
* @param {jQueryObject} $linkActionElement The element the link
* action was attached to.
* @param {Object} options Configuration of the link action
* request.
*/
$.pkp.classes.linkAction.NullAction =
function($linkActionElement, options) {
this.parent($linkActionElement, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.linkAction.NullAction,
$.pkp.classes.linkAction.LinkActionRequest);
//
// Public methods
//
/**
* @inheritDoc
*/
$.pkp.classes.linkAction.NullAction.prototype.activate =
function(element, event) {
return /** @type {boolean} */ (this.parent('activate', element, event));
};
/**
* Determine whether or not the link action should be debounced.
* @return {boolean} Whether or not to debounce the link action.
*/
$.pkp.classes.linkAction.NullAction.prototype.shouldDebounce =
function() {
return false;
};
}(jQuery));
@@ -0,0 +1,52 @@
/**
* @file js/classes/linkAction/OpenWindowRequest.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 OpenWindowRequest
* @ingroup js_classes_linkAction
*
* @brief A simple action request that will follow the given URL.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.linkAction.LinkActionRequest
*
* @param {jQueryObject} $linkActionElement The element the link
* action was attached to.
* @param {Object} options Configuration of the link action
* request.
*/
$.pkp.classes.linkAction.OpenWindowRequest =
function($linkActionElement, options) {
this.parent($linkActionElement, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.linkAction.OpenWindowRequest,
$.pkp.classes.linkAction.LinkActionRequest);
//
// Public methods
//
/**
* @inheritDoc
*/
$.pkp.classes.linkAction.OpenWindowRequest.prototype.activate =
function(element, event) {
var options = this.getOptions();
window.open(options.url);
return /** @type {boolean} */ (this.parent('activate', element, event));
};
}(jQuery));
@@ -0,0 +1,132 @@
/**
* @file js/classes/linkAction/PostAndRedirectRequest.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 PostAndRedirectRequest
* @ingroup js_classes_linkAction
*
* @brief An action request that will post data and then redirect, using two
* different urls. For both requests, it will post the passed data. If none is
* passed, then it will post nothing. You can provide a js event response for
* the first post request and it will be handled.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.linkAction.LinkActionRequest
*
* @param {jQueryObject} $linkActionElement The element the link
* action was attached to.
* @param {Object} options Configuration of the link action
* request.
*/
$.pkp.classes.linkAction.PostAndRedirectRequest =
function($linkActionElement, options) {
this.parent($linkActionElement, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.linkAction.PostAndRedirectRequest,
$.pkp.classes.linkAction.LinkActionRequest);
//
// Private properties
//
/**
* Post request response data.
* @private
* @type {?Object}
*/
$.pkp.classes.linkAction.PostAndRedirectRequest.prototype.
postJsonData_ = null;
//
// Public methods
//
/**
* @inheritDoc
*/
$.pkp.classes.linkAction.PostAndRedirectRequest.prototype.activate =
function(element, event) {
var returner = this.parent('activate', element, event),
options = this.getOptions(),
// Create a response handler for the first request (post).
responseHandler = $.pkp.classes.Helper.curry(
this.handleResponse_, this),
finishCallback;
// Post.
$.post(/** @type {{postUrl: string}} */ (options).postUrl,
responseHandler, 'json');
return /** @type {boolean} */ (returner);
};
//
// Private helper methods.
//
/**
* Callback to be called after a timeout.
* @private
*/
$.pkp.classes.linkAction.PostAndRedirectRequest.prototype.finishCallback_ =
function() {
var $linkActionElement = this.getLinkActionElement(),
// Get the link action handler to handle the json response.
linkActionHandler = $.pkp.classes.Handler.getHandler($linkActionElement);
this.finish();
linkActionHandler.handleJson(this.postJsonData_);
};
/**
* The post data response handler.
* @param {Object} jsonData A parsed JSON response object.
* @private
*/
$.pkp.classes.linkAction.PostAndRedirectRequest.prototype.handleResponse_ =
function(jsonData) {
var options = this.getOptions(), timer = null, finishCallback = null;
// Save return data to be handled at the finish callback. If
// the redirect action loads another page, then the interface
// will be updated anyway and any events that could be triggered
// by the post response will be useless, so that's ok that the
// finish callback is not called in that case, and that the post
// json answer is never handled.
// If a new page is not loaded, then we have to wait for the redirect
// action to start (probably a file download) and only then handle the post
// answer, avoiding triggering events that could replace the current link
// action element before the redirect request starts.
// In a download action, it avoids the activation of the download link
// action before the download triggered by the first click starts.
this.postJsonData_ = jsonData;
// Redirect, making sure there is no ajax request in progress,
// to avoid stoping them.
timer = setInterval(function() {
if ($.active == 0) {
clearInterval(timer);
window.location = /** @type {{url: string}} */ (options).url;
}
},100);
// When it's a download action, try to avoid double execution.
// Not ideal, see issue #247.
finishCallback = $.pkp.classes.Helper.curry(
this.finishCallback_, this);
setTimeout(finishCallback, 2000);
};
}(jQuery));
@@ -0,0 +1,53 @@
/**
* @file js/classes/linkAction/RedirectRequest.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 RedirectRequest
* @ingroup js_classes_linkAction
*
* @brief A simple action request that will follow the given URL.
*/
(function($) {
/**
* @constructor
*
* @extends $.pkp.classes.linkAction.LinkActionRequest
*
* @param {jQueryObject} $linkActionElement The element the link
* action was attached to.
* @param {Object} options Configuration of the link action
* request.
*/
$.pkp.classes.linkAction.RedirectRequest =
function($linkActionElement, options) {
this.parent($linkActionElement, options);
};
$.pkp.classes.Helper.inherits(
$.pkp.classes.linkAction.RedirectRequest,
$.pkp.classes.linkAction.LinkActionRequest);
//
// Public methods
//
/**
* @inheritDoc
*/
$.pkp.classes.linkAction.RedirectRequest.prototype.activate =
function(element, event) {
var options = this.getOptions();
window.open(options.url, options.name,
/** @type {{specs: string}} */ (options).specs);
return /** @type {boolean} */ (this.parent('activate', element, event));
};
}(jQuery));
@@ -0,0 +1,180 @@
/**
* @defgroup js_classes_notification
*/
/**
* @file js/classes/notification/NotificationHelper.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 NotificationHelper
* @ingroup js_classes_notification
*
* @brief Class that perform notification helper actions.
*/
(function($) {
/** @type {Object} */
$.pkp.classes.notification = $.pkp.classes.notification || {};
/**
* @constructor
*/
$.pkp.classes.notification.NotificationHelper = function() {
};
//
// Public static helper methods
//
/**
* Decides which notification will be used: in place or general.
* This method finds all notification widgets that are inside of the
* handled element of the controller that is handling the current
* notify user event (page or modal). We need to make sure that all
* notifications will be shown inside the same widget where the notify
* user was triggered. Beyond that, we also need to make sure that,
* inside some widgets (tabs and accordions, for example) we use the
* right notification controller.
* To do this, the notification element must follow these rules:
*
* 1 - the notification element must not have a hidden parent, although
* it can be hidden itself.
*
* 2 - the notification element first widget parent also needs to contain
* the element that triggered the notify user event.
*
* 3 - if the notification element is inside an accordion container, it
* will only notify user from events that have the trigger element also
* inside the accordion container.
*
* At the final, if this method find and select more than one
* notification element, we get the closest comparing to the element
* that triggered the event. If it don't find any visible element, it
* bubbles up the event so the site handler can show general
* notifications.
*
* @param {$.pkp.classes.Handler} handler The widget handler that is
* handling the notify user event.
* @param {HTMLElement|Object} triggerElement The element that triggered the
* notify user event.
*/
$.pkp.classes.notification.NotificationHelper.redirectNotifyUserEvent =
function(handler, triggerElement) {
// Get the selector for a notification element.
var $notificationSelector = '.pkp_notification',
$handledElement,
trivialAlreadyHandled,
$pageNotificationElements,
possibleNotificationWidgets,
i, length,
notificationsData,
$accordionContainer,
$element,
$elementParents, parentHandler,
j, parentsLength, $elementParentWidget;
// Sometimes the notification handler will bubble up
// the notifyUser event when in place notifications are
// not visible because of scrolling. When this happens, the
// trigger element will not be an element, but the notifications
// data that were shown by the in place but no visible. In those
// cases, just bubble up again the event until it gets the right
// handler (the site handler).
if (triggerElement !== undefined && triggerElement.content !== undefined) {
notificationsData = triggerElement;
handler.getHtmlElement().parent().trigger(
'notifyUser', [notificationsData]);
return; // no need to do any other event redirection.
}
// Get the html element of the handler.
$handledElement = handler.getHtmlElement();
// If the trigger element is inside a grid, let the site
// handler show TRIVIAL notifications.
trivialAlreadyHandled = false;
if (!(handler instanceof $.pkp.controllers.SiteHandler)) {
if (triggerElement !== undefined &&
$(triggerElement).parents('.pkp_controllers_grid').length > 0) {
$handledElement.parent().trigger('notifyUser');
trivialAlreadyHandled = true;
}
}
// Find all notification elements inside the handled element.
$pageNotificationElements = $($notificationSelector, $handledElement);
// Create a variable to store all possible notification widgets
// that can notify this event.
possibleNotificationWidgets = [];
for (i = 0, length = $pageNotificationElements.length; i < length; i++) {
$element = $($pageNotificationElements[i]);
// If it is inside a hidden parent, get next element.
if ($element.parents(':hidden').length > 0) {
continue;
}
// Find its parent widget.
// FIXME If we use a class to identify pkp widgets, we can avoid
// this code duplication from the get handler method in Handler class,
// unnecessary access to the element data and unnecessary loop.
$elementParents = $element.parents();
for (j = 0, parentsLength = $elementParents.length;
j < parentsLength; j++) {
parentHandler = $($elementParents[j]).data('pkp.handler');
if ((parentHandler instanceof $.pkp.classes.Handler)) {
$elementParentWidget = $($elementParents[j]);
break;
}
}
// If the element that triggered the event is inside of
// this widget or is the widget...
if (triggerElement !== undefined &&
($elementParentWidget.has(triggerElement[0]).length ||
$elementParentWidget[0] === triggerElement[0])) {
// If it is inside an accordion container, and this accordion container
// doesn't also contain the element that triggered the event, get other
// element.
if ($element.parents('.ui-accordion:first').length > 0) {
$accordionContainer = $element.parents('.ui-accordion:first');
if (!$accordionContainer.has(triggerElement[0])) {
continue;
}
}
// This notification element is able to notify this event.
possibleNotificationWidgets.push($element);
}
}
// Check if we found a notification element.
if (possibleNotificationWidgets.length) {
// Trigger all in place notification widgets found, from the
// closest to the element that triggered the action to the top.
for (i = possibleNotificationWidgets.length - 1; i > -1; i--) {
// Show in place notification to user.
possibleNotificationWidgets[i].triggerHandler('notifyUser');
}
} else {
if (!trivialAlreadyHandled) {
// Bubble up the notify user event so the site can handle the
// general notification.
handler.getHtmlElement().parent().trigger('notifyUser');
}
}
};
}(jQuery));