first commit
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
/*global pkp */
|
||||
/**
|
||||
* @defgroup js_controllers_modal
|
||||
*/
|
||||
/**
|
||||
* @file js/controllers/modal/ModalHandler.js
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class ModalHandler
|
||||
* @ingroup js_controllers_modal
|
||||
*
|
||||
* @brief Basic modal implementation.
|
||||
*
|
||||
* A modal that has only one button and expects a simple message string.
|
||||
*/
|
||||
(function($) {
|
||||
|
||||
/** @type {Object} */
|
||||
$.pkp.controllers.modal = $.pkp.controllers.modal || { };
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @extends $.pkp.classes.Handler
|
||||
*
|
||||
* @param {jQueryObject} $handledElement The modal.
|
||||
* @param {Object.<string, *>} options The modal options.
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler = function($handledElement, options) {
|
||||
this.parent($handledElement, options);
|
||||
|
||||
// Check the options.
|
||||
if (!this.checkOptions(options)) {
|
||||
throw new Error('Missing or invalid modal options!');
|
||||
}
|
||||
|
||||
// Clone the options object before we manipulate them.
|
||||
var internalOptions = $.extend(true, {}, options),
|
||||
canClose;
|
||||
|
||||
// Merge user and default options.
|
||||
this.options = /** @type {{ canClose: boolean, textTitle: string,
|
||||
title: string, titleIcon: string,
|
||||
closeCleanVueInstances: Array }} */
|
||||
(this.mergeOptions(internalOptions));
|
||||
|
||||
// Attach content to the modal
|
||||
$handledElement.html(this.modalBuild()[0].outerHTML);
|
||||
|
||||
// Open the modal
|
||||
this.modalOpen($handledElement);
|
||||
|
||||
// Set up close controls
|
||||
$handledElement.find(
|
||||
'.pkpModalCloseButton').click(this.callbackWrapper(this.modalClose));
|
||||
$handledElement.on(
|
||||
'click keyup', this.callbackWrapper(this.handleWrapperEvents));
|
||||
|
||||
// Publish some otherwise private events triggered
|
||||
// by nested widgets so that they can be handled by
|
||||
// the element that opened the modal.
|
||||
this.publishEvent('redirectRequested');
|
||||
this.publishEvent('dataChanged');
|
||||
this.publishEvent('updateHeader');
|
||||
this.publishEvent('gridRefreshRequested');
|
||||
|
||||
this.bind('notifyUser', this.redirectNotifyUserEventHandler_);
|
||||
this.bindGlobal('form-success', this.onFormSuccess_);
|
||||
};
|
||||
$.pkp.classes.Helper.inherits($.pkp.controllers.modal.ModalHandler,
|
||||
$.pkp.classes.Handler);
|
||||
|
||||
|
||||
//
|
||||
// Private static properties
|
||||
//
|
||||
/**
|
||||
* Default options
|
||||
* @private
|
||||
* @type {Object}
|
||||
* @const
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.DEFAULT_OPTIONS_ = {
|
||||
autoOpen: true,
|
||||
width: 710,
|
||||
modal: true,
|
||||
draggable: false,
|
||||
resizable: false,
|
||||
position: {my: 'center', at: 'center center-10%', of: window},
|
||||
canClose: true,
|
||||
closeCallback: false,
|
||||
// Vue components to destroy when when modal is closed
|
||||
closeCleanVueInstances: []
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Public properties
|
||||
//
|
||||
/**
|
||||
* Current options
|
||||
*
|
||||
* After passed options are merged with defaults.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.options = null;
|
||||
|
||||
|
||||
//
|
||||
// Protected methods
|
||||
//
|
||||
/**
|
||||
* Check whether the correct options have been
|
||||
* given for this modal.
|
||||
* @protected
|
||||
* @param {Object.<string, *>} options Modal options.
|
||||
* @return {boolean} True if options are ok.
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.prototype.checkOptions =
|
||||
function(options) {
|
||||
|
||||
// Check for basic configuration requirements.
|
||||
return typeof options === 'object' &&
|
||||
(/** @type {{ buttons: Object }} */ (options)).buttons === undefined;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine the options based on
|
||||
* default options.
|
||||
* @protected
|
||||
* @param {Object.<string, *>} options Non-default modal options.
|
||||
* @return {Object.<string, *>} The default options merged
|
||||
* with the non-default options.
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.prototype.mergeOptions =
|
||||
function(options) {
|
||||
|
||||
// Merge the user options into the default options.
|
||||
var mergedOptions = $.extend(true, { },
|
||||
this.self('DEFAULT_OPTIONS_'), options);
|
||||
return mergedOptions;
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Public methods
|
||||
//
|
||||
/**
|
||||
* Build the markup for a modal container, including the header, close
|
||||
* button and a container for the content to be placed in.
|
||||
* TODO: This kind of markup probably shouldn't be embedded within the JS...
|
||||
*
|
||||
* @protected
|
||||
* @return {Object} jQuery object representing modal content
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.prototype.modalBuild =
|
||||
function() {
|
||||
|
||||
var $titleDiv, $modal = $('<div class="pkp_modal_panel"></div>');
|
||||
|
||||
// Title bar
|
||||
if (typeof(this.options.textTitle) !== 'undefined') {
|
||||
$titleDiv = $('<div class="header"/>').text(this.options.textTitle);
|
||||
$modal.append($titleDiv);
|
||||
} else if (typeof(this.options.title) !== 'undefined') {
|
||||
$modal.append('<div class="header">' + this.options.title + '</div>');
|
||||
} else {
|
||||
$modal.append('<div class="header">' + '</div>');
|
||||
}
|
||||
|
||||
// Close button
|
||||
if (this.options.canClose) {
|
||||
$modal.append(
|
||||
'<a href="#" class="close pkpModalCloseButton">' +
|
||||
'<span :aria-hidden="true">×</span>' +
|
||||
'<span class="pkp_screen_reader">' +
|
||||
(/** @type {{ closeButtonText: string }} */ (this.options))
|
||||
.closeButtonText + '</span></a>');
|
||||
}
|
||||
|
||||
// Content
|
||||
$modal.append('<div class="content"></div>');
|
||||
|
||||
// Add aria role and label
|
||||
$modal.attr('role', 'dialog')
|
||||
.attr('aria-label', this.options.title);
|
||||
|
||||
return $modal;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attach a modal to the dom and make it visible
|
||||
* @param {jQueryObject} $handledElement The modal.
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.prototype.modalOpen =
|
||||
function($handledElement) {
|
||||
|
||||
// The $handledElement must be attached to the DOM before events will
|
||||
// bubble up to SiteHandler
|
||||
var $body = $('body');
|
||||
$body.append($handledElement);
|
||||
|
||||
// Trigger visibility state change on the next tick, so that CSS
|
||||
// transform animations will run
|
||||
setTimeout(function() {
|
||||
$handledElement.addClass('is_visible');
|
||||
},10);
|
||||
|
||||
// Set focus to the modal. Leave a sizeable delay here so that the
|
||||
// element can be added to the dom first
|
||||
setTimeout(function() {
|
||||
$handledElement.focus();
|
||||
}, 300);
|
||||
|
||||
// Trigger events
|
||||
$handledElement.trigger('pkpModalOpen', [$handledElement]);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Close the modal. Typically invoked via an event of some kind, such as
|
||||
* a `click` or `keyup`
|
||||
*
|
||||
* @param {Object=} opt_callingContext The calling element or object.
|
||||
* @param {Event=} opt_event The triggering event (e.g. a click on
|
||||
* a close button. Not set if called via callback.
|
||||
* @return {boolean} Should return false to stop event processing.
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.prototype.modalClose =
|
||||
function(opt_callingContext, opt_event) {
|
||||
|
||||
var modalHandler = this,
|
||||
$modalElement = this.getHtmlElement(),
|
||||
$form = $modalElement.find('form').first(),
|
||||
handler, informationObject;
|
||||
|
||||
// Unregister a form if attached to this modalElement
|
||||
// modalClose is called on both 'cancel' and 'close' events. With
|
||||
// callbacks both callingContext and event are undefined. So,
|
||||
// unregister this form with SiteHandler.
|
||||
if ($form.length == 1) {
|
||||
informationObject = {closePermitted: true};
|
||||
$form.trigger('containerClose', [informationObject]);
|
||||
if (!informationObject.closePermitted) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the modal, clean up any mounted vue instances, remove it from the
|
||||
// DOM and remove the handler once the CSS animation is complete
|
||||
$modalElement.removeClass('is_visible');
|
||||
this.trigger('pkpModalClose');
|
||||
setTimeout(function() {
|
||||
var vueInstances = modalHandler.options.closeCleanVueInstances,
|
||||
instance,
|
||||
i,
|
||||
id;
|
||||
if (vueInstances.length) {
|
||||
for (i = 0; i < vueInstances.length; i++) {
|
||||
id = vueInstances[i];
|
||||
if (typeof pkp.registry._instances[id] !== 'undefined') {
|
||||
instance = /** @type {{ $destroy: Function }} */
|
||||
(pkp.registry._instances[id]);
|
||||
instance.$destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
modalHandler.unbindPartial($modalElement);
|
||||
$modalElement.empty();
|
||||
modalHandler.remove();
|
||||
// Fire a callback function if one has been passed with options
|
||||
if (typeof modalHandler.options.closeCallback === 'function') {
|
||||
modalHandler.options.closeCallback.call();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Process events that reach the wrapper element.
|
||||
* Should NOT block other events from bubbling up. Doing so
|
||||
* can disable submit buttons in nested forms.
|
||||
*
|
||||
* @param {Object=} opt_callingContext The calling element or object.
|
||||
* @param {Event=} opt_event The triggering event (e.g. a click on
|
||||
* a close button. Not set if called via callback.
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.prototype.handleWrapperEvents =
|
||||
function(opt_callingContext, opt_event) {
|
||||
|
||||
// Close click events directly on modal (background screen)
|
||||
if (opt_event.type == 'click' && opt_callingContext == opt_event.target) {
|
||||
$.pkp.classes.Handler.getHandler($(opt_callingContext))
|
||||
.modalClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close for ESC keypresses (27) that have bubbled up
|
||||
if (opt_event.type == 'keyup' && opt_event.which == 27) {
|
||||
$.pkp.classes.Handler.getHandler($(opt_callingContext))
|
||||
.modalClose();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Private methods
|
||||
//
|
||||
/**
|
||||
* Handler to redirect to the correct notification widget the
|
||||
* notify user event.
|
||||
* @param {HTMLElement} sourceElement The element that issued the
|
||||
* "notifyUser" event.
|
||||
* @param {Event} event The "notify user" event.
|
||||
* @param {HTMLElement} triggerElement The element that triggered
|
||||
* the "notifyUser" event.
|
||||
* @private
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.prototype.redirectNotifyUserEventHandler_ =
|
||||
function(sourceElement, event, triggerElement) {
|
||||
|
||||
// Use the notification helper to redirect the notify user event.
|
||||
$.pkp.classes.notification.NotificationHelper.
|
||||
redirectNotifyUserEvent(this, triggerElement);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler to listen to global form success events, and close when an event
|
||||
* from a child form has been fired, and this form matches the config id
|
||||
*
|
||||
* @param {Object} source The Vue.js component which fired the event
|
||||
* @param {Object} formId The form component's id prop
|
||||
* @private
|
||||
*/
|
||||
$.pkp.controllers.modal.ModalHandler.prototype.onFormSuccess_ =
|
||||
function(source, formId) {
|
||||
if (this.options.closeOnFormSuccessId &&
|
||||
this.options.closeOnFormSuccessId === formId) {
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.modalClose();
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}(jQuery));
|
||||
Reference in New Issue
Block a user