From d3f8e38936624248001b6817479b4b63e2e08546 Mon Sep 17 00:00:00 2001 From: hideheader Date: Sat, 14 Mar 2015 07:51:41 -0400 Subject: [PATCH 1/3] Manage dirty flags on all inputs (1) Record initial value of text, select, and checkbox elements and re/set a dirty class on each when the value changes. (2) Combine the individual flags into a global dirty flag. (3) Add 'dirty' status to the document title. --- edit.html | 2 +- edit.js | 166 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 120 insertions(+), 48 deletions(-) diff --git a/edit.html b/edit.html index 6be58b2e..2a83f1c1 100644 --- a/edit.html +++ b/edit.html @@ -199,7 +199,7 @@

-
+

diff --git a/edit.js b/edit.js index 90c55157..534af891 100644 --- a/edit.js +++ b/edit.js @@ -1,3 +1,5 @@ +"use strict"; + var styleId = null; var dirty = false; var lockScroll; // ensure the section doesn't jump when clicking selected text @@ -12,10 +14,91 @@ appliesToEverythingTemplate.innerHTML = t("appliesToEverything") + ' '; +document.addEventListener("change", function(event) { + var node = event.target; + if (node.type && !node.form) { // INPUTs that aren't in a FORM are stylesheet + switch (node.type) { + case "checkbox": + clean(node, node.checked === node.defaultChecked); + break; + case "text": + case "select-one": + case "select-multiple": + clean(node, node.value === node.defaultValue); + break; + } + } +}); + +var clean = (function Clean() { + var items = {}, + __clean = true; + function isCleanGlobal(lastChange) { + if (typeof lastChange === "boolean") { + var wasClean = __clean; + __clean = !lastChange ? false : Object.keys(items).every(function(item) { + return items[item] + }); + if (wasClean !== __clean) initTitle(); + } + return __clean; + } + function isCleanItem(item, clean) { + if (item instanceof HTMLElement) { + var node = item; + item = node.id; + } + if (typeof clean === "boolean") { + if (node) { + if (!item) item = node.id = Date.now().toString(32).substr(-6); + node.classList[clean ? "remove" : "add"]("dirty"); + } + var wasClean = items[item]; + items[item] = clean; + if (wasClean !== clean) isCleanGlobal(clean); + } + return items[item]; + } + function initialize(form) { + if (!form) form = null; + Array.prototype.forEach.call(document.querySelectorAll("input, select"), function(node) { + if (node.form === form) { + if ("checkbox" === node.type) { + node.defaultChecked = node.checked; + } else { + node.defaultValue = node.value; + } + node.classList.remove("dirty"); + delete items[node.id]; + } + }); + editors.forEach(function(cm) { + cm.lastChange = cm.changeGeneration(); + indicateCodeChange(cm); + }); + // NB: items set with isClean(string, bool) aren't touched + + isCleanGlobal(true); + } + function isClean() { + // isClean() return global clean status + // isClean(bool) update global status + // isClean(element) get element clean status + // isClean(element, bool) update element status + switch (arguments.length) { + case 0: return isCleanGlobal(); + case 1: return ("boolean" === typeof arguments[0] ? isCleanGlobal : isCleanItem)(arguments[0]); + default: return isCleanItem.apply(this, arguments); + } + } + isClean.initialize = initialize; + isClean.items = items; + return isClean; +})(); + var editors = []; // array of all CodeMirror instances function initCodeMirror() { var CM = CodeMirror; - // default option values var userOptions = prefs.getPref("editor.options"); var stylishOptions = { @@ -153,10 +236,6 @@ document.addEventListener("keydown", function(e) { } }); -function makeDirty() { - dirty = true; -} - window.onbeforeunload = function() { prefs.setPref('windowPosition', { left: screenLeft, @@ -164,25 +243,13 @@ window.onbeforeunload = function() { width: outerWidth, height: outerHeight }); - return dirty || isCodeDirty() ? t('styleChangesNotSaved') : null; -} - -function isCodeDirty() { - for(var i=0; i < editors.length; i++) { - if (!editors[i].isClean(editors[i].lastChange)) { - return true; - } - } - return false; + document.activeElement.blur(); + return !clean() ? t('styleChangesNotSaved') : null; } function indicateCodeChange(cm) { - var clean = cm.isClean(cm.lastChange); - if (clean != cm.lastClean) { - cm.lastClean = clean; - cm.getTextArea().parentNode.classList[clean ? "remove" : "add"]("dirty"); - } -}; + clean(cm.getTextArea().parentNode, cm.isClean(cm.lastChange)); +} function addAppliesTo(list, name, value) { var showingEverything = list.querySelector(".applies-to-everything") != null; @@ -196,16 +263,12 @@ function addAppliesTo(list, name, value) { e.querySelector("[name=applies-type]").value = name; e.querySelector("[name=applies-value]").value = value; e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false); - e.querySelector(".applies-value").addEventListener("input", makeDirty, false); - e.querySelector(".applies-type").addEventListener("change", makeDirty, false); } else if (showingEverything || list.hasChildNodes()) { e = appliesToTemplate.cloneNode(true); if (list.hasChildNodes()) { e.querySelector("[name=applies-type]").value = list.querySelector("li:last-child [name='applies-type']").value; } e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false); - e.querySelector(".applies-value").addEventListener("input", makeDirty, false); - e.querySelector(".applies-type").addEventListener("change", makeDirty, false); } else { e = appliesToEverythingTemplate.cloneNode(true); } @@ -224,7 +287,8 @@ function addSection(event, section) { if (section) { var codeElement = div.querySelector(".code"); codeElement.value = section.code; - codeElement.addEventListener("change", makeDirty, false); + // codeElement.addEventListener("change", makeDirty, false); + // // Why is this here? Is it possible for CM to not load? if (section.urls) { section.urls.forEach(function(url) { addAppliesTo(appliesTo, "url", url); @@ -269,24 +333,31 @@ function addSection(event, section) { } function removeAppliesTo(event) { - var appliesToList = event.target.parentNode.parentNode; - appliesToList.removeChild(event.target.parentNode); + var appliesTo = event.target.parentNode, + appliesToList = appliesTo.parentNode; + appliesToList.removeChild(appliesTo); if (!appliesToList.hasChildNodes()) { var e = appliesToEverythingTemplate.cloneNode(true); e.querySelector(".add-applies-to").addEventListener("click", function() {addAppliesTo(this.parentNode.parentNode)}, false); appliesToList.appendChild(e); } - makeDirty(); + Array.prototype.forEach.call(appliesTo.querySelectorAll(".dirty"), function(node) { + clean(node, true); + }); } function removeSection(event) { var section = event.target.parentNode; - var cm = section.querySelector(".CodeMirror-wrap"); - if (cm && editors.indexOf(cm.CodeMirror) >= 0) { - editors.splice(editors.indexOf(cm.CodeMirror), 1); + var wrapper = section.querySelector(".CodeMirror-wrap"); + var idx = editors.indexOf(wrapper && wrapper.CodeMirror); + if (idx >= 0) { + editors.splice(idx, 1); + clean(wrapper.parentNode, true); } section.parentNode.removeChild(section); - makeDirty(); + Array.prototype.forEach.call(section.querySelectorAll(".dirty"), function(node) { + clean(node, true); + }); } function setupGlobalSearch() { @@ -406,9 +477,11 @@ function init() { addSection(null, section); // default to enabled document.getElementById("enabled").checked = true - document.title = t("addStyleTitle"); + // document.title = t("addStyleTitle"); tE("heading", "addStyleTitle"); setupGlobalSearch(); + clean.initialize(null); + initTitle(); return; } // This is an edit @@ -423,17 +496,23 @@ function initWithStyle(style) { document.getElementById("name").value = style.name; document.getElementById("enabled").checked = style.enabled == "true"; document.getElementById("heading").innerHTML = t("editStyleHeading"); - initTitle(style.name); // if this was done in response to an update, we need to clear existing sections Array.prototype.forEach.call(document.querySelectorAll("#sections > div"), function(div) { div.parentNode.removeChild(div); }); style.sections.forEach(function(section) { addSection(null, section) }); setupGlobalSearch(); + clean.initialize(null); + initTitle(); } -function initTitle(name) { - document.title = t('editStyleTitle', [name]); +function initTitle(name, dirty) { + const DIRTY_TITLE = "* $"; + + if (typeof name !== "string") name = document.getElementById("name").defaultValue; + if (typeof dirty !== "boolean") dirty = !clean(); + var title = styleId === null ? t("addStyleTitle") : t('editStyleTitle', [name]); + document.title = !dirty ? title : DIRTY_TITLE.replace("$", title); } function validate() { @@ -534,20 +613,15 @@ function getMeta(e) { } function saveComplete(style) { - dirty = false; - - for(var i=0; i < editors.length; i++) { - var cm = editors[i]; - cm.lastChange = cm.changeGeneration(true); - indicateCodeChange(cm); - } + styleId = style.id; + clean.initialize(null); // Go from new style URL to edit style URL if (location.href.indexOf("id=") == -1) { // give the code above a moment before we kill the page setTimeout(function() {location.href = "edit.html?id=" + style.id;}, 200); } else { - initTitle(document.getElementById("name").value); + initTitle(); } } @@ -630,8 +704,6 @@ tE("cancel-button", "styleCancelEditLabel"); tE("sections-heading", "styleSectionsTitle"); tE("options-heading", "optionsHeading"); -document.getElementById("name").addEventListener("change", makeDirty, false); -document.getElementById("enabled").addEventListener("change", makeDirty, false); document.getElementById("to-mozilla").addEventListener("click", showMozillaFormat, false); document.getElementById("to-mozilla-help").addEventListener("click", showToMozillaHelp, false); document.getElementById("save-button").addEventListener("click", save, false); From b7483035d5627c9b271db7f10e71925d98520beb Mon Sep 17 00:00:00 2001 From: hideheader Date: Sun, 15 Mar 2015 15:41:36 -0400 Subject: [PATCH 2/3] Flatten the closure, simplify its functions --- edit.html | 2 +- edit.js | 129 ++++++++++++++++++++++-------------------------------- 2 files changed, 53 insertions(+), 78 deletions(-) diff --git a/edit.html b/edit.html index 2a83f1c1..fbe7b101 100644 --- a/edit.html +++ b/edit.html @@ -229,7 +229,7 @@
-
+

