File size: 4,701 Bytes
81ed537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// Dev editor. Loaded only by dev/server.py, never by the published site.
// "Edit page" makes the post's title, byline and article text editable in the page.
// "Save" (or Ctrl+S) sends them to the server, which writes them back into the .html file.
(function () {
  var article = document.querySelector(".article");
  var band = document.querySelector(".band");
  if (!article || !band) return; // only posts have an editable article

  var EDITABLE = ".band h1, .band .dek, .band time, .band .read, .band .byline strong, .band .byline span, .article";
  var editing = false;

  var style = document.createElement("style");
  style.textContent =
    "#dev-editor{position:fixed;right:16px;bottom:16px;z-index:1000;display:flex;gap:8px;align-items:center;" +
    "padding:8px;border-radius:999px;background:#0b1238;color:#e8edfb;font:14px/1 'Pixelify Sans',Arial,sans-serif;" +
    "box-shadow:0 6px 20px rgba(0,0,0,.25)}" +
    "#dev-editor button{border:0;border-radius:999px;padding:9px 16px;background:#002fa7;color:#ffffff;" +
    "font:inherit;font-weight:500;cursor:pointer}" +
    "#dev-editor button:disabled{opacity:.4;cursor:default}" +
    "#dev-editor .dev-status{padding:0 6px;opacity:.85;max-width:18rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
    "body.dev-editing [contenteditable=true]{outline:1px dashed rgba(251,188,4,.7);outline-offset:4px;border-radius:4px}";
  document.head.appendChild(style);

  var bar = document.createElement("div");
  bar.id = "dev-editor";
  bar.innerHTML =
    '<button type="button" data-act="edit">Edit page</button>' +
    '<button type="button" data-act="save" disabled>Save</button>' +
    '<span class="dev-status"></span>';
  document.body.appendChild(bar);

  var editBtn = bar.querySelector('[data-act="edit"]');
  var saveBtn = bar.querySelector('[data-act="save"]');
  var statusEl = bar.querySelector(".dev-status");

  function status(msg) { statusEl.textContent = msg; }

  function setEditing(on) {
    editing = on;
    document.querySelectorAll(EDITABLE).forEach(function (el) {
      el.contentEditable = on ? "true" : "false";
    });
    document.body.classList.toggle("dev-editing", on);
    editBtn.textContent = on ? "Stop editing" : "Edit page";
    saveBtn.disabled = !on;
    status(on ? "Editing. Save writes to the .html file." : "");
  }

  // While editing, clicks in the text shouldn't start audio playback or jump the reader.
  document.addEventListener("click", function (e) {
    if (editing && e.target.closest && e.target.closest(".article, .band") && !e.target.closest("button, .listen")) {
      e.stopPropagation();
    }
  }, true);

  // Copy of an element with the editor's and the page script's runtime state removed.
  function clean(el) {
    var c = el.cloneNode(true);
    c.querySelectorAll("[contenteditable]").forEach(function (e) { e.removeAttribute("contenteditable"); });
    c.querySelectorAll("[style]").forEach(function (e) { e.removeAttribute("style"); });
    c.querySelectorAll(".speaking").forEach(function (e) { e.classList.remove("speaking"); });
    c.querySelectorAll("[class]").forEach(function (e) {
      if (!e.getAttribute("class").trim()) e.removeAttribute("class");
    });
    return c;
  }

  function serialize() {
    var a = clean(article);
    a.querySelectorAll(".listen").forEach(function (e) { e.remove(); });   // generated by listen.js
    a.querySelectorAll("h2[id]").forEach(function (h) { h.removeAttribute("id"); }); // generated by script.js
    var b = clean(band);
    var share = b.querySelector("#share span");
    if (share) share.textContent = "Share";                                 // may read "Link copied"
    return { article: a.innerHTML, band: b.innerHTML };
  }

  function save() {
    if (!editing) return;
    status("Saving…");
    var body = serialize();
    body.page = location.pathname;
    fetch("/__dev/save", {
      method: "POST",
      headers: { "Content-Type": "application/json", "X-Dev-Editor": "1" },
      body: JSON.stringify(body)
    })
      .then(function (r) {
        return r.json().then(function (j) {
          if (!r.ok) throw new Error(j.error || "HTTP " + r.status);
          return j;
        });
      })
      .then(function (j) { status("Saved " + j.page + " at " + new Date().toLocaleTimeString()); })
      .catch(function (err) { status("Not saved: " + err.message); });
  }

  editBtn.addEventListener("click", function () { setEditing(!editing); });
  saveBtn.addEventListener("click", save);
  document.addEventListener("keydown", function (e) {
    if (editing && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
      e.preventDefault();
      save();
    }
  });
})();