66
77import { icsToEvents } from "@opentechevents/import-ics" ;
88import { htmlToEvents } from "@opentechevents/import-jsonld" ;
9+ import { marked } from "marked" ;
910
1011import { loadAdopters , type Adopter } from "./lib/adopters.js" ;
1112import { findCollisions } from "./lib/collisions.js" ;
@@ -988,6 +989,87 @@ async function startEditor(repo: string | null): Promise<void> {
988989 ) ;
989990 }
990991
992+ // --- description: inline Edit/Preview toggle (always visible, see
993+ // ui/form.ts's fieldId === "description" block) + a "⤢ expand" icon
994+ // opening a larger dialog for the same textarea. Both write straight to
995+ // state.description live, no separate confirm step — same as the geo
996+ // map. Wired the same way mountMap wires the geo slot: main.ts finds the
997+ // elements ui/form.ts built after each render, since they don't exist
998+ // beforehand. ---
999+ const descriptionEditorDialog = el < HTMLDialogElement > ( "description-editor-dialog" ) ;
1000+ const descriptionEditorTextarea = el < HTMLTextAreaElement > ( "description-editor-textarea" ) ;
1001+ const descriptionEditorPreview = el < HTMLDivElement > ( "description-editor-preview" ) ;
1002+
1003+ function renderDescriptionPreview ( target : HTMLElement , source : string ) : void {
1004+ target . innerHTML = marked . parse ( source , { async : false } ) ;
1005+ }
1006+
1007+ /** The compact field's own preview (if that's its active tab) doesn't
1008+ * see edits made from the larger dialog — its textarea has its own input
1009+ * listener, but the dialog writes state.description directly. Called
1010+ * after every dialog edit to keep the two in sync. */
1011+ function syncInlineDescriptionPreview ( ) : void {
1012+ const preview = form . querySelector < HTMLDivElement > (
1013+ '[data-role="description-inline-preview"]' ,
1014+ ) ;
1015+ if ( ! preview || preview . hidden ) return ;
1016+ renderDescriptionPreview ( preview , state . description ) ;
1017+ }
1018+
1019+ descriptionEditorTextarea . addEventListener ( "input" , ( ) => {
1020+ onInput ( "description" , descriptionEditorTextarea . value ) ;
1021+ setControlValue ( "description" , descriptionEditorTextarea . value ) ;
1022+ syncInlineDescriptionPreview ( ) ;
1023+ } ) ;
1024+
1025+ el < HTMLDivElement > ( "description-editor-mode" ) . addEventListener ( "input" , ( ) => {
1026+ const isPreview =
1027+ descriptionEditorDialog . querySelector < HTMLInputElement > (
1028+ 'input[name="description-editor-mode"]:checked' ,
1029+ ) ?. value === "preview" ;
1030+ descriptionEditorTextarea . hidden = isPreview ;
1031+ descriptionEditorPreview . hidden = ! isPreview ;
1032+ if ( isPreview ) renderDescriptionPreview ( descriptionEditorPreview , descriptionEditorTextarea . value ) ;
1033+ } ) ;
1034+
1035+ el < HTMLButtonElement > ( "description-editor-done" ) . addEventListener ( "click" , ( ) =>
1036+ descriptionEditorDialog . close ( ) ,
1037+ ) ;
1038+
1039+ function mountDescriptionExpand ( ) : void {
1040+ const toolbar = form . querySelector < HTMLElement > ( '[data-role="description-mode-toggle"]' ) ;
1041+ const textarea = form . querySelector < HTMLTextAreaElement > ( '[data-key="description"]' ) ;
1042+ const preview = form . querySelector < HTMLDivElement > (
1043+ '[data-role="description-inline-preview"]' ,
1044+ ) ;
1045+ const expandBtn = form . querySelector < HTMLButtonElement > ( '[data-role="description-expand"]' ) ;
1046+ if ( ! toolbar || ! textarea || ! preview || ! expandBtn ) return ;
1047+
1048+ toolbar . addEventListener ( "input" , ( ) => {
1049+ const isPreview =
1050+ toolbar . querySelector < HTMLInputElement > ( "input:checked" ) ?. value === "preview" ;
1051+ textarea . hidden = isPreview ;
1052+ preview . hidden = ! isPreview ;
1053+ if ( isPreview ) renderDescriptionPreview ( preview , textarea . value ) ;
1054+ } ) ;
1055+
1056+ expandBtn . addEventListener ( "click" , ( ) => {
1057+ const mode =
1058+ toolbar . querySelector < HTMLInputElement > ( "input:checked" ) ?. value === "preview"
1059+ ? "preview"
1060+ : "edit" ;
1061+ descriptionEditorTextarea . value = state . description ;
1062+ descriptionEditorTextarea . hidden = mode === "preview" ;
1063+ descriptionEditorPreview . hidden = mode !== "preview" ;
1064+ if ( mode === "preview" ) renderDescriptionPreview ( descriptionEditorPreview , state . description ) ;
1065+ const radio = descriptionEditorDialog . querySelector < HTMLInputElement > (
1066+ `input[name="description-editor-mode"][value="${ mode } "]` ,
1067+ ) ;
1068+ if ( radio ) radio . checked = true ;
1069+ descriptionEditorDialog . showModal ( ) ;
1070+ } ) ;
1071+ }
1072+
9911073 /** One pill per rendered section — jumps to it, opening it first if collapsed. */
9921074 /** Wires a toggle button + dropdown panel: click toggles, outside click/Escape closes. Returns close(). */
9931075 function wireDropdown (
@@ -1049,6 +1131,16 @@ async function startEditor(repo: string | null): Promise<void> {
10491131 sectionNav . append ( toggle , list ) ;
10501132 }
10511133
1134+ /** Passed into renderForm as onSectionRebuilt: a chippable section (Where,
1135+ * What) rebuilding its own subtree — e.g. Description added via its "+"
1136+ * chip — doesn't go through render() below, so anything that mounts
1137+ * after the fact (the geo map, the description toolbar/expand wiring)
1138+ * has to be re-run from here too, not just at the bottom of render(). */
1139+ function onSectionRebuilt ( ) : void {
1140+ mountMap ( ) ;
1141+ mountDescriptionExpand ( ) ;
1142+ }
1143+
10521144 function render ( extra : ReadonlySet < string > = new Set ( ) ) : void {
10531145 const rendered = renderForm (
10541146 form ,
@@ -1058,7 +1150,7 @@ async function startEditor(repo: string | null): Promise<void> {
10581150 onInput ,
10591151 onArrayInput ,
10601152 onTranslationsCommit ,
1061- mountMap ,
1153+ onSectionRebuilt ,
10621154 onCustomizeRecurrenceRule ,
10631155 ) ;
10641156 refreshTranslations = rendered . refreshTranslations ;
@@ -1067,6 +1159,7 @@ async function startEditor(repo: string | null): Promise<void> {
10671159 setAllDay ( form , state . allDay ) ;
10681160 markImportGaps ( form , importMissing ?? new Set ( ) ) ;
10691161 mountMap ( ) ;
1162+ mountDescriptionExpand ( ) ;
10701163 refresh ( ) ;
10711164 }
10721165
0 commit comments