- Words light up as you type (@1, times, cuts, quotes, labels), with @ autocomplete and a ? cheat sheet - Timeline bar shows every picture a line names; drag a block to reorder, drag its edge to set its time, click an edge for a cut, click a block to jump to its line - Sound and Music boxes (new optional inputs, appended) fill H3's overall_soundscape and non_diegetic_music - Pictures and video as chips with their jobs; Length, Camera and Look stay plain rows so they can still be wired - Help page and changelog updated; tests for the colors, autocomplete, timeline edits and the new inputs
184 lines
8.9 KiB
JavaScript
184 lines
8.9 KiB
JavaScript
// Help while typing in H3 Prompt Writer's script box:
|
||
// - a "?" beside "What happens" opens a card of everything the node reads in
|
||
// the words (@1, 3s:, a blank line, quotes, sound:, music:, camera:, look:);
|
||
// - typing @ lists the pictures plugged in, with their job; the first letters
|
||
// of a line offer sound:, music:, camera: and look:; a number offers a time;
|
||
// after camera: or look: the node's own choices are listed.
|
||
// Suggestions only open on those, so they stay out of other packs' way.
|
||
// Picking one types it into the box the browser's way, so undo still works.
|
||
import { CHEAT_SHEET, suggestAt } from "./script_assist_math.mjs";
|
||
|
||
const CSS_ID = "ausboss-script-assist-css";
|
||
|
||
function ensureCss() {
|
||
if (document.getElementById(CSS_ID)) return;
|
||
const style = document.createElement("style");
|
||
style.id = CSS_ID;
|
||
style.textContent = `
|
||
.ausboss-help-dot{margin-left:auto;pointer-events:auto;width:15px;height:15px;padding:0;border:1px solid #2f4a48;border-radius:50%;background:transparent;color:#6f8886;font:700 9.5px/13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:0;text-align:center;cursor:help}
|
||
.ausboss-help-dot:hover,.ausboss-help-dot.open{border-color:#00b4aa;color:#a5f3ec}
|
||
.ausboss-assist-pop{position:fixed;z-index:10000;box-sizing:border-box;border:1px solid #2a3437;border-radius:8px;background:#0f1516;color:#d8ecea;box-shadow:0 8px 28px rgba(0,0,0,.55);font:500 12px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||
.ausboss-assist-help{width:300px;padding:10px 12px}
|
||
.ausboss-assist-help h4{margin:0 0 6px;color:#a5f3ec;font-size:12px;font-weight:700}
|
||
.ausboss-assist-help dl{display:grid;grid-template-columns:auto 1fr;gap:5px 10px;margin:0}
|
||
.ausboss-assist-help dt{color:#f5b53d;font:600 11px/1.35 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap}
|
||
.ausboss-assist-help dd{margin:0;color:#c3d6d4}
|
||
.ausboss-assist-help p{margin:8px 0 0;color:#6f8886;font-size:11px}
|
||
.ausboss-assist-list{min-width:170px;max-width:280px;padding:4px}
|
||
.ausboss-assist-item{display:flex;align-items:center;gap:7px;padding:4px 6px;border-radius:5px;cursor:pointer;white-space:nowrap}
|
||
.ausboss-assist-item.active{background:rgba(0,180,170,.22)}
|
||
.ausboss-assist-item img{flex:none;width:20px;height:20px;border-radius:4px;object-fit:cover}
|
||
.ausboss-assist-item b{color:#a5f3ec;font:700 11.5px ui-monospace,SFMono-Regular,Menlo,monospace}
|
||
.ausboss-assist-item em{overflow:hidden;color:#6f8886;font-style:normal;text-overflow:ellipsis}
|
||
.ausboss-assist-foot{padding:3px 6px 1px;color:#56706d;font-size:10px}
|
||
`;
|
||
document.head.append(style);
|
||
}
|
||
|
||
const make = (tag, className, text) => {
|
||
const element = document.createElement(tag);
|
||
if (className) element.className = className;
|
||
if (text !== undefined) element.textContent = text;
|
||
return element;
|
||
};
|
||
|
||
// Screen position of the caret: a hidden copy of the box with a marker where
|
||
// the caret is. The copy sits in the box's own place, so zoom is included.
|
||
function caretPoint(area, layer) {
|
||
const copy = layer.cloneNode(false);
|
||
copy.className = "ausboss-colours";
|
||
copy.style.visibility = "hidden";
|
||
copy.textContent = area.value.slice(0, area.selectionEnd);
|
||
const mark = make("span", "", "");
|
||
copy.append(mark);
|
||
area.parentElement.append(copy);
|
||
copy.scrollTop = area.scrollTop;
|
||
const box = mark.getBoundingClientRect();
|
||
copy.remove();
|
||
return { x: box.left, y: box.bottom };
|
||
}
|
||
|
||
// node: the H3 Prompt Writer; area: its script box; layer: the coloured copy
|
||
// under it; context(): { pictures, cameras, looks, seconds }.
|
||
export function attachScriptAssist(node, area, layer, context) {
|
||
const none = { dispose: () => {} };
|
||
if (!area?.parentElement) return none;
|
||
ensureCss();
|
||
|
||
// The "?" card.
|
||
const row = area.closest(".ausboss-card-row");
|
||
const header = row?.previousElementSibling?.classList?.contains("ausboss-card-section") ? row.previousElementSibling : null;
|
||
let help = null;
|
||
let dot = null;
|
||
let closeHelpTimer = null;
|
||
const closeHelp = () => { help?.remove(); help = null; dot?.classList.remove("open"); };
|
||
const openHelp = () => {
|
||
clearTimeout(closeHelpTimer);
|
||
if (help) return;
|
||
help = make("div", "ausboss-assist-pop ausboss-assist-help");
|
||
help.append(make("h4", "", "What you can type"));
|
||
const list = make("dl");
|
||
for (const entry of CHEAT_SHEET) list.append(make("dt", "", entry.code), make("dd", "", entry.text));
|
||
help.append(list, make("p", "", "Typing @ lists your pictures. Tab picks a suggestion, Esc closes it."));
|
||
help.addEventListener("mouseenter", () => clearTimeout(closeHelpTimer));
|
||
help.addEventListener("mouseleave", () => { closeHelpTimer = setTimeout(closeHelp, 250); });
|
||
document.body.append(help);
|
||
const box = dot.getBoundingClientRect();
|
||
const left = Math.max(8, Math.min(window.innerWidth - help.offsetWidth - 8, box.right - help.offsetWidth));
|
||
const below = box.bottom + 6;
|
||
help.style.left = `${left}px`;
|
||
help.style.top = `${below + help.offsetHeight > window.innerHeight - 8 ? Math.max(8, box.top - help.offsetHeight - 6) : below}px`;
|
||
dot.classList.add("open");
|
||
};
|
||
if (header) {
|
||
dot = make("button", "ausboss-help-dot", "?");
|
||
dot.type = "button";
|
||
dot.title = "";
|
||
dot.setAttribute("aria-label", "What you can type in the words");
|
||
dot.addEventListener("mouseenter", openHelp);
|
||
dot.addEventListener("mouseleave", () => { closeHelpTimer = setTimeout(closeHelp, 250); });
|
||
dot.addEventListener("pointerdown", (event) => event.stopPropagation());
|
||
dot.addEventListener("click", (event) => { event.stopPropagation(); if (help) closeHelp(); else openHelp(); });
|
||
header.append(dot);
|
||
}
|
||
|
||
// Suggestions.
|
||
let pop = null;
|
||
let offer = null;
|
||
let active = 0;
|
||
const close = () => { pop?.remove(); pop = null; offer = null; };
|
||
const take = (item) => {
|
||
if (!offer) return;
|
||
area.focus();
|
||
area.setSelectionRange(offer.from, offer.to);
|
||
// insertText keeps the browser's own undo.
|
||
if (!document.execCommand?.("insertText", false, item.insert)) {
|
||
area.setRangeText(item.insert, offer.from, offer.to, "end");
|
||
area.dispatchEvent(new Event("input", { bubbles: true }));
|
||
}
|
||
close();
|
||
};
|
||
const draw = () => {
|
||
if (!pop || !offer) return;
|
||
pop.textContent = "";
|
||
offer.items.forEach((item, index) => {
|
||
const line = make("div", `ausboss-assist-item${index === active ? " active" : ""}`);
|
||
if (item.number && item.picture) { const img = make("img"); img.src = item.picture; line.append(img); }
|
||
line.append(make("b", "", item.label));
|
||
if (item.hint) line.append(make("em", "", item.hint));
|
||
line.addEventListener("pointerdown", (event) => { event.preventDefault(); event.stopPropagation(); take(item); });
|
||
pop.append(line);
|
||
});
|
||
pop.append(make("div", "ausboss-assist-foot", offer.strong ? "Enter or Tab to pick · Esc to close" : "Tab to pick · Esc to close"));
|
||
};
|
||
const update = () => {
|
||
if (document.activeElement !== area || area.selectionStart !== area.selectionEnd) return close();
|
||
const ctx = context();
|
||
const next = suggestAt(area.value, area.selectionEnd, ctx);
|
||
if (!next) return close();
|
||
for (const item of next.items) if (item.number) item.picture = ctx.pictures.find((picture) => picture.number === item.number)?.url ?? "";
|
||
const same = offer && offer.from === next.from && offer.items.map((item) => item.label).join() === next.items.map((item) => item.label).join();
|
||
offer = next;
|
||
if (!same) active = 0;
|
||
if (!pop) { pop = make("div", "ausboss-assist-pop ausboss-assist-list"); document.body.append(pop); }
|
||
draw();
|
||
const point = caretPoint(area, layer);
|
||
pop.style.left = `${Math.max(8, Math.min(window.innerWidth - pop.offsetWidth - 8, point.x))}px`;
|
||
pop.style.top = `${point.y + pop.offsetHeight + 4 > window.innerHeight ? point.y - pop.offsetHeight - 22 : point.y + 4}px`;
|
||
};
|
||
const onKey = (event) => {
|
||
if (!offer) return;
|
||
const count = offer.items.length;
|
||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||
active = (active + (event.key === "ArrowDown" ? 1 : count - 1)) % count;
|
||
offer.strong = true; // Choosing with the arrows makes Enter pick too.
|
||
draw();
|
||
} else if (event.key === "Tab" || (event.key === "Enter" && offer.strong && !event.shiftKey)) {
|
||
take(offer.items[active]);
|
||
} else if (event.key === "Escape") {
|
||
close();
|
||
} else {
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
};
|
||
const onBlur = () => setTimeout(() => { if (document.activeElement !== area) close(); }, 0);
|
||
area.addEventListener("input", update);
|
||
area.addEventListener("keydown", onKey, true);
|
||
area.addEventListener("blur", onBlur);
|
||
area.addEventListener("click", update);
|
||
|
||
return {
|
||
dispose() {
|
||
close();
|
||
closeHelp();
|
||
dot?.remove();
|
||
area.removeEventListener("input", update);
|
||
area.removeEventListener("keydown", onKey, true);
|
||
area.removeEventListener("blur", onBlur);
|
||
area.removeEventListener("click", update);
|
||
},
|
||
};
|
||
}
|