361 lines
10 KiB
JavaScript
361 lines
10 KiB
JavaScript
/*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));
|