Bug 826982 - [style editor] Show a notification when navigating away from page if there are unsaved changes; r=paul

This commit is contained in:
Heather Arthur 2013-03-08 19:11:22 -08:00
parent 5a0713f2f8
commit 2d1c745b79
5 changed files with 243 additions and 4 deletions

View File

@ -177,6 +177,28 @@ StyleEditorChrome.prototype = {
return editors;
},
/**
* Get whether any of the editors have unsaved changes.
*
* @return boolean
*/
get isDirty()
{
if (this._markedDirty === true) {
return true;
}
return this.editors.some(function(editor) {
return editor.sourceEditor && editor.sourceEditor.dirty;
});
},
/*
* Mark the style editor as having unsaved changes.
*/
markDirty: function SEC_markDirty() {
this._markedDirty = true;
},
/**
* Add a listener for StyleEditorChrome events.
*

View File

@ -4,11 +4,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
this.EXPORTED_SYMBOLS = ["StyleEditorPanel"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource:///modules/devtools/EventEmitter.jsm");
@ -21,11 +22,11 @@ this.StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) {
this._toolbox = toolbox;
this._target = toolbox.target;
this.reset = this.reset.bind(this);
this.newPage = this.newPage.bind(this);
this.destroy = this.destroy.bind(this);
this.beforeNavigate = this.beforeNavigate.bind(this);
this._target.on("will-navigate", this.reset);
this._target.on("will-navigate", this.beforeNavigate);
this._target.on("navigate", this.newPage);
this._target.on("close", this.destroy);
@ -86,9 +87,87 @@ StyleEditorPanel.prototype = {
* Navigated to a new page.
*/
newPage: function StyleEditor_newPage(event, window) {
this.reset();
this.setPage(window);
},
/**
* Before navigating to a new page or reloading the page.
*/
beforeNavigate: function StyleEditor_beforeNavigate(event, request) {
if (this.styleEditorChrome.isDirty) {
this.preventNavigate(request);
}
},
/**
* Show a notificiation about losing unsaved changes.
*/
preventNavigate: function StyleEditor_preventNavigate(request) {
request.suspend();
let notificationBox = null;
if (this.target.isLocalTab) {
let gBrowser = this.target.tab.ownerDocument.defaultView.gBrowser;
notificationBox = gBrowser.getNotificationBox();
}
else {
notificationBox = this._toolbox.getNotificationBox();
}
let notification = notificationBox.
getNotificationWithValue("styleeditor-page-navigation");
if (notification) {
notificationBox.removeNotification(notification, true);
}
let cancelRequest = function onCancelRequest() {
if (request) {
request.cancel(Cr.NS_BINDING_ABORTED);
request.resume(); // needed to allow the connection to be cancelled.
request = null;
}
};
let eventCallback = function onNotificationCallback(event) {
if (event == "removed") {
cancelRequest();
}
};
let buttons = [
{
id: "styleeditor.confirmNavigationAway.buttonLeave",
label: this.strings.GetStringFromName("confirmNavigationAway.buttonLeave"),
accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonLeaveAccesskey"),
callback: function onButtonLeave() {
if (request) {
request.resume();
request = null;
}
}.bind(this),
},
{
id: "styleeditor.confirmNavigationAway.buttonStay",
label: this.strings.GetStringFromName("confirmNavigationAway.buttonStay"),
accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonStayAccesskey"),
callback: cancelRequest
},
];
let message = this.strings.GetStringFromName("confirmNavigationAway.message");
notification = notificationBox.appendNotification(message,
"styleeditor-page-navigation", "chrome://browser/skin/Info.png",
notificationBox.PRIORITY_WARNING_HIGH, buttons, eventCallback);
// Make sure this not a transient notification, to avoid the automatic
// transient notification removal.
notification.persistence = -1;
},
/**
* No window available anymore.
*/
@ -110,7 +189,7 @@ StyleEditorPanel.prototype = {
if (!this._destroyed) {
this._destroyed = true;
this._target.off("will-navigate", this.reset);
this._target.off("will-navigate", this.beforeNavigate);
this._target.off("navigate", this.newPage);
this._target.off("close", this.destroy);
this._target = null;
@ -122,3 +201,9 @@ StyleEditorPanel.prototype = {
return Promise.resolve(null);
},
}
XPCOMUtils.defineLazyGetter(StyleEditorPanel.prototype, "strings",
function () {
return Services.strings.createBundle(
"chrome://browser/locale/devtools/styleeditor.properties");
});

View File

@ -27,6 +27,7 @@ _BROWSER_TEST_FILES = \
browser_styleeditor_reopen.js \
browser_styleeditor_sv_keynav.js \
browser_styleeditor_sv_resize.js \
browser_styleeditor_bug_826982_location_changed.js \
head.js \
helpers.js \
four.html \

View File

@ -0,0 +1,123 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let tempScope = {};
Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
let TargetFactory = tempScope.TargetFactory;
function test() {
let notificationBox, styleEditor;
let alertActive1_called = false;
let alertActive2_called = false;
function startLocationTests() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
runTests(toolbox.getCurrentPanel(), toolbox);
}).then(null, console.error);
}
function runTests(aStyleEditor) {
styleEditor = aStyleEditor;
let para = content.document.querySelector("p");
ok(para, "found the paragraph element");
is(para.textContent, "init", "paragraph content is correct");
styleEditor.styleEditorChrome.markDirty();
notificationBox = gBrowser.getNotificationBox();
notificationBox.addEventListener("AlertActive", alertActive1, false);
gBrowser.selectedBrowser.addEventListener("load", onPageLoad, true);
content.location = "data:text/html,<div>location change test 1 for " +
"styleeditor</div><p>test1</p>";
}
function alertActive1() {
alertActive1_called = true;
notificationBox.removeEventListener("AlertActive", alertActive1, false);
let notification = notificationBox.
getNotificationWithValue("styleeditor-page-navigation");
ok(notification, "found the styleeditor-page-navigation notification");
// By closing the notification it is expected that page navigation is
// canceled.
executeSoon(function() {
notification.close();
locationTest2();
});
}
function locationTest2() {
// Location did not change.
let para = content.document.querySelector("p");
ok(para, "found the paragraph element, second time");
is(para.textContent, "init", "paragraph content is correct");
notificationBox.addEventListener("AlertActive", alertActive2, false);
content.location = "data:text/html,<div>location change test 2 for " +
"styleeditor</div><p>test2</p>";
}
function alertActive2() {
alertActive2_called = true;
notificationBox.removeEventListener("AlertActive", alertActive2, false);
let notification = notificationBox.
getNotificationWithValue("styleeditor-page-navigation");
ok(notification, "found the styleeditor-page-navigation notification");
let buttons = notification.querySelectorAll("button");
let buttonLeave = null;
for (let i = 0; i < buttons.length; i++) {
if (buttons[i].buttonInfo.id == "styleeditor.confirmNavigationAway.buttonLeave") {
buttonLeave = buttons[i];
break;
}
}
ok(buttonLeave, "the Leave page button was found");
// Accept page navigation.
executeSoon(function(){
buttonLeave.doCommand();
});
}
function onPageLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onPageLoad, true);
isnot(content.location.href.indexOf("test2"), -1,
"page navigated to the correct location");
let para = content.document.querySelector("p");
ok(para, "found the paragraph element, third time");
is(para.textContent, "test2", "paragraph content is correct");
ok(alertActive1_called, "first notification box has been shown");
ok(alertActive2_called, "second notification box has been shown");
testEnd();
}
function testEnd() {
notificationBox = null;
gBrowser.removeCurrentTab();
executeSoon(finish);
}
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true);
waitForFocus(startLocationTests, content);
}, true);
content.location = "data:text/html,<div>location change tests for " +
"styleeditor.</div><p>init</p>";
}

View File

@ -83,3 +83,11 @@ ToolboxStyleEditor.label=Style Editor
# This string is displayed in the tooltip of the tab when the debugger is
# displayed inside the developer tools window.
ToolboxStyleEditor.tooltip=CSS Stylesheets Editor
# LOCALIZATION NOTE (confirmNavigationAway): Shown in a notification box when
# the user tries to navigate away from a web page.
confirmNavigationAway.message=If you leave this page, unsaved changes in the Style Editor will be lost.
confirmNavigationAway.buttonLeave=Leave Page
confirmNavigationAway.buttonLeaveAccesskey=L
confirmNavigationAway.buttonStay=Stay on Page
confirmNavigationAway.buttonStayAccesskey=S