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