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