@panphora/hyper-cms 0.11.6 → 0.12.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 +91 -6
- package/dist/hypercms.min.js +169 -84
- package/package.json +5 -3
- package/src/apply-loop.js +3 -2
- package/src/catalogs/colors.js +293 -0
- package/src/catalogs/fonts.js +126 -0
- package/src/catalogs/index.js +3 -0
- package/src/catalogs/variant-icons.js +24 -0
- package/src/color-control.js +196 -0
- package/src/events.js +25 -2
- package/src/font-control.js +142 -0
- package/src/fonts-link.js +83 -0
- package/src/form-builder.js +85 -3
- package/src/form-rules.js +48 -3
- package/src/hypercms.js +111 -21
- package/src/page-root.js +28 -0
- package/src/refresh.js +4 -1
- package/src/session.js +51 -3
- package/src/targets.js +55 -13
- package/src/templates.js +100 -6
- package/src/theme/pixel-quiet.overrides.css +91 -0
- package/src/theme.generated.css +91 -0
- package/src/toggle.js +80 -296
- package/src/variant-control.js +46 -0
- package/src/views/inline-layer.js +71 -15
- package/src/views/inline.js +87 -17
package/src/toggle.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Floating bottom-right button that toggles the CMS open/closed, so a page
|
|
2
2
|
// owner can reopen the CMS after closing it (the shell's X is the only other
|
|
3
|
-
// affordance, and nothing reopens it). Injected at page load
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// affordance, and nothing reopens it). Injected at page load when the visitor
|
|
4
|
+
// may edit AND the page actually has cms rules, so regular visitors and
|
|
5
|
+
// rules-less pages (hypercms rides in broad presets) never see it. On a ClayJS
|
|
6
|
+
// page "may edit" is clay.canEdit alone: view mode is where the toggle is
|
|
7
|
+
// needed most, since it is the way into editing.
|
|
6
8
|
// Runtime-only chrome: no-save + snapshot-remove keep it out of every save and
|
|
7
9
|
// snapshot (a static no-save element written into the file would be stripped
|
|
8
10
|
// from disk on the first save), save-ignore keeps it out of autosave/undo,
|
|
@@ -16,16 +18,9 @@
|
|
|
16
18
|
// public custom property, so overriding still works. Everything cosmetic is a
|
|
17
19
|
// normal declaration selected by [data-hcms-toggle-host], which an author's own
|
|
18
20
|
// #hcms-toggle rule outranks on specificity.
|
|
19
|
-
//
|
|
20
|
-
// With two views to choose between it is a split button: the main button plus an
|
|
21
|
-
// arrow that opens a menu. The arrow's background and the menu's whole geometry
|
|
22
|
-
// are pinned for exactly the reason the host's position is — an unpinned arrow
|
|
23
|
-
// goes transparent under a page's `button { background: none !important }`, and
|
|
24
|
-
// an unpinned menu drops into the flow and shoves the page. The menu's `display`
|
|
25
|
-
// is pinned too: [hidden] is a UA rule with no specificity, so any page rule
|
|
26
|
-
// matching the element beats it and the menu would sit open forever.
|
|
27
21
|
|
|
28
22
|
import { reensureStyles } from './shell.js'
|
|
23
|
+
import { modeLifecycle } from './session.js'
|
|
29
24
|
|
|
30
25
|
const TOGGLE_ID = 'hcms-toggle'
|
|
31
26
|
const HOST_ATTR = 'data-hcms-toggle-host'
|
|
@@ -36,39 +31,9 @@ const STYLE_ATTR = 'data-hcms-toggle-style'
|
|
|
36
31
|
// sets nothing on <body>, so a body-class label reads "Edit content" over an
|
|
37
32
|
// open inline editor.
|
|
38
33
|
const SESSION_ATTR = 'data-hcms-session'
|
|
39
|
-
const SPLIT_ATTR = 'data-hcms-split'
|
|
40
|
-
|
|
41
|
-
// The preference, one setting across the origin. No version suffix: there are
|
|
42
|
-
// exactly two valid values and the membership check on read is what makes any
|
|
43
|
-
// other value — a stale name, a hand-edited entry, a future view this build does
|
|
44
|
-
// not have — read as first run rather than as a broken session.
|
|
45
|
-
export const VIEW_STORAGE_KEY = 'hcms.view'
|
|
46
|
-
const VIEW_NAMES = ['sidebar', 'inline']
|
|
47
|
-
const VIEW_LABELS = { sidebar: 'In the sidebar', inline: 'On the page' }
|
|
48
34
|
|
|
49
35
|
const SURFACE_VAR = 'var(--hcms-toggle-bg, var(--hcms-toggle-_surface))'
|
|
50
36
|
|
|
51
|
-
export function readStoredView(win) {
|
|
52
|
-
try {
|
|
53
|
-
const value = win && win.localStorage ? win.localStorage.getItem(VIEW_STORAGE_KEY) : null
|
|
54
|
-
return VIEW_NAMES.includes(value) ? value : null
|
|
55
|
-
} catch (_) {
|
|
56
|
-
return null
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Written when someone picks a view, and only once that view is actually up: a
|
|
61
|
-
// mount that throws must not teach the next page load to try the same broken
|
|
62
|
-
// view. localStorage throws outright in a few configurations (Safari private
|
|
63
|
-
// mode, cookies disabled), and a remembered view is a convenience, not a
|
|
64
|
-
// requirement.
|
|
65
|
-
export function writeStoredView(win, view) {
|
|
66
|
-
if (!VIEW_NAMES.includes(view)) return
|
|
67
|
-
try {
|
|
68
|
-
win?.localStorage?.setItem(VIEW_STORAGE_KEY, view)
|
|
69
|
-
} catch (_) {}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
37
|
// The pill's surface is opaque and neutral, picked at inject time from the two
|
|
73
38
|
// values below by whichever has more contrast against the page's own ink. It is
|
|
74
39
|
// not a tint of currentColor: the button floats over a backdrop it cannot see,
|
|
@@ -89,8 +54,7 @@ export const TOGGLE_STYLE = `
|
|
|
89
54
|
[${HOST_ATTR}][data-hcms-surface="dark"] {
|
|
90
55
|
--hcms-toggle-_surface: ${SURFACE_DARK};
|
|
91
56
|
}
|
|
92
|
-
[${HOST_ATTR}] .hcms-toggle__main
|
|
93
|
-
[${HOST_ATTR}] .hcms-toggle__arrow {
|
|
57
|
+
[${HOST_ATTR}] .hcms-toggle__main {
|
|
94
58
|
all: unset;
|
|
95
59
|
box-sizing: border-box;
|
|
96
60
|
display: inline-flex;
|
|
@@ -109,75 +73,22 @@ export const TOGGLE_STYLE = `
|
|
|
109
73
|
cursor: pointer;
|
|
110
74
|
box-shadow: none;
|
|
111
75
|
}
|
|
112
|
-
[${HOST_ATTR}] .hcms-toggle__main:hover
|
|
113
|
-
[${HOST_ATTR}] .hcms-toggle__arrow:hover {
|
|
76
|
+
[${HOST_ATTR}] .hcms-toggle__main:hover {
|
|
114
77
|
border-color: var(--mirk-focus-color, #C7AE93);
|
|
115
78
|
box-shadow: none;
|
|
116
79
|
}
|
|
117
|
-
[${HOST_ATTR}] .hcms-toggle__main:active
|
|
118
|
-
[${HOST_ATTR}] .hcms-toggle__arrow:active {
|
|
80
|
+
[${HOST_ATTR}] .hcms-toggle__main:active {
|
|
119
81
|
border-color: var(--mirk-bevel-br, #E2D4BF) var(--mirk-bevel-tl, #F0E7D8) var(--mirk-bevel-tl, #F0E7D8) var(--mirk-bevel-br, #E2D4BF);
|
|
120
82
|
}
|
|
121
|
-
[${HOST_ATTR}] .hcms-toggle__main:focus-visible
|
|
122
|
-
[${HOST_ATTR}] .hcms-toggle__arrow:focus-visible {
|
|
83
|
+
[${HOST_ATTR}] .hcms-toggle__main:focus-visible {
|
|
123
84
|
outline: 2px solid currentColor;
|
|
124
85
|
outline-offset: -5px;
|
|
125
86
|
}
|
|
126
87
|
[${HOST_ATTR}] .hcms-toggle__close { display: none; }
|
|
127
88
|
[${HOST_ATTR}][${SESSION_ATTR}="open"] .hcms-toggle__open { display: none; }
|
|
128
89
|
[${HOST_ATTR}][${SESSION_ATTR}="open"] .hcms-toggle__close { display: inline; }
|
|
129
|
-
[${HOST_ATTR}][${SPLIT_ATTR}] .hcms-toggle__main {
|
|
130
|
-
border-radius: 0;
|
|
131
|
-
padding-right: 12px;
|
|
132
|
-
}
|
|
133
|
-
[${HOST_ATTR}] .hcms-toggle__arrow {
|
|
134
|
-
gap: 0;
|
|
135
|
-
padding: 0 10px;
|
|
136
|
-
border-radius: 0;
|
|
137
|
-
border-left-width: 0;
|
|
138
|
-
}
|
|
139
|
-
[${HOST_ATTR}] .hcms-toggle__menu {
|
|
140
|
-
all: unset;
|
|
141
|
-
box-sizing: border-box;
|
|
142
|
-
min-width: 180px;
|
|
143
|
-
padding: 4px;
|
|
144
|
-
font-family: inherit;
|
|
145
|
-
font-size: 14px;
|
|
146
|
-
line-height: 1.5;
|
|
147
|
-
color: var(--hcms-toggle-color, currentColor);
|
|
148
|
-
background: ${SURFACE_VAR};
|
|
149
|
-
border: 1px solid var(--mirk-input-border, #D8C8AF);
|
|
150
|
-
border-radius: var(--mirk-radius, 5px);
|
|
151
|
-
box-shadow: 0 14px 34px -14px rgba(0, 0, 0, .45);
|
|
152
|
-
}
|
|
153
|
-
[${HOST_ATTR}] .hcms-toggle__item {
|
|
154
|
-
all: unset;
|
|
155
|
-
box-sizing: border-box;
|
|
156
|
-
display: block;
|
|
157
|
-
width: 100%;
|
|
158
|
-
padding: 9px 10px;
|
|
159
|
-
border-radius: 0;
|
|
160
|
-
cursor: pointer;
|
|
161
|
-
}
|
|
162
|
-
[${HOST_ATTR}] .hcms-toggle__item::before {
|
|
163
|
-
content: "○";
|
|
164
|
-
margin-right: 8px;
|
|
165
|
-
opacity: .55;
|
|
166
|
-
}
|
|
167
|
-
[${HOST_ATTR}] .hcms-toggle__item[aria-checked="true"]::before {
|
|
168
|
-
content: "●";
|
|
169
|
-
opacity: 1;
|
|
170
|
-
}
|
|
171
|
-
[${HOST_ATTR}] .hcms-toggle__item:hover {
|
|
172
|
-
background: color-mix(in srgb, currentColor 12%, transparent);
|
|
173
|
-
}
|
|
174
|
-
[${HOST_ATTR}] .hcms-toggle__item:focus-visible {
|
|
175
|
-
outline: 2px solid currentColor;
|
|
176
|
-
outline-offset: -3px;
|
|
177
|
-
}
|
|
178
90
|
@media (pointer: coarse) {
|
|
179
|
-
[${HOST_ATTR}] .hcms-toggle__main
|
|
180
|
-
[${HOST_ATTR}] .hcms-toggle__arrow { min-height: 44px; }
|
|
91
|
+
[${HOST_ATTR}] .hcms-toggle__main { min-height: 44px; }
|
|
181
92
|
}
|
|
182
93
|
`
|
|
183
94
|
|
|
@@ -385,7 +296,7 @@ function scheduleSurface(host) {
|
|
|
385
296
|
}
|
|
386
297
|
}
|
|
387
298
|
|
|
388
|
-
export function injectToggle({ open, close, isOpen, getTheme = () => null
|
|
299
|
+
export function injectToggle({ open, close, isOpen, getTheme = () => null }, doc = document) {
|
|
389
300
|
const existing = doc.querySelector(`[${HOST_ATTR}]`)
|
|
390
301
|
if (existing) return existing
|
|
391
302
|
|
|
@@ -419,21 +330,7 @@ export function injectToggle({ open, close, isOpen, getTheme = () => null, views
|
|
|
419
330
|
'<span class="hcms-toggle__close">Close editor</span>' +
|
|
420
331
|
'</button>'
|
|
421
332
|
|
|
422
|
-
const win = doc.defaultView
|
|
423
333
|
const main = host.querySelector('.hcms-toggle__main')
|
|
424
|
-
// Fixed order, so the menu does not reorder itself from page to page, and
|
|
425
|
-
// filtered against the views this build actually has.
|
|
426
|
-
const available = VIEW_NAMES.filter((name) => views.includes(name))
|
|
427
|
-
// An arrow offering a choice of one is a menu that can only repeat what the
|
|
428
|
-
// button already does, so with a single view there is no arrow at all.
|
|
429
|
-
const split = available.length > 1
|
|
430
|
-
const arrow = split ? makeArrow(doc) : null
|
|
431
|
-
const menu = split ? makeMenu(doc, available) : null
|
|
432
|
-
if (split) {
|
|
433
|
-
host.setAttribute(SPLIT_ATTR, '')
|
|
434
|
-
host.appendChild(arrow)
|
|
435
|
-
host.appendChild(menu)
|
|
436
|
-
}
|
|
437
334
|
|
|
438
335
|
// The only declarations a page's `* { ... !important }` reset can take from
|
|
439
336
|
// us and leave the control broken rather than merely ugly. Each reads its
|
|
@@ -445,100 +342,40 @@ export function injectToggle({ open, close, isOpen, getTheme = () => null, views
|
|
|
445
342
|
'z-index': 'var(--hcms-toggle-z, 2147482900)',
|
|
446
343
|
})
|
|
447
344
|
pin(main, { background: SURFACE_VAR })
|
|
448
|
-
if (arrow) pin(arrow, { background: SURFACE_VAR })
|
|
449
|
-
if (menu) {
|
|
450
|
-
pin(menu, {
|
|
451
|
-
position: 'absolute',
|
|
452
|
-
right: '0',
|
|
453
|
-
bottom: 'calc(100% + 8px)',
|
|
454
|
-
'z-index': 'var(--hcms-toggle-z, 2147482900)',
|
|
455
|
-
display: 'none',
|
|
456
|
-
})
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const items = () => (menu ? [...menu.querySelectorAll('[role="menuitemradio"]')] : [])
|
|
460
|
-
|
|
461
|
-
function focusEl(el) {
|
|
462
|
-
if (!el || typeof el.focus !== 'function') return
|
|
463
|
-
try { el.focus({ preventScroll: true }) } catch (_) { el.focus() }
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function focusItem(index) {
|
|
467
|
-
const list = items()
|
|
468
|
-
if (!list.length) return
|
|
469
|
-
focusEl(list[(index + list.length) % list.length])
|
|
470
|
-
}
|
|
471
345
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function onOutsidePointer(event) {
|
|
482
|
-
if (host.contains(event.target)) return
|
|
483
|
-
closeMenu()
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function setMenuOpen(show) {
|
|
487
|
-
if (!menu) return
|
|
488
|
-
menu.hidden = !show
|
|
489
|
-
pin(menu, { display: show ? 'block' : 'none' })
|
|
490
|
-
arrow.setAttribute('aria-expanded', String(show))
|
|
491
|
-
if (show) {
|
|
492
|
-
markChecked()
|
|
493
|
-
// Capture, so a page that swallows pointer events on the way up cannot
|
|
494
|
-
// leave the menu standing open over content it no longer belongs to.
|
|
495
|
-
doc.addEventListener('pointerdown', onOutsidePointer, true)
|
|
496
|
-
} else {
|
|
497
|
-
doc.removeEventListener('pointerdown', onOutsidePointer, true)
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function openMenu(index = 0) {
|
|
502
|
-
setMenuOpen(true)
|
|
503
|
-
focusItem(index)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function closeMenu({ focusArrow = false } = {}) {
|
|
507
|
-
if (!menu || menu.hidden) return
|
|
508
|
-
setMenuOpen(false)
|
|
509
|
-
if (focusArrow) focusEl(arrow)
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// A remembered view this page cannot render is ignored HERE and left in
|
|
513
|
-
// storage: the preference belongs to the person, not to the page, and the
|
|
514
|
-
// next page may well be able to honour it.
|
|
515
|
-
function rememberedView() {
|
|
516
|
-
const stored = readStoredView(win)
|
|
517
|
-
return stored && available.includes(stored) ? stored : null
|
|
346
|
+
function requestMode(mode) {
|
|
347
|
+
const win = doc.defaultView
|
|
348
|
+
if (!modeLifecycle(win)) return null
|
|
349
|
+
const setMode = win.clay && win.clay.setMode
|
|
350
|
+
if (typeof setMode !== 'function') return null
|
|
351
|
+
return setMode.call(win.clay, mode)
|
|
518
352
|
}
|
|
519
353
|
|
|
520
|
-
// The
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
writeStoredView(win, name)
|
|
354
|
+
// The mode the page is actually in. clay.mode is a live getter over the
|
|
355
|
+
// committed facts, so it answers the same in view and after a live switch.
|
|
356
|
+
function currentMode() {
|
|
357
|
+
const win = doc.defaultView
|
|
358
|
+
return win && win.clay ? win.clay.mode : null
|
|
526
359
|
}
|
|
527
360
|
|
|
528
361
|
async function mainAction() {
|
|
529
362
|
if (isOpen()) {
|
|
363
|
+
const exit = requestMode('view')
|
|
364
|
+
if (exit) return exit
|
|
530
365
|
close()
|
|
531
366
|
return
|
|
532
367
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
368
|
+
// Already in edit: the CMS is the only thing left to do, and the page is
|
|
369
|
+
// already writable.
|
|
370
|
+
if (currentMode() === 'edit') return open()
|
|
371
|
+
// In view the switch comes first and the CMS only after it committed. A
|
|
372
|
+
// refusal (busy, not-allowed, save-failed) leaves the CMS closed, since a
|
|
373
|
+
// page that never became writable cannot host it.
|
|
374
|
+
const enter = requestMode('edit')
|
|
375
|
+
if (!enter) return open()
|
|
376
|
+
const receipt = await enter
|
|
377
|
+
if (receipt && receipt.ok === false) return
|
|
378
|
+
return open()
|
|
542
379
|
}
|
|
543
380
|
|
|
544
381
|
async function run(action) {
|
|
@@ -550,63 +387,11 @@ export function injectToggle({ open, close, isOpen, getTheme = () => null, views
|
|
|
550
387
|
}
|
|
551
388
|
|
|
552
389
|
// Delegated, so both a real click on the inner button and a programmatic
|
|
553
|
-
// host.click() fire exactly once.
|
|
554
|
-
|
|
555
|
-
host.addEventListener('click', async (event) => {
|
|
556
|
-
if (arrow && arrow.contains(event.target)) return
|
|
557
|
-
if (menu && menu.contains(event.target)) return
|
|
390
|
+
// host.click() fire exactly once.
|
|
391
|
+
host.addEventListener('click', async () => {
|
|
558
392
|
await run(mainAction)
|
|
559
393
|
})
|
|
560
394
|
|
|
561
|
-
if (arrow) {
|
|
562
|
-
arrow.addEventListener('click', (event) => {
|
|
563
|
-
event.preventDefault()
|
|
564
|
-
if (menu.hidden) openMenu()
|
|
565
|
-
else closeMenu({ focusArrow: true })
|
|
566
|
-
})
|
|
567
|
-
arrow.addEventListener('keydown', (event) => {
|
|
568
|
-
if (event.key === 'ArrowUp') {
|
|
569
|
-
event.preventDefault()
|
|
570
|
-
openMenu(-1)
|
|
571
|
-
return
|
|
572
|
-
}
|
|
573
|
-
// preventDefault on Enter and Space stops the button's own activation
|
|
574
|
-
// click, which would otherwise arrive and toggle the menu straight back
|
|
575
|
-
// shut.
|
|
576
|
-
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
|
|
577
|
-
event.preventDefault()
|
|
578
|
-
openMenu(0)
|
|
579
|
-
}
|
|
580
|
-
})
|
|
581
|
-
menu.addEventListener('click', (event) => {
|
|
582
|
-
const item = event.target.closest?.('[role="menuitemradio"]')
|
|
583
|
-
if (!item) return
|
|
584
|
-
event.preventDefault()
|
|
585
|
-
closeMenu()
|
|
586
|
-
run(() => chooseView(item.getAttribute('data-hcms-view')))
|
|
587
|
-
})
|
|
588
|
-
menu.addEventListener('keydown', (event) => {
|
|
589
|
-
const list = items()
|
|
590
|
-
const current = list.indexOf(doc.activeElement)
|
|
591
|
-
switch (event.key) {
|
|
592
|
-
case 'ArrowDown': event.preventDefault(); focusItem(current + 1); break
|
|
593
|
-
case 'ArrowUp': event.preventDefault(); focusItem(current - 1); break
|
|
594
|
-
case 'Home': event.preventDefault(); focusItem(0); break
|
|
595
|
-
case 'End': event.preventDefault(); focusItem(list.length - 1); break
|
|
596
|
-
case 'Enter':
|
|
597
|
-
case ' ':
|
|
598
|
-
event.preventDefault()
|
|
599
|
-
if (current === -1) break
|
|
600
|
-
closeMenu()
|
|
601
|
-
run(() => chooseView(list[current].getAttribute('data-hcms-view')))
|
|
602
|
-
break
|
|
603
|
-
case 'Escape': event.preventDefault(); closeMenu({ focusArrow: true }); break
|
|
604
|
-
// No preventDefault: Tab leaves the menu, it does not cycle inside it.
|
|
605
|
-
case 'Tab': closeMenu(); break
|
|
606
|
-
}
|
|
607
|
-
})
|
|
608
|
-
}
|
|
609
|
-
|
|
610
395
|
// Which label the main button shows, and nothing else decides it. hcms:close
|
|
611
396
|
// fires BEFORE the session is torn down, so isOpen() still answers true inside
|
|
612
397
|
// that handler: the event says what happened and re-asking would leave the
|
|
@@ -628,54 +413,53 @@ export function injectToggle({ open, close, isOpen, getTheme = () => null, views
|
|
|
628
413
|
return host
|
|
629
414
|
}
|
|
630
415
|
|
|
631
|
-
function makeArrow(doc) {
|
|
632
|
-
const arrow = doc.createElement('button')
|
|
633
|
-
arrow.type = 'button'
|
|
634
|
-
arrow.className = 'hcms-toggle__arrow mirk-button'
|
|
635
|
-
arrow.setAttribute('aria-haspopup', 'menu')
|
|
636
|
-
arrow.setAttribute('aria-expanded', 'false')
|
|
637
|
-
// The arrow is a glyph, so unlike the main button it has no visible text to
|
|
638
|
-
// take its accessible name from.
|
|
639
|
-
arrow.setAttribute('aria-label', 'Choose where to edit')
|
|
640
|
-
arrow.textContent = '▾'
|
|
641
|
-
return arrow
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function makeMenu(doc, available) {
|
|
645
|
-
const menu = doc.createElement('div')
|
|
646
|
-
menu.className = 'hcms-toggle__menu'
|
|
647
|
-
menu.setAttribute('role', 'menu')
|
|
648
|
-
menu.setAttribute('aria-label', 'Where to edit')
|
|
649
|
-
menu.hidden = true
|
|
650
|
-
for (const name of available) {
|
|
651
|
-
const item = doc.createElement('button')
|
|
652
|
-
item.type = 'button'
|
|
653
|
-
item.className = 'hcms-toggle__item'
|
|
654
|
-
// menuitemradio, not menuitem: the two are one choice with one answer, and
|
|
655
|
-
// the remembered one is checked.
|
|
656
|
-
item.setAttribute('role', 'menuitemradio')
|
|
657
|
-
item.setAttribute('aria-checked', 'false')
|
|
658
|
-
item.setAttribute('data-hcms-view', name)
|
|
659
|
-
// Roving focus: the menu is entered from the arrow and moved through with
|
|
660
|
-
// the arrow keys, never tabbed into item by item.
|
|
661
|
-
item.tabIndex = -1
|
|
662
|
-
item.textContent = VIEW_LABELS[name]
|
|
663
|
-
menu.appendChild(item)
|
|
664
|
-
}
|
|
665
|
-
return menu
|
|
666
|
-
}
|
|
667
|
-
|
|
668
416
|
// Inject at page load when in edit mode and the page has cms rules. DOM-ready
|
|
669
417
|
// deferral matters when the module evaluates while the document is still
|
|
670
418
|
// parsing (the rules tag or <body> may not exist yet).
|
|
671
419
|
export function maybeInjectToggle(api) {
|
|
672
420
|
if (typeof window === 'undefined' || typeof document === 'undefined') return
|
|
673
|
-
|
|
674
|
-
|
|
421
|
+
// On a ClayJS page the toggle is the way INTO editing, so it is mounted exactly
|
|
422
|
+
// when ClayJS says the person may edit (clay.canEdit), in view as well as in
|
|
423
|
+
// edit. That permission can arrive after load, and ClayJS announces every
|
|
424
|
+
// change of it with clay:capabilitychange on the document: the first change to
|
|
425
|
+
// canEdit === true mounts, and losing it afterwards takes the toggle away.
|
|
426
|
+
// Without ClayJS the boot global keeps deciding, exactly as before.
|
|
427
|
+
const clay = window.clay
|
|
428
|
+
const managed = !!clay && typeof clay.setMode === 'function'
|
|
675
429
|
const run = () => { if (document.body && api.hasRules(document)) injectToggle(api) }
|
|
676
|
-
|
|
677
|
-
document.
|
|
678
|
-
|
|
679
|
-
|
|
430
|
+
const atDomReady = (fn) => {
|
|
431
|
+
if (document.readyState === 'loading') {
|
|
432
|
+
document.addEventListener('DOMContentLoaded', fn, { once: true })
|
|
433
|
+
} else {
|
|
434
|
+
fn()
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (managed) {
|
|
438
|
+
// A page that loads ClayJS's menu opens the CMS from there, so the toggle stays away
|
|
439
|
+
// while that menu is planned, and comes back only if the menu fails to load.
|
|
440
|
+
let menuFailed = false
|
|
441
|
+
const menuOwnsLauncher = () => {
|
|
442
|
+
const plugins = window.clay && window.clay.plugins
|
|
443
|
+
return !menuFailed && Array.isArray(plugins) && plugins.includes('menu')
|
|
444
|
+
}
|
|
445
|
+
const sync = () => {
|
|
446
|
+
if (window.clay && window.clay.canEdit === true && !menuOwnsLauncher()) {
|
|
447
|
+
run()
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
const host = document.querySelector(`[${HOST_ATTR}]`)
|
|
451
|
+
if (host) host.remove()
|
|
452
|
+
}
|
|
453
|
+
atDomReady(sync)
|
|
454
|
+
document.addEventListener('clay:capabilitychange', sync)
|
|
455
|
+
document.addEventListener('clay:plugin-failed', (event) => {
|
|
456
|
+
if (!event.detail || event.detail.plugin !== 'menu') return
|
|
457
|
+
menuFailed = true
|
|
458
|
+
sync()
|
|
459
|
+
})
|
|
460
|
+
return
|
|
680
461
|
}
|
|
462
|
+
const forced = window.__hyperclayEditMode != null ? window.__hyperclayEditMode : null
|
|
463
|
+
if (!detectEditMode({ search: window.location.search, cookie: document.cookie, forced })) return
|
|
464
|
+
atDomReady(run)
|
|
681
465
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { VARIANT_ICONS } from './catalogs/variant-icons.js'
|
|
2
|
+
import { humanize } from './templates.js'
|
|
3
|
+
|
|
4
|
+
// "2:cols-2" → { value: '2', icon: 'cols-2' }; a bare "cards" → its own icon
|
|
5
|
+
// when one exists. Split on the first colon only, so a value never loses text.
|
|
6
|
+
export function parseVariantOptions(tokens) {
|
|
7
|
+
return (tokens || []).map((token) => {
|
|
8
|
+
const colon = token.indexOf(':')
|
|
9
|
+
const value = colon === -1 ? token : token.slice(0, colon)
|
|
10
|
+
const iconName = colon === -1 ? token : token.slice(colon + 1)
|
|
11
|
+
const icon = VARIANT_ICONS[iconName] || null
|
|
12
|
+
return { value, icon, label: icon ? icon.label : humanize(value) }
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Build one native radio per option from the template's prototype row. The
|
|
17
|
+
// current value is prepended when it is not offered, so opening the editor
|
|
18
|
+
// never silently changes the page.
|
|
19
|
+
export function populateVariant(node, { tokens, value, name, doc }) {
|
|
20
|
+
const options = parseVariantOptions(tokens)
|
|
21
|
+
const current = value == null ? '' : String(value)
|
|
22
|
+
if (current !== '' && !options.some((o) => o.value === current)) {
|
|
23
|
+
options.unshift({ value: current, icon: null, label: humanize(current) })
|
|
24
|
+
}
|
|
25
|
+
const proto = node.querySelector('.hcms-variant-option')
|
|
26
|
+
if (!proto || !proto.parentNode) return options.length
|
|
27
|
+
for (const o of options) {
|
|
28
|
+
const row = proto.cloneNode(true)
|
|
29
|
+
const input = row.querySelector('input[type="radio"]')
|
|
30
|
+
if (input) {
|
|
31
|
+
input.value = o.value
|
|
32
|
+
input.name = name
|
|
33
|
+
}
|
|
34
|
+
const iconSlot = row.querySelector('.hcms-variant-icon')
|
|
35
|
+
if (iconSlot) {
|
|
36
|
+
if (o.icon) iconSlot.innerHTML = o.icon.svg
|
|
37
|
+
else iconSlot.remove()
|
|
38
|
+
}
|
|
39
|
+
const label = row.querySelector('.hcms-variant-label')
|
|
40
|
+
if (label) label.textContent = o.label
|
|
41
|
+
row.setAttribute('title', o.label)
|
|
42
|
+
proto.parentNode.insertBefore(row, proto)
|
|
43
|
+
}
|
|
44
|
+
proto.remove()
|
|
45
|
+
return options.length
|
|
46
|
+
}
|