@obelism/content 0.12.0 → 0.13.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 +17 -0
- package/dist/bin/content.js +2 -1
- package/dist/bridge.d.mts +40 -1
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +40 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +59 -3
- package/dist/bridge.js +671 -34
- package/dist/bridge.mjs +671 -34
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/site.js +5 -1
- package/dist/site.mjs +5 -1
- package/dist/{types-hmpsikmo.d.ts → types-m3j4tltp.d.ts} +2 -2
- package/dist/{types-hmpsikmo.d.ts.map → types-m3j4tltp.d.ts.map} +1 -1
- package/package.json +1 -1
package/dist/bridge.mjs
CHANGED
|
@@ -222,10 +222,15 @@ const createEmitter = ()=>{
|
|
|
222
222
|
};
|
|
223
223
|
const inertPrompt = ()=>Promise.reject(new Error('Prompts only work inside the visual editor'));
|
|
224
224
|
const onlyOnSite = ()=>Promise.reject(new Error('Save and publish in the visual editor itself'));
|
|
225
|
+
const onlyWhileEditingSite = ()=>Promise.reject(new Error('Only while signed in to edit on the site'));
|
|
225
226
|
// What every bridge without a feature does for it.
|
|
226
227
|
const INERT_METHODS = {
|
|
227
228
|
prompt: inertPrompt,
|
|
228
229
|
createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
|
|
230
|
+
view: onlyWhileEditingSite,
|
|
231
|
+
applyJson: onlyWhileEditingSite,
|
|
232
|
+
pages: onlyWhileEditingSite,
|
|
233
|
+
openPage: ()=>{},
|
|
229
234
|
undo: ()=>{},
|
|
230
235
|
save: onlyOnSite,
|
|
231
236
|
publish: onlyOnSite,
|
|
@@ -510,7 +515,26 @@ const DEFAULT_PROMPT_LABELS = {
|
|
|
510
515
|
path: 'Path',
|
|
511
516
|
pathPlaceholder: '/pricing',
|
|
512
517
|
creating: 'Writing the page…',
|
|
513
|
-
created: 'Opening the new page…'
|
|
518
|
+
created: 'Opening the new page…',
|
|
519
|
+
modes: 'Edit with',
|
|
520
|
+
modePrompt: 'Prompt',
|
|
521
|
+
modeJson: 'JSON',
|
|
522
|
+
json: 'The content as JSON',
|
|
523
|
+
jsonLoading: 'Loading the JSON…',
|
|
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',
|
|
531
|
+
pages: 'Pages',
|
|
532
|
+
pagesSearch: 'Find a page',
|
|
533
|
+
pagesLoading: 'Loading pages…',
|
|
534
|
+
pagesEmpty: 'No pages match',
|
|
535
|
+
pageCurrent: 'This page',
|
|
536
|
+
pageDraft: 'Draft',
|
|
537
|
+
pageChanged: 'Changed'
|
|
514
538
|
};
|
|
515
539
|
// Icons (Lucide), drawn in the text colour.
|
|
516
540
|
const ICONS = {
|
|
@@ -519,6 +543,10 @@ const ICONS = {
|
|
|
519
543
|
block: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',
|
|
520
544
|
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
|
|
521
545
|
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
|
+
chevrons: '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
|
|
548
|
+
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
549
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
522
550
|
send: '<path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/>'
|
|
523
551
|
};
|
|
524
552
|
// Obelism's design tokens (obelism.studio/design.md), under their own names:
|
|
@@ -620,6 +648,13 @@ textarea {
|
|
|
620
648
|
font-size: 16px; line-height: 1.6; color: rgb(var(--theme-foreground-rgb));
|
|
621
649
|
}
|
|
622
650
|
textarea::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
|
|
651
|
+
/* JSON: a plain code editor, taller, scrolling instead of wrapping. */
|
|
652
|
+
textarea.code {
|
|
653
|
+
height: 40vh; min-height: 160px; max-height: 50vh; overflow: auto; resize: vertical;
|
|
654
|
+
white-space: pre; overflow-wrap: normal; tab-size: 2;
|
|
655
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
656
|
+
font-size: 13px; line-height: 1.55;
|
|
657
|
+
}
|
|
623
658
|
/* New page: where it goes, above what it is for. */
|
|
624
659
|
.path {
|
|
625
660
|
display: flex; align-items: center; gap: 10px; padding: 16px 22px 0;
|
|
@@ -663,6 +698,21 @@ button.chip:disabled { opacity: .5; }
|
|
|
663
698
|
.icon-button.primary { background: rgb(var(--theme-accent-rgb)); color: #fff; }
|
|
664
699
|
.icon-button.primary:hover:not(:disabled) { background: rgb(var(--theme-accent-rgb)); color: #fff; opacity: .88; }
|
|
665
700
|
.icon-button.primary:disabled { opacity: .35; }
|
|
701
|
+
/* Prompt or JSON: two small pills in one track. */
|
|
702
|
+
.modes {
|
|
703
|
+
display: inline-flex; flex: none; gap: 2px; padding: 2px; border-radius: var(--border-radius-pill);
|
|
704
|
+
border: 1px solid rgb(var(--theme-border-rgb) / .2); background: rgb(var(--theme-background-actions-rgb) / .6);
|
|
705
|
+
}
|
|
706
|
+
.modes button {
|
|
707
|
+
height: 28px; padding: 0 12px; border-radius: var(--border-radius-pill); font-size: 13px;
|
|
708
|
+
color: rgb(var(--theme-foreground-muted-rgb));
|
|
709
|
+
transition: background-color .25s var(--ease-roll), color .25s var(--ease-roll);
|
|
710
|
+
}
|
|
711
|
+
.modes button:hover { color: rgb(var(--theme-foreground-rgb)); }
|
|
712
|
+
.modes button[aria-pressed='true'] {
|
|
713
|
+
background: rgb(var(--theme-background-alt-rgb)); color: rgb(var(--theme-foreground-rgb));
|
|
714
|
+
box-shadow: 0 1px 2px rgb(var(--color-black-rgb) / .08);
|
|
715
|
+
}
|
|
666
716
|
.divider { width: 1px; height: 20px; background: rgb(var(--theme-border-rgb) / .3); margin: 0 4px; }
|
|
667
717
|
|
|
668
718
|
/* On the site: storing the draft, and the way out. */
|
|
@@ -679,6 +729,38 @@ button.chip:disabled { opacity: .5; }
|
|
|
679
729
|
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
|
|
680
730
|
background: rgb(var(--theme-accent-rgb)); margin-right: 8px;
|
|
681
731
|
}
|
|
732
|
+
|
|
733
|
+
/* The pages: a search above a list, in place of the prompt while open. */
|
|
734
|
+
.page-chip { min-width: 0; max-width: 50%; gap: 6px; }
|
|
735
|
+
.page-chip > span:not(.icon) { overflow: hidden; text-overflow: ellipsis; }
|
|
736
|
+
.page-chip .icon:last-child { color: rgb(var(--theme-foreground-muted-rgb)); margin-right: -4px; }
|
|
737
|
+
.page-chip .icon:last-child svg { width: 14px; height: 14px; }
|
|
738
|
+
.pages { display: grid; gap: 8px; padding: 16px 12px 12px; }
|
|
739
|
+
.search {
|
|
740
|
+
display: flex; align-items: center; gap: 8px; height: 38px; padding: 0 14px; margin: 0 4px;
|
|
741
|
+
border: 1px solid rgb(var(--theme-border-rgb) / .25); border-radius: var(--border-radius-pill);
|
|
742
|
+
background: rgb(var(--theme-background-actions-rgb) / .45); color: rgb(var(--theme-foreground-muted-rgb));
|
|
743
|
+
transition: border-color .25s var(--ease-roll);
|
|
744
|
+
}
|
|
745
|
+
.search:focus-within { border-color: rgb(var(--theme-accent-rgb)); }
|
|
746
|
+
.search input { flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; color: rgb(var(--theme-foreground-rgb)); }
|
|
747
|
+
.search input::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
|
|
748
|
+
.options { list-style: none; padding: 0; max-height: min(300px, 40vh); overflow-y: auto; display: grid; gap: 2px; }
|
|
749
|
+
.option {
|
|
750
|
+
display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 12px; cursor: pointer;
|
|
751
|
+
transition: background-color .15s var(--ease-roll);
|
|
752
|
+
}
|
|
753
|
+
.option:hover, .option.active { background: rgb(var(--theme-background-actions-rgb) / .7); }
|
|
754
|
+
.option-text { flex: 1; min-width: 0; display: grid; }
|
|
755
|
+
.option-text > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
756
|
+
.option-path { font-size: 12px; color: rgb(var(--theme-foreground-muted-rgb)); }
|
|
757
|
+
.badge {
|
|
758
|
+
flex: none; font-size: 12px; padding: 1px 8px; border-radius: var(--border-radius-pill);
|
|
759
|
+
border: 1px solid rgb(var(--theme-border-rgb) / .3); color: rgb(var(--theme-foreground-muted-rgb));
|
|
760
|
+
}
|
|
761
|
+
.option .icon { color: rgb(var(--theme-accent-rgb)); }
|
|
762
|
+
.empty { padding: 8px 12px; color: rgb(var(--theme-foreground-muted-rgb)); }
|
|
763
|
+
.empty.error { color: var(--theme-error); }
|
|
682
764
|
[hidden] { display: none !important; }
|
|
683
765
|
`;
|
|
684
766
|
const element = (doc, tag, props = {}, ...children)=>{
|
|
@@ -699,6 +781,101 @@ const icon = (doc, name)=>{
|
|
|
699
781
|
span.innerHTML = `<svg viewBox="0 0 24 24">${ICONS[name]}</svg>`;
|
|
700
782
|
return span;
|
|
701
783
|
};
|
|
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
|
+
};
|
|
702
879
|
// Mounts the box on the page; returns a function that removes it.
|
|
703
880
|
const mountPromptBox = (doc, bridge, options = {})=>{
|
|
704
881
|
const labels = {
|
|
@@ -742,6 +919,14 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
742
919
|
rows: 1
|
|
743
920
|
});
|
|
744
921
|
input.setAttribute('aria-label', labels.open);
|
|
922
|
+
// JSON mode (on the site): the entry or block as JSON, edited by hand.
|
|
923
|
+
const code = element(doc, 'textarea', {
|
|
924
|
+
className: 'code',
|
|
925
|
+
spellcheck: false,
|
|
926
|
+
autocomplete: 'off'
|
|
927
|
+
});
|
|
928
|
+
code.setAttribute('aria-label', labels.json);
|
|
929
|
+
code.setAttribute('autocapitalize', 'off');
|
|
745
930
|
// What the prompt acts on: the whole page, or the selected block with a
|
|
746
931
|
// way back to the page.
|
|
747
932
|
const pageIcon = icon(doc, 'page');
|
|
@@ -760,6 +945,28 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
760
945
|
className: 'action chip',
|
|
761
946
|
type: 'button'
|
|
762
947
|
}, icon(doc, 'undo'), labels.undo);
|
|
948
|
+
// Prompt or JSON (on the site).
|
|
949
|
+
const modeButton = (text)=>{
|
|
950
|
+
const button = element(doc, 'button', {
|
|
951
|
+
type: 'button',
|
|
952
|
+
textContent: text
|
|
953
|
+
});
|
|
954
|
+
button.setAttribute('aria-pressed', 'false');
|
|
955
|
+
return button;
|
|
956
|
+
};
|
|
957
|
+
const promptMode = modeButton(labels.modePrompt);
|
|
958
|
+
const jsonMode = modeButton(labels.modeJson);
|
|
959
|
+
const modes = element(doc, 'span', {
|
|
960
|
+
className: 'modes'
|
|
961
|
+
}, promptMode, jsonMode);
|
|
962
|
+
modes.setAttribute('role', 'group');
|
|
963
|
+
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);
|
|
763
970
|
// New page (on the site, for people who may add pages): the scope becomes
|
|
764
971
|
// the new page, with a way back, and a path field shows above the prompt.
|
|
765
972
|
const newPage = element(doc, 'button', {
|
|
@@ -802,6 +1009,51 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
802
1009
|
textContent: labels.send
|
|
803
1010
|
}), icon(doc, 'send'));
|
|
804
1011
|
send.title = labels.send;
|
|
1012
|
+
const toolbar = element(doc, 'div', {
|
|
1013
|
+
className: 'toolbar'
|
|
1014
|
+
}, scope, newScope, modes, undo, newPage, element(doc, 'span', {
|
|
1015
|
+
className: 'spacer'
|
|
1016
|
+
}), reload, close, element(doc, 'span', {
|
|
1017
|
+
className: 'divider'
|
|
1018
|
+
}), send);
|
|
1019
|
+
// The pages (on the site): a chip naming this page, opening a search
|
|
1020
|
+
// and a list of every page in place of the prompt.
|
|
1021
|
+
const pageChipText = element(doc, 'span', {
|
|
1022
|
+
textContent: labels.pages
|
|
1023
|
+
});
|
|
1024
|
+
const pageChip = element(doc, 'button', {
|
|
1025
|
+
className: 'chip page-chip',
|
|
1026
|
+
type: 'button',
|
|
1027
|
+
title: labels.pages
|
|
1028
|
+
}, icon(doc, 'page'), pageChipText, icon(doc, 'chevrons'));
|
|
1029
|
+
pageChip.setAttribute('aria-haspopup', 'listbox');
|
|
1030
|
+
pageChip.setAttribute('aria-expanded', 'false');
|
|
1031
|
+
pageChip.setAttribute('aria-controls', 'obelism-pages');
|
|
1032
|
+
const search = element(doc, 'input', {
|
|
1033
|
+
type: 'text',
|
|
1034
|
+
placeholder: labels.pagesSearch,
|
|
1035
|
+
autocomplete: 'off',
|
|
1036
|
+
spellcheck: false
|
|
1037
|
+
});
|
|
1038
|
+
search.setAttribute('role', 'combobox');
|
|
1039
|
+
search.setAttribute('aria-label', labels.pagesSearch);
|
|
1040
|
+
search.setAttribute('aria-autocomplete', 'list');
|
|
1041
|
+
search.setAttribute('aria-expanded', 'true');
|
|
1042
|
+
search.setAttribute('aria-controls', 'obelism-pages');
|
|
1043
|
+
const optionList = element(doc, 'ul', {
|
|
1044
|
+
className: 'options',
|
|
1045
|
+
id: 'obelism-pages'
|
|
1046
|
+
});
|
|
1047
|
+
optionList.setAttribute('role', 'listbox');
|
|
1048
|
+
optionList.setAttribute('aria-label', labels.pages);
|
|
1049
|
+
const pagesNote = element(doc, 'p', {
|
|
1050
|
+
className: 'empty'
|
|
1051
|
+
});
|
|
1052
|
+
const pagesSection = element(doc, 'div', {
|
|
1053
|
+
className: 'pages'
|
|
1054
|
+
}, element(doc, 'label', {
|
|
1055
|
+
className: 'search'
|
|
1056
|
+
}, icon(doc, 'search'), search), optionList, pagesNote);
|
|
805
1057
|
const unsaved = element(doc, 'span', {
|
|
806
1058
|
className: 'unsaved'
|
|
807
1059
|
});
|
|
@@ -826,18 +1078,12 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
826
1078
|
});
|
|
827
1079
|
const footer = element(doc, 'div', {
|
|
828
1080
|
className: 'footer'
|
|
829
|
-
}, unsaved, element(doc, 'span', {
|
|
1081
|
+
}, pageChip, unsaved, element(doc, 'span', {
|
|
830
1082
|
className: 'spacer'
|
|
831
1083
|
}), signOut, save, publish);
|
|
832
1084
|
const panel = element(doc, 'form', {
|
|
833
1085
|
className: 'panel'
|
|
834
|
-
}, status, pathRow, input,
|
|
835
|
-
className: 'toolbar'
|
|
836
|
-
}, scope, newScope, undo, newPage, element(doc, 'span', {
|
|
837
|
-
className: 'spacer'
|
|
838
|
-
}), close, element(doc, 'span', {
|
|
839
|
-
className: 'divider'
|
|
840
|
-
}), send), footer);
|
|
1086
|
+
}, status, pathRow, input, code, toolbar, pagesSection, footer);
|
|
841
1087
|
const root = element(doc, 'div', {
|
|
842
1088
|
className: 'root'
|
|
843
1089
|
}, element(doc, 'div', {
|
|
@@ -849,6 +1095,26 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
849
1095
|
let open = false;
|
|
850
1096
|
let creating = false;
|
|
851
1097
|
let message = null;
|
|
1098
|
+
// JSON mode: the text as last loaded, and what it is of (the block's
|
|
1099
|
+
// uid, null for the whole entry). `seenUid` is the selection last loaded
|
|
1100
|
+
// for: a new one loads its JSON, and undefined loads it again.
|
|
1101
|
+
let json = false;
|
|
1102
|
+
let loaded = '';
|
|
1103
|
+
let viewUid = null;
|
|
1104
|
+
let seenUid;
|
|
1105
|
+
let loadingView = false;
|
|
1106
|
+
let viewRequest = 0;
|
|
1107
|
+
// The pages, fetched once the panel opens and kept; `active` is the
|
|
1108
|
+
// option arrow keys are on.
|
|
1109
|
+
let picking = false;
|
|
1110
|
+
let pages = null;
|
|
1111
|
+
let pagesLoad = null;
|
|
1112
|
+
let pagesError = null;
|
|
1113
|
+
let active = 0;
|
|
1114
|
+
const failure = (error)=>({
|
|
1115
|
+
text: error instanceof Error ? error.message : String(error),
|
|
1116
|
+
error: true
|
|
1117
|
+
});
|
|
852
1118
|
const render = (state)=>{
|
|
853
1119
|
const available = state.features.includes('prompt');
|
|
854
1120
|
const storing = state.saving || state.publishing;
|
|
@@ -864,23 +1130,40 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
864
1130
|
const onlyNew = pageless && canCreate;
|
|
865
1131
|
if (!canCreate) creating = false;
|
|
866
1132
|
if (onlyNew) creating = true;
|
|
1133
|
+
if (creating || pageless) json = false;
|
|
867
1134
|
pillText.textContent = onlyNew ? labels.newPage : labels.open;
|
|
868
1135
|
cancelNew.hidden = onlyNew;
|
|
869
1136
|
scope.hidden = creating;
|
|
870
1137
|
newScope.hidden = !creating;
|
|
871
|
-
newPage.hidden = creating || !canCreate;
|
|
1138
|
+
newPage.hidden = creating || json || !canCreate;
|
|
872
1139
|
newPage.disabled = busy;
|
|
873
|
-
pathRow.hidden = !creating;
|
|
1140
|
+
pathRow.hidden = !creating || picking;
|
|
874
1141
|
pathInput.disabled = busy;
|
|
1142
|
+
input.hidden = json || picking;
|
|
1143
|
+
code.hidden = !json || picking;
|
|
1144
|
+
toolbar.hidden = picking;
|
|
1145
|
+
pagesSection.hidden = !picking;
|
|
875
1146
|
input.placeholder = creating ? labels.newPagePlaceholder : labels.placeholder;
|
|
876
|
-
|
|
1147
|
+
// In JSON mode the scope is what the JSON is of.
|
|
1148
|
+
const scopeUid = json ? viewUid : state.selectedUid;
|
|
1149
|
+
scopeText.textContent = scopeUid ? labels.scopeBlock : labels.scopeEntry;
|
|
877
1150
|
clearBlock.hidden = !state.selectedUid;
|
|
878
|
-
pageIcon.hidden = !!
|
|
879
|
-
blockIcon.hidden = !
|
|
1151
|
+
pageIcon.hidden = !!scopeUid;
|
|
1152
|
+
blockIcon.hidden = !scopeUid;
|
|
1153
|
+
modes.hidden = !site || creating || pageless;
|
|
1154
|
+
promptMode.setAttribute('aria-pressed', String(!json));
|
|
1155
|
+
jsonMode.setAttribute('aria-pressed', String(json));
|
|
1156
|
+
modes.querySelectorAll('button').forEach((button)=>{
|
|
1157
|
+
button.disabled = busy || loadingView;
|
|
1158
|
+
});
|
|
1159
|
+
reload.hidden = !json;
|
|
1160
|
+
reload.disabled = busy || loadingView;
|
|
880
1161
|
undo.hidden = creating || !state.canUndo;
|
|
881
|
-
undo.disabled = busy;
|
|
882
|
-
|
|
1162
|
+
undo.disabled = busy || loadingView;
|
|
1163
|
+
const text = json ? code.value : input.value;
|
|
1164
|
+
send.disabled = busy || loadingView || text.trim() === '' || creating && pathInput.value.trim() === '';
|
|
883
1165
|
input.disabled = busy;
|
|
1166
|
+
code.disabled = busy || loadingView;
|
|
884
1167
|
save.hidden = !state.features.includes('save') || pageless;
|
|
885
1168
|
save.disabled = busy || !state.unsaved;
|
|
886
1169
|
save.textContent = state.saving ? labels.saving : labels.save;
|
|
@@ -890,17 +1173,144 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
890
1173
|
signOut.hidden = !site;
|
|
891
1174
|
footer.hidden = !site;
|
|
892
1175
|
unsaved.hidden = !state.unsaved;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1176
|
+
pageChip.hidden = !site;
|
|
1177
|
+
pageChip.setAttribute('aria-expanded', String(picking));
|
|
1178
|
+
pageChipText.textContent = pages?.find(({ entryId })=>entryId === bridge.entryId)?.name ?? labels.pages;
|
|
1179
|
+
const working = state.prompting || loadingView;
|
|
1180
|
+
const note = state.prompting ? creating ? labels.creating : labels.working : loadingView ? labels.jsonLoading : message?.text ?? (site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
|
|
1181
|
+
status.textContent = note;
|
|
1182
|
+
status.hidden = !note;
|
|
1183
|
+
status.className = `status${working ? ' working' : ''}${!busy && !loadingView && message?.error ? ' error' : ''}`;
|
|
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
|
+
}
|
|
897
1190
|
};
|
|
898
1191
|
const refresh = ()=>render(bridge.getState());
|
|
899
1192
|
const grow = ()=>{
|
|
900
1193
|
input.style.height = 'auto';
|
|
901
1194
|
input.style.height = `${input.scrollHeight}px`;
|
|
902
1195
|
};
|
|
1196
|
+
// Loads the JSON of the selection. Unless `force`, keeps text edited
|
|
1197
|
+
// since the last load, with a note. True once loaded.
|
|
1198
|
+
const loadView = async (force)=>{
|
|
1199
|
+
const uid = bridge.getState().selectedUid;
|
|
1200
|
+
seenUid = uid;
|
|
1201
|
+
if (!force && code.value !== loaded) {
|
|
1202
|
+
message = {
|
|
1203
|
+
text: labels.jsonKept,
|
|
1204
|
+
error: false
|
|
1205
|
+
};
|
|
1206
|
+
refresh();
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
const request = ++viewRequest;
|
|
1210
|
+
loadingView = true;
|
|
1211
|
+
message = null;
|
|
1212
|
+
refresh();
|
|
1213
|
+
try {
|
|
1214
|
+
const view = await bridge.view({
|
|
1215
|
+
uid
|
|
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;
|
|
1229
|
+
refresh();
|
|
1230
|
+
}
|
|
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();
|
|
1244
|
+
};
|
|
1245
|
+
// Where the text stops being JSON, as a line and column.
|
|
1246
|
+
const parseProblem = (text, error)=>{
|
|
1247
|
+
const offset = jsonErrorAt(text);
|
|
1248
|
+
if (offset < 0) {
|
|
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
|
+
};
|
|
1261
|
+
};
|
|
1262
|
+
const applyJson = async ()=>{
|
|
1263
|
+
const text = code.value;
|
|
1264
|
+
if (!text.trim() || bridge.getState().prompting || loadingView) return;
|
|
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
|
+
}
|
|
1288
|
+
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
|
+
refresh();
|
|
1310
|
+
code.focus();
|
|
1311
|
+
};
|
|
903
1312
|
const submit = async ()=>{
|
|
1313
|
+
if (json) return applyJson();
|
|
904
1314
|
const prompt = input.value.trim();
|
|
905
1315
|
if (!prompt || bridge.getState().prompting) return;
|
|
906
1316
|
message = null;
|
|
@@ -910,6 +1320,8 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
910
1320
|
if (!path) return pathInput.focus();
|
|
911
1321
|
// The bridge opens the new page; this shows until it does.
|
|
912
1322
|
await bridge.createPage(path, prompt);
|
|
1323
|
+
pages = null;
|
|
1324
|
+
pagesLoad = null;
|
|
913
1325
|
message = {
|
|
914
1326
|
text: labels.created,
|
|
915
1327
|
error: false
|
|
@@ -924,18 +1336,102 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
924
1336
|
input.value = '';
|
|
925
1337
|
grow();
|
|
926
1338
|
} catch (error) {
|
|
927
|
-
message =
|
|
928
|
-
text: error instanceof Error ? error.message : String(error),
|
|
929
|
-
error: true
|
|
930
|
-
};
|
|
1339
|
+
message = failure(error);
|
|
931
1340
|
}
|
|
932
1341
|
refresh();
|
|
933
1342
|
input.focus();
|
|
934
1343
|
};
|
|
1344
|
+
const fetchPages = ()=>{
|
|
1345
|
+
pagesLoad ??= bridge.pages().then((list)=>{
|
|
1346
|
+
pages = list;
|
|
1347
|
+
pagesError = null;
|
|
1348
|
+
renderPages();
|
|
1349
|
+
refresh();
|
|
1350
|
+
}, (error)=>{
|
|
1351
|
+
pagesLoad = null;
|
|
1352
|
+
pagesError = failure(error).text;
|
|
1353
|
+
renderPages();
|
|
1354
|
+
});
|
|
1355
|
+
return pagesLoad;
|
|
1356
|
+
};
|
|
1357
|
+
const matching = ()=>{
|
|
1358
|
+
const query = search.value.trim().toLowerCase();
|
|
1359
|
+
return (pages ?? []).filter((page)=>!query || page.name.toLowerCase().includes(query) || page.url.toLowerCase().includes(query) || `/${page.path}`.toLowerCase().includes(query));
|
|
1360
|
+
};
|
|
1361
|
+
const renderPages = ()=>{
|
|
1362
|
+
const list = matching();
|
|
1363
|
+
active = Math.max(0, Math.min(active, list.length - 1));
|
|
1364
|
+
optionList.textContent = '';
|
|
1365
|
+
list.forEach((page, index)=>{
|
|
1366
|
+
const current = page.entryId === bridge.entryId;
|
|
1367
|
+
const option = element(doc, 'li', {
|
|
1368
|
+
className: `option${index === active ? ' active' : ''}`,
|
|
1369
|
+
id: `obelism-page-${index}`
|
|
1370
|
+
}, element(doc, 'span', {
|
|
1371
|
+
className: 'option-text'
|
|
1372
|
+
}, element(doc, 'span', {
|
|
1373
|
+
textContent: page.name
|
|
1374
|
+
}), element(doc, 'span', {
|
|
1375
|
+
className: 'option-path',
|
|
1376
|
+
textContent: page.url
|
|
1377
|
+
})));
|
|
1378
|
+
option.setAttribute('role', 'option');
|
|
1379
|
+
option.setAttribute('aria-selected', String(index === active));
|
|
1380
|
+
if (page.status !== 'published') {
|
|
1381
|
+
option.appendChild(element(doc, 'span', {
|
|
1382
|
+
className: 'badge',
|
|
1383
|
+
textContent: page.status === 'draft' ? labels.pageDraft : labels.pageChanged
|
|
1384
|
+
}));
|
|
1385
|
+
}
|
|
1386
|
+
if (current) {
|
|
1387
|
+
option.setAttribute('aria-current', 'page');
|
|
1388
|
+
const check = icon(doc, 'check');
|
|
1389
|
+
check.removeAttribute('aria-hidden');
|
|
1390
|
+
check.setAttribute('role', 'img');
|
|
1391
|
+
check.setAttribute('aria-label', labels.pageCurrent);
|
|
1392
|
+
option.appendChild(check);
|
|
1393
|
+
}
|
|
1394
|
+
option.addEventListener('click', ()=>choose(page));
|
|
1395
|
+
optionList.appendChild(option);
|
|
1396
|
+
});
|
|
1397
|
+
const activeOption = optionList.children[active];
|
|
1398
|
+
if (activeOption) {
|
|
1399
|
+
search.setAttribute('aria-activedescendant', activeOption.id);
|
|
1400
|
+
activeOption.scrollIntoView?.({
|
|
1401
|
+
block: 'nearest'
|
|
1402
|
+
});
|
|
1403
|
+
} else {
|
|
1404
|
+
search.removeAttribute('aria-activedescendant');
|
|
1405
|
+
}
|
|
1406
|
+
const note = pagesError ?? (pages ? '' : labels.pagesLoading);
|
|
1407
|
+
pagesNote.textContent = note || (list.length === 0 ? labels.pagesEmpty : '');
|
|
1408
|
+
pagesNote.hidden = !pagesNote.textContent;
|
|
1409
|
+
pagesNote.className = `empty${pagesError ? ' error' : ''}`;
|
|
1410
|
+
};
|
|
1411
|
+
const setPicking = (next)=>{
|
|
1412
|
+
picking = next;
|
|
1413
|
+
message = null;
|
|
1414
|
+
if (picking) {
|
|
1415
|
+
search.value = '';
|
|
1416
|
+
active = Math.max(0, (pages ?? []).findIndex(({ entryId })=>entryId === bridge.entryId));
|
|
1417
|
+
void fetchPages();
|
|
1418
|
+
renderPages();
|
|
1419
|
+
}
|
|
1420
|
+
refresh();
|
|
1421
|
+
(picking ? search : pageChip).focus();
|
|
1422
|
+
};
|
|
1423
|
+
const choose = (page)=>{
|
|
1424
|
+
if (page.entryId === bridge.entryId) return setPicking(false);
|
|
1425
|
+
// Asks first when changes aren't saved; stays here if told no.
|
|
1426
|
+
bridge.openPage(page);
|
|
1427
|
+
};
|
|
935
1428
|
const setOpen = (next)=>{
|
|
936
1429
|
open = next;
|
|
1430
|
+
if (!open) picking = false;
|
|
937
1431
|
refresh();
|
|
938
1432
|
if (site) bridge.setSelecting(open && !creating);
|
|
1433
|
+
if (open && site) void fetchPages();
|
|
1434
|
+
if (open && json) requestView();
|
|
939
1435
|
};
|
|
940
1436
|
const setCreating = (next)=>{
|
|
941
1437
|
creating = next;
|
|
@@ -954,16 +1450,13 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
954
1450
|
error: false
|
|
955
1451
|
};
|
|
956
1452
|
} catch (error) {
|
|
957
|
-
message =
|
|
958
|
-
text: error instanceof Error ? error.message : String(error),
|
|
959
|
-
error: true
|
|
960
|
-
};
|
|
1453
|
+
message = failure(error);
|
|
961
1454
|
}
|
|
962
1455
|
refresh();
|
|
963
1456
|
};
|
|
964
1457
|
pill.addEventListener('click', ()=>{
|
|
965
1458
|
setOpen(true);
|
|
966
|
-
(creating ? pathInput : input).focus();
|
|
1459
|
+
(creating ? pathInput : json ? code : input).focus();
|
|
967
1460
|
});
|
|
968
1461
|
close.addEventListener('click', ()=>setOpen(false));
|
|
969
1462
|
save.addEventListener('click', ()=>void store('save'));
|
|
@@ -973,6 +1466,13 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
973
1466
|
clearBlock.addEventListener('click', ()=>bridge.select(null));
|
|
974
1467
|
newPage.addEventListener('click', ()=>setCreating(true));
|
|
975
1468
|
cancelNew.addEventListener('click', ()=>setCreating(false));
|
|
1469
|
+
promptMode.addEventListener('click', ()=>setJson(false));
|
|
1470
|
+
jsonMode.addEventListener('click', ()=>setJson(true));
|
|
1471
|
+
reload.addEventListener('click', ()=>{
|
|
1472
|
+
void loadView(true);
|
|
1473
|
+
code.focus();
|
|
1474
|
+
});
|
|
1475
|
+
pageChip.addEventListener('click', ()=>setPicking(!picking));
|
|
976
1476
|
pathInput.addEventListener('input', refresh);
|
|
977
1477
|
pathInput.addEventListener('keydown', (event)=>{
|
|
978
1478
|
if (event.key === 'Enter' && !event.isComposing) {
|
|
@@ -982,9 +1482,30 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
982
1482
|
setOpen(false);
|
|
983
1483
|
}
|
|
984
1484
|
});
|
|
1485
|
+
search.addEventListener('input', ()=>{
|
|
1486
|
+
active = 0;
|
|
1487
|
+
renderPages();
|
|
1488
|
+
});
|
|
1489
|
+
search.addEventListener('keydown', (event)=>{
|
|
1490
|
+
const count = optionList.children.length;
|
|
1491
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1492
|
+
event.preventDefault();
|
|
1493
|
+
if (count === 0) return;
|
|
1494
|
+
active = (active + (event.key === 'ArrowDown' ? 1 : -1) + count) % count;
|
|
1495
|
+
renderPages();
|
|
1496
|
+
} else if (event.key === 'Enter' && !event.isComposing) {
|
|
1497
|
+
event.preventDefault();
|
|
1498
|
+
const page = matching()[active];
|
|
1499
|
+
if (page) choose(page);
|
|
1500
|
+
} else if (event.key === 'Escape') {
|
|
1501
|
+
event.preventDefault();
|
|
1502
|
+
setPicking(false);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
985
1505
|
undo.addEventListener('click', ()=>{
|
|
986
1506
|
message = null;
|
|
987
1507
|
bridge.undo();
|
|
1508
|
+
if (json) requestView();
|
|
988
1509
|
});
|
|
989
1510
|
panel.addEventListener('submit', (event)=>{
|
|
990
1511
|
event.preventDefault();
|
|
@@ -1002,6 +1523,28 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1002
1523
|
setOpen(false);
|
|
1003
1524
|
}
|
|
1004
1525
|
});
|
|
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
|
+
});
|
|
1005
1548
|
const unsubscribe = bridge.on('change', render);
|
|
1006
1549
|
refresh();
|
|
1007
1550
|
doc.body.appendChild(host);
|
|
@@ -1020,6 +1563,9 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1020
1563
|
//
|
|
1021
1564
|
// What it edits is the page's entry — the outermost element marked with
|
|
1022
1565
|
// `editable(entry)` — or, for a selected block, the entry around that block.
|
|
1566
|
+
// Besides prompts it edits JSON (`view`, then `applyJson`: the entry as a
|
|
1567
|
+
// language model sees it, changed by hand), and lists the space's pages to
|
|
1568
|
+
// go to one.
|
|
1023
1569
|
// The base path of the site's handler while signed in to edit, else null.
|
|
1024
1570
|
const readSiteEditing = (doc)=>{
|
|
1025
1571
|
for (const part of doc.cookie.split(';')){
|
|
@@ -1110,6 +1656,19 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1110
1656
|
working.set(entryId, fresh);
|
|
1111
1657
|
return fresh;
|
|
1112
1658
|
};
|
|
1659
|
+
// The entry a prompt or JSON acts on, for the block `uid` (the selected
|
|
1660
|
+
// one when left out).
|
|
1661
|
+
const target = (option)=>{
|
|
1662
|
+
const uid = option === undefined ? state.selectedUid : option;
|
|
1663
|
+
const entryId = entryFor(uid);
|
|
1664
|
+
if (!entryId) {
|
|
1665
|
+
throw new Error('Nothing on this page is marked as an entry: add editable(entry)');
|
|
1666
|
+
}
|
|
1667
|
+
return {
|
|
1668
|
+
uid,
|
|
1669
|
+
entryId
|
|
1670
|
+
};
|
|
1671
|
+
};
|
|
1113
1672
|
const highlight = (uid)=>{
|
|
1114
1673
|
doc.querySelectorAll('[data-cms-selected]').forEach((element)=>{
|
|
1115
1674
|
element.removeAttribute('data-cms-selected');
|
|
@@ -1166,11 +1725,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1166
1725
|
const text = prompt.trim();
|
|
1167
1726
|
if (!text) throw new Error('Say what to change');
|
|
1168
1727
|
if (!has('prompt')) throw new Error('You may not edit content here');
|
|
1169
|
-
const
|
|
1170
|
-
const entryId = entryFor(uid);
|
|
1171
|
-
if (!entryId) {
|
|
1172
|
-
throw new Error('Nothing on this page is marked as an entry: add editable(entry)');
|
|
1173
|
-
}
|
|
1728
|
+
const { uid, entryId } = target(options.uid);
|
|
1174
1729
|
setState({
|
|
1175
1730
|
prompting: true
|
|
1176
1731
|
});
|
|
@@ -1231,6 +1786,88 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1231
1786
|
});
|
|
1232
1787
|
}
|
|
1233
1788
|
},
|
|
1789
|
+
async view (options = {}) {
|
|
1790
|
+
const { uid, entryId } = target(options.uid);
|
|
1791
|
+
const entry = await load(entryId);
|
|
1792
|
+
const { view } = await api(`entries/${encodeURIComponent(entryId)}/view`, {
|
|
1793
|
+
content: entry.content,
|
|
1794
|
+
locale: locale(),
|
|
1795
|
+
uid
|
|
1796
|
+
});
|
|
1797
|
+
return view;
|
|
1798
|
+
},
|
|
1799
|
+
async applyJson (changes, options = {}) {
|
|
1800
|
+
if (!has('prompt')) throw new Error('You may not edit content here');
|
|
1801
|
+
if (!changes || typeof changes !== 'object' || Array.isArray(changes)) {
|
|
1802
|
+
throw new Error('Send the fields to change as an object');
|
|
1803
|
+
}
|
|
1804
|
+
const { uid, entryId } = target(options.uid);
|
|
1805
|
+
setState({
|
|
1806
|
+
prompting: true
|
|
1807
|
+
});
|
|
1808
|
+
try {
|
|
1809
|
+
const entry = await load(entryId);
|
|
1810
|
+
const result = await api(`entries/${encodeURIComponent(entryId)}/apply`, {
|
|
1811
|
+
changes,
|
|
1812
|
+
content: entry.content,
|
|
1813
|
+
locale: locale(),
|
|
1814
|
+
uid
|
|
1815
|
+
});
|
|
1816
|
+
// Nothing to undo when nothing changed.
|
|
1817
|
+
if (!same(result.content, entry.content)) {
|
|
1818
|
+
history.push({
|
|
1819
|
+
entryId,
|
|
1820
|
+
content: entry.content
|
|
1821
|
+
});
|
|
1822
|
+
entry.content = result.content;
|
|
1823
|
+
refreshUnsaved();
|
|
1824
|
+
await render(entryId);
|
|
1825
|
+
}
|
|
1826
|
+
return {
|
|
1827
|
+
changed: result.changed ?? [],
|
|
1828
|
+
...result.view && {
|
|
1829
|
+
view: result.view
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
} finally{
|
|
1833
|
+
setState({
|
|
1834
|
+
prompting: false
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
async pages () {
|
|
1839
|
+
const { pages } = await api('pages');
|
|
1840
|
+
const preferred = [
|
|
1841
|
+
locale(),
|
|
1842
|
+
session?.space.defaultLocale
|
|
1843
|
+
];
|
|
1844
|
+
return pages.flatMap((page)=>{
|
|
1845
|
+
// The page's path in this locale, else the default's, else any.
|
|
1846
|
+
const code = preferred.find((code)=>code !== undefined && page.paths[code] !== undefined) ?? Object.keys(page.paths)[0];
|
|
1847
|
+
const path = code === undefined ? undefined : page.paths[code];
|
|
1848
|
+
if (code === undefined || path === undefined) return [];
|
|
1849
|
+
return [
|
|
1850
|
+
{
|
|
1851
|
+
entryId: page.entryId,
|
|
1852
|
+
name: page.name,
|
|
1853
|
+
contentType: page.contentType,
|
|
1854
|
+
status: page.status,
|
|
1855
|
+
path,
|
|
1856
|
+
url: pagePath(path, code)
|
|
1857
|
+
}
|
|
1858
|
+
];
|
|
1859
|
+
});
|
|
1860
|
+
},
|
|
1861
|
+
openPage (page) {
|
|
1862
|
+
if (state.unsaved && !win.confirm('Leave this page and lose the changes you have not saved?')) {
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
// Asked already: no second question from `beforeunload`.
|
|
1866
|
+
working.clear();
|
|
1867
|
+
history.length = 0;
|
|
1868
|
+
refreshUnsaved();
|
|
1869
|
+
win.location.assign(page.url);
|
|
1870
|
+
},
|
|
1234
1871
|
undo () {
|
|
1235
1872
|
const last = history.pop();
|
|
1236
1873
|
const entry = last && working.get(last.entryId);
|