// app-notes.jsx — NotesEditor: textarea with motion chips + inline shorthand expansion (function () { const { useRef, useCallback } = React; const { MOTIONS, CLAUSES, INLINE, PLACEHOLDER } = window.MeetingData; // Insert `text` into a textarea at the caret, replacing the {{}} marker with // PLACEHOLDER and returning the [start,end] range of that placeholder so the // caller can select it. Handles spacing so words don't run together. function spliceTemplate(el, template, onChange) { const value = el.value; let start = el.selectionStart; let end = el.selectionEnd; const markerIndex = template.indexOf("{{}}"); let out = template.replace("{{}}", PLACEHOLDER); // pad with a leading space if mid-sentence, trailing handled by caller const before = value.slice(0, start); const needsLeadSpace = before.length > 0 && !/\s$/.test(before); if (needsLeadSpace) out = " " + out; const after = value.slice(end); const needsTrailSpace = after.length > 0 && !/^\s/.test(after); if (needsTrailSpace) out = out + " "; const newValue = before + out + after; onChange(newValue); // compute placeholder selection range const lead = needsLeadSpace ? 1 : 0; const phStart = start + lead + (markerIndex >= 0 ? markerIndex : out.length); const phEnd = phStart + PLACEHOLDER.length; requestAnimationFrame(() => { el.focus(); try { el.setSelectionRange(phStart, phEnd); } catch (e) {} }); } // Insert a vote clause before the trailing period of the current sentence. function insertClause(el, clause, onChange) { const value = el.value; const caret = el.selectionStart; // find the period that ends the sentence the caret is in / nearest before const upto = value.slice(0, caret); const lastPeriod = upto.lastIndexOf("."); let insertAt; if (lastPeriod >= 0 && lastPeriod === upto.length - 1) { insertAt = lastPeriod; // caret right after a period -> put clause before it } else { const nextPeriod = value.indexOf(".", caret); insertAt = nextPeriod >= 0 ? nextPeriod : caret; } const text = clause.replace("{{}}", PLACEHOLDER); const newValue = value.slice(0, insertAt) + text + value.slice(insertAt); onChange(newValue); const phRel = text.indexOf(PLACEHOLDER); const phStart = insertAt + phRel; requestAnimationFrame(() => { el.focus(); try { el.setSelectionRange(phStart, phStart + PLACEHOLDER.length); } catch (e) {} }); } // Inline expansion: when user types a space right after a known shorthand, // expand it. Called from onChange after detecting the trigger. function tryInlineExpand(el, value, onChange) { const caret = el.selectionStart; // only when the char just typed is a space if (value[caret - 1] !== " ") return false; const before = value.slice(0, caret - 1); // without the trailing space const m = before.match(/(^|[\s(])((?:m\/s\/[ucf])|tabled)$/i); if (!m) return false; const code = m[2].toLowerCase(); const motion = INLINE[code]; if (!motion) return false; const codeStart = caret - 1 - m[2].length; const newBefore = value.slice(0, codeStart); const after = value.slice(caret); // text after the space we typed let expanded = motion.template.replace("{{}}", PLACEHOLDER); const needLead = newBefore.length > 0 && !/\s$/.test(newBefore); if (needLead) expanded = " " + expanded; const newValue = newBefore + expanded + after; onChange(newValue); const lead = needLead ? 1 : 0; const phStart = codeStart + lead + motion.template.indexOf("{{}}"); requestAnimationFrame(() => { el.focus(); try { el.setSelectionRange(phStart, phStart + PLACEHOLDER.length); } catch (e) {} }); return true; } function NotesEditor({ value, onChange, placeholder }) { const ref = useRef(null); const handleChange = useCallback((e) => { const el = ref.current; const v = e.target.value; // attempt inline expansion; if it fires it calls onChange itself if (tryInlineExpand(el, v, onChange)) return; onChange(v); }, [onChange]); return (
Insert motion {MOTIONS.map((mo) => ( ))} {CLAUSES.map((c) => ( ))}