Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Fix bug where a confirmation message wasn't displayed when leaving th…
…e PortalEditor.

The way we were setting the beforeunload event listener has since been 
deprecated, so it may not have been working in all browsers. (See 
https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload)
Moved some of this functionality to the new parent EditorView, since it 
is shared funcitnality between the EML and Portal editors.
Closes #1222
  • Loading branch information
laurenwalker committed Dec 19, 2019
1 parent 4f6b480 commit 766aa80
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 47 deletions.
32 changes: 22 additions & 10 deletions src/js/views/AppView.js
Expand Up @@ -124,16 +124,28 @@ define(['jquery',
// close the current view
if (this.currentView){

if( typeof this.currentView.confirmClose == "function" ){
var confirmMsg = this.currentView.confirmClose();

if(confirmMsg){
var leave = confirm(confirmMsg);
if( !leave ){
MetacatUI.uiRouter.undoLastRoute();
return;
}
}
//If the current view has a function to confirm closing of the view, call it
if( typeof this.currentView.canClose == "function" ){

//If the user or view confirmed that the view shouldn't be closed, then don't navigate to the next route
if( !this.currentView.canClose() ){

//Get a confirmation message from the view, or use a default one
if( typeof this.currentView.getConfirmCloseMessage == "function" ){
var confirmMessage = this.currentView.getConfirmCloseMessage();
}
else{
var confirmMessage = "Leave this page?";
}

//Show a confirm alert to the user and wait for their response
var leave = confirm(confirmMessage);
//If they clicked Cancel, then don't navigate to the next route
if(!leave){
MetacatUI.uiRouter.undoLastRoute();
return;
}
}
}

// need reference to the old/current view for the callback method
Expand Down
80 changes: 79 additions & 1 deletion src/js/views/EditorView.js
Expand Up @@ -7,6 +7,10 @@ function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){

/**
* @class EditorView
* @classdesc A basic shell of a view, primarily meant to be extended for views that allow editing capabilities.
* @name EditorView
* @extends Backbone.View
* @constructs
*/
var EditorView = Backbone.View.extend({

Expand Down Expand Up @@ -37,7 +41,8 @@ function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){
},

render: function(){

//Style the body as an Editor
$("body").addClass("Editor rendering");
},

/**
Expand All @@ -57,6 +62,21 @@ function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){
this.listenTo(this.model, "successSaving", this.saveSuccess);
this.listenTo(this.model, "invalid", this.showValidation);

//Set a beforeunload event only if there isn't one already
if( !this.beforeunloadCallback ){
var view = this;
//When the Window is about to be closed, show a confirmation message
this.beforeunloadCallback = function(e){
if( !view.canClose() ){
//Browsers don't support custom confirmation messages anymore,
// so preventDefault() needs to be called or the return value has to be set
e.preventDefault();
e.returnValue = "";
}
return;
}
window.addEventListener("beforeunload", this.beforeunloadCallback);
}
},

/**
Expand Down Expand Up @@ -309,7 +329,65 @@ function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){
}

}, this);
},

/**
* Checks if there are unsaved changes in this Editor that should prevent closing of this view.
* This function is also executed by the AppView, which controls the top-level navigation.
* @returns {boolean} Returns true if this view should be closed. False if it should remain opened and active.
*/
canClose: function(){

//If the user isn't logged in, we can leave this view without confirmation
if( !MetacatUI.appUserModel.get("loggedIn") )
return true;

//If there are no unsaved changes, we can leave this view without confirmation
if( !this.hasUnsavedChanges() ){
return true;
}

return false;

},

/**
* This function is called whenever the user is about to leave this view.
* @returns {string} The message that asks the user if they are sure they want to close this view
*/
getConfirmCloseMessage: function(){

//Return a confirmation message
return "Leave this page? All of your unsaved changes will be lost.";

},

/**
* Returns true if there are unsaved changes in this Editor
* This function should be exended by each subclass of EditorView to check for unsaved changes for that model type
* @returns {boolean}
*/
hasUnsavedChanges: function(){
return true;
},

/**
* Perform clean-up functions when this view is about to be removed from the page or navigated away from.
*/
onClose: function(){

//Remove the listener on the Window
if( this.beforeunloadCallback ){
window.removeEventListener("beforeunload", this.beforeunloadCallback);
delete this.beforeunloadCallback;
}

//Remove the class from the body element
$("body").removeClass("Editor rendering");

//Remove listeners
this.stopListening();
this.undelegateEvents();

}

Expand Down
78 changes: 52 additions & 26 deletions src/js/views/metadata/EML211EditorView.js
Expand Up @@ -20,6 +20,13 @@ define(['underscore',
EditorView, CitationView, DataPackageView, EMLView, EMLEntityView, SignInView,
EditorTemplate, ObjectFormats, EditorSubmitMessageTemplate){

/**
* @class EML211EditorView
* @classdesc A view of a form for creating and editing EML 2.1.1 documents
* @name EML211EditorView
* @extends EditorView
* @constructs
*/
var EML211EditorView = EditorView.extend({

type: "EML211Editor",
Expand Down Expand Up @@ -91,10 +98,12 @@ define(['underscore',
/* Render the view */
render: function() {

//Execute the superclass render() function, which will add some basic Editor functionality
EditorView.prototype.render.call(this);

MetacatUI.appModel.set('headerType', 'default');

//Style the body as an Editor
$("body").addClass("Editor rendering");
//Empty the view element first
this.$el.empty();

//Inert the basic template on the page
Expand All @@ -119,11 +128,9 @@ define(['underscore',
//If we haven't checked for authentication yet,
//wait until the user info is loaded before we request the Metadata
else{
this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.fetchModel);
this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.fetchModel);
}

window.onbeforeunload = this.confirmClose;

// When the user mistakenly drops a file into an area in the window
// that isn't a proper drop-target, prevent navigating away from the
// page. Without this, the user will lose their progress in the
Expand Down Expand Up @@ -526,11 +533,28 @@ define(['underscore',
this.listenTo(this.model, "invalid", this.showValidation);
this.listenTo(this.model, "valid", this.showValidation);

// When a data package member fails to load, remove it and warn the user
this.listenTo(MetacatUI.eventDispatcher, "fileLoadError", this.handleFileLoadError);
// When a data package member fails to load, remove it and warn the user
this.listenTo(MetacatUI.eventDispatcher, "fileLoadError", this.handleFileLoadError);

// When a data package member fails to be read, remove it and warn the user
this.listenTo(MetacatUI.eventDispatcher, "fileReadError", this.handleFileReadError);

//Set a beforeunload event only if there isn't one already
if( !this.beforeunloadCallback ){
var view = this;
//When the Window is about to be closed, show a confirmation message
this.beforeunloadCallback = function(e){
if( !view.canClose() ){
//Browsers don't support custom confirmation messages anymore,
// so preventDefault() needs to be called or the return value has to be set
e.preventDefault();
e.returnValue = "";
}
return;
}
window.addEventListener("beforeunload", this.beforeunloadCallback);
}

// When a data package member fails to be read, remove it and warn the user
this.listenTo(MetacatUI.eventDispatcher, "fileReadError", this.handleFileReadError);
},

/**
Expand Down Expand Up @@ -880,26 +904,31 @@ define(['underscore',

},

/*
* This function is called whenever the user is about to leave the webpage
/**
* @inheritdoc
*/
confirmClose: function(){

//If the user isn't logged in, we can leave this view without confirmation
if(!MetacatUI.appUserModel.get("loggedIn"))
return;

hasUnsavedChanges: function(){
//If the form hasn't been edited, we can close this view without confirmation
if( typeof MetacatUI.rootDataPackage.getQueue != "function" || !MetacatUI.rootDataPackage.getQueue().length)
return;

return "Are you sure you want to leave this page? All of your changes will be lost.";

if( typeof MetacatUI.rootDataPackage.getQueue != "function" || MetacatUI.rootDataPackage.getQueue().length)
return true;
else
return false;
},

/* Close the view and its sub views */
/**
* @inheritdoc
*/
onClose: function() {

//Execute the parent class onClose() function
//EditorView.prototype.onClose.call(this);

//Remove the listener on the Window
if( this.beforeunloadCallback ){
window.removeEventListener("beforeunload", this.beforeunloadCallback);
delete this.beforeunloadCallback;
}

//Stop listening to the "add" event so that new package members aren't rendered.
//Check first if the DataPackage has been intialized. An easy check is to see is
// the 'models' attribute is undefined. If the DataPackage collection has been intialized,
Expand All @@ -925,9 +954,6 @@ define(['underscore',

this.subviews = [];

//Remove the click listener on the onbeforeunload event
window.onbeforeunload = null;

},

/*
Expand Down
28 changes: 18 additions & 10 deletions src/js/views/portals/editor/PortalEditorView.js
Expand Up @@ -18,6 +18,10 @@ function(_, $, Backbone, Portal, PortalImage, Filters, EditorView, SignInView,

/**
* @class PortalEditorView
* @classdesc A view of a form for creating and editing DataONE Portal documents
* @name PortalEditorView
* @extends EditorView
* @constructs
*/
var PortalEditorView = EditorView.extend({

Expand Down Expand Up @@ -110,8 +114,10 @@ function(_, $, Backbone, Portal, PortalImage, Filters, EditorView, SignInView,
*/
render: function(){

$("body").addClass("Editor")
.addClass("Portal");
//Execute the superclass render() function, which will add some basic Editor functionality
EditorView.prototype.render.call(this);

$("body").addClass("Portal");

// Display a spinner to indicate loading until model is created.
this.$el.html(this.loadingTemplate({
Expand Down Expand Up @@ -244,6 +250,9 @@ function(_, $, Backbone, Portal, PortalImage, Filters, EditorView, SignInView,
accentColorTransparent: this.model.get("accentColorTransparent")
}));

//Remove the rendering class from the body element
$("body").removeClass("rendering");

// Auto-resize the height of the portal title field on user-input and on
// window resize events.
$( window ).resize(function() {
Expand Down Expand Up @@ -689,18 +698,17 @@ function(_, $, Backbone, Portal, PortalImage, Filters, EditorView, SignInView,
},

/**
* This function is called when the app navigates away from this view.
* Any clean-up or housekeeping happens at this time.
* @inheritdoc
*/
onClose: function(){

$("body")
.removeClass("Editor")
.removeClass("Portal");
//Call the superclass onClose() function
EditorView.prototype.onClose.call(this);

//Remove the Portal class from the body element
$("body").removeClass("Portal");

//Remove listeners
this.stopListening();
this.undelegateEvents();
//Remove the scroll listener
$(window).off("scroll", "", this.handleScroll);
},

Expand Down

0 comments on commit 766aa80

Please sign in to comment.