diff --git a/edit.js b/edit.js index 534af891..5b9ba6db 100644 --- a/edit.js +++ b/edit.js @@ -19,82 +19,58 @@ document.addEventListener("change", function(event) { if (node.type && !node.form) { // INPUTs that aren't in a FORM are stylesheet switch (node.type) { case "checkbox": - clean(node, node.checked === node.defaultChecked); + setCleanItem(node, node.checked === node.defaultChecked); break; case "text": case "select-one": case "select-multiple": - clean(node, node.value === node.defaultValue); + setCleanItem(node, node.value === node.defaultValue); break; } } }); -var clean = (function Clean() { - var items = {}, - __clean = true; - function isCleanGlobal(lastChange) { - if (typeof lastChange === "boolean") { - var wasClean = __clean; - __clean = !lastChange ? false : Object.keys(items).every(function(item) { - return items[item] - }); - if (wasClean !== __clean) initTitle(); - } - return __clean; - } - function isCleanItem(item, clean) { - if (item instanceof HTMLElement) { - var node = item; - item = node.id; - } - if (typeof clean === "boolean") { - if (node) { - if (!item) item = node.id = Date.now().toString(32).substr(-6); - node.classList[clean ? "remove" : "add"]("dirty"); - } - var wasClean = items[item]; - items[item] = clean; - if (wasClean !== clean) isCleanGlobal(clean); - } - return items[item]; - } - function initialize(form) { - if (!form) form = null; - Array.prototype.forEach.call(document.querySelectorAll("input, select"), function(node) { - if (node.form === form) { - if ("checkbox" === node.type) { - node.defaultChecked = node.checked; - } else { - node.defaultValue = node.value; - } - node.classList.remove("dirty"); - delete items[node.id]; - } - }); - editors.forEach(function(cm) { - cm.lastChange = cm.changeGeneration(); - indicateCodeChange(cm); - }); - // NB: items set with isClean(string, bool) aren't touched +// Set .dirty on stylesheet contributors that have changed +var items = {}; +function isCleanItem(node) { + return items[node.id]; +} +function setCleanItem(node, clean) { + var id = node.id; + if (!id) id = node.id = Date.now().toString(32).substr(-6); + items[id] = clean; - isCleanGlobal(true); - } - function isClean() { - // isClean() return global clean status - // isClean(bool) update global status - // isClean(element) get element clean status - // isClean(element, bool) update element status - switch (arguments.length) { - case 0: return isCleanGlobal(); - case 1: return ("boolean" === typeof arguments[0] ? isCleanGlobal : isCleanItem)(arguments[0]); - default: return isCleanItem.apply(this, arguments); + if (clean) node.classList.remove("dirty"); + else node.classList.add("dirty"); + + initTitle(); +} +function isCleanGlobal() { + return Object.keys(items) + .every(function(item) { return items[item] }); +} +function setCleanGlobal(form) { + if (!form) form = null; + Array.prototype.forEach.call(document.querySelectorAll("input, select"), function(node) { + if (node.form === form) { + if ("checkbox" === node.type) { + node.defaultChecked = node.checked; + } else { + node.defaultValue = node.value; + } + + node.classList.remove("dirty"); + delete items[node.id]; } - } - isClean.initialize = initialize; - isClean.items = items; - return isClean; -})(); + }); + + editors.forEach(function(cm) { + cm.lastChange = cm.changeGeneration(); + indicateCodeChange(cm); + }); + + initTitle(); +} var editors = []; // array of all CodeMirror instances function initCodeMirror() { @@ -244,11 +220,11 @@ window.onbeforeunload = function() { height: outerHeight }); document.activeElement.blur(); - return !clean() ? t('styleChangesNotSaved') : null; + return !isCleanGlobal() ? t('styleChangesNotSaved') : null; } function indicateCodeChange(cm) { - clean(cm.getTextArea().parentNode, cm.isClean(cm.lastChange)); + setCleanItem(cm.getTextArea().parentNode, cm.isClean(cm.lastChange)); } function addAppliesTo(list, name, value) { @@ -342,7 +318,7 @@ function removeAppliesTo(event) { appliesToList.appendChild(e); } Array.prototype.forEach.call(appliesTo.querySelectorAll(".dirty"), function(node) { - clean(node, true); + setCleanItem(node, true); }); } @@ -352,11 +328,11 @@ function removeSection(event) { var idx = editors.indexOf(wrapper && wrapper.CodeMirror); if (idx >= 0) { editors.splice(idx, 1); - clean(wrapper.parentNode, true); + setCleanItem(wrapper.parentNode, true); } section.parentNode.removeChild(section); Array.prototype.forEach.call(section.querySelectorAll(".dirty"), function(node) { - clean(node, true); + setCleanItem(node, true); }); } @@ -477,10 +453,9 @@ function init() { addSection(null, section); // default to enabled document.getElementById("enabled").checked = true - // document.title = t("addStyleTitle"); tE("heading", "addStyleTitle"); setupGlobalSearch(); - clean.initialize(null); + setCleanGlobal(null); initTitle(); return; } @@ -502,15 +477,15 @@ function initWithStyle(style) { }); style.sections.forEach(function(section) { addSection(null, section) }); setupGlobalSearch(); - clean.initialize(null); + setCleanGlobal(null); initTitle(); } -function initTitle(name, dirty) { +function initTitle() { const DIRTY_TITLE = "* $"; - if (typeof name !== "string") name = document.getElementById("name").defaultValue; - if (typeof dirty !== "boolean") dirty = !clean(); + var name = document.getElementById("name").defaultValue; + var dirty = !isCleanGlobal(); var title = styleId === null ? t("addStyleTitle") : t('editStyleTitle', [name]); document.title = !dirty ? title : DIRTY_TITLE.replace("$", title); } @@ -614,7 +589,7 @@ function getMeta(e) { function saveComplete(style) { styleId = style.id; - clean.initialize(null); + setCleanGlobal(null); // Go from new style URL to edit style URL if (location.href.indexOf("id=") == -1) { From 4e7b15f491ae4c1e6e230ee4c6f5edcd48e91f88 Mon Sep 17 00:00:00 2001 From: hideheader Date: Mon, 16 Mar 2015 16:47:56 -0400 Subject: [PATCH 3/3] Report element removals correctly An element with a `defaultValue` now becomes dirty when removed; elements without become clean. As a result, removing an existing section makes the document dirty while removing a new section makes it clean(er). --- edit.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/edit.js b/edit.js index 5b9ba6db..929c97fb 100644 --- a/edit.js +++ b/edit.js @@ -46,8 +46,13 @@ function setCleanItem(node, clean) { initTitle(); } function isCleanGlobal() { - return Object.keys(items) - .every(function(item) { return items[item] }); + var clean = Object.keys(items) + .every(function(item) { return items[item] }); + + if (clean) document.body.classList.remove("dirty"); + else document.body.classList.add("dirty"); + + return clean; } function setCleanGlobal(form) { if (!form) form = null; @@ -66,6 +71,7 @@ function setCleanGlobal(form) { editors.forEach(function(cm) { cm.lastChange = cm.changeGeneration(); + cm.getTextArea().parentNode.defaultValue = cm.lastChange; indicateCodeChange(cm); }); @@ -317,8 +323,8 @@ function removeAppliesTo(event) { e.querySelector(".add-applies-to").addEventListener("click", function() {addAppliesTo(this.parentNode.parentNode)}, false); appliesToList.appendChild(e); } - Array.prototype.forEach.call(appliesTo.querySelectorAll(".dirty"), function(node) { - setCleanItem(node, true); + Array.prototype.forEach.call(appliesTo.querySelectorAll("input, select"), function(node) { + setCleanItem(node, !node.defaultValue); }); } @@ -331,9 +337,10 @@ function removeSection(event) { setCleanItem(wrapper.parentNode, true); } section.parentNode.removeChild(section); - Array.prototype.forEach.call(section.querySelectorAll(".dirty"), function(node) { - setCleanItem(node, true); + Array.prototype.forEach.call(section.querySelectorAll("input, select"), function(node) { + setCleanItem(node, !node.defaultValue); }); + setCleanItem(section, !section.defaultValue); } function setupGlobalSearch() {