From e87ee63c121426a47b643f3adb8a092a18771d5e Mon Sep 17 00:00:00 2001 From: Thomas Wilburn Date: Sun, 4 May 2014 15:34:51 -0700 Subject: [PATCH 1/5] Added the elementData module for JS<->DOM data lookups --- js/util/dom2.js | 218 +++++++++++++++++++++-------------------- js/util/elementData.js | 39 ++++++++ 2 files changed, 153 insertions(+), 104 deletions(-) create mode 100644 js/util/elementData.js diff --git a/js/util/dom2.js b/js/util/dom2.js index f19b5d9..9981451 100644 --- a/js/util/dom2.js +++ b/js/util/dom2.js @@ -1,109 +1,119 @@ -define(function() { -"use strict"; - -/* - -Yes, you're not supposed to extend native prototypes. But in Chrome OS, who cares? Nobody's sharing your context. Why not paper over the cracks in the DOM? This library extends elements to do the following: - -- create a findAll() method that returns an array, instead of a nodeList -- create a findUp() method to work up through ancestors -- create remove() and append() that work the way you expect them to. -- alias event listener methods to the much shorter on() and off() -- add a style method that handles prefixed CSS -- ensure that elements have a matches() method -- provide convenience methods for the classList - -*/ - -var el = Element.prototype; -var doc = Document.prototype; -var frag = DocumentFragment.prototype; -var win = Window.prototype; - - -//alias of querySelector and qSA -el.find = doc.find = frag.find = function(selector) { - return this.querySelector(selector); -}; - -el.findAll = doc.findAll = frag.findAll = function(selector) { - var a = []; - a.push.apply(a, this.querySelectorAll(selector)); - return a; -}; - -//equivalent of $().closest() -el.findUp = function(selector) { - var target = this; - while (target && !target.matches(selector)) { - target = target.parentElement; +define([ + "util/elementData" + ], function(elementData) { + "use strict"; + + /* + + Yes, you're not supposed to extend native prototypes. But in Chrome OS, who cares? Nobody's sharing your context. Why not paper over the cracks in the DOM? This library extends elements to do the following: + + - create a findAll() method that returns an array, instead of a nodeList + - create a findUp() method to work up through ancestors + - create remove() and append() that work the way you expect them to. + - alias event listener methods to the much shorter on() and off() + - add a style method that handles prefixed CSS + - ensure that elements have a matches() method + - provide convenience methods for the classList + + */ + + var el = Element.prototype; + var doc = Document.prototype; + var frag = DocumentFragment.prototype; + var win = Window.prototype; + + + //alias of querySelector and qSA + el.find = doc.find = frag.find = function(selector) { + return this.querySelector(selector); + }; + + el.findAll = doc.findAll = frag.findAll = function(selector) { + var a = []; + a.push.apply(a, this.querySelectorAll(selector)); + return a; + }; + + //equivalent of $().closest() + el.findUp = function(selector) { + var target = this; + while (target && !target.matches(selector)) { + target = target.parentElement; + } + return target; } - return target; -} - -el.matches = el.matches || el.webkitMatchesSelector; - -el.remove = function() { - this.parentElement.removeChild(this); -}; - -el.append = frag.append = function(element) { - if (typeof element == "string") { - this.innerHTML += element; + + el.matches = el.matches || el.webkitMatchesSelector; + + el.remove = function() { + this.parentElement.removeChild(this); + }; + + el.append = frag.append = function(element) { + if (typeof element == "string") { + this.innerHTML += element; + } else { + this.appendChild(element); + } + }; + + win.on = el.on = function(type, listener) { + this.addEventListener(type, listener); + return this; + }; + + win.off = el.off = function(type, listener) { + this.removeEventListener(type, listener); + }; + + el.css = function(style, one) { + if (typeof style === "string") { + var hash = {}; + hash[style] = one; + style = hash; + } + for (var key in style) { + var val = style[key]; + if (key.indexOf("-") > 0) { + key.replace(/-(\w)/g, function(_, match) { + return match.toUpperCase(); + }); + } + if (!(key in this.style)) { + ["webkit", "moz", "ms"].some(function(prefix) { + var test = prefix + key[0].toUpperCase() + key.substr(1); + if (test in this.style) { + key = test; + return true; + } + }, this); + } + this.style[key] = val; + } + }; + + el.addClass = function(name) { + this.classList.add(name); + }; + + el.removeClass = function(name) { + this.classList.remove(name); + }; + + el.toggle = function(name) { + this.classList.toggle(name); + }; + + el.hasClass = function(name) { + return this.classList.contains(name); + }; + + el.data = function(value) { + if (value) { + elementData.set(this, value); } else { - this.appendChild(element); + return elementData.get(this); } -}; - -win.on = el.on = function(type, listener) { - this.addEventListener(type, listener); - return this; -}; - -win.off = el.off = function(type, listener) { - this.removeEventListener(type, listener); -}; - -el.css = function(style, one) { - if (typeof style === "string") { - var hash = {}; - hash[style] = one; - style = hash; - } - for (var key in style) { - var val = style[key]; - if (key.indexOf("-") > 0) { - key.replace(/-(\w)/g, function(_, match) { - return match.toUpperCase(); - }); - } - if (!(key in this.style)) { - ["webkit", "moz", "ms"].some(function(prefix) { - var test = prefix + key[0].toUpperCase() + key.substr(1); - if (test in this.style) { - key = test; - return true; - } - }, this); - } - this.style[key] = val; - } -}; - -el.addClass = function(name) { - this.classList.add(name); -}; - -el.removeClass = function(name) { - this.classList.remove(name); -}; - -el.toggle = function(name) { - this.classList.toggle(name); -}; - -el.hasClass = function(name) { - return this.classList.contains(name); -} + } }) \ No newline at end of file diff --git a/js/util/elementData.js b/js/util/elementData.js new file mode 100644 index 0000000..702fa64 --- /dev/null +++ b/js/util/elementData.js @@ -0,0 +1,39 @@ +define(function() { + + /* + + Storing JS data on HTML elements has, in the past, been a likely source of + memory leaks one way or the other. In this module, we create a set of + matching WeakMaps for connecting data to DOM and vice versa. The dom2 module + uses this to emulate jQuery's data() method, and other modules can import this + to ask for an element given a JS object (such as a tab instance). + + */ + + var elements = new WeakMap(); + var objects = new WeakMap(); + + return { + get: function(key) { + var map = key instanceof HTMLElement ? elements : objects; + return map.get(key); + }, + set: function(key, value) { + var isDom = key instanceof HTMLElement; + var forward = isDom ? elements : objects; + var reverse = isDom ? objects : elements; + forward.set(key, value); + reverse.set(value, key); + }, + "delete": function(key) { + var isDom = key instanceof HTMLElement; + var forward = isDom ? elements : objects; + var reverse = isDom ? objects : elements; + var data = forward.get(key); + forward.delete(key); + reverse.delete(data); + } + } + + +}); \ No newline at end of file From bf1eaadd8591b70a1bca072d4f058db18e439033 Mon Sep 17 00:00:00 2001 From: Thomas Wilburn Date: Sun, 4 May 2014 15:34:51 -0700 Subject: [PATCH 2/5] Added the elementData module for JS<->DOM data lookups --- js/util/dom2.js | 218 +++++++++++++++++++++-------------------- js/util/elementData.js | 39 ++++++++ 2 files changed, 153 insertions(+), 104 deletions(-) create mode 100644 js/util/elementData.js diff --git a/js/util/dom2.js b/js/util/dom2.js index f19b5d9..9981451 100644 --- a/js/util/dom2.js +++ b/js/util/dom2.js @@ -1,109 +1,119 @@ -define(function() { -"use strict"; - -/* - -Yes, you're not supposed to extend native prototypes. But in Chrome OS, who cares? Nobody's sharing your context. Why not paper over the cracks in the DOM? This library extends elements to do the following: - -- create a findAll() method that returns an array, instead of a nodeList -- create a findUp() method to work up through ancestors -- create remove() and append() that work the way you expect them to. -- alias event listener methods to the much shorter on() and off() -- add a style method that handles prefixed CSS -- ensure that elements have a matches() method -- provide convenience methods for the classList - -*/ - -var el = Element.prototype; -var doc = Document.prototype; -var frag = DocumentFragment.prototype; -var win = Window.prototype; - - -//alias of querySelector and qSA -el.find = doc.find = frag.find = function(selector) { - return this.querySelector(selector); -}; - -el.findAll = doc.findAll = frag.findAll = function(selector) { - var a = []; - a.push.apply(a, this.querySelectorAll(selector)); - return a; -}; - -//equivalent of $().closest() -el.findUp = function(selector) { - var target = this; - while (target && !target.matches(selector)) { - target = target.parentElement; +define([ + "util/elementData" + ], function(elementData) { + "use strict"; + + /* + + Yes, you're not supposed to extend native prototypes. But in Chrome OS, who cares? Nobody's sharing your context. Why not paper over the cracks in the DOM? This library extends elements to do the following: + + - create a findAll() method that returns an array, instead of a nodeList + - create a findUp() method to work up through ancestors + - create remove() and append() that work the way you expect them to. + - alias event listener methods to the much shorter on() and off() + - add a style method that handles prefixed CSS + - ensure that elements have a matches() method + - provide convenience methods for the classList + + */ + + var el = Element.prototype; + var doc = Document.prototype; + var frag = DocumentFragment.prototype; + var win = Window.prototype; + + + //alias of querySelector and qSA + el.find = doc.find = frag.find = function(selector) { + return this.querySelector(selector); + }; + + el.findAll = doc.findAll = frag.findAll = function(selector) { + var a = []; + a.push.apply(a, this.querySelectorAll(selector)); + return a; + }; + + //equivalent of $().closest() + el.findUp = function(selector) { + var target = this; + while (target && !target.matches(selector)) { + target = target.parentElement; + } + return target; } - return target; -} - -el.matches = el.matches || el.webkitMatchesSelector; - -el.remove = function() { - this.parentElement.removeChild(this); -}; - -el.append = frag.append = function(element) { - if (typeof element == "string") { - this.innerHTML += element; + + el.matches = el.matches || el.webkitMatchesSelector; + + el.remove = function() { + this.parentElement.removeChild(this); + }; + + el.append = frag.append = function(element) { + if (typeof element == "string") { + this.innerHTML += element; + } else { + this.appendChild(element); + } + }; + + win.on = el.on = function(type, listener) { + this.addEventListener(type, listener); + return this; + }; + + win.off = el.off = function(type, listener) { + this.removeEventListener(type, listener); + }; + + el.css = function(style, one) { + if (typeof style === "string") { + var hash = {}; + hash[style] = one; + style = hash; + } + for (var key in style) { + var val = style[key]; + if (key.indexOf("-") > 0) { + key.replace(/-(\w)/g, function(_, match) { + return match.toUpperCase(); + }); + } + if (!(key in this.style)) { + ["webkit", "moz", "ms"].some(function(prefix) { + var test = prefix + key[0].toUpperCase() + key.substr(1); + if (test in this.style) { + key = test; + return true; + } + }, this); + } + this.style[key] = val; + } + }; + + el.addClass = function(name) { + this.classList.add(name); + }; + + el.removeClass = function(name) { + this.classList.remove(name); + }; + + el.toggle = function(name) { + this.classList.toggle(name); + }; + + el.hasClass = function(name) { + return this.classList.contains(name); + }; + + el.data = function(value) { + if (value) { + elementData.set(this, value); } else { - this.appendChild(element); + return elementData.get(this); } -}; - -win.on = el.on = function(type, listener) { - this.addEventListener(type, listener); - return this; -}; - -win.off = el.off = function(type, listener) { - this.removeEventListener(type, listener); -}; - -el.css = function(style, one) { - if (typeof style === "string") { - var hash = {}; - hash[style] = one; - style = hash; - } - for (var key in style) { - var val = style[key]; - if (key.indexOf("-") > 0) { - key.replace(/-(\w)/g, function(_, match) { - return match.toUpperCase(); - }); - } - if (!(key in this.style)) { - ["webkit", "moz", "ms"].some(function(prefix) { - var test = prefix + key[0].toUpperCase() + key.substr(1); - if (test in this.style) { - key = test; - return true; - } - }, this); - } - this.style[key] = val; - } -}; - -el.addClass = function(name) { - this.classList.add(name); -}; - -el.removeClass = function(name) { - this.classList.remove(name); -}; - -el.toggle = function(name) { - this.classList.toggle(name); -}; - -el.hasClass = function(name) { - return this.classList.contains(name); -} + } }) \ No newline at end of file diff --git a/js/util/elementData.js b/js/util/elementData.js new file mode 100644 index 0000000..702fa64 --- /dev/null +++ b/js/util/elementData.js @@ -0,0 +1,39 @@ +define(function() { + + /* + + Storing JS data on HTML elements has, in the past, been a likely source of + memory leaks one way or the other. In this module, we create a set of + matching WeakMaps for connecting data to DOM and vice versa. The dom2 module + uses this to emulate jQuery's data() method, and other modules can import this + to ask for an element given a JS object (such as a tab instance). + + */ + + var elements = new WeakMap(); + var objects = new WeakMap(); + + return { + get: function(key) { + var map = key instanceof HTMLElement ? elements : objects; + return map.get(key); + }, + set: function(key, value) { + var isDom = key instanceof HTMLElement; + var forward = isDom ? elements : objects; + var reverse = isDom ? objects : elements; + forward.set(key, value); + reverse.set(value, key); + }, + "delete": function(key) { + var isDom = key instanceof HTMLElement; + var forward = isDom ? elements : objects; + var reverse = isDom ? objects : elements; + var data = forward.get(key); + forward.delete(key); + reverse.delete(data); + } + } + + +}); \ No newline at end of file From 64a98b3f8208600d18fbe0e34f3813d2c180366a Mon Sep 17 00:00:00 2001 From: Thomas Wilburn Date: Sun, 4 May 2014 19:37:11 -0700 Subject: [PATCH 3/5] Start of weakmaps tied to tab rendering. Tabs still need to be re-rendered when their state changes. --- js/sessions.js | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/js/sessions.js b/js/sessions.js index e98ebd4..f2e2623 100644 --- a/js/sessions.js +++ b/js/sessions.js @@ -6,10 +6,10 @@ define([ "editor", "command", "storage/settingsProvider", - "util/template!templates/newTabButton.html", + "util/elementData", "aceBindings" ], - function(state, addRemove, switching, bindEvents, editor, command, Settings, inflate) { + function(state, addRemove, switching, bindEvents, editor, command, Settings, elementData) { /* @@ -33,19 +33,30 @@ define([ var renderTabs = function() { var tabContainer = document.find(".tabs"); - var contents = ""; var current = editor.getSession(); - tabContainer.innerHTML = ""; + //find and destroy tabs that do not exist anymore + tabContainer.findAll(".tab").forEach(function(element) { + var tab = element.data(); + if (state.tabs.indexOf(tab) == -1) { + element.remove(); + } + }); state.tabs.forEach(function(tab, i) { - var element = tab.render(i); + var element = elementData.get(tab); + if (!element) { + element = tab.render(i); + element.data(tab); + element.addClass("enter"); + } + element.setAttribute("tab-id", i); + element.find(".label").setAttribute("argument", i); if (tab === current) { element.addClass("active"); + } else { + element.removeClass("active"); } tabContainer.append(element); }); - if (Settings.get("user").showNewTabButton === true) { - tabContainer.append(inflate.get("templates/newTabButton.html")); - } setTimeout(function() { //wait for render before triggering the enter animation tabContainer.findAll(".enter").forEach(function(element) { element.removeClass("enter") }); @@ -90,7 +101,7 @@ define([ tab.syntaxMode = "javascript"; tab.detectSyntax(); tab.fileName = name + ".json"; - renderTabs(); + renderTabs(); }); }, getAllTabs: function() { From d6e1ec6c73b4e48d9b0b9f9513f8cef977df4da5 Mon Sep 17 00:00:00 2001 From: Thomas Wilburn Date: Sun, 4 May 2014 20:00:19 -0700 Subject: [PATCH 4/5] Enable re-rendering tab contents --- js/sessions.js | 7 ++++--- js/sessions/binding.js | 3 ++- js/tab.js | 7 +------ templates/tab.html | 28 +++++++++++----------------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/js/sessions.js b/js/sessions.js index f2e2623..205e642 100644 --- a/js/sessions.js +++ b/js/sessions.js @@ -44,12 +44,13 @@ define([ state.tabs.forEach(function(tab, i) { var element = elementData.get(tab); if (!element) { - element = tab.render(i); + element = document.createElement("span"); + element.className = "tab enter"; + element.setAttribute("draggable", true); element.data(tab); - element.addClass("enter"); } + element.innerHTML = tab.render(i); element.setAttribute("tab-id", i); - element.find(".label").setAttribute("argument", i); if (tab === current) { element.addClass("active"); } else { diff --git a/js/sessions/binding.js b/js/sessions/binding.js index 246f0b0..391aadc 100644 --- a/js/sessions/binding.js +++ b/js/sessions/binding.js @@ -89,6 +89,7 @@ define([ }); state.tabs = reordered; } + tabContainer.find(".hovering").removeClass("hovering"); command.fire("session:render"); }); @@ -115,7 +116,7 @@ define([ var enableDblClickNewTab = function() { var tabContainer = document.find(".tabs"); - tabContainer.on("dblclick", function(e) { + tabContainer.on("dblclick", function(e) { e.preventDefault(); if (e.button == 0) command.fire("session:new-file"); diff --git a/js/tab.js b/js/tab.js index f9fe072..4b49a4f 100644 --- a/js/tab.js +++ b/js/tab.js @@ -39,9 +39,6 @@ define([ self.modified = true; command.fire("session:render"); }); - - this.animationClass = "enter"; - }; //hopefully this never screws up unaugmented Ace sessions. @@ -115,14 +112,12 @@ define([ }; Tab.prototype.render = function(index) { - var element = inflate.get("templates/tab.html", { + var element = inflate.getHTML("templates/tab.html", { index: index, fileName: this.fileName, modified: this.modified, - animation: this.animationClass, path: this.path }); - this.animationClass = ""; return element; } diff --git a/templates/tab.html b/templates/tab.html index e004a5a..ca98fcb 100644 --- a/templates/tab.html +++ b/templates/tab.html @@ -1,17 +1,11 @@ - - {{fileName}} - - {{#modified}}○{{/modified}} - {{^modified}}×{{/modified}} - - \ No newline at end of file +{{fileName}} + + {{#modified}}○{{/modified}} + {{^modified}}×{{/modified}} + \ No newline at end of file From 078d5471a75a34077987a84e010dcf44627e7cbd Mon Sep 17 00:00:00 2001 From: Thomas Wilburn Date: Sun, 4 May 2014 20:55:02 -0700 Subject: [PATCH 5/5] Remove vestiges of the new tab button --- config/user.json | 5 +---- css/tabs.less | 19 ------------------- templates/newTabButton.html | 9 --------- 3 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 templates/newTabButton.html diff --git a/config/user.json b/config/user.json index 8947c24..085cd72 100644 --- a/config/user.json +++ b/config/user.json @@ -71,8 +71,5 @@ "ignoreFiles": "node_modules", //Crazy? You might like Vim keybindings. Only takes effect on restart - "emulateVim": false, - - //Whether to show the "New file" tab button on the tab bar; - "showNewTabButton": false + "emulateVim": false } \ No newline at end of file diff --git a/css/tabs.less b/css/tabs.less index 3c9d48c..1d02dcb 100644 --- a/css/tabs.less +++ b/css/tabs.less @@ -89,24 +89,5 @@ color: @foreground; } } - - &.newtab { - opacity: .25; - min-width: 30px; - max-width: 30px; - margin: 2px 0 4px 0; - border-radius: 2px; - display: flex; - align-items: center; - justify-content: center; - align-content: center; - - .big-label { - font-size: 24px; - color: lighten(@foreground, 60%); - font-weight: bold; - text-align: center; - } - } } } \ No newline at end of file diff --git a/templates/newTabButton.html b/templates/newTabButton.html deleted file mode 100644 index 82aa4f4..0000000 --- a/templates/newTabButton.html +++ /dev/null @@ -1,9 +0,0 @@ - - + - \ No newline at end of file