@panphora/hyper-cms 0.4.1 → 0.5.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 +26 -4
- package/dist/hypercms.min.js +1040 -220
- package/package.json +5 -3
- package/src/events.js +221 -14
- package/src/form-builder.js +32 -3
- package/src/form-rules.js +7 -5
- package/src/hypercms-bundle.js +5 -2
- package/src/hypercms.js +249 -4
- package/src/refresh.js +2 -1
- package/src/shell.js +103 -16
- package/src/templates.js +165 -24
- package/src/theme/pixel-quiet.overrides.css +503 -0
- package/src/theme.generated.css +1074 -0
- package/src/vendor/mirk.vendor.js +197 -0
package/src/templates.js
CHANGED
|
@@ -10,11 +10,23 @@ export function humanize(key) {
|
|
|
10
10
|
.replace(/^./, (c) => c.toUpperCase())
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// The default templates emit real mirk-interface markup so the generated form
|
|
14
|
+
// renders in the Pixel Quiet look out of the box. Every data-hcms-* hook
|
|
15
|
+
// (shape, label, field, action, the .hcms-array-items / .hcms-card-fields slots,
|
|
16
|
+
// the .hcms-error inline slots) is preserved exactly, so the engine, events,
|
|
17
|
+
// add/remove/reorder, and inline-error placement bind unchanged. The CSS that
|
|
18
|
+
// dresses these lives in src/theme.generated.css (scoped mirk + pixel-quiet).
|
|
19
|
+
const SORTABLE_GRIP = `<div class="hcms-drag-handle mirk-sortable__grip" aria-hidden="true"><div class="mirk-sortable__dots"><span class="mirk-sortable__dot"></span><span class="mirk-sortable__dot"></span><span class="mirk-sortable__dot"></span><span class="mirk-sortable__dot"></span><span class="mirk-sortable__dot"></span><span class="mirk-sortable__dot"></span><span class="mirk-sortable__dot"></span><span class="mirk-sortable__dot"></span></div></div>`
|
|
20
|
+
|
|
21
|
+
// Crisp-line × (square caps + crispEdges): a plain pixel-sharp close glyph that
|
|
22
|
+
// stays hard-edged inside the small square corner button on object-array cards.
|
|
23
|
+
const CLOSE_ICON = `<svg class="hcms-x" viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path d="M4 4 L12 12 M12 4 L4 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square"></path></svg>`
|
|
24
|
+
|
|
13
25
|
const DEFAULT_TEMPLATES = {
|
|
14
26
|
'@scalar': `
|
|
15
27
|
<label class="hcms-field" data-hcms-shape="scalar">
|
|
16
28
|
<span class="hcms-label" data-hcms-label></span>
|
|
17
|
-
<input class="
|
|
29
|
+
<input class="mirk-input" data-hcms-field />
|
|
18
30
|
<div class="hcms-error" hidden></div>
|
|
19
31
|
</label>
|
|
20
32
|
`,
|
|
@@ -32,56 +44,183 @@ const DEFAULT_TEMPLATES = {
|
|
|
32
44
|
</header>
|
|
33
45
|
<ul class="hcms-array-items"></ul>
|
|
34
46
|
<div class="hcms-error" hidden></div>
|
|
35
|
-
<button type="button" class="hcms-add" data-hcms-action="add">+ Add</button>
|
|
47
|
+
<button type="button" class="hcms-add mirk-button mirk-button--small" data-hcms-action="add"><span class="mirk-button__label">+ Add</span></button>
|
|
36
48
|
</section>
|
|
37
49
|
`,
|
|
38
50
|
'@scalar-array-item': `
|
|
39
51
|
<li class="hcms-array-item" draggable="true">
|
|
40
|
-
<
|
|
41
|
-
<input class="hcms-input" data-hcms-field />
|
|
52
|
+
<input class="mirk-input" data-hcms-field />
|
|
42
53
|
<button type="button" class="hcms-move hcms-move-up hcms-sr-only" data-hcms-action="move-up" aria-label="Move up">↑</button>
|
|
43
54
|
<button type="button" class="hcms-move hcms-move-down hcms-sr-only" data-hcms-action="move-down" aria-label="Move down">↓</button>
|
|
44
|
-
<button type="button" class="hcms-remove" data-hcms-action="remove" aria-label="Remove"
|
|
55
|
+
<button type="button" class="hcms-remove" data-hcms-action="remove" aria-label="Remove">×</button>
|
|
45
56
|
<div class="hcms-error" hidden></div>
|
|
46
57
|
</li>
|
|
47
58
|
`,
|
|
48
59
|
'@object-array': `
|
|
49
|
-
<section class="hcms-array hcms-object-array" data-hcms-shape="object-array">
|
|
60
|
+
<section class="hcms-array hcms-object-array hcms-array--cards" data-hcms-shape="object-array">
|
|
50
61
|
<header class="hcms-array-header">
|
|
51
62
|
<h3 class="hcms-array-title" data-hcms-label></h3>
|
|
52
63
|
</header>
|
|
53
64
|
<div class="hcms-array-items"></div>
|
|
54
65
|
<div class="hcms-error" hidden></div>
|
|
55
|
-
<button type="button" class="hcms-add" data-hcms-action="add">+ Add</button>
|
|
66
|
+
<button type="button" class="hcms-add mirk-button mirk-button--small" data-hcms-action="add"><span class="mirk-button__label">+ Add</span></button>
|
|
56
67
|
</section>
|
|
57
68
|
`,
|
|
58
69
|
'@object-array-item': `
|
|
59
|
-
<article class="hcms-card" draggable="true">
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
<article class="hcms-card mirk-sortable__item" draggable="true">
|
|
71
|
+
${SORTABLE_GRIP}
|
|
72
|
+
<div class="hcms-card-body mirk-sortable__body">
|
|
73
|
+
<div class="hcms-card-fields"></div>
|
|
74
|
+
<div class="hcms-card-controls">
|
|
75
|
+
<button type="button" class="hcms-move hcms-move-up hcms-sr-only" data-hcms-action="move-up" aria-label="Move up">↑</button>
|
|
76
|
+
<button type="button" class="hcms-move hcms-move-down hcms-sr-only" data-hcms-action="move-down" aria-label="Move down">↓</button>
|
|
77
|
+
<button type="button" class="hcms-remove hcms-remove--card" data-hcms-action="remove" aria-label="Remove">${CLOSE_ICON}</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
67
80
|
<div class="hcms-error" hidden></div>
|
|
68
81
|
</article>
|
|
69
82
|
`,
|
|
83
|
+
// Opt-in upload components. Built on the kit's consolidated mirk chrome (all
|
|
84
|
+
// themed in theme.generated.css), with interactive hooks kept on data-hcms-*
|
|
85
|
+
// so the vendored mirk runtime (which keys off .mirk-*__input / .mirk-*__remove)
|
|
86
|
+
// never double-handles a CMS field. The bound value is the leaf's URL: the
|
|
87
|
+
// <a href> for @file, the <img src> for @image. Empty/filled chrome is driven
|
|
88
|
+
// by that attribute in CSS (.hcms-upload* in pixel-quiet.overrides.css), no JS.
|
|
89
|
+
'@file': `
|
|
90
|
+
<div class="hcms-field hcms-upload hcms-upload--file" data-hcms-shape="scalar">
|
|
91
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
92
|
+
<div class="mirk-file mirk-file--compact mirk-file--round">
|
|
93
|
+
<label class="mirk-button mirk-button--round mirk-button--small">
|
|
94
|
+
<input type="file" data-hcms-upload />
|
|
95
|
+
<span class="mirk-button__label">Choose</span>
|
|
96
|
+
</label>
|
|
97
|
+
<a class="mirk-file__name" data-hcms-field></a>
|
|
98
|
+
<button type="button" class="hcms-upload-clear" data-hcms-action="clear-upload" aria-label="Remove file">${CLOSE_ICON}</button>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="hcms-error" hidden></div>
|
|
101
|
+
</div>
|
|
102
|
+
`,
|
|
103
|
+
'@image': `
|
|
104
|
+
<div class="hcms-field hcms-upload hcms-upload--image" data-hcms-shape="scalar">
|
|
105
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
106
|
+
<div class="mirk-image mirk-image--compact mirk-image--rounded">
|
|
107
|
+
<label class="mirk-button mirk-button--small mirk-image__upload">
|
|
108
|
+
<input type="file" accept="image/*" data-hcms-upload />
|
|
109
|
+
<span class="mirk-button__label">Upload image</span>
|
|
110
|
+
</label>
|
|
111
|
+
<figure class="mirk-image__thumb">
|
|
112
|
+
<span class="mirk-image__frame"><img class="mirk-image__preview" data-hcms-field alt="" /></span>
|
|
113
|
+
<button type="button" class="hcms-upload-clear hcms-upload-clear--badge" data-hcms-action="clear-upload" aria-label="Remove image">${CLOSE_ICON}</button>
|
|
114
|
+
</figure>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="hcms-error" hidden></div>
|
|
117
|
+
</div>
|
|
118
|
+
`,
|
|
70
119
|
}
|
|
71
120
|
|
|
72
|
-
|
|
121
|
+
// Shape templates fill-the-gaps on every managed page. The opt-in upload
|
|
122
|
+
// components (@file/@image) are injected on demand the first time a field
|
|
123
|
+
// selects them (the Slice-2 component seam), so pages using neither stay clean.
|
|
124
|
+
const ALWAYS_INJECT_KEYS = [
|
|
125
|
+
'@scalar',
|
|
126
|
+
'@object',
|
|
127
|
+
'@scalar-array',
|
|
128
|
+
'@scalar-array-item',
|
|
129
|
+
'@object-array',
|
|
130
|
+
'@object-array-item',
|
|
131
|
+
]
|
|
73
132
|
|
|
74
133
|
export function injectDefaults(doc) {
|
|
75
134
|
const head = doc.head || doc.documentElement
|
|
76
135
|
if (!head) return
|
|
77
|
-
for (const key of
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
136
|
+
for (const key of ALWAYS_INJECT_KEYS) injectTemplate(doc, head, key)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Inject one opt-in component template (@file/@image) if the page hasn't
|
|
140
|
+
// already defined its own. Called by the component-selection seam the first
|
|
141
|
+
// time a field resolves to an upload component.
|
|
142
|
+
export function injectComponentTemplate(doc, key) {
|
|
143
|
+
if (!DEFAULT_TEMPLATES[key]) return null
|
|
144
|
+
const head = doc && (doc.head || doc.documentElement)
|
|
145
|
+
if (!head) return null
|
|
146
|
+
return injectTemplate(doc, head, key)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Opt-in upload-component selection. A scalar field upgrades from a plain text
|
|
150
|
+
// input to an image-upload widget when its rule ends in @src (uploading is the
|
|
151
|
+
// dominant image-CMS workflow). @href is NOT inferred: editing a link/URL is far
|
|
152
|
+
// more common than uploading a file, so an a@href rule stays a plain URL field.
|
|
153
|
+
// A file upload (and an image override) is opt-in via data-hcms-component on the
|
|
154
|
+
// page element the rule points at — the only branch that reads the page DOM.
|
|
155
|
+
// Suffix is split like the engine (lastIndexOf('@'), see engine/extract.js).
|
|
156
|
+
// Returns a DEFAULT_TEMPLATES key.
|
|
157
|
+
const PROP_TO_COMPONENT = { src: '@image' }
|
|
158
|
+
const UPLOAD_COMPONENT_KEYS = new Set(['@image', '@file'])
|
|
159
|
+
|
|
160
|
+
export function componentForScalarRule(rule, doc) {
|
|
161
|
+
if (typeof rule !== 'string') return '@scalar'
|
|
162
|
+
const at = rule.lastIndexOf('@')
|
|
163
|
+
if (at >= 0) {
|
|
164
|
+
const prop = rule.slice(at + 1)
|
|
165
|
+
if (PROP_TO_COMPONENT[prop]) return PROP_TO_COMPONENT[prop]
|
|
84
166
|
}
|
|
167
|
+
const override = readComponentOverride(rule, at, doc)
|
|
168
|
+
if (override === 'image') return '@image'
|
|
169
|
+
if (override === 'file') return '@file'
|
|
170
|
+
return '@scalar'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readComponentOverride(rule, at, doc) {
|
|
174
|
+
if (!doc || !doc.querySelector) return null
|
|
175
|
+
const selector = at >= 0 ? rule.slice(0, at) : rule
|
|
176
|
+
if (!selector || selector === '.') return null
|
|
177
|
+
let el = null
|
|
178
|
+
try {
|
|
179
|
+
el = doc.querySelector(selector)
|
|
180
|
+
} catch {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
return el && el.getAttribute ? el.getAttribute('data-hcms-component') : null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Pre-inject the @file/@image templates this page's rules actually select, so
|
|
187
|
+
// both deriveFormRules (selectors) and buildForm (DOM) resolve them. Walks the
|
|
188
|
+
// same rule shapes as buildTemplateMap; idempotent and author-template-safe.
|
|
189
|
+
export function injectUploadComponents(doc, pageRules) {
|
|
190
|
+
if (!doc || pageRules == null) return
|
|
191
|
+
walk(pageRules)
|
|
192
|
+
|
|
193
|
+
function walk(rule) {
|
|
194
|
+
const kind = shapeKindOf(rule)
|
|
195
|
+
if (kind === 'scalar') {
|
|
196
|
+
const key = componentForScalarRule(rule, doc)
|
|
197
|
+
if (UPLOAD_COMPONENT_KEYS.has(key)) injectComponentTemplate(doc, key)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
if (kind === 'object') {
|
|
201
|
+
for (const child of Object.values(rule)) walk(child)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
if (kind === 'object-array') {
|
|
205
|
+
const itemShape = rule[1]
|
|
206
|
+
if (itemShape && typeof itemShape === 'object' && !Array.isArray(itemShape)) {
|
|
207
|
+
for (const child of Object.values(itemShape)) walk(child)
|
|
208
|
+
} else {
|
|
209
|
+
walk(itemShape)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function injectTemplate(doc, head, key) {
|
|
216
|
+
const existing = findTemplate(doc, key)
|
|
217
|
+
if (existing) return existing
|
|
218
|
+
const tpl = doc.createElement('template')
|
|
219
|
+
tpl.setAttribute('data-hcms-tpl', key)
|
|
220
|
+
tpl.setAttribute('save-remove', '')
|
|
221
|
+
tpl.innerHTML = DEFAULT_TEMPLATES[key].trim()
|
|
222
|
+
head.appendChild(tpl)
|
|
223
|
+
return tpl
|
|
85
224
|
}
|
|
86
225
|
|
|
87
226
|
export function findTemplate(doc, key) {
|
|
@@ -136,11 +275,13 @@ export function buildTemplateMap(pageRules, doc) {
|
|
|
136
275
|
const pathStr = pathArr.join('.')
|
|
137
276
|
const wildcardKey = pathArr.map((s) => (typeof s === 'number' ? '*' : s)).join('.')
|
|
138
277
|
const shape = shapeKindOf(rule)
|
|
278
|
+
const defaultKey =
|
|
279
|
+
shape === 'scalar' ? componentForScalarRule(rule, doc) : SHAPE_TO_DEFAULT_KEY[shape] || '@scalar'
|
|
139
280
|
|
|
140
281
|
let tpl =
|
|
141
282
|
(pathStr && findTemplate(doc, pathStr)) ||
|
|
142
283
|
(wildcardKey && wildcardKey !== pathStr && findTemplate(doc, wildcardKey)) ||
|
|
143
|
-
findTemplate(doc,
|
|
284
|
+
findTemplate(doc, defaultKey)
|
|
144
285
|
|
|
145
286
|
map.set(pathStr, tpl)
|
|
146
287
|
|