1/* ========================================================================== 2 edit.js â a text editor that cannot save. 3 4 It opens anything on this machine that is text, it edits properly, and 5 Save does not work. That is not a limitation dressed up as a feature; it 6 is the only correct behaviour and the application says so plainly. 7 8 The reason is worth stating once, in the software rather than in a 9 README: this site has no server. There is nothing at the other end of a 10 save â no session, no account, no endpoint, nothing that could accept a 11 write. A web page that let you edit somebody's files and appeared to save 12 them would either be lying to you or be a security hole, and both of 13 those are worse than a button that refuses. 14 15 So: edit freely, the buffer is yours, it lives in this tab and dies with 16 it. `Save a copy` writes the file to your own disk through a download, 17 which is a browser doing a browser thing and involves no request at all. 18 The status line keeps a running count of what you have changed, because 19 an editor that cannot save should at least be honest about how much you 20 are about to lose. 21 ========================================================================== */ 22(function (global) { 23 "use strict"; 24 var el = U.el; 25 26 /* every text file on the machine, newest first */ 27 function textFiles() { 28 var out = []; 29 (function walk(n) { 30 FS.children(n, false).forEach(function (k) { 31 if (FS.isDir(k)) { walk(k); return; } 32 if (k.kind !== "text") return; 33 out.push(k); 34 }); 35 })(FS.home || global.DB.fs); 36 out.sort(function (a, b) { return (b.mtime || 0) - (a.mtime || 0); }); 37 return out; 38 } 39 40 function mount(body, win) { 41 var files = textFiles(); 42 var list = el("div", { class: "col ed-list" }); 43 var area = el("textarea", { class: "ed-area", spellcheck: "false", 44 "aria-label": "Editor" }); 45 var status = el("div", { class: "statusline" }); 46 var cur = null, original = "", dirty = false; 47 48 function count(s) { 49 return { ch: s.length, ln: s ? s.split("\n").length : 0, 50 wd: s.trim() ? s.trim().split(/\s+/).length : 0 }; 51 } 52 53 function say(extra) { 54 if (!cur) { status.textContent = "no file open"; return; } 55 var c = count(area.value); 56 var diff = c.ch - original.length; 57 status.textContent = FS.path(cur).replace("/Users/dominic", "~") + 58 " · " + c.ln + " lines, " + U.num(c.wd) + " words, " + U.num(c.ch) + " characters" + 59 (dirty ? " · modified (" + (diff >= 0 ? "+" : "") + U.num(diff) + ")" : " · unmodified") + 60 (extra ? " · " + extra : ""); 61 } 62 63 function drawList() { 64 list.textContent = ""; 65 var year = null; 66 files.forEach(function (f) { 67 var y = f.mtime ? new Date(f.mtime).getFullYear() : ""; 68 if (y !== year) { year = y; list.appendChild(el("div", { class: "ed-year", text: String(y) })); } 69 list.appendChild(el("div", { 70 class: "row ed-row" + (cur === f ? " sel" : ""), 71 tabindex: "0", role: "button", 72 onclick: function () { load(f); }, 73 onkeydown: function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); this.click(); } } 74 }, [ 75 el("span", { class: "ed-nm", text: f.name }), 76 el("span", { class: "ed-wh", text: FS.when(f) }) 77 ])); 78 }); 79 } 80 81 function load(f) { 82 if (dirty && !global.confirm("This buffer has changes that cannot be saved anywhere. Open another file and lose them?")) return; 83 cur = f; 84 win.route = FS.path(f).replace(/^\/Users\/dominic\/?/, ""); 85 WM.setRoute(win); 86 original = FS.text(f) || ""; 87 area.value = original; 88 dirty = false; 89 drawList(); 90 win.setTitle("Edit â " + f.name); 91 U.look({ app: "files", key: FS.path(f), el: body, text: original, 92 label: FS.path(f).replace("/Users/dominic", "~") }); 93 say(); 94 area.focus(); 95 } 96 97 area.addEventListener("input", function () { 98 dirty = area.value !== original; 99 say(); 100 }); 101 102 /* the one that refuses */ 103 function save() { 104 if (!cur) return; 105 var box = el("div", { class: "ed-alert" }, [ 106 el("h3", { text: "This volume is read-only" }), 107 el("p", { text: 108 "There is no server behind this page. Nothing is listening, so there " + 109 "is nowhere for a save to go â an editor on a website that appeared " + 110 "to write to somebody's disk would either be lying to you or be a " + 111 "hole, and this one would rather be a disappointment." }), 112 el("p", { text: 113 "Your changes are in this tab and nowhere else. Save a copy puts them " + 114 "on your own disk, which your browser can do without asking anything " + 115 "of anyone." }), 116 el("div", { class: "ed-alert-act" }, [ 117 el("button", { class: "btn default", text: "Save a copyâ¦", 118 onclick: function () { box.remove();
118 copy(); } }), 119 el("button", { class: "btn", text: "Keep editing", 120 onclick: function () { box.remove(); area.focus(); } }) 121 ]) 122 ]); 123 body.appendChild(box); 124 } 125 126 /* a download is a browser element, not a request */ 127 function copy() { 128 if (!cur) return; 129 var blob = new Blob([area.value], { type: "text/plain;charset=utf-8" }); 130 var url = URL.createObjectURL(blob); 131 var a = el("a", { href: url, download: cur.name, style: "display:none" }); 132 body.appendChild(a); 133 a.click(); 134 setTimeout(function () { URL.revokeObjectURL(url); a.remove(); }, 4000); 135 say("copy written to your own disk"); 136 } 137 138 function revert() { 139 if (!cur) return; 140 area.value = original; 141 dirty = false; 142 say("reverted"); 143 area.focus(); 144 } 145 146 body.appendChild(el("div", { class: "shelf ed-shelf" }, [ 147 el("button", { class: "btn", text: "Save", onclick: save }), 148 el("button", { class: "btn", text: "Save a copyâ¦", onclick: copy }), 149 el("button", { class: "btn", text: "Revert", onclick: revert }), 150 el("span", { class: "spacer" }), 151 el("span", { class: "meta", style: "font-size:11px", 152 text: "edits live in this tab only · nothing is written to this machine" }) 153 ])); 154 body.appendChild(el("div", { class: "browser ed-wrap" }, [ 155 list, el("div", { class: "ed-pane" }, [area]) 156 ])); 157 body.appendChild(status); 158 159 drawList(); 160 /* `WM.open("edit", "Desktop/notes.txt")` opens straight into a file; 161 the Workspace's Open-in-Edit button uses it. */ 162 var route = win.route || ""; 163 var want = route && files.filter(function (f) { 164 return FS.path(f).indexOf(route) >= 0 || f.name === route; 165 })[0]; 166 if (want || files.length) load(want || files[0]); 167 else { 168 area.value = ""; 169 status.textContent = "no text files on this volume"; 170 } 171 172 /* reopening the same window with a different route lands on that file */ 173 win.navigate = function (r) { 174 var f = files.filter(function (x) { 175 return FS.path(x).indexOf(r) >= 0 || x.name === r; 176 })[0]; 177 if (f) load(f); 178 }; 179 180 win.onClose = function () { 181 if (dirty) try { Observer.record("browsed", "Edit â unsaved buffer discarded"); } catch (e) {} 182 }; 183 } 184 185 WM.define({ id: "edit", title: "Edit", icon: "doc", w: 760, h: 540, mount: mount }); 186})(window);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.