| |
| |
| |
| |
| |
| |
| |
| |
| |
| (function () { |
| "use strict"; |
|
|
| var API = "/api/memos"; |
| var state = { instanceId: null, memos: [], editingId: null, enabled: false }; |
|
|
| function el(id) { return document.getElementById(id); } |
|
|
| function currentInstanceId() { |
| var input = el("instance_id"); |
| return input ? input.value : null; |
| } |
|
|
| function esc(s) { |
| var d = document.createElement("div"); |
| d.textContent = s == null ? "" : String(s); |
| return d.innerHTML; |
| } |
|
|
| function selectionInText() { |
| |
| |
| var sel = window.getSelection(); |
| if (!sel || sel.isCollapsed || sel.rangeCount === 0) return null; |
| var container = document.getElementById("text-content"); |
| if (!container) return null; |
| var range = sel.getRangeAt(0); |
| if (!container.contains(range.startContainer) || |
| !container.contains(range.endContainer)) return null; |
| var pre = range.cloneRange(); |
| pre.selectNodeContents(container); |
| pre.setEnd(range.startContainer, range.startOffset); |
| var start = pre.toString().length; |
| var quote = sel.toString(); |
| if (!quote.trim()) return null; |
| return { start: start, end: start + quote.length, field: "text", quote: quote }; |
| } |
|
|
| function api(method, path, body) { |
| return fetch(API + path, { |
| method: method, |
| headers: { "Content-Type": "application/json" }, |
| body: body ? JSON.stringify(body) : undefined, |
| }); |
| } |
|
|
| function render() { |
| var list = el("memo-list"); |
| if (!list) return; |
| if (!state.memos.length) { |
| list.innerHTML = '<div class="memo-empty">No notes on this instance yet.</div>'; |
| return; |
| } |
| var me = (window.config && window.config.username) || null; |
| list.innerHTML = state.memos.map(function (m) { |
| var own = m.created_by === me; |
| var vis = m.visibility === "shared" ? "shared" : "private"; |
| var anchorBadge = m.anchor |
| ? '<span class="memo-badge anchor" title="Attached to a text selection">quote</span>' |
| : ""; |
| var actions = own |
| ? '<span class="memo-actions">' |
| + '<button data-act="edit" data-id="' + m.id + '">Edit</button>' |
| + '<button data-act="del" data-id="' + m.id + '">Delete</button>' |
| + "</span>" |
| : ""; |
| return '<div class="memo-item">' |
| + '<div class="memo-body">' + esc(m.body) + "</div>" |
| + '<div class="memo-meta">' |
| + "<span>" + esc(m.created_by) + "</span>" |
| + '<span class="memo-badge ' + vis + '">' + vis + "</span>" |
| + anchorBadge + actions |
| + "</div></div>"; |
| }).join(""); |
| list.querySelectorAll("button[data-act]").forEach(function (b) { |
| b.addEventListener("click", function () { |
| var id = b.getAttribute("data-id"); |
| if (b.getAttribute("data-act") === "del") return doDelete(id); |
| return startEdit(id); |
| }); |
| }); |
| } |
|
|
| function load() { |
| state.instanceId = currentInstanceId(); |
| if (!state.instanceId) return Promise.resolve(); |
| return api("GET", "?instance_id=" + encodeURIComponent(state.instanceId)) |
| .then(function (res) { |
| if (res.status === 503) { state.enabled = false; return null; } |
| state.enabled = true; |
| var t = el("memo-panel-toggle"); |
| if (t) t.hidden = false; |
| return res.json(); |
| }) |
| .then(function (data) { |
| if (!data) return; |
| state.memos = data.memos || []; |
| render(); |
| }) |
| .catch(function () { }); |
| } |
|
|
| function resetComposer() { |
| state.editingId = null; |
| var body = el("memo-new-body"); |
| if (body) body.value = ""; |
| var anchorWrap = el("memo-anchor-wrap"); |
| if (anchorWrap) anchorWrap.hidden = true; |
| var btn = el("memo-add-btn"); |
| if (btn) btn.textContent = "Add note"; |
| } |
|
|
| function startEdit(id) { |
| var m = state.memos.filter(function (x) { return x.id === id; })[0]; |
| if (!m) return; |
| state.editingId = id; |
| el("memo-new-body").value = m.body; |
| el("memo-new-visibility").value = m.visibility; |
| el("memo-add-btn").textContent = "Save"; |
| el("memo-new-body").focus(); |
| } |
|
|
| function doDelete(id) { |
| api("DELETE", "/" + encodeURIComponent(id)).then(function () { |
| load(); |
| }); |
| } |
|
|
| function submit() { |
| var bodyEl = el("memo-new-body"); |
| var body = (bodyEl && bodyEl.value || "").trim(); |
| if (!body) return; |
| var visibility = el("memo-new-visibility").value; |
| if (state.editingId) { |
| api("PATCH", "/" + encodeURIComponent(state.editingId), |
| { body: body, visibility: visibility }) |
| .then(function () { resetComposer(); load(); }); |
| return; |
| } |
| var payload = { |
| instance_id: state.instanceId, |
| body: body, |
| visibility: visibility, |
| }; |
| var anchorCheck = el("memo-anchor-check"); |
| if (anchorCheck && anchorCheck.checked && state._pendingAnchor) { |
| payload.anchor = { |
| start: state._pendingAnchor.start, |
| end: state._pendingAnchor.end, |
| field: state._pendingAnchor.field, |
| }; |
| } |
| api("POST", "", payload).then(function (res) { |
| if (res.ok) { resetComposer(); load(); } |
| }); |
| } |
|
|
| function wireSelectionAffordance() { |
| document.addEventListener("selectionchange", function () { |
| var wrap = el("memo-anchor-wrap"); |
| if (!wrap) return; |
| var s = selectionInText(); |
| state._pendingAnchor = s; |
| if (s) { |
| wrap.hidden = false; |
| el("memo-anchor-quote").textContent = |
| s.quote.length > 60 ? s.quote.slice(0, 60) + "…" : s.quote; |
| } else { |
| wrap.hidden = true; |
| } |
| }); |
| } |
|
|
| function wire() { |
| var toggle = el("memo-panel-toggle"); |
| var panel = el("memo-panel"); |
| var close = el("memo-panel-close"); |
| if (toggle && panel) { |
| toggle.addEventListener("click", function () { |
| panel.hidden = false; |
| toggle.hidden = true; |
| render(); |
| }); |
| } |
| if (close && panel && toggle) { |
| close.addEventListener("click", function () { |
| panel.hidden = true; |
| toggle.hidden = false; |
| }); |
| } |
| var add = el("memo-add-btn"); |
| if (add) add.addEventListener("click", submit); |
| wireSelectionAffordance(); |
| } |
|
|
| function init() { |
| if (!window.config || !window.config.is_annotation_page) return; |
| if (!el("memo-panel")) return; |
| wire(); |
| load(); |
| } |
|
|
| window.MemoPanel = { reload: load }; |
|
|
| if (document.readyState === "loading") { |
| document.addEventListener("DOMContentLoaded", init); |
| } else { |
| init(); |
| } |
| })(); |
|
|