@obelism/content 0.13.0 → 0.14.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.
- package/README.md +20 -10
- package/dist/bridge.d.mts +13 -11
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +13 -11
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +10 -12
- package/dist/bridge.js +302 -323
- package/dist/bridge.mjs +302 -323
- package/dist/site.js +5 -0
- package/dist/site.mjs +5 -0
- package/package.json +1 -1
package/dist/bridge.mjs
CHANGED
|
@@ -229,6 +229,12 @@ const INERT_METHODS = {
|
|
|
229
229
|
createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
|
|
230
230
|
view: onlyWhileEditingSite,
|
|
231
231
|
applyJson: onlyWhileEditingSite,
|
|
232
|
+
openForm: (_container, options = {})=>{
|
|
233
|
+
queueMicrotask(()=>options.onError?.('Only while signed in to edit on the site'));
|
|
234
|
+
return {
|
|
235
|
+
close: ()=>{}
|
|
236
|
+
};
|
|
237
|
+
},
|
|
232
238
|
pages: onlyWhileEditingSite,
|
|
233
239
|
openPage: ()=>{},
|
|
234
240
|
undo: ()=>{},
|
|
@@ -518,16 +524,9 @@ const DEFAULT_PROMPT_LABELS = {
|
|
|
518
524
|
created: 'Opening the new page…',
|
|
519
525
|
modes: 'Edit with',
|
|
520
526
|
modePrompt: 'Prompt',
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
jsonReload: 'Reload the JSON',
|
|
525
|
-
jsonKept: 'Kept your unapplied JSON. Reload to see the current content.',
|
|
526
|
-
jsonInvalidAt: 'Not valid JSON at line {line}, column {column}',
|
|
527
|
-
jsonInvalid: 'Not valid JSON',
|
|
528
|
-
jsonNotObject: 'The JSON has to be an object of fields, like { "title": … }',
|
|
529
|
-
applied: 'Applied',
|
|
530
|
-
unchanged: 'Nothing changed',
|
|
527
|
+
form: 'Form',
|
|
528
|
+
formTitle: 'The content form',
|
|
529
|
+
formLoading: 'Opening the form…',
|
|
531
530
|
pages: 'Pages',
|
|
532
531
|
pagesSearch: 'Find a page',
|
|
533
532
|
pagesLoading: 'Loading pages…',
|
|
@@ -543,7 +542,6 @@ const ICONS = {
|
|
|
543
542
|
block: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',
|
|
544
543
|
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
|
|
545
544
|
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
546
|
-
reload: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
|
|
547
545
|
chevrons: '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
|
|
548
546
|
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
549
547
|
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
@@ -648,13 +646,13 @@ textarea {
|
|
|
648
646
|
font-size: 16px; line-height: 1.6; color: rgb(var(--theme-foreground-rgb));
|
|
649
647
|
}
|
|
650
648
|
textarea::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
|
|
651
|
-
/*
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
font-size: 13px; line-height: 1.55;
|
|
649
|
+
/* Form: the CMS's entry form, edited live. */
|
|
650
|
+
.form { padding: 12px 12px 0; }
|
|
651
|
+
.form iframe {
|
|
652
|
+
display: block; width: 100%; height: min(560px, 60vh); border: none; border-radius: 12px;
|
|
653
|
+
background: rgb(var(--theme-background-actions-rgb) / .45);
|
|
657
654
|
}
|
|
655
|
+
.form.loading iframe { visibility: hidden; }
|
|
658
656
|
/* New page: where it goes, above what it is for. */
|
|
659
657
|
.path {
|
|
660
658
|
display: flex; align-items: center; gap: 10px; padding: 16px 22px 0;
|
|
@@ -698,7 +696,7 @@ button.chip:disabled { opacity: .5; }
|
|
|
698
696
|
.icon-button.primary { background: rgb(var(--theme-accent-rgb)); color: #fff; }
|
|
699
697
|
.icon-button.primary:hover:not(:disabled) { background: rgb(var(--theme-accent-rgb)); color: #fff; opacity: .88; }
|
|
700
698
|
.icon-button.primary:disabled { opacity: .35; }
|
|
701
|
-
/* Prompt or
|
|
699
|
+
/* Prompt or Form: two small pills in one track. */
|
|
702
700
|
.modes {
|
|
703
701
|
display: inline-flex; flex: none; gap: 2px; padding: 2px; border-radius: var(--border-radius-pill);
|
|
704
702
|
border: 1px solid rgb(var(--theme-border-rgb) / .2); background: rgb(var(--theme-background-actions-rgb) / .6);
|
|
@@ -781,101 +779,6 @@ const icon = (doc, name)=>{
|
|
|
781
779
|
span.innerHTML = `<svg viewBox="0 0 24 24">${ICONS[name]}</svg>`;
|
|
782
780
|
return span;
|
|
783
781
|
};
|
|
784
|
-
// Where `text` stops being JSON: the offset of the first character that
|
|
785
|
-
// doesn't fit, or -1 when it is JSON. JSON.parse's errors don't all say
|
|
786
|
-
// where (V8's "Unexpected token" doesn't, Safari's never do).
|
|
787
|
-
const jsonErrorAt = (text)=>{
|
|
788
|
-
let i = 0;
|
|
789
|
-
const fail = ()=>{
|
|
790
|
-
throw i;
|
|
791
|
-
};
|
|
792
|
-
const space = ()=>{
|
|
793
|
-
while(i < text.length && ' \t\n\r'.includes(text[i]))i++;
|
|
794
|
-
};
|
|
795
|
-
const string = ()=>{
|
|
796
|
-
i++;
|
|
797
|
-
while(i < text.length){
|
|
798
|
-
const char = text[i];
|
|
799
|
-
if (char === '"') {
|
|
800
|
-
i++;
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
if (char === '\\') {
|
|
804
|
-
const next = text[i + 1] ?? '';
|
|
805
|
-
if (next === 'u' && /^[\da-f]{4}$/i.test(text.slice(i + 2, i + 6))) {
|
|
806
|
-
i += 6;
|
|
807
|
-
} else if (next !== '' && '"\\/bfnrt'.includes(next)) {
|
|
808
|
-
i += 2;
|
|
809
|
-
} else {
|
|
810
|
-
i++;
|
|
811
|
-
fail();
|
|
812
|
-
}
|
|
813
|
-
} else if (char < ' ') {
|
|
814
|
-
fail();
|
|
815
|
-
} else {
|
|
816
|
-
i++;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
fail();
|
|
820
|
-
};
|
|
821
|
-
const items = (close, item)=>{
|
|
822
|
-
i++;
|
|
823
|
-
space();
|
|
824
|
-
if (text[i] === close) {
|
|
825
|
-
i++;
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
for(;;){
|
|
829
|
-
item();
|
|
830
|
-
space();
|
|
831
|
-
if (text[i] === close) {
|
|
832
|
-
i++;
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
if (text[i] !== ',') fail();
|
|
836
|
-
i++;
|
|
837
|
-
}
|
|
838
|
-
};
|
|
839
|
-
const value = ()=>{
|
|
840
|
-
space();
|
|
841
|
-
const char = text[i] ?? '';
|
|
842
|
-
if (char === '{') {
|
|
843
|
-
return items('}', ()=>{
|
|
844
|
-
space();
|
|
845
|
-
if (text[i] !== '"') fail();
|
|
846
|
-
string();
|
|
847
|
-
space();
|
|
848
|
-
if (text[i] !== ':') fail();
|
|
849
|
-
i++;
|
|
850
|
-
value();
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
if (char === '[') return items(']', value);
|
|
854
|
-
if (char === '"') return string();
|
|
855
|
-
for (const word of [
|
|
856
|
-
'true',
|
|
857
|
-
'false',
|
|
858
|
-
'null'
|
|
859
|
-
]){
|
|
860
|
-
if (text.startsWith(word, i)) {
|
|
861
|
-
i += word.length;
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
const number = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text.slice(i));
|
|
866
|
-
if (!number) return fail();
|
|
867
|
-
i += number[0].length;
|
|
868
|
-
};
|
|
869
|
-
try {
|
|
870
|
-
value();
|
|
871
|
-
space();
|
|
872
|
-
if (i < text.length) fail();
|
|
873
|
-
return -1;
|
|
874
|
-
} catch (at) {
|
|
875
|
-
if (typeof at !== 'number') throw at;
|
|
876
|
-
return Math.min(at, text.length);
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
782
|
// Mounts the box on the page; returns a function that removes it.
|
|
880
783
|
const mountPromptBox = (doc, bridge, options = {})=>{
|
|
881
784
|
const labels = {
|
|
@@ -919,14 +822,10 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
919
822
|
rows: 1
|
|
920
823
|
});
|
|
921
824
|
input.setAttribute('aria-label', labels.open);
|
|
922
|
-
//
|
|
923
|
-
const
|
|
924
|
-
className: '
|
|
925
|
-
spellcheck: false,
|
|
926
|
-
autocomplete: 'off'
|
|
825
|
+
// Form mode (on the site): the CMS's form, mounted while shown.
|
|
826
|
+
const formBox = element(doc, 'div', {
|
|
827
|
+
className: 'form'
|
|
927
828
|
});
|
|
928
|
-
code.setAttribute('aria-label', labels.json);
|
|
929
|
-
code.setAttribute('autocapitalize', 'off');
|
|
930
829
|
// What the prompt acts on: the whole page, or the selected block with a
|
|
931
830
|
// way back to the page.
|
|
932
831
|
const pageIcon = icon(doc, 'page');
|
|
@@ -945,7 +844,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
945
844
|
className: 'action chip',
|
|
946
845
|
type: 'button'
|
|
947
846
|
}, icon(doc, 'undo'), labels.undo);
|
|
948
|
-
// Prompt or
|
|
847
|
+
// Prompt or Form (on the site).
|
|
949
848
|
const modeButton = (text)=>{
|
|
950
849
|
const button = element(doc, 'button', {
|
|
951
850
|
type: 'button',
|
|
@@ -955,18 +854,12 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
955
854
|
return button;
|
|
956
855
|
};
|
|
957
856
|
const promptMode = modeButton(labels.modePrompt);
|
|
958
|
-
const
|
|
857
|
+
const formMode = modeButton(labels.form);
|
|
959
858
|
const modes = element(doc, 'span', {
|
|
960
859
|
className: 'modes'
|
|
961
|
-
}, promptMode,
|
|
860
|
+
}, promptMode, formMode);
|
|
962
861
|
modes.setAttribute('role', 'group');
|
|
963
862
|
modes.setAttribute('aria-label', labels.modes);
|
|
964
|
-
const reload = element(doc, 'button', {
|
|
965
|
-
className: 'icon-button',
|
|
966
|
-
type: 'button',
|
|
967
|
-
title: labels.jsonReload
|
|
968
|
-
}, icon(doc, 'reload'));
|
|
969
|
-
reload.setAttribute('aria-label', labels.jsonReload);
|
|
970
863
|
// New page (on the site, for people who may add pages): the scope becomes
|
|
971
864
|
// the new page, with a way back, and a path field shows above the prompt.
|
|
972
865
|
const newPage = element(doc, 'button', {
|
|
@@ -1009,13 +902,14 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1009
902
|
textContent: labels.send
|
|
1010
903
|
}), icon(doc, 'send'));
|
|
1011
904
|
send.title = labels.send;
|
|
905
|
+
const divider = element(doc, 'span', {
|
|
906
|
+
className: 'divider'
|
|
907
|
+
});
|
|
1012
908
|
const toolbar = element(doc, 'div', {
|
|
1013
909
|
className: 'toolbar'
|
|
1014
910
|
}, scope, newScope, modes, undo, newPage, element(doc, 'span', {
|
|
1015
911
|
className: 'spacer'
|
|
1016
|
-
}),
|
|
1017
|
-
className: 'divider'
|
|
1018
|
-
}), send);
|
|
912
|
+
}), close, divider, send);
|
|
1019
913
|
// The pages (on the site): a chip naming this page, opening a search
|
|
1020
914
|
// and a list of every page in place of the prompt.
|
|
1021
915
|
const pageChipText = element(doc, 'span', {
|
|
@@ -1083,7 +977,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1083
977
|
}), signOut, save, publish);
|
|
1084
978
|
const panel = element(doc, 'form', {
|
|
1085
979
|
className: 'panel'
|
|
1086
|
-
}, status, pathRow, input,
|
|
980
|
+
}, status, pathRow, input, formBox, toolbar, pagesSection, footer);
|
|
1087
981
|
const root = element(doc, 'div', {
|
|
1088
982
|
className: 'root'
|
|
1089
983
|
}, element(doc, 'div', {
|
|
@@ -1095,15 +989,11 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1095
989
|
let open = false;
|
|
1096
990
|
let creating = false;
|
|
1097
991
|
let message = null;
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
|
|
1101
|
-
let
|
|
1102
|
-
let
|
|
1103
|
-
let viewUid = null;
|
|
1104
|
-
let seenUid;
|
|
1105
|
-
let loadingView = false;
|
|
1106
|
-
let viewRequest = 0;
|
|
992
|
+
// Form mode, and the form while mounted: a fresh one (and one-time
|
|
993
|
+
// address) every time it shows.
|
|
994
|
+
let form = false;
|
|
995
|
+
let formHandle = null;
|
|
996
|
+
let formLoading = false;
|
|
1107
997
|
// The pages, fetched once the panel opens and kept; `active` is the
|
|
1108
998
|
// option arrow keys are on.
|
|
1109
999
|
let picking = false;
|
|
@@ -1130,40 +1020,43 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1130
1020
|
const onlyNew = pageless && canCreate;
|
|
1131
1021
|
if (!canCreate) creating = false;
|
|
1132
1022
|
if (onlyNew) creating = true;
|
|
1133
|
-
if (creating || pageless)
|
|
1023
|
+
if (creating || pageless || !site) form = false;
|
|
1024
|
+
// The form is there only while shown in the open panel.
|
|
1025
|
+
const showForm = open && available && form;
|
|
1026
|
+
if (showForm && !formHandle) mountForm();
|
|
1027
|
+
else if (!showForm && formHandle) unmountForm();
|
|
1134
1028
|
pillText.textContent = onlyNew ? labels.newPage : labels.open;
|
|
1135
1029
|
cancelNew.hidden = onlyNew;
|
|
1136
1030
|
scope.hidden = creating;
|
|
1137
1031
|
newScope.hidden = !creating;
|
|
1138
|
-
newPage.hidden = creating ||
|
|
1032
|
+
newPage.hidden = creating || form || !canCreate;
|
|
1139
1033
|
newPage.disabled = busy;
|
|
1140
1034
|
pathRow.hidden = !creating || picking;
|
|
1141
1035
|
pathInput.disabled = busy;
|
|
1142
|
-
input.hidden =
|
|
1143
|
-
|
|
1036
|
+
input.hidden = form || picking;
|
|
1037
|
+
formBox.hidden = !form || picking;
|
|
1038
|
+
formBox.classList.toggle('loading', formLoading);
|
|
1144
1039
|
toolbar.hidden = picking;
|
|
1145
1040
|
pagesSection.hidden = !picking;
|
|
1146
1041
|
input.placeholder = creating ? labels.newPagePlaceholder : labels.placeholder;
|
|
1147
|
-
|
|
1148
|
-
const scopeUid = json ? viewUid : state.selectedUid;
|
|
1149
|
-
scopeText.textContent = scopeUid ? labels.scopeBlock : labels.scopeEntry;
|
|
1042
|
+
scopeText.textContent = state.selectedUid ? labels.scopeBlock : labels.scopeEntry;
|
|
1150
1043
|
clearBlock.hidden = !state.selectedUid;
|
|
1151
|
-
pageIcon.hidden = !!
|
|
1152
|
-
blockIcon.hidden = !
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1044
|
+
pageIcon.hidden = !!state.selectedUid;
|
|
1045
|
+
blockIcon.hidden = !state.selectedUid;
|
|
1046
|
+
// A CMS from before the form doesn't offer it.
|
|
1047
|
+
modes.hidden = !site || creating || pageless || !state.features.includes('form');
|
|
1048
|
+
promptMode.setAttribute('aria-pressed', String(!form));
|
|
1049
|
+
formMode.setAttribute('aria-pressed', String(form));
|
|
1156
1050
|
modes.querySelectorAll('button').forEach((button)=>{
|
|
1157
|
-
button.disabled = busy
|
|
1051
|
+
button.disabled = busy;
|
|
1158
1052
|
});
|
|
1159
|
-
reload.hidden = !json;
|
|
1160
|
-
reload.disabled = busy || loadingView;
|
|
1161
1053
|
undo.hidden = creating || !state.canUndo;
|
|
1162
|
-
undo.disabled = busy
|
|
1163
|
-
|
|
1164
|
-
send.
|
|
1054
|
+
undo.disabled = busy;
|
|
1055
|
+
// The form applies edits as they are made.
|
|
1056
|
+
send.hidden = form;
|
|
1057
|
+
divider.hidden = form;
|
|
1058
|
+
send.disabled = busy || input.value.trim() === '' || creating && pathInput.value.trim() === '';
|
|
1165
1059
|
input.disabled = busy;
|
|
1166
|
-
code.disabled = busy || loadingView;
|
|
1167
1060
|
save.hidden = !state.features.includes('save') || pageless;
|
|
1168
1061
|
save.disabled = busy || !state.unsaved;
|
|
1169
1062
|
save.textContent = state.saving ? labels.saving : labels.save;
|
|
@@ -1176,141 +1069,57 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1176
1069
|
pageChip.hidden = !site;
|
|
1177
1070
|
pageChip.setAttribute('aria-expanded', String(picking));
|
|
1178
1071
|
pageChipText.textContent = pages?.find(({ entryId })=>entryId === bridge.entryId)?.name ?? labels.pages;
|
|
1179
|
-
const
|
|
1180
|
-
const
|
|
1072
|
+
const loadingForm = form && formLoading && !picking;
|
|
1073
|
+
const working = state.prompting || loadingForm;
|
|
1074
|
+
const note = state.prompting ? creating ? labels.creating : labels.working : loadingForm ? labels.formLoading : message?.text ?? (site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
|
|
1181
1075
|
status.textContent = note;
|
|
1182
1076
|
status.hidden = !note;
|
|
1183
|
-
status.className = `status${working ? ' working' : ''}${!busy && !
|
|
1184
|
-
// A new selection while editing JSON (or a reload asked for): its
|
|
1185
|
-
// JSON, after this render.
|
|
1186
|
-
if (open && json && !picking && state.selectedUid !== seenUid) {
|
|
1187
|
-
seenUid = state.selectedUid;
|
|
1188
|
-
queueMicrotask(()=>void loadView(false));
|
|
1189
|
-
}
|
|
1077
|
+
status.className = `status${working ? ' working' : ''}${!busy && !loadingForm && message?.error ? ' error' : ''}`;
|
|
1190
1078
|
};
|
|
1191
1079
|
const refresh = ()=>render(bridge.getState());
|
|
1192
1080
|
const grow = ()=>{
|
|
1193
1081
|
input.style.height = 'auto';
|
|
1194
1082
|
input.style.height = `${input.scrollHeight}px`;
|
|
1195
1083
|
};
|
|
1196
|
-
//
|
|
1197
|
-
//
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
});
|
|
1217
|
-
if (request !== viewRequest) return false;
|
|
1218
|
-
loaded = `${JSON.stringify(view, null, 2)}\n`;
|
|
1219
|
-
code.value = loaded;
|
|
1220
|
-
code.scrollTop = 0;
|
|
1221
|
-
viewUid = uid;
|
|
1222
|
-
return true;
|
|
1223
|
-
} catch (error) {
|
|
1224
|
-
if (request === viewRequest) message = failure(error);
|
|
1225
|
-
return false;
|
|
1226
|
-
} finally{
|
|
1227
|
-
if (request === viewRequest) {
|
|
1228
|
-
loadingView = false;
|
|
1084
|
+
// Callbacks come later (the address is fetched first), never during a
|
|
1085
|
+
// render.
|
|
1086
|
+
const mountForm = ()=>{
|
|
1087
|
+
formLoading = true;
|
|
1088
|
+
formBox.classList.add('loading');
|
|
1089
|
+
const handle = bridge.openForm(formBox, {
|
|
1090
|
+
title: labels.formTitle,
|
|
1091
|
+
onReady: ()=>{
|
|
1092
|
+
if (formHandle !== handle) return;
|
|
1093
|
+
formLoading = false;
|
|
1094
|
+
message = null;
|
|
1095
|
+
refresh();
|
|
1096
|
+
},
|
|
1097
|
+
onError: (text)=>{
|
|
1098
|
+
if (formHandle !== handle) return;
|
|
1099
|
+
formLoading = false;
|
|
1100
|
+
message = {
|
|
1101
|
+
text,
|
|
1102
|
+
error: true
|
|
1103
|
+
};
|
|
1229
1104
|
refresh();
|
|
1230
1105
|
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// Loads the JSON again once rendered, keeping edited text.
|
|
1234
|
-
const requestView = ()=>{
|
|
1235
|
-
seenUid = undefined;
|
|
1236
|
-
refresh();
|
|
1237
|
-
};
|
|
1238
|
-
const setJson = (next)=>{
|
|
1239
|
-
if (json === next) return;
|
|
1240
|
-
json = next;
|
|
1241
|
-
message = null;
|
|
1242
|
-
requestView();
|
|
1243
|
-
(json ? code : input).focus();
|
|
1106
|
+
});
|
|
1107
|
+
formHandle = handle;
|
|
1244
1108
|
};
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
1250
|
-
return {
|
|
1251
|
-
text: `${labels.jsonInvalid}: ${reason}`
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
const before = text.slice(0, offset);
|
|
1255
|
-
const line = before.split('\n').length;
|
|
1256
|
-
const column = offset - before.lastIndexOf('\n');
|
|
1257
|
-
return {
|
|
1258
|
-
text: labels.jsonInvalidAt.replace('{line}', String(line)).replace('{column}', String(column)),
|
|
1259
|
-
offset
|
|
1260
|
-
};
|
|
1109
|
+
const unmountForm = ()=>{
|
|
1110
|
+
formHandle?.close();
|
|
1111
|
+
formHandle = null;
|
|
1112
|
+
formLoading = false;
|
|
1261
1113
|
};
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
let changes;
|
|
1266
|
-
try {
|
|
1267
|
-
changes = JSON.parse(text);
|
|
1268
|
-
} catch (error) {
|
|
1269
|
-
const problem = parseProblem(text, error);
|
|
1270
|
-
message = {
|
|
1271
|
-
text: problem.text,
|
|
1272
|
-
error: true
|
|
1273
|
-
};
|
|
1274
|
-
refresh();
|
|
1275
|
-
code.focus();
|
|
1276
|
-
if (problem.offset !== undefined) {
|
|
1277
|
-
code.setSelectionRange(problem.offset, problem.offset);
|
|
1278
|
-
}
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
if (!changes || typeof changes !== 'object' || Array.isArray(changes)) {
|
|
1282
|
-
message = {
|
|
1283
|
-
text: labels.jsonNotObject,
|
|
1284
|
-
error: true
|
|
1285
|
-
};
|
|
1286
|
-
return refresh();
|
|
1287
|
-
}
|
|
1114
|
+
const setForm = (next)=>{
|
|
1115
|
+
if (form === next) return;
|
|
1116
|
+
form = next;
|
|
1288
1117
|
message = null;
|
|
1289
|
-
try {
|
|
1290
|
-
const { changed, view } = await bridge.applyJson(changes, {
|
|
1291
|
-
uid: viewUid
|
|
1292
|
-
});
|
|
1293
|
-
// What the CMS made of it, from now on what's edited: in the
|
|
1294
|
-
// answer, or asked for (a CMS from before `view` in it).
|
|
1295
|
-
if (view) {
|
|
1296
|
-
++viewRequest;
|
|
1297
|
-
loaded = `${JSON.stringify(view, null, 2)}\n`;
|
|
1298
|
-
code.value = loaded;
|
|
1299
|
-
}
|
|
1300
|
-
if (view || await loadView(true)) {
|
|
1301
|
-
message = {
|
|
1302
|
-
text: changed.length > 0 ? labels.applied : labels.unchanged,
|
|
1303
|
-
error: false
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
} catch (error) {
|
|
1307
|
-
message = failure(error);
|
|
1308
|
-
}
|
|
1309
1118
|
refresh();
|
|
1310
|
-
|
|
1119
|
+
if (!form) input.focus();
|
|
1311
1120
|
};
|
|
1312
1121
|
const submit = async ()=>{
|
|
1313
|
-
if (
|
|
1122
|
+
if (form) return;
|
|
1314
1123
|
const prompt = input.value.trim();
|
|
1315
1124
|
if (!prompt || bridge.getState().prompting) return;
|
|
1316
1125
|
message = null;
|
|
@@ -1431,7 +1240,6 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1431
1240
|
refresh();
|
|
1432
1241
|
if (site) bridge.setSelecting(open && !creating);
|
|
1433
1242
|
if (open && site) void fetchPages();
|
|
1434
|
-
if (open && json) requestView();
|
|
1435
1243
|
};
|
|
1436
1244
|
const setCreating = (next)=>{
|
|
1437
1245
|
creating = next;
|
|
@@ -1456,7 +1264,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1456
1264
|
};
|
|
1457
1265
|
pill.addEventListener('click', ()=>{
|
|
1458
1266
|
setOpen(true);
|
|
1459
|
-
(creating ? pathInput :
|
|
1267
|
+
if (!form) (creating ? pathInput : input).focus();
|
|
1460
1268
|
});
|
|
1461
1269
|
close.addEventListener('click', ()=>setOpen(false));
|
|
1462
1270
|
save.addEventListener('click', ()=>void store('save'));
|
|
@@ -1466,12 +1274,8 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1466
1274
|
clearBlock.addEventListener('click', ()=>bridge.select(null));
|
|
1467
1275
|
newPage.addEventListener('click', ()=>setCreating(true));
|
|
1468
1276
|
cancelNew.addEventListener('click', ()=>setCreating(false));
|
|
1469
|
-
promptMode.addEventListener('click', ()=>
|
|
1470
|
-
|
|
1471
|
-
reload.addEventListener('click', ()=>{
|
|
1472
|
-
void loadView(true);
|
|
1473
|
-
code.focus();
|
|
1474
|
-
});
|
|
1277
|
+
promptMode.addEventListener('click', ()=>setForm(false));
|
|
1278
|
+
formMode.addEventListener('click', ()=>setForm(true));
|
|
1475
1279
|
pageChip.addEventListener('click', ()=>setPicking(!picking));
|
|
1476
1280
|
pathInput.addEventListener('input', refresh);
|
|
1477
1281
|
pathInput.addEventListener('keydown', (event)=>{
|
|
@@ -1505,7 +1309,6 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1505
1309
|
undo.addEventListener('click', ()=>{
|
|
1506
1310
|
message = null;
|
|
1507
1311
|
bridge.undo();
|
|
1508
|
-
if (json) requestView();
|
|
1509
1312
|
});
|
|
1510
1313
|
panel.addEventListener('submit', (event)=>{
|
|
1511
1314
|
event.preventDefault();
|
|
@@ -1523,37 +1326,92 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1523
1326
|
setOpen(false);
|
|
1524
1327
|
}
|
|
1525
1328
|
});
|
|
1526
|
-
code.addEventListener('input', refresh);
|
|
1527
|
-
code.addEventListener('keydown', (event)=>{
|
|
1528
|
-
// Enter is a new line here; Ctrl or Cmd + Enter applies.
|
|
1529
|
-
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey) && !event.isComposing) {
|
|
1530
|
-
event.preventDefault();
|
|
1531
|
-
void submit();
|
|
1532
|
-
} else if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
1533
|
-
// A tab, not the next button (Shift + Tab still leaves). Through
|
|
1534
|
-
// execCommand where it works, so the browser's undo has it.
|
|
1535
|
-
event.preventDefault();
|
|
1536
|
-
let inserted = false;
|
|
1537
|
-
try {
|
|
1538
|
-
inserted = doc.execCommand('insertText', false, '\t');
|
|
1539
|
-
} catch {}
|
|
1540
|
-
if (!inserted) {
|
|
1541
|
-
code.setRangeText('\t', code.selectionStart, code.selectionEnd, 'end');
|
|
1542
|
-
}
|
|
1543
|
-
refresh();
|
|
1544
|
-
} else if (event.key === 'Escape') {
|
|
1545
|
-
setOpen(false);
|
|
1546
|
-
}
|
|
1547
|
-
});
|
|
1548
1329
|
const unsubscribe = bridge.on('change', render);
|
|
1549
1330
|
refresh();
|
|
1550
1331
|
doc.body.appendChild(host);
|
|
1551
1332
|
return ()=>{
|
|
1552
1333
|
unsubscribe();
|
|
1334
|
+
unmountForm();
|
|
1553
1335
|
host.remove();
|
|
1554
1336
|
};
|
|
1555
1337
|
};
|
|
1556
1338
|
|
|
1339
|
+
// The CMS's entry form in an iframe, for editing on the site (the prompt
|
|
1340
|
+
// box's Form mode). The CMS renders the form at a one-time URL (`/site/form#
|
|
1341
|
+
// code=…`, from the site's `/cms/api/form`); this keeps it in step with the
|
|
1342
|
+
// bridge's in-memory content.
|
|
1343
|
+
//
|
|
1344
|
+
// Messages are only exchanged with the URL's origin, and only with the
|
|
1345
|
+
// iframe's window:
|
|
1346
|
+
// - form → bridge: { source: 'obelism-content-form', type: 'ready' |
|
|
1347
|
+
// 'change' | 'error', content?, message? } — `change` carries the whole
|
|
1348
|
+
// entry content after an edit
|
|
1349
|
+
// - bridge → form: { source: 'obelism-content-bridge', type: 'content',
|
|
1350
|
+
// entryId, content, locale, selectedUid }
|
|
1351
|
+
const FORM_SOURCE = 'obelism-content-form';
|
|
1352
|
+
// How long the form may take to say it is ready.
|
|
1353
|
+
const FORM_READY_TIMEOUT_MS = 30_000;
|
|
1354
|
+
const isObject = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
|
|
1355
|
+
// Mounts the form at `url` in `container`. The URL is single use: mount a
|
|
1356
|
+
// fresh one every time.
|
|
1357
|
+
const mountFormFrame = (win, container, url, title, handlers)=>{
|
|
1358
|
+
let parsed = null;
|
|
1359
|
+
try {
|
|
1360
|
+
parsed = new URL(url);
|
|
1361
|
+
} catch {}
|
|
1362
|
+
if (!parsed || !/^https?:$/.test(parsed.protocol)) {
|
|
1363
|
+
throw new Error('Obelism Content sent no form to open');
|
|
1364
|
+
}
|
|
1365
|
+
const origin = parsed.origin;
|
|
1366
|
+
const iframe = container.ownerDocument.createElement('iframe');
|
|
1367
|
+
iframe.title = title;
|
|
1368
|
+
iframe.src = url;
|
|
1369
|
+
let ready = false;
|
|
1370
|
+
let latest = null;
|
|
1371
|
+
const post = ()=>{
|
|
1372
|
+
if (!ready || !latest) return;
|
|
1373
|
+
iframe.contentWindow?.postMessage({
|
|
1374
|
+
source: BRIDGE_SOURCE,
|
|
1375
|
+
type: 'content',
|
|
1376
|
+
...latest
|
|
1377
|
+
}, origin);
|
|
1378
|
+
};
|
|
1379
|
+
const timer = setTimeout(()=>{
|
|
1380
|
+
if (!ready) handlers.onError('The form did not open');
|
|
1381
|
+
}, FORM_READY_TIMEOUT_MS);
|
|
1382
|
+
const onMessage = (event)=>{
|
|
1383
|
+
if (event.origin !== origin || !iframe.contentWindow || event.source !== iframe.contentWindow) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const data = event.data;
|
|
1387
|
+
if (!data || data.source !== FORM_SOURCE) return;
|
|
1388
|
+
if (data.type === 'ready') {
|
|
1389
|
+
// Again after the form reloads itself: it gets the content again.
|
|
1390
|
+
ready = true;
|
|
1391
|
+
clearTimeout(timer);
|
|
1392
|
+
handlers.onReady();
|
|
1393
|
+
post();
|
|
1394
|
+
} else if (data.type === 'change' && isObject(data.content)) {
|
|
1395
|
+
handlers.onChange(data.content, typeof data.entryId === 'string' ? data.entryId : undefined);
|
|
1396
|
+
} else if (data.type === 'error') {
|
|
1397
|
+
handlers.onError(typeof data.message === 'string' && data.message ? data.message : 'The form failed');
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
win.addEventListener('message', onMessage);
|
|
1401
|
+
container.appendChild(iframe);
|
|
1402
|
+
return {
|
|
1403
|
+
show (content) {
|
|
1404
|
+
latest = content;
|
|
1405
|
+
post();
|
|
1406
|
+
},
|
|
1407
|
+
destroy () {
|
|
1408
|
+
clearTimeout(timer);
|
|
1409
|
+
win.removeEventListener('message', onMessage);
|
|
1410
|
+
iframe.remove();
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1557
1415
|
// The bridge on a site signed in to edit (`/cms/login`, from
|
|
1558
1416
|
// @obelism/content/site), outside the visual editor. It plays the editor's
|
|
1559
1417
|
// part itself: it keeps the changed content in memory, asks the site's
|
|
@@ -1563,9 +1421,10 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1563
1421
|
//
|
|
1564
1422
|
// What it edits is the page's entry — the outermost element marked with
|
|
1565
1423
|
// `editable(entry)` — or, for a selected block, the entry around that block.
|
|
1566
|
-
// Besides prompts it
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1424
|
+
// Besides prompts it shows the CMS's own entry form (`openForm`, an iframe
|
|
1425
|
+
// kept in step with the content here, see formFrame.ts), edits JSON (`view`,
|
|
1426
|
+
// then `applyJson`: the raw alternative), and lists the space's pages to go
|
|
1427
|
+
// to one.
|
|
1569
1428
|
// The base path of the site's handler while signed in to edit, else null.
|
|
1570
1429
|
const readSiteEditing = (doc)=>{
|
|
1571
1430
|
for (const part of doc.cookie.split(';')){
|
|
@@ -1581,6 +1440,11 @@ const readSiteEditing = (doc)=>{
|
|
|
1581
1440
|
return null;
|
|
1582
1441
|
};
|
|
1583
1442
|
const same = (a, b)=>JSON.stringify(a) === JSON.stringify(b);
|
|
1443
|
+
// Edits in the form further apart than this are separate undo steps.
|
|
1444
|
+
const FORM_BURST_MS = 1000;
|
|
1445
|
+
// How long the page waits for the form's edits to settle before it shows
|
|
1446
|
+
// them.
|
|
1447
|
+
const FORM_RENDER_MS = 300;
|
|
1584
1448
|
const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})=>{
|
|
1585
1449
|
const doc = win.document;
|
|
1586
1450
|
const emitter = createEmitter();
|
|
@@ -1591,6 +1455,11 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1591
1455
|
// Entries changed here since the last publish.
|
|
1592
1456
|
const touched = new Set();
|
|
1593
1457
|
let session = null;
|
|
1458
|
+
// The open form's, if any; and when the form last changed content, for
|
|
1459
|
+
// undo steps (-Infinity: the next edit starts a new step).
|
|
1460
|
+
let closeForm = null;
|
|
1461
|
+
let lastFormEdit = -Infinity;
|
|
1462
|
+
const renderTimers = new Map();
|
|
1594
1463
|
let state = INITIAL_STATE;
|
|
1595
1464
|
const setState = (next)=>{
|
|
1596
1465
|
state = {
|
|
@@ -1644,17 +1513,26 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1644
1513
|
const base = lang.split('-')[0] ?? '';
|
|
1645
1514
|
return codes.includes(base) ? base : session.space.defaultLocale;
|
|
1646
1515
|
};
|
|
1516
|
+
// One request per entry at a time: a second would overwrite edits made
|
|
1517
|
+
// while the first was out.
|
|
1518
|
+
const loading = new Map();
|
|
1647
1519
|
const load = async (entryId)=>{
|
|
1648
1520
|
const loaded = working.get(entryId);
|
|
1649
1521
|
if (loaded) return loaded;
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1522
|
+
let pending = loading.get(entryId);
|
|
1523
|
+
if (!pending) {
|
|
1524
|
+
pending = api(`entries/${encodeURIComponent(entryId)}`).then((entry)=>{
|
|
1525
|
+
const fresh = working.get(entryId) ?? {
|
|
1526
|
+
content: entry.content,
|
|
1527
|
+
saved: entry.content,
|
|
1528
|
+
revision: entry.draftRevision
|
|
1529
|
+
};
|
|
1530
|
+
working.set(entryId, fresh);
|
|
1531
|
+
return fresh;
|
|
1532
|
+
}).finally(()=>loading.delete(entryId));
|
|
1533
|
+
loading.set(entryId, pending);
|
|
1534
|
+
}
|
|
1535
|
+
return pending;
|
|
1658
1536
|
};
|
|
1659
1537
|
// The entry a prompt or JSON acts on, for the block `uid` (the selected
|
|
1660
1538
|
// one when left out).
|
|
@@ -1693,6 +1571,15 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1693
1571
|
});
|
|
1694
1572
|
win.requestAnimationFrame(()=>highlight(state.selectedUid));
|
|
1695
1573
|
};
|
|
1574
|
+
const logError = (error)=>console.error('[@obelism/content bridge]', error);
|
|
1575
|
+
// Renders once the form's edits settle.
|
|
1576
|
+
const renderSoon = (entryId)=>{
|
|
1577
|
+
clearTimeout(renderTimers.get(entryId));
|
|
1578
|
+
renderTimers.set(entryId, setTimeout(()=>{
|
|
1579
|
+
renderTimers.delete(entryId);
|
|
1580
|
+
void render(entryId).catch(logError);
|
|
1581
|
+
}, FORM_RENDER_MS));
|
|
1582
|
+
};
|
|
1696
1583
|
const afterStore = (event)=>{
|
|
1697
1584
|
emitter.emit(event, {});
|
|
1698
1585
|
if (emitter.listeners.update.size === 0) win.location.reload();
|
|
@@ -1709,6 +1596,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1709
1596
|
touched.add(entryId);
|
|
1710
1597
|
}
|
|
1711
1598
|
history.length = 0;
|
|
1599
|
+
lastFormEdit = -Infinity;
|
|
1712
1600
|
refreshUnsaved();
|
|
1713
1601
|
};
|
|
1714
1602
|
const has = (feature)=>state.features.includes(feature);
|
|
@@ -1741,6 +1629,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1741
1629
|
entryId,
|
|
1742
1630
|
content: entry.content
|
|
1743
1631
|
});
|
|
1632
|
+
lastFormEdit = -Infinity;
|
|
1744
1633
|
entry.content = result.content;
|
|
1745
1634
|
refreshUnsaved();
|
|
1746
1635
|
await render(entryId);
|
|
@@ -1819,6 +1708,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1819
1708
|
entryId,
|
|
1820
1709
|
content: entry.content
|
|
1821
1710
|
});
|
|
1711
|
+
lastFormEdit = -Infinity;
|
|
1822
1712
|
entry.content = result.content;
|
|
1823
1713
|
refreshUnsaved();
|
|
1824
1714
|
await render(entryId);
|
|
@@ -1835,6 +1725,91 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1835
1725
|
});
|
|
1836
1726
|
}
|
|
1837
1727
|
},
|
|
1728
|
+
openForm (container, options = {}) {
|
|
1729
|
+
const { title = 'Content form', onReady, onError } = options;
|
|
1730
|
+
closeForm?.();
|
|
1731
|
+
let closed = false;
|
|
1732
|
+
let frame = null;
|
|
1733
|
+
// What the form was last sent, or sent back itself.
|
|
1734
|
+
let shown = null;
|
|
1735
|
+
let unsubscribe = ()=>{};
|
|
1736
|
+
const fail = (error)=>{
|
|
1737
|
+
if (!closed) onError?.(error instanceof Error ? error.message : String(error));
|
|
1738
|
+
};
|
|
1739
|
+
const close = ()=>{
|
|
1740
|
+
if (closed) return;
|
|
1741
|
+
closed = true;
|
|
1742
|
+
unsubscribe();
|
|
1743
|
+
frame?.destroy();
|
|
1744
|
+
if (closeForm === close) closeForm = null;
|
|
1745
|
+
};
|
|
1746
|
+
closeForm = close;
|
|
1747
|
+
// Sends the form the entry for the selection, when anything of it
|
|
1748
|
+
// changed: prompts, undo, a reload after saving, the selection.
|
|
1749
|
+
const show = ()=>{
|
|
1750
|
+
if (closed || !frame) return;
|
|
1751
|
+
const entryId = entryFor(state.selectedUid);
|
|
1752
|
+
if (!entryId) return;
|
|
1753
|
+
const entry = working.get(entryId);
|
|
1754
|
+
if (!entry) {
|
|
1755
|
+
load(entryId).then(show, fail);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const next = {
|
|
1759
|
+
entryId,
|
|
1760
|
+
content: entry.content,
|
|
1761
|
+
locale: locale(),
|
|
1762
|
+
selectedUid: state.selectedUid
|
|
1763
|
+
};
|
|
1764
|
+
if (shown && shown.entryId === next.entryId && shown.content === next.content && shown.locale === next.locale && shown.selectedUid === next.selectedUid) {
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
shown = next;
|
|
1768
|
+
frame.show(next);
|
|
1769
|
+
};
|
|
1770
|
+
const change = (content, from)=>{
|
|
1771
|
+
const entryId = from ?? shown?.entryId;
|
|
1772
|
+
const entry = entryId === undefined ? undefined : working.get(entryId);
|
|
1773
|
+
if (closed || entryId === undefined || !entry || same(content, entry.content)) {
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
const now = Date.now();
|
|
1777
|
+
if (now - lastFormEdit >= FORM_BURST_MS) {
|
|
1778
|
+
history.push({
|
|
1779
|
+
entryId,
|
|
1780
|
+
content: entry.content
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
lastFormEdit = now;
|
|
1784
|
+
entry.content = content;
|
|
1785
|
+
// Not sent back: the form has it.
|
|
1786
|
+
if (shown?.entryId === entryId) shown = {
|
|
1787
|
+
...shown,
|
|
1788
|
+
content
|
|
1789
|
+
};
|
|
1790
|
+
refreshUnsaved();
|
|
1791
|
+
renderSoon(entryId);
|
|
1792
|
+
};
|
|
1793
|
+
const start = async ()=>{
|
|
1794
|
+
if (!has('form')) throw new Error('You may not edit content here');
|
|
1795
|
+
await load(target(undefined).entryId);
|
|
1796
|
+
// A fresh one every time: the address works once, for a minute.
|
|
1797
|
+
const { url } = await api('form', {});
|
|
1798
|
+
if (closed) return;
|
|
1799
|
+
const address = typeof url === 'string' ? url : '';
|
|
1800
|
+
frame = mountFormFrame(win, container, address, title, {
|
|
1801
|
+
onReady: ()=>onReady?.(),
|
|
1802
|
+
onChange: change,
|
|
1803
|
+
onError: (message)=>onError?.(message)
|
|
1804
|
+
});
|
|
1805
|
+
unsubscribe = emitter.on('change', show);
|
|
1806
|
+
show();
|
|
1807
|
+
};
|
|
1808
|
+
start().catch(fail);
|
|
1809
|
+
return {
|
|
1810
|
+
close
|
|
1811
|
+
};
|
|
1812
|
+
},
|
|
1838
1813
|
async pages () {
|
|
1839
1814
|
const { pages } = await api('pages');
|
|
1840
1815
|
const preferred = [
|
|
@@ -1872,9 +1847,10 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1872
1847
|
const last = history.pop();
|
|
1873
1848
|
const entry = last && working.get(last.entryId);
|
|
1874
1849
|
if (!last || !entry) return;
|
|
1850
|
+
lastFormEdit = -Infinity;
|
|
1875
1851
|
entry.content = last.content;
|
|
1876
1852
|
refreshUnsaved();
|
|
1877
|
-
void render(last.entryId).catch(
|
|
1853
|
+
void render(last.entryId).catch(logError);
|
|
1878
1854
|
},
|
|
1879
1855
|
async save () {
|
|
1880
1856
|
if (!has('save')) throw new Error('You may not edit content here');
|
|
@@ -1937,6 +1913,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1937
1913
|
});
|
|
1938
1914
|
},
|
|
1939
1915
|
destroy () {
|
|
1916
|
+
closeForm?.();
|
|
1917
|
+
renderTimers.forEach((timer)=>clearTimeout(timer));
|
|
1918
|
+
renderTimers.clear();
|
|
1940
1919
|
cleanup.splice(0).forEach((undo)=>undo());
|
|
1941
1920
|
}
|
|
1942
1921
|
};
|