243 lines
8.2 KiB
JavaScript
243 lines
8.2 KiB
JavaScript
|
/**
|
||
|
* This program is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public License
|
||
|
* as published by the Free Software Foundation; under version 2
|
||
|
* of the License (non-upgradable).
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program; if not, write to the Free Software
|
||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
*
|
||
|
* Copyright (c) 2020 Open Assessment Technologies SA ;
|
||
|
*/
|
||
|
/**
|
||
|
* Track the change within the testCreator
|
||
|
* @author Juan Luis Gutierrez Dos Santos <juanluis.gutierrezdossantos@taotesting.com>
|
||
|
*/
|
||
|
define([
|
||
|
'jquery',
|
||
|
'lodash',
|
||
|
'i18n',
|
||
|
'lib/uuid',
|
||
|
'core/eventifier',
|
||
|
'ui/dialog'
|
||
|
], function (
|
||
|
$,
|
||
|
_,
|
||
|
__,
|
||
|
uuid,
|
||
|
eventifier,
|
||
|
dialog
|
||
|
) {
|
||
|
'use strict';
|
||
|
|
||
|
/**
|
||
|
* The messages asking to save
|
||
|
*/
|
||
|
const messages = {
|
||
|
preview: __('The test needs to be saved before it can be previewed.'),
|
||
|
leave: __('The test has unsaved changes, are you sure you want to leave?'),
|
||
|
exit: __('The test has unsaved changes, would you like to save it?')
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param {HTMLElement} container
|
||
|
* @param {testCreator} testCreator
|
||
|
* @param {String} [wrapperSelector]
|
||
|
* @returns {changeTracker}
|
||
|
*/
|
||
|
function changeTrackerFactory(container, testCreator, wrapperSelector = 'body') {
|
||
|
|
||
|
// internal namespace for global registered events
|
||
|
const eventNS = `.ct-${uuid(8, 16)}`;
|
||
|
|
||
|
// keep the value of the test before changes
|
||
|
let originalTest;
|
||
|
|
||
|
// are we in the middle of the confirm process?
|
||
|
let asking = false;
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} changeTracker
|
||
|
*/
|
||
|
const changeTracker = eventifier({
|
||
|
/**
|
||
|
* Initialized the changed state
|
||
|
* @returns {changeTracker}
|
||
|
*/
|
||
|
init() {
|
||
|
originalTest = this.getSerializedTest();
|
||
|
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Installs the change tracker, registers listeners
|
||
|
* @returns {changeTracker}
|
||
|
*/
|
||
|
install() {
|
||
|
this.init();
|
||
|
asking = false;
|
||
|
|
||
|
// add a browser popup to prevent leaving the browser
|
||
|
$(window)
|
||
|
.on(`beforeunload${eventNS}`, () => {
|
||
|
if (!asking && this.hasChanged()) {
|
||
|
return messages.leave;
|
||
|
}
|
||
|
})
|
||
|
// since we don't know how to prevent history based events, we just stop the handling
|
||
|
.on('popstate', this.uninstall);
|
||
|
|
||
|
// every click outside the authoring
|
||
|
$(wrapperSelector)
|
||
|
.on(`click${eventNS}`, e => {
|
||
|
if (!$.contains(container, e.target) && this.hasChanged()) {
|
||
|
e.stopImmediatePropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
this.confirmBefore('exit')
|
||
|
.then(whatToDo => {
|
||
|
this.ifWantSave(whatToDo);
|
||
|
this.uninstall();
|
||
|
e.target.click();
|
||
|
})
|
||
|
//do nothing but prevent uncaught error
|
||
|
.catch(() => {});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
testCreator
|
||
|
.on(`ready${eventNS} saved${eventNS}`, () => this.init())
|
||
|
.before(`creatorclose${eventNS}`, () => this.confirmBefore('exit').then(whatToDo => {
|
||
|
this.ifWantSave(whatToDo);
|
||
|
}))
|
||
|
.before(`preview${eventNS}`, () => this.confirmBefore('preview').then(whatToDo => {
|
||
|
this.ifWantSave(whatToDo);
|
||
|
}))
|
||
|
.before(`exit${eventNS}`, () => this.uninstall());
|
||
|
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Check if we need to trigger save
|
||
|
* @param {Object} whatToDo
|
||
|
* @fires {save}
|
||
|
*/
|
||
|
ifWantSave(whatToDo) {
|
||
|
if (whatToDo && whatToDo.ifWantSave) {
|
||
|
testCreator.trigger('save');
|
||
|
}
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Uninstalls the change tracker, unregisters listeners
|
||
|
* @returns {changeTracker}
|
||
|
*/
|
||
|
uninstall() {
|
||
|
// remove all global handlers
|
||
|
$(window.document)
|
||
|
.off(eventNS);
|
||
|
$(window).off(eventNS);
|
||
|
$(wrapperSelector).off(eventNS);
|
||
|
testCreator.off(eventNS);
|
||
|
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Displays a confirmation dialog,
|
||
|
* The "ok" button will save and resolve.
|
||
|
* The "cancel" button will reject.
|
||
|
*
|
||
|
* @param {String} message - the confirm message to display
|
||
|
* @returns {Promise} resolves once saved
|
||
|
*/
|
||
|
confirmBefore(message) {
|
||
|
// if a key is given load the related message
|
||
|
message = messages[message] || message;
|
||
|
|
||
|
return new Promise((resolve, reject) => {
|
||
|
if (asking) {
|
||
|
return reject();
|
||
|
}
|
||
|
|
||
|
if (!this.hasChanged()) {
|
||
|
return resolve();
|
||
|
}
|
||
|
|
||
|
asking = true;
|
||
|
|
||
|
const confirmDlg = dialog({
|
||
|
message: message,
|
||
|
buttons: [{
|
||
|
id: 'dontsave',
|
||
|
type: 'regular',
|
||
|
label: __('Don\'t save'),
|
||
|
close: true
|
||
|
}, {
|
||
|
id: 'cancel',
|
||
|
type: 'regular',
|
||
|
label: __('Cancel'),
|
||
|
close: true
|
||
|
}, {
|
||
|
id: 'save',
|
||
|
type: 'info',
|
||
|
label: __('Save'),
|
||
|
close: true
|
||
|
}],
|
||
|
autoRender: true,
|
||
|
autoDestroy: true,
|
||
|
onSaveBtn: () => resolve({ ifWantSave: true }),
|
||
|
onDontsaveBtn: () => resolve({ ifWantSave: false }),
|
||
|
onCancelBtn: () => {
|
||
|
confirmDlg.hide();
|
||
|
reject({ cancel: true });
|
||
|
}
|
||
|
})
|
||
|
.on('closed.modal', () => asking = false);
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Does the test have changed?
|
||
|
* @returns {Boolean}
|
||
|
*/
|
||
|
hasChanged() {
|
||
|
const currentTest = this.getSerializedTest();
|
||
|
return originalTest !== currentTest || (null === currentTest && null === originalTest);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Get a string representation of the current test, used for comparison
|
||
|
* @returns {String} the test
|
||
|
*/
|
||
|
getSerializedTest() {
|
||
|
let serialized = '';
|
||
|
try {
|
||
|
// create a string from the test content
|
||
|
serialized = JSON.stringify(testCreator.getModelOverseer().getModel());
|
||
|
|
||
|
// sometimes the creator strip spaces between tags, so we do the same
|
||
|
serialized = serialized.replace(/ {2,}/g, ' ');
|
||
|
} catch (err) {
|
||
|
serialized = null;
|
||
|
}
|
||
|
return serialized;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return changeTracker.install();
|
||
|
}
|
||
|
|
||
|
return changeTrackerFactory;
|
||
|
});
|