256 lines
9.2 KiB
JavaScript
256 lines
9.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) 2019 Open Assessment Technologies SA ;
|
|
*/
|
|
/**
|
|
* Track the change within the itemCreator
|
|
* @author Bertrand Chevrier <bertrand@taotesting.com>
|
|
* @author Jean-Sébastien Conan <jean-sebastien@taotesting.com>
|
|
*/
|
|
define([
|
|
'jquery',
|
|
'lodash',
|
|
'i18n',
|
|
'lib/uuid',
|
|
'core/eventifier',
|
|
'ui/dialog',
|
|
'taoQtiItem/qtiCreator/helper/saveChanges'
|
|
], function ($, _, __, uuid, eventifier, dialog, saveChanges) {
|
|
'use strict';
|
|
|
|
/**
|
|
* The messages asking to save
|
|
*/
|
|
const messages = {
|
|
preview: __('The item needs to be saved before it can be previewed'),
|
|
leave: __('The item has unsaved changes, are you sure you want to leave ?'),
|
|
exit: __('The item has unsaved changes, would you like to save it ?')
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {HTMLElement} container
|
|
* @param {itemCreator} itemCreator
|
|
* @param {String} [wrapperSelector]
|
|
* @returns {changeTracker}
|
|
* @fires stylechange when the item's style changed
|
|
*/
|
|
function changeTrackerFactory(container, itemCreator, wrapperSelector = 'body') {
|
|
let changeTracker;
|
|
|
|
// internal namespace for global registered events
|
|
const eventNS = `.ct-${uuid(8, 16)}`;
|
|
|
|
// keep the value of the item before changes
|
|
let originalItem;
|
|
|
|
// does the item styles have changed
|
|
let styleChanges = false;
|
|
|
|
// are we in the middle of the confirm process ?
|
|
let asking = false;
|
|
|
|
// take care of the change in item style
|
|
const onStyleChange = (e, detail) => {
|
|
if (!detail || !detail.initializing) {
|
|
styleChanges = true;
|
|
/**
|
|
* Change in item style
|
|
* @event stylechange
|
|
*/
|
|
changeTracker.trigger('stylechange');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @typedef {Object} changeTracker
|
|
*/
|
|
changeTracker = eventifier({
|
|
/**
|
|
* Initialized the changed state
|
|
* @returns {changeTracker}
|
|
*/
|
|
init() {
|
|
originalItem = this.getSerializedItem();
|
|
styleChanges = false;
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Installs the change tracker, registers listeners
|
|
* @returns {changeTracker}
|
|
*/
|
|
install() {
|
|
this.init();
|
|
asking = false;
|
|
|
|
// track style changes
|
|
$(window.document)
|
|
.one('customcssloaded.styleeditor', () => this.init())
|
|
.on('stylechange.qti-creator', onStyleChange);
|
|
|
|
// 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 (except feedback message)
|
|
$(wrapperSelector).on(`click${eventNS}`, e => {
|
|
if (
|
|
!$.contains(container, e.target) &&
|
|
!$(e.target).parents('#feedback-box').length &&
|
|
!$(e.target).hasClass('icon-close') &&
|
|
this.hasChanged()
|
|
) {
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
|
|
this.confirmBefore('exit')
|
|
.then(() => {
|
|
// @todo improve this:
|
|
// When clicking outside, and accepting the confirm dialog (one way or another),
|
|
// the tracker is disabled, and changes won't be detected anymore. So it could be
|
|
// an issue if the click was not triggering any move.
|
|
this.uninstall();
|
|
e.target.click();
|
|
})
|
|
//do nothing but prevent uncaught error
|
|
.catch(() => {});
|
|
}
|
|
});
|
|
|
|
itemCreator
|
|
.on(`ready${eventNS} saved${eventNS}`, () => this.init())
|
|
.before(`exit${eventNS}`, () => this.confirmBefore('exit').then(() => this.uninstall()))
|
|
.before(`preview${eventNS}`, () => this.confirmBefore('preview'));
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Uninstalls the change tracker, unregisters listeners
|
|
* @returns {changeTracker}
|
|
*/
|
|
uninstall() {
|
|
// remove all global handlers
|
|
$(window.document).off(eventNS).off('stylechange.qti-creator', onStyleChange);
|
|
$(window).off(eventNS);
|
|
$(wrapperSelector).off(eventNS);
|
|
itemCreator.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: () => saveChanges(itemCreator).then(resolve).catch(reject),
|
|
onDontsaveBtn: resolve,
|
|
onCancelBtn: () => {
|
|
confirmDlg.hide();
|
|
reject({ cancel: true });
|
|
}
|
|
}).on('closed.modal', () => (asking = false));
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Does the item have changed?
|
|
* @returns {Boolean}
|
|
*/
|
|
hasChanged() {
|
|
if (styleChanges) {
|
|
return true;
|
|
}
|
|
const currentItem = this.getSerializedItem();
|
|
return originalItem !== currentItem || (null === currentItem && null === originalItem);
|
|
},
|
|
|
|
/**
|
|
* Get a string representation of the current item, used for comparison
|
|
* @returns {String} the item
|
|
*/
|
|
getSerializedItem() {
|
|
let serialized = '';
|
|
try {
|
|
// create a string from the item content
|
|
serialized = JSON.stringify(itemCreator.getItem().toArray());
|
|
|
|
// 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;
|
|
});
|