proctolog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/README.md +178 -0
- data/app/controllers/proctolog/acceptances_controller.rb +29 -0
- data/app/controllers/proctolog/application_controller.rb +32 -0
- data/app/controllers/proctolog/bursts_controller.rb +34 -0
- data/app/controllers/proctolog/choices_controller.rb +30 -0
- data/app/controllers/proctolog/code_reviews_controller.rb +26 -0
- data/app/controllers/proctolog/decisions_controller.rb +33 -0
- data/app/controllers/proctolog/entries_controller.rb +14 -0
- data/app/controllers/proctolog/evidence_controller.rb +23 -0
- data/app/controllers/proctolog/freshness_controller.rb +27 -0
- data/app/controllers/proctolog/good_to_go_controller.rb +29 -0
- data/app/controllers/proctolog/memberships_controller.rb +20 -0
- data/app/controllers/proctolog/mentions_controller.rb +16 -0
- data/app/controllers/proctolog/moments_controller.rb +26 -0
- data/app/controllers/proctolog/note_params.rb +24 -0
- data/app/controllers/proctolog/notes_controller.rb +56 -0
- data/app/controllers/proctolog/open_pull_requests_controller.rb +59 -0
- data/app/controllers/proctolog/pictures_controller.rb +27 -0
- data/app/controllers/proctolog/prompts_controller.rb +28 -0
- data/app/controllers/proctolog/pull_request_scoped.rb +21 -0
- data/app/controllers/proctolog/pull_requests_controller.rb +34 -0
- data/app/controllers/proctolog/put_away_controller.rb +19 -0
- data/app/controllers/proctolog/recordings_controller.rb +28 -0
- data/app/controllers/proctolog/rendered_pieces_controller.rb +11 -0
- data/app/controllers/proctolog/review_data_controller.rb +12 -0
- data/app/controllers/proctolog/rules_controller.rb +20 -0
- data/app/controllers/proctolog/scenario_notes_controller.rb +26 -0
- data/app/controllers/proctolog/scenario_scoped.rb +21 -0
- data/app/controllers/proctolog/scenarios_controller.rb +67 -0
- data/app/controllers/proctolog/sitting_servers_controller.rb +30 -0
- data/app/controllers/proctolog/waiting_controller.rb +14 -0
- data/app/controllers/proctolog/with_decisions.rb +28 -0
- data/app/helpers/proctolog/bursts_helper.rb +52 -0
- data/app/helpers/proctolog/decisions_helper.rb +102 -0
- data/app/helpers/proctolog/desk_helper.rb +61 -0
- data/app/helpers/proctolog/mentions_helper.rb +27 -0
- data/app/helpers/proctolog/open_pull_requests_helper.rb +31 -0
- data/app/helpers/proctolog/prompts_helper.rb +21 -0
- data/app/helpers/proctolog/pull_requests_helper.rb +67 -0
- data/app/helpers/proctolog/recordings_helper.rb +30 -0
- data/app/helpers/proctolog/rounds_helper.rb +27 -0
- data/app/helpers/proctolog/scenarios_helper.rb +123 -0
- data/app/helpers/proctolog/standings_helper.rb +80 -0
- data/app/jobs/proctolog/application_job.rb +8 -0
- data/app/jobs/proctolog/ask_for_release_job.rb +9 -0
- data/app/jobs/proctolog/fetch_pictures_ahead_job.rb +36 -0
- data/app/jobs/proctolog/prune_fresh_profiles_job.rb +9 -0
- data/app/jobs/proctolog/read_mentions_ahead_job.rb +12 -0
- data/app/models/proctolog/acceptance.rb +47 -0
- data/app/models/proctolog/app.rb +92 -0
- data/app/models/proctolog/browser.rb +42 -0
- data/app/models/proctolog/burst.rb +268 -0
- data/app/models/proctolog/burst_notices.rb +143 -0
- data/app/models/proctolog/chapters.rb +42 -0
- data/app/models/proctolog/check.rb +122 -0
- data/app/models/proctolog/choices.rb +112 -0
- data/app/models/proctolog/code_review.rb +251 -0
- data/app/models/proctolog/decision_record.rb +12 -0
- data/app/models/proctolog/decisions.rb +199 -0
- data/app/models/proctolog/description.rb +165 -0
- data/app/models/proctolog/difference.rb +138 -0
- data/app/models/proctolog/doctor.rb +127 -0
- data/app/models/proctolog/evidence.rb +29 -0
- data/app/models/proctolog/failure.rb +7 -0
- data/app/models/proctolog/focus.rb +48 -0
- data/app/models/proctolog/fresh_sitting.rb +35 -0
- data/app/models/proctolog/github.rb +353 -0
- data/app/models/proctolog/handover/brief.rb +138 -0
- data/app/models/proctolog/handover/layers.rb +128 -0
- data/app/models/proctolog/handover/places.rb +98 -0
- data/app/models/proctolog/handover/taken.rb +41 -0
- data/app/models/proctolog/handover/worklist.rb +35 -0
- data/app/models/proctolog/handover.rb +123 -0
- data/app/models/proctolog/housekeeping.rb +23 -0
- data/app/models/proctolog/kit.rb +36 -0
- data/app/models/proctolog/marks.rb +130 -0
- data/app/models/proctolog/membership.rb +91 -0
- data/app/models/proctolog/mentions.rb +117 -0
- data/app/models/proctolog/note.rb +94 -0
- data/app/models/proctolog/outside.rb +45 -0
- data/app/models/proctolog/overlay/brief.rb +52 -0
- data/app/models/proctolog/overlay/written.rb +33 -0
- data/app/models/proctolog/overlay.rb +87 -0
- data/app/models/proctolog/owner.rb +16 -0
- data/app/models/proctolog/pick.rb +47 -0
- data/app/models/proctolog/picture.rb +138 -0
- data/app/models/proctolog/pieces_rendering.rb +40 -0
- data/app/models/proctolog/project.rb +118 -0
- data/app/models/proctolog/pull_request.rb +89 -0
- data/app/models/proctolog/recording.rb +194 -0
- data/app/models/proctolog/release.rb +59 -0
- data/app/models/proctolog/rendering.rb +303 -0
- data/app/models/proctolog/reports.rb +66 -0
- data/app/models/proctolog/response_files.rb +55 -0
- data/app/models/proctolog/rounds.rb +305 -0
- data/app/models/proctolog/route.rb +151 -0
- data/app/models/proctolog/rules.rb +121 -0
- data/app/models/proctolog/scenario.rb +126 -0
- data/app/models/proctolog/server.rb +273 -0
- data/app/models/proctolog/since.rb +108 -0
- data/app/models/proctolog/size.rb +30 -0
- data/app/models/proctolog/standing.rb +132 -0
- data/app/models/proctolog/theme.rb +40 -0
- data/app/models/proctolog/unopened/numbers.rb +48 -0
- data/app/models/proctolog/unopened/opening.rb +88 -0
- data/app/models/proctolog/unopened.rb +203 -0
- data/app/models/proctolog/visit.rb +26 -0
- data/app/views/layouts/proctolog/application.html.erb +58 -0
- data/app/views/proctolog/acceptances/new.html.erb +34 -0
- data/app/views/proctolog/bursts/_line.html.erb +28 -0
- data/app/views/proctolog/bursts/_merged.html.erb +23 -0
- data/app/views/proctolog/bursts/_moved.html.erb +16 -0
- data/app/views/proctolog/bursts/_notices.html.erb +39 -0
- data/app/views/proctolog/bursts/_put_away.html.erb +11 -0
- data/app/views/proctolog/bursts/_rules.html.erb +27 -0
- data/app/views/proctolog/bursts/_side.html.erb +49 -0
- data/app/views/proctolog/bursts/_side_stop.html.erb +10 -0
- data/app/views/proctolog/bursts/show.html.erb +54 -0
- data/app/views/proctolog/code_reviews/show.html.erb +33 -0
- data/app/views/proctolog/decisions/_alternative.html.erb +5 -0
- data/app/views/proctolog/decisions/_beneath.html.erb +21 -0
- data/app/views/proctolog/decisions/_box.html.erb +27 -0
- data/app/views/proctolog/decisions/_choices.html.erb +9 -0
- data/app/views/proctolog/decisions/_draft_tools.html.erb +47 -0
- data/app/views/proctolog/decisions/_part.html.erb +61 -0
- data/app/views/proctolog/decisions/_round_data.html.erb +1 -0
- data/app/views/proctolog/decisions/_theme.html.erb +13 -0
- data/app/views/proctolog/decisions/index.html.erb +75 -0
- data/app/views/proctolog/notes/_card.html.erb +19 -0
- data/app/views/proctolog/notes/_template.html.erb +41 -0
- data/app/views/proctolog/open_pull_requests/_side.html.erb +30 -0
- data/app/views/proctolog/open_pull_requests/index.html.erb +106 -0
- data/app/views/proctolog/prompts/show.html.erb +41 -0
- data/app/views/proctolog/pull_requests/_connections.html.erb +65 -0
- data/app/views/proctolog/pull_requests/_finish.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_margin.html.erb +33 -0
- data/app/views/proctolog/pull_requests/_note.html.erb +4 -0
- data/app/views/proctolog/pull_requests/_outline.html.erb +26 -0
- data/app/views/proctolog/pull_requests/_paragraphs.html.erb +9 -0
- data/app/views/proctolog/pull_requests/_scenarios.html.erb +16 -0
- data/app/views/proctolog/pull_requests/_since.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_standing.html.erb +51 -0
- data/app/views/proctolog/pull_requests/_status.html.erb +5 -0
- data/app/views/proctolog/pull_requests/_try.html.erb +122 -0
- data/app/views/proctolog/pull_requests/show.html.erb +145 -0
- data/app/views/proctolog/recordings/_dialog.html.erb +26 -0
- data/app/views/proctolog/recordings/_list.html.erb +8 -0
- data/app/views/proctolog/recordings/_note.html.erb +22 -0
- data/app/views/proctolog/recordings/index.html.erb +33 -0
- data/app/views/proctolog/recordings/show.html.erb +118 -0
- data/app/views/proctolog/scenarios/_dialog.html.erb +22 -0
- data/app/views/proctolog/scenarios/_note.html.erb +18 -0
- data/app/views/proctolog/scenarios/_open.html.erb +9 -0
- data/app/views/proctolog/scenarios/_scenario.html.erb +48 -0
- data/app/views/proctolog/scenarios/_server.html.erb +20 -0
- data/app/views/proctolog/scenarios/index.html.erb +62 -0
- data/bin/proctolog +330 -0
- data/completions/_proctolog +143 -0
- data/config/application.rb +94 -0
- data/config/database.yml +16 -0
- data/config/environment.rb +6 -0
- data/config/initializers/grouse.rb +113 -0
- data/config/initializers/grouse_notes.rb +9 -0
- data/config/keys.yml +6 -0
- data/config/routes.rb +71 -0
- data/config/storage.yml +4 -0
- data/db/migrate/20261019213542_create_grouse_tables.grouse.rb +128 -0
- data/db/migrate/20261019213550_create_active_storage_tables.active_storage.rb +58 -0
- data/docs/manual/app.md +157 -0
- data/docs/manual/bursts.md +129 -0
- data/docs/manual/deciding.md +89 -0
- data/docs/manual/desk.png +0 -0
- data/docs/manual/films.md +20 -0
- data/docs/manual/handover.md +93 -0
- data/docs/manual/pr-format.md +75 -0
- data/docs/manual/reading.md +233 -0
- data/docs/manual/unopened.md +80 -0
- data/docs/manual/updates.md +55 -0
- data/lib/proctolog/installation.rb +125 -0
- data/lib/proctolog/version.rb +7 -0
- data/public/asking.js +50 -0
- data/public/code-review.js +40 -0
- data/public/cursor.js +292 -0
- data/public/decision-screenshots.js +97 -0
- data/public/decisions.css +949 -0
- data/public/decisions.js +1289 -0
- data/public/desk.css +582 -0
- data/public/desk.js +126 -0
- data/public/favicon.png +0 -0
- data/public/keys.js +228 -0
- data/public/mentions.js +389 -0
- data/public/messages.js +103 -0
- data/public/notes.js +731 -0
- data/public/pick.css +99 -0
- data/public/plaque.png +0 -0
- data/public/pr-number.js +11 -0
- data/public/prompt.css +46 -0
- data/public/prompt.js +16 -0
- data/public/pull_request.css +805 -0
- data/public/recording.css +379 -0
- data/public/recording.js +394 -0
- data/public/release.js +25 -0
- data/public/respond.css +260 -0
- data/public/respond.js +95 -0
- data/public/review-drafts.js +949 -0
- data/public/review-rounds.js +1447 -0
- data/public/scenarios.css +253 -0
- data/public/scenarios.js +36 -0
- data/public/schemas/proctolog-response-v1.json +101 -0
- data/public/side.js +133 -0
- data/public/standing.css +478 -0
- data/public/terminal-apl.svg +6 -0
- data/public/terminal-cuneiform.svg +3 -0
- data/public/terminal-grain-dots.svg +6 -0
- data/public/terminal-grain-weave.svg +6 -0
- data/public/terminal-grain-wedges.svg +6 -0
- data/public/terminal-shades.svg +3 -0
- data/public/terminal.css +4426 -0
- data/public/toast.js +26 -0
- data/public/try.js +124 -0
- data/public/viewer.js +808 -0
- data/public/words.js +81 -0
- data/skills/proctolog-handover/SKILL.md +77 -0
- data/skills/proctolog-pr-writing/SKILL.md +228 -0
- data/skills/proctolog-pr-writing/references/example-description.md +33 -0
- metadata +366 -0
data/public/notes.js
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
// The page of one pull request: notes on words, on any block of the
|
|
2
|
+
// description, on pictures, on the whole of it and on its scenarios.
|
|
3
|
+
//
|
|
4
|
+
// Words are quoted from the text of their section, never from what the layout
|
|
5
|
+
// shows (words.js): the server found them in the same text, so a note finds
|
|
6
|
+
// its words again wherever an edit moves them. A region of a picture is four
|
|
7
|
+
// fractions of the picture as shown, so it holds at any size. A note is
|
|
8
|
+
// written in a small form right under what it is about (respond.js).
|
|
9
|
+
|
|
10
|
+
import { helping, keyOf } from "./keys.js";
|
|
11
|
+
import { respond } from "./respond.js";
|
|
12
|
+
import { toast } from "./toast.js";
|
|
13
|
+
import { tryIt } from "./try.js";
|
|
14
|
+
import { viewPictures } from "./viewer.js";
|
|
15
|
+
import {
|
|
16
|
+
blockOf,
|
|
17
|
+
blocksIn,
|
|
18
|
+
elementOf,
|
|
19
|
+
hasWords,
|
|
20
|
+
ownWords,
|
|
21
|
+
quoteOf,
|
|
22
|
+
} from "./words.js";
|
|
23
|
+
|
|
24
|
+
const REGION_DRAG = 4;
|
|
25
|
+
|
|
26
|
+
const selectionButton = document.querySelector("#note-selection");
|
|
27
|
+
|
|
28
|
+
// Remembers which sections are unfolded, for this tab and this pull request.
|
|
29
|
+
|
|
30
|
+
const sections = [...document.querySelectorAll("details.section")];
|
|
31
|
+
const foldsKey = `proctolog:folds:${location.pathname}`;
|
|
32
|
+
|
|
33
|
+
const remembered = () => {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(sessionStorage.getItem(foldsKey)) ?? {};
|
|
36
|
+
} catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const remember = () => {
|
|
42
|
+
const folds = Object.fromEntries(
|
|
43
|
+
sections.map((details) => [details.dataset.heading, details.open]),
|
|
44
|
+
);
|
|
45
|
+
try {
|
|
46
|
+
sessionStorage.setItem(foldsKey, JSON.stringify(folds));
|
|
47
|
+
} catch {
|
|
48
|
+
// A tab that keeps nothing simply opens as the server says.
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const folds = remembered();
|
|
53
|
+
for (const details of sections) {
|
|
54
|
+
if (details.dataset.heading in folds)
|
|
55
|
+
details.open = folds[details.dataset.heading];
|
|
56
|
+
details.addEventListener("toggle", remember);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Finding things: a note scrolls to its mark, a mark lights its note.
|
|
60
|
+
|
|
61
|
+
const unfold = (element) => {
|
|
62
|
+
for (let at = element.parentElement; at; at = at.parentElement)
|
|
63
|
+
if (at.localName === "details") at.open = true;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const light = (element) => {
|
|
67
|
+
clearTimeout(element.fadeOut);
|
|
68
|
+
element.classList.add("lit");
|
|
69
|
+
element.fadeOut = setTimeout(() => element.classList.remove("lit"), 1800);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const showNote = (id) => {
|
|
73
|
+
const marks = document.querySelectorAll(`[data-note="${id}"]`);
|
|
74
|
+
if (!marks.length) return;
|
|
75
|
+
unfold(marks[0]);
|
|
76
|
+
marks[0].scrollIntoView({ block: "center" });
|
|
77
|
+
for (const mark of marks) light(mark);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const showCard = (id) => {
|
|
81
|
+
const card = document.querySelector(`[data-card="${id}"]`);
|
|
82
|
+
if (!card) return;
|
|
83
|
+
unfold(card);
|
|
84
|
+
card.scrollIntoView({ block: "nearest" });
|
|
85
|
+
light(card);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
document.addEventListener("click", (event) => {
|
|
89
|
+
if (event.target.closest("button, form, summary, a")) return;
|
|
90
|
+
|
|
91
|
+
const card = event.target.closest("[data-card]");
|
|
92
|
+
if (card) return showNote(card.dataset.card);
|
|
93
|
+
|
|
94
|
+
const mark = event.target.closest("[data-note]");
|
|
95
|
+
if (mark && getSelection().isCollapsed) showCard(mark.dataset.note);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const arrive = () => {
|
|
99
|
+
const target =
|
|
100
|
+
location.hash && document.getElementById(location.hash.slice(1));
|
|
101
|
+
if (!target) return;
|
|
102
|
+
unfold(target);
|
|
103
|
+
target.scrollIntoView({ block: "center" });
|
|
104
|
+
light(target);
|
|
105
|
+
const card = document.querySelector(`[data-card="${target.dataset.note}"]`);
|
|
106
|
+
if (card) light(card);
|
|
107
|
+
};
|
|
108
|
+
arrive();
|
|
109
|
+
addEventListener("hashchange", arrive);
|
|
110
|
+
|
|
111
|
+
// Where the form of a note opens. The desk's own elements are marked
|
|
112
|
+
// data-desk, so that none of them is counted among the words of a section.
|
|
113
|
+
|
|
114
|
+
const made = (tag, className) => {
|
|
115
|
+
const element = document.createElement(tag);
|
|
116
|
+
element.className = className;
|
|
117
|
+
element.setAttribute("data-desk", "");
|
|
118
|
+
return element;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Right under a block, of its own kind, so that a list stays a list and a
|
|
122
|
+
// table a table.
|
|
123
|
+
const slotAfter = (block) => {
|
|
124
|
+
const next = block.nextElementSibling;
|
|
125
|
+
if (next?.matches(".slot")) return next.querySelector("td") ?? next;
|
|
126
|
+
|
|
127
|
+
const row = block.localName === "tr";
|
|
128
|
+
const slot = made(
|
|
129
|
+
row ? "tr" : block.localName === "li" ? "li" : "div",
|
|
130
|
+
"slot",
|
|
131
|
+
);
|
|
132
|
+
block.after(slot);
|
|
133
|
+
return row
|
|
134
|
+
? slot.appendChild(
|
|
135
|
+
Object.assign(document.createElement("td"), { colSpan: 99 }),
|
|
136
|
+
)
|
|
137
|
+
: slot;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const slotIn = (element) =>
|
|
141
|
+
element.querySelector(":scope > .slot") ??
|
|
142
|
+
element.appendChild(made("div", "slot"));
|
|
143
|
+
|
|
144
|
+
// A picture's draft rectangle goes with the form that was written for it.
|
|
145
|
+
document.addEventListener("closed", ({ target }) => {
|
|
146
|
+
(
|
|
147
|
+
target.previousElementSibling?.closest("[data-reading]") ||
|
|
148
|
+
document.querySelector("[data-note-whole]")
|
|
149
|
+
)?.focus({ preventScroll: true });
|
|
150
|
+
for (const draft of target.previousElementSibling?.querySelectorAll(
|
|
151
|
+
".draft",
|
|
152
|
+
) ?? [])
|
|
153
|
+
draft.remove();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Words. A selection is noted under the block it begins in.
|
|
157
|
+
|
|
158
|
+
const selectedWords = () => {
|
|
159
|
+
const selection = getSelection();
|
|
160
|
+
if (!selection.rangeCount || selection.isCollapsed) return null;
|
|
161
|
+
|
|
162
|
+
const range = selection.getRangeAt(0);
|
|
163
|
+
const quote = quoteOf(range);
|
|
164
|
+
return quote && { range, quote };
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const noteSelection = () => {
|
|
168
|
+
const words = selectedWords();
|
|
169
|
+
if (!words) return false;
|
|
170
|
+
|
|
171
|
+
const { range, quote } = words;
|
|
172
|
+
selectionButton.hidden = true;
|
|
173
|
+
const block =
|
|
174
|
+
blockOf(range.startContainer) ??
|
|
175
|
+
blockOf(range.endContainer) ??
|
|
176
|
+
elementOf(range.commonAncestorContainer).closest("[data-quotable]");
|
|
177
|
+
respond(slotAfter(block), { on: quote.exact, quote });
|
|
178
|
+
return true;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// A block is noted for its own words, without selecting anything.
|
|
182
|
+
|
|
183
|
+
const noteBlock = (block) => {
|
|
184
|
+
const quote = quoteOf(ownWords(block));
|
|
185
|
+
if (quote) respond(slotAfter(block), { on: quote.exact, quote });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// A place on a picture, as the note's form takes it.
|
|
189
|
+
const placeOn = (figure, box) => ({
|
|
190
|
+
on: figure.dataset.picture,
|
|
191
|
+
picture: {
|
|
192
|
+
picture: figure.dataset.picture,
|
|
193
|
+
asset: figure.dataset.asset,
|
|
194
|
+
box: box.map(round).join(","),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// A note on a pair's half is written under the pair.
|
|
199
|
+
const notePicture = (figure, box = [0, 0, 1, 1]) =>
|
|
200
|
+
respond(slotAfter(figure.closest("figure.picture")), placeOn(figure, box));
|
|
201
|
+
|
|
202
|
+
let pending;
|
|
203
|
+
document.addEventListener("selectionchange", () => {
|
|
204
|
+
clearTimeout(pending);
|
|
205
|
+
pending = setTimeout(() => {
|
|
206
|
+
const words = selectedWords();
|
|
207
|
+
selectionButton.hidden = !words;
|
|
208
|
+
if (!words) return;
|
|
209
|
+
const rect = words.range.getBoundingClientRect();
|
|
210
|
+
const left = Math.min(
|
|
211
|
+
rect.right,
|
|
212
|
+
innerWidth - selectionButton.offsetWidth - 8,
|
|
213
|
+
);
|
|
214
|
+
selectionButton.style.left = `${Math.max(8, left) + scrollX}px`;
|
|
215
|
+
selectionButton.style.top = `${rect.top + scrollY - selectionButton.offsetHeight - 6}px`;
|
|
216
|
+
}, 120);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Keeps the selection alive while the button is pressed.
|
|
220
|
+
selectionButton.addEventListener("mousedown", (event) =>
|
|
221
|
+
event.preventDefault(),
|
|
222
|
+
);
|
|
223
|
+
selectionButton.addEventListener("click", noteSelection);
|
|
224
|
+
|
|
225
|
+
// A "+" in the gutter of each block that has words of its own. It shows while
|
|
226
|
+
// the pointer is on the block, or the keyboard is in it, and stays on the
|
|
227
|
+
// last block until another is entered, so that the pointer can cross to it.
|
|
228
|
+
|
|
229
|
+
const pluses = new Map();
|
|
230
|
+
|
|
231
|
+
for (const body of document.querySelectorAll("[data-quotable]")) {
|
|
232
|
+
for (const block of blocksIn(body)) {
|
|
233
|
+
if (block.matches("figure.picture") || !hasWords(block)) continue;
|
|
234
|
+
|
|
235
|
+
const plus = made("button", "plus");
|
|
236
|
+
plus.type = "button";
|
|
237
|
+
plus.textContent = "+";
|
|
238
|
+
plus.title = "Note on this block";
|
|
239
|
+
plus.setAttribute("aria-label", "Note on this block");
|
|
240
|
+
(block.localName === "tr" ? block.firstElementChild : block).prepend(plus);
|
|
241
|
+
pluses.set(block, plus);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let here = null;
|
|
246
|
+
|
|
247
|
+
const rest = (block) => {
|
|
248
|
+
if (block === here) return;
|
|
249
|
+
pluses.get(here)?.classList.remove("here");
|
|
250
|
+
here = block;
|
|
251
|
+
pluses.get(here)?.classList.add("here");
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const enter = ({ target }) => {
|
|
255
|
+
const block = blockOf(target);
|
|
256
|
+
if (block) rest(block);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
document.addEventListener("pointerover", enter);
|
|
260
|
+
document.addEventListener("focusin", enter);
|
|
261
|
+
document
|
|
262
|
+
.querySelector("main.description")
|
|
263
|
+
?.addEventListener("pointerleave", () => rest(null));
|
|
264
|
+
|
|
265
|
+
document.addEventListener("click", (event) => {
|
|
266
|
+
const plus = event.target.closest(".plus");
|
|
267
|
+
if (plus) noteBlock(blockOf(plus));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Pictures: a drag draws a rectangle, a click pins a point.
|
|
271
|
+
|
|
272
|
+
const round = (fraction) => Math.round(fraction * 1e4) / 1e4;
|
|
273
|
+
const within = (fraction) => Math.min(Math.max(fraction, 0), 1);
|
|
274
|
+
|
|
275
|
+
let drag = null;
|
|
276
|
+
|
|
277
|
+
const pointOn = (event, frame) => ({
|
|
278
|
+
x: within((event.clientX - frame.left) / frame.width),
|
|
279
|
+
y: within((event.clientY - frame.top) / frame.height),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const boxBetween = (from, to) => [
|
|
283
|
+
Math.min(from.x, to.x),
|
|
284
|
+
Math.min(from.y, to.y),
|
|
285
|
+
Math.abs(to.x - from.x),
|
|
286
|
+
Math.abs(to.y - from.y),
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const draw = (box) => {
|
|
290
|
+
drag.shape ??= drag.figure.appendChild(
|
|
291
|
+
Object.assign(document.createElement("span"), {
|
|
292
|
+
className: "region draft",
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
const [left, top, width, height] = box.map(
|
|
296
|
+
(fraction) => `${fraction * 100}%`,
|
|
297
|
+
);
|
|
298
|
+
const pin = box[2] === 0 && box[3] === 0;
|
|
299
|
+
drag.shape.classList.toggle("pin", pin);
|
|
300
|
+
Object.assign(drag.shape.style, {
|
|
301
|
+
left,
|
|
302
|
+
top,
|
|
303
|
+
width: pin ? "" : width,
|
|
304
|
+
height: pin ? "" : height,
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
document.addEventListener("pointerdown", (event) => {
|
|
309
|
+
const image = event.target;
|
|
310
|
+
// A pair's half is a picture of its own to draw on.
|
|
311
|
+
const figure = image.closest("figure.pair > .half, figure.picture");
|
|
312
|
+
if (event.button !== 0 || image.localName !== "img" || !figure) return;
|
|
313
|
+
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
image.setPointerCapture(event.pointerId);
|
|
316
|
+
for (const draft of figure.querySelectorAll(".draft")) draft.remove();
|
|
317
|
+
const frame = image.getBoundingClientRect();
|
|
318
|
+
drag = {
|
|
319
|
+
figure,
|
|
320
|
+
frame,
|
|
321
|
+
from: pointOn(event, frame),
|
|
322
|
+
start: event,
|
|
323
|
+
shape: null,
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
document.addEventListener("pointermove", (event) => {
|
|
328
|
+
if (!drag) return;
|
|
329
|
+
const moved = Math.hypot(
|
|
330
|
+
event.clientX - drag.start.clientX,
|
|
331
|
+
event.clientY - drag.start.clientY,
|
|
332
|
+
);
|
|
333
|
+
if (moved >= REGION_DRAG)
|
|
334
|
+
draw(boxBetween(drag.from, pointOn(event, drag.frame)));
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
document.addEventListener("pointerup", (event) => {
|
|
338
|
+
if (!drag) return;
|
|
339
|
+
const { figure, frame, from, start } = drag;
|
|
340
|
+
const dragged =
|
|
341
|
+
Math.hypot(event.clientX - start.clientX, event.clientY - start.clientY) >=
|
|
342
|
+
REGION_DRAG;
|
|
343
|
+
const box = dragged
|
|
344
|
+
? boxBetween(from, pointOn(event, frame))
|
|
345
|
+
: [from.x, from.y, 0, 0];
|
|
346
|
+
draw(box);
|
|
347
|
+
drag = null;
|
|
348
|
+
notePicture(figure, box);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
document.addEventListener("pointercancel", () => {
|
|
352
|
+
drag?.shape?.remove();
|
|
353
|
+
drag = null;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Reading has its own cursor; Tab still reaches every link and control.
|
|
357
|
+
for (const element of document.querySelectorAll(
|
|
358
|
+
"h1, h2, h3, h4, summary, .conditions > li, li.scenario, [data-card]",
|
|
359
|
+
)) {
|
|
360
|
+
if (element.closest("[data-desk]")) continue;
|
|
361
|
+
element.dataset.reading = "";
|
|
362
|
+
if (element.localName !== "summary") element.tabIndex = -1;
|
|
363
|
+
}
|
|
364
|
+
for (const body of document.querySelectorAll("[data-quotable]")) {
|
|
365
|
+
for (const block of blocksIn(body)) {
|
|
366
|
+
if (!block.matches("figure.picture") && !hasWords(block)) continue;
|
|
367
|
+
block.dataset.reading = "";
|
|
368
|
+
block.tabIndex = -1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// How the reading moves, the same on every part of the page, returned work
|
|
372
|
+
// included: ↓ and j go to the next block the page shows, ↑ and k to the one
|
|
373
|
+
// before, in the order the page is read, through whatever is unfolded and
|
|
374
|
+
// past nothing; a fold shut is one block, its heading. They stop at the first
|
|
375
|
+
// and the last block, and leave the page for nowhere.
|
|
376
|
+
const reading = () =>
|
|
377
|
+
[...document.querySelectorAll("[data-reading]")].filter(
|
|
378
|
+
(element) =>
|
|
379
|
+
element.getClientRects().length &&
|
|
380
|
+
// What a shut fold holds keeps its boxes, but is not shown.
|
|
381
|
+
!element.closest("details:not([open]) > :not(summary)") &&
|
|
382
|
+
!element.parentElement.closest("[data-decision]"),
|
|
383
|
+
);
|
|
384
|
+
// What the page shows between its top and the bar at its foot.
|
|
385
|
+
const inView = () => ({
|
|
386
|
+
top: 0,
|
|
387
|
+
bottom:
|
|
388
|
+
document.querySelector("footer.foot")?.getBoundingClientRect().top ??
|
|
389
|
+
innerHeight,
|
|
390
|
+
});
|
|
391
|
+
const focusReading = (element) => {
|
|
392
|
+
if (!element) return;
|
|
393
|
+
element.focus({ preventScroll: true });
|
|
394
|
+
const box = element.getBoundingClientRect();
|
|
395
|
+
const { bottom } = inView();
|
|
396
|
+
// A block taller than the page shows is brought in from its first line.
|
|
397
|
+
if (box.height > bottom) element.scrollIntoView({ block: "start" });
|
|
398
|
+
else element.scrollIntoView({ block: "nearest" });
|
|
399
|
+
};
|
|
400
|
+
// A block taller than the page shows is read a screen at a time, the way
|
|
401
|
+
// the reading goes, before the reading moves on from it: nothing is passed
|
|
402
|
+
// over unseen.
|
|
403
|
+
const SCREEN = 0.8;
|
|
404
|
+
const readOn = (block, direction) => {
|
|
405
|
+
if (!block) return false;
|
|
406
|
+
const box = block.getBoundingClientRect();
|
|
407
|
+
const { top, bottom } = inView();
|
|
408
|
+
if (box.height <= bottom - top) return false;
|
|
409
|
+
const beyond = direction > 0 ? box.bottom - bottom : top - box.top;
|
|
410
|
+
if (beyond <= 1) return false;
|
|
411
|
+
const was = scrollY;
|
|
412
|
+
scrollBy({ top: direction * Math.min(beyond, (bottom - top) * SCREEN) });
|
|
413
|
+
// At the end of the page there is nothing more to scroll to.
|
|
414
|
+
return scrollY !== was;
|
|
415
|
+
};
|
|
416
|
+
const moveReading = (direction) => {
|
|
417
|
+
const items = reading();
|
|
418
|
+
const current = document.activeElement.closest("[data-reading]");
|
|
419
|
+
if (readOn(current, direction)) return;
|
|
420
|
+
const at = items.indexOf(current);
|
|
421
|
+
focusReading(items[Math.min(Math.max(at + direction, 0), items.length - 1)]);
|
|
422
|
+
};
|
|
423
|
+
// [ and ] go from heading to heading: the description's sections, the
|
|
424
|
+
// returned work and each response in it.
|
|
425
|
+
const HEADING =
|
|
426
|
+
"details.section > summary, [data-round-results] > h2, [data-request] > h3";
|
|
427
|
+
const moveSection = (direction) => {
|
|
428
|
+
const items = reading();
|
|
429
|
+
const at = items.indexOf(document.activeElement.closest("[data-reading]"));
|
|
430
|
+
const headings = items.filter((element) => element.matches(HEADING));
|
|
431
|
+
// The heading of the part the reading is in: the last at it or before it.
|
|
432
|
+
const current = items
|
|
433
|
+
.slice(0, at + 1)
|
|
434
|
+
.findLast((element) => element.matches(HEADING));
|
|
435
|
+
const index = headings.indexOf(current);
|
|
436
|
+
focusReading(
|
|
437
|
+
headings[Math.min(Math.max(index + direction, 0), headings.length - 1)],
|
|
438
|
+
);
|
|
439
|
+
};
|
|
440
|
+
const foldSection = (open) => {
|
|
441
|
+
const section = document.activeElement.closest("details");
|
|
442
|
+
if (!section) return;
|
|
443
|
+
section.open = open;
|
|
444
|
+
if (!open) focusReading(section.querySelector("summary"));
|
|
445
|
+
};
|
|
446
|
+
const initial = document.getElementById(location.hash.slice(1));
|
|
447
|
+
(initial?.matches("[data-reading]")
|
|
448
|
+
? initial
|
|
449
|
+
: document.querySelector("main.description > details > summary")
|
|
450
|
+
)?.focus({ preventScroll: true });
|
|
451
|
+
|
|
452
|
+
// Keys. They are ignored while typing.
|
|
453
|
+
|
|
454
|
+
// To another page by the link that names the key, once the desk has the
|
|
455
|
+
// owner's last choice: the page it goes to counts them. `absent` is said when
|
|
456
|
+
// this page has no such link.
|
|
457
|
+
const follow = async (key, absent) => {
|
|
458
|
+
const link = document.querySelector(`a[data-key="${key}"]`);
|
|
459
|
+
if (!link) return toast(absent);
|
|
460
|
+
await globalThis.proctologSettled?.();
|
|
461
|
+
location.assign(link.href);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const keys = {
|
|
465
|
+
n: () => {
|
|
466
|
+
if (noteSelection()) return;
|
|
467
|
+
const block = blockOf(document.activeElement) || here;
|
|
468
|
+
if (block?.matches("figure.picture")) notePicture(block);
|
|
469
|
+
else if (block) noteBlock(block);
|
|
470
|
+
else
|
|
471
|
+
toast(
|
|
472
|
+
"Move onto a block or a picture first, then n writes a note on it.",
|
|
473
|
+
);
|
|
474
|
+
},
|
|
475
|
+
c: () =>
|
|
476
|
+
toast(
|
|
477
|
+
"n writes a note here. c suggests a change on a decision: f goes to the first one.",
|
|
478
|
+
),
|
|
479
|
+
w: () => respond(document.querySelector("#whole-slot"), {}),
|
|
480
|
+
j: () => moveReading(1),
|
|
481
|
+
ArrowDown: () => moveReading(1),
|
|
482
|
+
J: () => follow("J", "This is the last pull request of the route."),
|
|
483
|
+
k: () => moveReading(-1),
|
|
484
|
+
ArrowUp: () => moveReading(-1),
|
|
485
|
+
K: () => follow("K", "This is the first pull request of the route."),
|
|
486
|
+
N: () => follow("N", "No other pull request has decisions left to review."),
|
|
487
|
+
"[": () => moveSection(-1),
|
|
488
|
+
"]": () => moveSection(1),
|
|
489
|
+
ArrowLeft: () => foldSection(false),
|
|
490
|
+
ArrowRight: () => foldSection(true),
|
|
491
|
+
Home: () => focusReading(reading()[0]),
|
|
492
|
+
End: () => focusReading(reading().at(-1)),
|
|
493
|
+
h: () => follow("h"),
|
|
494
|
+
r: () => follow("r"),
|
|
495
|
+
d: () => document.querySelector("form[data-code]")?.requestSubmit(),
|
|
496
|
+
t: tryIt,
|
|
497
|
+
// A click, so the browser opens the pull request in a tab of its own.
|
|
498
|
+
G: () => {
|
|
499
|
+
const link = document.querySelector('.whole a[data-key="G"]');
|
|
500
|
+
if (link) link.click();
|
|
501
|
+
else toast("Nobody has opened this pull request yet: GitHub has nothing of it.");
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Looking closely at a picture (viewer.js): the one the reading rests on, among
|
|
506
|
+
// all of the description's, or the pictures a decision links to while it is
|
|
507
|
+
// the one being decided. A place marked there is noted on the picture itself,
|
|
508
|
+
// in the description, which the page shows again to write the note.
|
|
509
|
+
const figures = () => [
|
|
510
|
+
...document.querySelectorAll("main.description figure.picture"),
|
|
511
|
+
];
|
|
512
|
+
// A picture as the viewer takes it: one alone, or a pair by its halves,
|
|
513
|
+
// each a picture a mark can be made on.
|
|
514
|
+
const shown = (figure) => {
|
|
515
|
+
if (figure.matches(".pair"))
|
|
516
|
+
return {
|
|
517
|
+
name: figure.dataset.picture,
|
|
518
|
+
figure,
|
|
519
|
+
pair: [...figure.querySelectorAll(":scope > .half")].map(shown),
|
|
520
|
+
};
|
|
521
|
+
const image = figure.querySelector("img");
|
|
522
|
+
return {
|
|
523
|
+
src: image.currentSrc || image.src,
|
|
524
|
+
alt: image.alt,
|
|
525
|
+
name: figure.dataset.picture ?? image.alt,
|
|
526
|
+
figure,
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
// A picture a decision links to, as the description has it: the link may
|
|
530
|
+
// name a pair, or one half of it.
|
|
531
|
+
const linkedShown = (preview) => {
|
|
532
|
+
const figure = [
|
|
533
|
+
...document.querySelectorAll(
|
|
534
|
+
"main.description figure.picture, main.description figure.pair > .half",
|
|
535
|
+
),
|
|
536
|
+
].find((one) => one.dataset.picture === preview.dataset.picture);
|
|
537
|
+
return figure ? shown(figure) : shown(preview);
|
|
538
|
+
};
|
|
539
|
+
// A note on a place marked in the viewer is written beside it, and saved
|
|
540
|
+
// without leaving it, so that the owner goes on looking where they were. A
|
|
541
|
+
// note drawn there opens beside it the same way, with its words, to be
|
|
542
|
+
// written over or withdrawn. The page comes back at the last of them once the
|
|
543
|
+
// viewer closes. A note the desk would not take is sent as the page sends it,
|
|
544
|
+
// which says why.
|
|
545
|
+
let notedAt;
|
|
546
|
+
const notesAddress = document
|
|
547
|
+
.querySelector("#note-form")
|
|
548
|
+
?.content.querySelector("form")
|
|
549
|
+
.getAttribute("action");
|
|
550
|
+
// What the notes saved in the viewer say now, which their cards do not yet.
|
|
551
|
+
const savedHere = new Map();
|
|
552
|
+
const writtenNote = (id) => {
|
|
553
|
+
const card = document.querySelector(`[data-card="${id}"]`);
|
|
554
|
+
const said = savedHere.get(id) ?? card?.dataset;
|
|
555
|
+
return said && { address: `${notesAddress}/${id}`, kind: said.kind, text: said.text };
|
|
556
|
+
};
|
|
557
|
+
const noteInViewer = ({ figure }, box, beside, id) => {
|
|
558
|
+
if (!figure?.dataset.asset) {
|
|
559
|
+
toast("That picture is not among the description's, so it takes no note.");
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
const written = id && writtenNote(id);
|
|
563
|
+
if (id && !written) {
|
|
564
|
+
toast("That note is not on this page, so it cannot be opened here.");
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
respond(beside, { ...placeOn(figure, box), written });
|
|
568
|
+
const form = beside.querySelector("form");
|
|
569
|
+
if (form.dataset.inViewer === undefined) {
|
|
570
|
+
form.dataset.inViewer = "";
|
|
571
|
+
form.addEventListener("submit", async (event) => {
|
|
572
|
+
event.preventDefault();
|
|
573
|
+
const response = await fetch(form.action, {
|
|
574
|
+
method: "POST",
|
|
575
|
+
body: new FormData(form, event.submitter),
|
|
576
|
+
headers: { Accept: "application/json" },
|
|
577
|
+
}).catch(() => undefined);
|
|
578
|
+
if (!response?.headers.get("content-type")?.includes("json"))
|
|
579
|
+
return form.submit();
|
|
580
|
+
const saved = await response.json();
|
|
581
|
+
const withdrawn = event.submitter?.matches("[data-withdraw]") ?? false;
|
|
582
|
+
notedAt = saved.at;
|
|
583
|
+
savedHere.set(saved.id, {
|
|
584
|
+
kind: form.elements.kind.value,
|
|
585
|
+
text: form.elements.text.value.trim(),
|
|
586
|
+
});
|
|
587
|
+
beside.dispatchEvent(
|
|
588
|
+
new CustomEvent("noted", { detail: { id: saved.id, withdrawn } }),
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return true;
|
|
593
|
+
};
|
|
594
|
+
const backAtNote = () => {
|
|
595
|
+
if (!notedAt) return;
|
|
596
|
+
const at = new URL(notedAt, location.href);
|
|
597
|
+
notedAt = undefined;
|
|
598
|
+
// A decision being decided keeps the page where it is: the page shows
|
|
599
|
+
// the note the next time it loads.
|
|
600
|
+
if (document.body.dataset.focus)
|
|
601
|
+
return toast(
|
|
602
|
+
"Your note is kept. The description shows it the next time the page loads.",
|
|
603
|
+
);
|
|
604
|
+
if (at.pathname !== location.pathname) return location.assign(at);
|
|
605
|
+
history.replaceState(history.state, "", at);
|
|
606
|
+
location.reload();
|
|
607
|
+
};
|
|
608
|
+
// The pictures Space would open here, and which of them first; nothing when
|
|
609
|
+
// the owner is not at a picture, and Space is the browser's to scroll with.
|
|
610
|
+
const picturesHere = () => {
|
|
611
|
+
const at = document.activeElement;
|
|
612
|
+
const figure =
|
|
613
|
+
at.closest("figure.picture") ??
|
|
614
|
+
(here?.matches("figure.picture") && !at.closest("[data-decision]")
|
|
615
|
+
? here
|
|
616
|
+
: null);
|
|
617
|
+
if (figure) {
|
|
618
|
+
const all = figures().filter((one) => one.querySelector("img"));
|
|
619
|
+
return {
|
|
620
|
+
pictures: all.map(shown),
|
|
621
|
+
first: Math.max(all.indexOf(figure), 0),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const linked = [
|
|
625
|
+
...(at
|
|
626
|
+
.closest("[data-decision]")
|
|
627
|
+
?.querySelectorAll(".decision-pictures figure") ?? []),
|
|
628
|
+
];
|
|
629
|
+
if (!linked.length) return;
|
|
630
|
+
return { pictures: linked.map(linkedShown), first: 0 };
|
|
631
|
+
};
|
|
632
|
+
const lookClosely = (found = picturesHere(), first = found?.first) => {
|
|
633
|
+
if (found)
|
|
634
|
+
viewPictures(found.pictures, first, {
|
|
635
|
+
note: noteInViewer,
|
|
636
|
+
closed: backAtNote,
|
|
637
|
+
});
|
|
638
|
+
};
|
|
639
|
+
keys[" "] = lookClosely;
|
|
640
|
+
|
|
641
|
+
// A picture a decision links to opens at a click, on that one.
|
|
642
|
+
document.addEventListener("click", (event) => {
|
|
643
|
+
const image = event.target.closest?.(".decision-pictures img");
|
|
644
|
+
if (!image) return;
|
|
645
|
+
const preview = image.closest("figure");
|
|
646
|
+
const linked = [
|
|
647
|
+
...preview.closest(".decision-pictures").querySelectorAll("figure"),
|
|
648
|
+
];
|
|
649
|
+
const found = picturesHere();
|
|
650
|
+
lookClosely(
|
|
651
|
+
found ?? { pictures: linked.map(linkedShown) },
|
|
652
|
+
linked.indexOf(preview),
|
|
653
|
+
);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// A picture a scenario's step names opens at a press, among the others that
|
|
657
|
+
// step names, as the description has them, so a note can be made on it.
|
|
658
|
+
const PICTURE_LINK = 'a[href^="#screenshot-"]';
|
|
659
|
+
const pictureNamed = (link) => {
|
|
660
|
+
const name = decodeURIComponent(link.hash.slice("#screenshot-".length));
|
|
661
|
+
return [
|
|
662
|
+
...document.querySelectorAll(
|
|
663
|
+
"main.description figure.picture, main.description figure.pair > .half",
|
|
664
|
+
),
|
|
665
|
+
].find(
|
|
666
|
+
(figure) => figure.dataset.picture === name && figure.querySelector("img"),
|
|
667
|
+
);
|
|
668
|
+
};
|
|
669
|
+
document.addEventListener("click", (event) => {
|
|
670
|
+
const link = event.target.closest?.(`.pr-scenarios .steps ${PICTURE_LINK}`);
|
|
671
|
+
if (!link || event.metaKey || event.ctrlKey || event.shiftKey) return;
|
|
672
|
+
event.preventDefault();
|
|
673
|
+
const named = [...link.closest("li").querySelectorAll(PICTURE_LINK)].filter(
|
|
674
|
+
pictureNamed,
|
|
675
|
+
);
|
|
676
|
+
if (!named.includes(link))
|
|
677
|
+
return toast(
|
|
678
|
+
`The description shows no picture called ${link.hash.slice(1)}.`,
|
|
679
|
+
);
|
|
680
|
+
lookClosely(
|
|
681
|
+
{ pictures: named.map((one) => shown(pictureNamed(one))) },
|
|
682
|
+
named.indexOf(link),
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
document.addEventListener("keydown", (event) => {
|
|
687
|
+
// In a response that came back, c and d are its own (review-rounds.js).
|
|
688
|
+
if (
|
|
689
|
+
event.target.closest("[data-request]") &&
|
|
690
|
+
["c", "d"].includes(keyOf(event))
|
|
691
|
+
)
|
|
692
|
+
return;
|
|
693
|
+
if (event.isComposing || event.metaKey || event.ctrlKey || event.altKey)
|
|
694
|
+
return;
|
|
695
|
+
if (helping()) return;
|
|
696
|
+
// While the page shows the decisions alone, one being decided or the
|
|
697
|
+
// panel where they end, moving is the decisions' own script's to do.
|
|
698
|
+
const deciding = ["c", "j", "k", "ArrowDown", "ArrowUp", "Home", "End"];
|
|
699
|
+
const folding = ["[", "]", "ArrowLeft", "ArrowRight"];
|
|
700
|
+
if (
|
|
701
|
+
(document.body.dataset.decisionMode || document.body.dataset.focus) &&
|
|
702
|
+
[...deciding, ...folding].includes(keyOf(event))
|
|
703
|
+
)
|
|
704
|
+
return;
|
|
705
|
+
if (!Object.hasOwn(keys, keyOf(event))) return;
|
|
706
|
+
if (event.target.closest("input, textarea, select, [contenteditable]"))
|
|
707
|
+
return;
|
|
708
|
+
// Space presses a button and scrolls a page: it is a key of this page only at a picture.
|
|
709
|
+
if (
|
|
710
|
+
keyOf(event) === " " &&
|
|
711
|
+
(event.target.closest("button, a, summary") || !picturesHere())
|
|
712
|
+
)
|
|
713
|
+
return;
|
|
714
|
+
// On a decision the reading rests on, c is for the decisions' script: it opens the decision to suggest a change.
|
|
715
|
+
if (keyOf(event) === "c" && event.target.matches?.("li[data-decision]"))
|
|
716
|
+
return;
|
|
717
|
+
|
|
718
|
+
event.preventDefault();
|
|
719
|
+
keys[keyOf(event)]();
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
for (const button of document.querySelectorAll("[data-note-whole]"))
|
|
723
|
+
button.addEventListener("click", keys.w);
|
|
724
|
+
|
|
725
|
+
for (const button of document.querySelectorAll("[data-note-scenario]"))
|
|
726
|
+
button.addEventListener("click", () =>
|
|
727
|
+
respond(slotIn(button.closest("li.scenario")), {
|
|
728
|
+
action: button.dataset.noteScenario,
|
|
729
|
+
on: button.dataset.name,
|
|
730
|
+
}),
|
|
731
|
+
);
|