@aldi/ralma 1.0.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/LICENSE +21 -0
- package/README.md +67 -0
- package/bin/ralma.js +683 -0
- package/bin/ralma.min.js +175 -0
- package/package.json +81 -0
- package/src/index.js +783 -0
- package/types/index.d.ts +47 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Ractive component definition as accepted by `Ractive.extend()`.
|
|
3
|
+
*
|
|
4
|
+
* @typedef {object} RalmaComponentDefinition
|
|
5
|
+
* @property {string} template
|
|
6
|
+
* @property {boolean} [isolated]
|
|
7
|
+
* @property {() => Record<string, unknown>} [data]
|
|
8
|
+
* @property {() => void} [oninit]
|
|
9
|
+
* @property {Record<string, () => unknown>} [computed]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The subset of the `Ractive` constructor that Ralma needs.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {object} RactiveLike
|
|
16
|
+
* @property {(definition: RalmaComponentDefinition) => unknown} extend
|
|
17
|
+
* @property {Record<string | symbol, unknown>} components
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The subset of a live Ractive component instance that Ralma's hooks use.
|
|
22
|
+
*
|
|
23
|
+
* @typedef {object} RactiveInstance
|
|
24
|
+
* @property {(keypath: string) => any} get
|
|
25
|
+
* @property {(keypath: string, value: unknown) => unknown} set
|
|
26
|
+
* @property {(keypath: string, handler: (value: any) => void, options?: { init?: boolean }) => unknown} observe
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const REGISTRY_MARKER = Symbol.for('ralma.__registered__');
|
|
30
|
+
const UNSAFE_HREF_SCHEME_PATTERN = /^\s*(?:javascript|data|vbscript):/i;
|
|
31
|
+
/**
|
|
32
|
+
* Component names whose `href` runs through {@link sanitizeHref}. A component that renders
|
|
33
|
+
* an `href` and is missing from this set skips sanitizing entirely.
|
|
34
|
+
*/
|
|
35
|
+
export const hrefAwareComponentNames = new Set([
|
|
36
|
+
'button',
|
|
37
|
+
'card-footer-item',
|
|
38
|
+
'dropdown-item',
|
|
39
|
+
'navbar-item',
|
|
40
|
+
'navbar-link',
|
|
41
|
+
'pagination-previous',
|
|
42
|
+
'pagination-next',
|
|
43
|
+
'pagination-link',
|
|
44
|
+
'panel-block',
|
|
45
|
+
'tab-link',
|
|
46
|
+
]);
|
|
47
|
+
const columnFractionModifiers = [
|
|
48
|
+
'three-quarters',
|
|
49
|
+
'two-thirds',
|
|
50
|
+
'half',
|
|
51
|
+
'one-third',
|
|
52
|
+
'one-quarter',
|
|
53
|
+
'four-fifths',
|
|
54
|
+
'three-fifths',
|
|
55
|
+
'two-fifths',
|
|
56
|
+
'one-fifth',
|
|
57
|
+
];
|
|
58
|
+
const columnNumberModifiers = Array.from({ length: 12 }, (_, index) => String(index + 1));
|
|
59
|
+
const columnBreakpoints = ['mobile', 'tablet', 'touch', 'desktop', 'widescreen', 'fullhd'];
|
|
60
|
+
// Cast, not annotation: the nested flatMaps below produce `string[][]`, and TS won't narrow an
|
|
61
|
+
// array literal to a tuple through a callback return.
|
|
62
|
+
const columnModifierClassNames = /** @type {Array<[string, string]>} */ ([
|
|
63
|
+
...columnFractionModifiers.flatMap((modifier) => [
|
|
64
|
+
[modifier, `is-${modifier}`],
|
|
65
|
+
[`offset-${modifier}`, `is-offset-${modifier}`],
|
|
66
|
+
]),
|
|
67
|
+
...columnNumberModifiers.flatMap((modifier) => [
|
|
68
|
+
[modifier, `is-${modifier}`],
|
|
69
|
+
[`offset-${modifier}`, `is-offset-${modifier}`],
|
|
70
|
+
]),
|
|
71
|
+
['narrow', 'is-narrow'],
|
|
72
|
+
...columnBreakpoints.map((breakpoint) => [`narrow-${breakpoint}`, `is-narrow-${breakpoint}`]),
|
|
73
|
+
...columnBreakpoints.flatMap((breakpoint) =>
|
|
74
|
+
columnFractionModifiers.flatMap((modifier) => [
|
|
75
|
+
[`${modifier}-${breakpoint}`, `is-${modifier}-${breakpoint}`],
|
|
76
|
+
[`offset-${modifier}-${breakpoint}`, `is-offset-${modifier}-${breakpoint}`],
|
|
77
|
+
]),
|
|
78
|
+
),
|
|
79
|
+
...columnBreakpoints.flatMap((breakpoint) =>
|
|
80
|
+
columnNumberModifiers.flatMap((modifier) => [
|
|
81
|
+
[`${modifier}-${breakpoint}`, `is-${modifier}-${breakpoint}`],
|
|
82
|
+
[`offset-${modifier}-${breakpoint}`, `is-offset-${modifier}-${breakpoint}`],
|
|
83
|
+
]),
|
|
84
|
+
),
|
|
85
|
+
]);
|
|
86
|
+
const columnsModifierClassNames = /** @type {Array<[string, string]>} */ ([
|
|
87
|
+
['centered', 'is-centered'],
|
|
88
|
+
['gapless', 'is-gapless'],
|
|
89
|
+
['multiline', 'is-multiline'],
|
|
90
|
+
['vcentered', 'is-vcentered'],
|
|
91
|
+
['mobile', 'is-mobile'],
|
|
92
|
+
['desktop', 'is-desktop'],
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Builds the `{{#flag}}is-flag{{/}}` chain Bulma modifiers take inside a `class` attribute.
|
|
97
|
+
* Property name and class suffix are always the same, which is what makes this mechanical.
|
|
98
|
+
*
|
|
99
|
+
* @param {...string} modifiers
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function modifierSections(...modifiers) {
|
|
103
|
+
return modifiers.map((modifier) => `{{#${modifier}}}is-${modifier}{{/}}`).join(' ');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Bulma's colors, in the order the templates have always emitted them. Order is part of the rendered
|
|
107
|
+
// output, so these lists are not interchangeable even where they hold the same names.
|
|
108
|
+
const colorModifiers = [
|
|
109
|
+
'white',
|
|
110
|
+
'light',
|
|
111
|
+
'dark',
|
|
112
|
+
'black',
|
|
113
|
+
'text',
|
|
114
|
+
'primary',
|
|
115
|
+
'link',
|
|
116
|
+
'info',
|
|
117
|
+
'success',
|
|
118
|
+
'warning',
|
|
119
|
+
'danger',
|
|
120
|
+
];
|
|
121
|
+
// Panels and messages emit a shorter list, in a different order, with `dark` last.
|
|
122
|
+
const panelColorModifiers = ['primary', 'link', 'info', 'success', 'warning', 'danger', 'dark'];
|
|
123
|
+
const sizeModifiers = ['small', 'medium', 'large'];
|
|
124
|
+
const buttonStateModifiers = [
|
|
125
|
+
'outlined',
|
|
126
|
+
'inverted',
|
|
127
|
+
'rounded',
|
|
128
|
+
'hovered',
|
|
129
|
+
'focused',
|
|
130
|
+
'loading',
|
|
131
|
+
'static',
|
|
132
|
+
'active',
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const buttonModifiers = modifierSections(
|
|
136
|
+
...colorModifiers,
|
|
137
|
+
...sizeModifiers,
|
|
138
|
+
...buttonStateModifiers,
|
|
139
|
+
);
|
|
140
|
+
const navbarModifiers = modifierSections(
|
|
141
|
+
'transparent',
|
|
142
|
+
'spaced',
|
|
143
|
+
'fixed-top',
|
|
144
|
+
'fixed-bottom',
|
|
145
|
+
...colorModifiers,
|
|
146
|
+
);
|
|
147
|
+
const messageModifiers = modifierSections(...panelColorModifiers, ...sizeModifiers);
|
|
148
|
+
const panelModifiers = modifierSections(...panelColorModifiers);
|
|
149
|
+
|
|
150
|
+
const buttonTemplate = `
|
|
151
|
+
{{#if safeHref}}
|
|
152
|
+
<a
|
|
153
|
+
class="button ${buttonModifiers} {{class}}"
|
|
154
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
155
|
+
{{#if title}}title="{{title}}"{{/if}}
|
|
156
|
+
{{#if disabled}}aria-disabled="true" tabindex="-1"{{/if}}
|
|
157
|
+
{{#if dropdown}}aria-haspopup="true"{{/if}}
|
|
158
|
+
{{#if ariaControls}}aria-controls="{{ariaControls}}"{{/if}}
|
|
159
|
+
{{#if safeHref}}{{#if disabled}}{{else}}href="{{safeHref}}"{{/if}}{{/if}}
|
|
160
|
+
>{{yield}}</a>
|
|
161
|
+
{{else}}
|
|
162
|
+
<button
|
|
163
|
+
type="{{buttonType}}"
|
|
164
|
+
class="button ${buttonModifiers} {{class}}"
|
|
165
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
166
|
+
{{#if title}}title="{{title}}"{{/if}}
|
|
167
|
+
{{#if disabled}}disabled{{/if}}
|
|
168
|
+
{{#if dropdown}}aria-haspopup="true"{{/if}}
|
|
169
|
+
{{#if ariaControls}}aria-controls="{{ariaControls}}"{{/if}}
|
|
170
|
+
>{{yield}}</button>
|
|
171
|
+
{{/if}}
|
|
172
|
+
`.trim();
|
|
173
|
+
|
|
174
|
+
const componentDefinitions = {
|
|
175
|
+
button: {
|
|
176
|
+
isolated: true,
|
|
177
|
+
data() {
|
|
178
|
+
return {
|
|
179
|
+
buttonType: 'button',
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
template: buttonTemplate,
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// Breadcrumb
|
|
186
|
+
breadcrumb: {
|
|
187
|
+
isolated: true,
|
|
188
|
+
template: `
|
|
189
|
+
<nav
|
|
190
|
+
class="breadcrumb {{#centered}}is-centered{{/}} {{#right}}is-right{{/}} {{#small}}is-small{{/}} {{#medium}}is-medium{{/}} {{#large}}is-large{{/}} {{#arrow}}has-arrow-separator{{/}} {{#bullet}}has-bullet-separator{{/}} {{#dot}}has-dot-separator{{/}} {{#succeeds}}has-succeeds-separator{{/}} {{class}}"
|
|
191
|
+
aria-label="{{#if ariaLabel}}{{ariaLabel}}{{else}}breadcrumbs{{/if}}"
|
|
192
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
193
|
+
>
|
|
194
|
+
<ul>{{yield}}</ul>
|
|
195
|
+
</nav>
|
|
196
|
+
`.trim(),
|
|
197
|
+
},
|
|
198
|
+
'breadcrumb-item': {
|
|
199
|
+
isolated: true,
|
|
200
|
+
template: `
|
|
201
|
+
<li class="{{#active}}is-active{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
|
202
|
+
{{yield}}
|
|
203
|
+
</li>
|
|
204
|
+
`.trim(),
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// Card
|
|
208
|
+
card: {
|
|
209
|
+
isolated: true,
|
|
210
|
+
template: `<div class="card {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
211
|
+
},
|
|
212
|
+
'card-header': {
|
|
213
|
+
isolated: true,
|
|
214
|
+
template: `<header class="card-header {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</header>`,
|
|
215
|
+
},
|
|
216
|
+
'card-header-title': {
|
|
217
|
+
isolated: true,
|
|
218
|
+
template: `<p class="card-header-title {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</p>`,
|
|
219
|
+
},
|
|
220
|
+
'card-header-icon': {
|
|
221
|
+
isolated: true,
|
|
222
|
+
template: `
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
class="card-header-icon {{class}}"
|
|
226
|
+
aria-label="{{#if ariaLabel}}{{ariaLabel}}{{else}}more options{{/if}}"
|
|
227
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
228
|
+
{{#if onclick}}on-click="{{onclick}}"{{/if}}
|
|
229
|
+
>{{yield}}</button>
|
|
230
|
+
`.trim(),
|
|
231
|
+
},
|
|
232
|
+
'card-image': {
|
|
233
|
+
isolated: true,
|
|
234
|
+
template: `<div class="card-image {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
235
|
+
},
|
|
236
|
+
'card-content': {
|
|
237
|
+
isolated: true,
|
|
238
|
+
template: `<div class="card-content {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
239
|
+
},
|
|
240
|
+
'card-footer': {
|
|
241
|
+
isolated: true,
|
|
242
|
+
template: `<footer class="card-footer {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</footer>`,
|
|
243
|
+
},
|
|
244
|
+
'card-footer-item': {
|
|
245
|
+
isolated: true,
|
|
246
|
+
template: `
|
|
247
|
+
{{#if safeHref}}
|
|
248
|
+
<a class="card-footer-item {{class}}" href="{{safeHref}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</a>
|
|
249
|
+
{{else}}
|
|
250
|
+
<div class="card-footer-item {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>
|
|
251
|
+
{{/if}}
|
|
252
|
+
`.trim(),
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// Dropdowns (beta)
|
|
256
|
+
dropdown: {
|
|
257
|
+
isolated: true,
|
|
258
|
+
template:
|
|
259
|
+
"<div class='dropdown {{#active}}is-active{{/}} {{class}}' {{#if id}} id='{{id}}' {{/if}}>{{yield}}</div>",
|
|
260
|
+
},
|
|
261
|
+
'dropdown-trigger': {
|
|
262
|
+
isolated: true,
|
|
263
|
+
template:
|
|
264
|
+
"<div class='dropdown-trigger {{class}}' {{#if id}} id='{{id}}' {{/if}}>{{yield}}</div>",
|
|
265
|
+
},
|
|
266
|
+
'dropdown-button': {
|
|
267
|
+
isolated: true,
|
|
268
|
+
template:
|
|
269
|
+
"<button type='button' class='button {{class}}' {{#if id}} id='{{id}}' {{/if}} aria-haspopup='true' {{#if ariaControls}}aria-controls='{{ariaControls}}'{{/if}}><span>{{yield}}</span><span class='icon is-small'><i class='fas fa-angle-down' aria-hidden='true'></i></span></button>",
|
|
270
|
+
},
|
|
271
|
+
'dropdown-item': {
|
|
272
|
+
isolated: true,
|
|
273
|
+
// Without an href this must be a <button>: an <a> with no href is not keyboard-focusable, and
|
|
274
|
+
// this element carries an on-click. Bulma styles both a.dropdown-item and button.dropdown-item.
|
|
275
|
+
template: `
|
|
276
|
+
{{#if safeHref}}
|
|
277
|
+
<a
|
|
278
|
+
class="dropdown-item {{class}} {{#active}}is-active{{/}}"
|
|
279
|
+
href="{{safeHref}}"
|
|
280
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
281
|
+
{{#if onclick}}on-click="{{onclick}}"{{/if}}
|
|
282
|
+
>{{yield}}</a>
|
|
283
|
+
{{else}}
|
|
284
|
+
<button
|
|
285
|
+
type="button"
|
|
286
|
+
class="dropdown-item {{class}} {{#active}}is-active{{/}}"
|
|
287
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
288
|
+
{{#if onclick}}on-click="{{onclick}}"{{/if}}
|
|
289
|
+
>{{yield}}</button>
|
|
290
|
+
{{/if}}
|
|
291
|
+
`.trim(),
|
|
292
|
+
},
|
|
293
|
+
'dropdown-menu': {
|
|
294
|
+
isolated: true,
|
|
295
|
+
// No default role. A real ARIA menu needs role="menuitem" on every child and arrow-key focus
|
|
296
|
+
// management, and Ralma ships no behavior — an announced "menu" that doesn't respond to arrow
|
|
297
|
+
// keys is worse for assistive tech than a plain container. Pass `role` to opt in.
|
|
298
|
+
template:
|
|
299
|
+
"<div class='dropdown-menu {{class}}' {{#if id}} id='{{id}}' {{/if}} {{#if role}}role='{{role}}'{{/if}}>{{yield}}</div>",
|
|
300
|
+
},
|
|
301
|
+
'dropdown-content': {
|
|
302
|
+
isolated: true,
|
|
303
|
+
template:
|
|
304
|
+
"<div class='dropdown-content {{class}}' {{#if id}} id='{{id}}' {{/if}}>{{yield}}</div>",
|
|
305
|
+
},
|
|
306
|
+
'dropdown-divider': {
|
|
307
|
+
isolated: true,
|
|
308
|
+
template: "<hr class='dropdown-divider {{class}}' {{#if id}} id='{{id}}' {{/if}}>",
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// Menu
|
|
312
|
+
menu: {
|
|
313
|
+
isolated: true,
|
|
314
|
+
template: `<aside class="menu {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</aside>`,
|
|
315
|
+
},
|
|
316
|
+
'menu-label': {
|
|
317
|
+
isolated: true,
|
|
318
|
+
template: `<p class="menu-label {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</p>`,
|
|
319
|
+
},
|
|
320
|
+
'menu-list': {
|
|
321
|
+
isolated: true,
|
|
322
|
+
template: `<ul class="menu-list {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</ul>`,
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// Message
|
|
326
|
+
message: {
|
|
327
|
+
isolated: true,
|
|
328
|
+
template: `
|
|
329
|
+
<article class="message ${messageModifiers} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
|
330
|
+
{{yield}}
|
|
331
|
+
</article>
|
|
332
|
+
`.trim(),
|
|
333
|
+
},
|
|
334
|
+
'message-header': {
|
|
335
|
+
isolated: true,
|
|
336
|
+
template: `<div class="message-header {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
337
|
+
},
|
|
338
|
+
'message-body': {
|
|
339
|
+
isolated: true,
|
|
340
|
+
template: `<div class="message-body {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// Modal
|
|
344
|
+
modal: {
|
|
345
|
+
isolated: true,
|
|
346
|
+
template: `<div class="modal {{#active}}is-active{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
347
|
+
},
|
|
348
|
+
'modal-background': {
|
|
349
|
+
isolated: true,
|
|
350
|
+
template: `<div class="modal-background {{class}}" {{#if id}}id="{{id}}"{{/if}}></div>`,
|
|
351
|
+
},
|
|
352
|
+
'modal-content': {
|
|
353
|
+
isolated: true,
|
|
354
|
+
template: `<div class="modal-content {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
355
|
+
},
|
|
356
|
+
'modal-close': {
|
|
357
|
+
isolated: true,
|
|
358
|
+
template: `
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
class="modal-close {{#small}}is-small{{/}} {{#medium}}is-medium{{/}} {{#large}}is-large{{/}} {{class}}"
|
|
362
|
+
aria-label="{{#if ariaLabel}}{{ariaLabel}}{{else}}close{{/if}}"
|
|
363
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
364
|
+
{{#if onclick}}on-click="{{onclick}}"{{/if}}
|
|
365
|
+
></button>
|
|
366
|
+
`.trim(),
|
|
367
|
+
},
|
|
368
|
+
'modal-card': {
|
|
369
|
+
isolated: true,
|
|
370
|
+
template: `<div class="modal-card {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
371
|
+
},
|
|
372
|
+
'modal-card-head': {
|
|
373
|
+
isolated: true,
|
|
374
|
+
template: `<header class="modal-card-head {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</header>`,
|
|
375
|
+
},
|
|
376
|
+
'modal-card-title': {
|
|
377
|
+
isolated: true,
|
|
378
|
+
template: `<p class="modal-card-title {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</p>`,
|
|
379
|
+
},
|
|
380
|
+
'modal-card-body': {
|
|
381
|
+
isolated: true,
|
|
382
|
+
template: `<section class="modal-card-body {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</section>`,
|
|
383
|
+
},
|
|
384
|
+
'modal-card-foot': {
|
|
385
|
+
isolated: true,
|
|
386
|
+
template: `<footer class="modal-card-foot {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</footer>`,
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
// Navbar
|
|
390
|
+
navbar: {
|
|
391
|
+
isolated: true,
|
|
392
|
+
template: `
|
|
393
|
+
<nav
|
|
394
|
+
class="navbar ${navbarModifiers} {{class}}"
|
|
395
|
+
role="{{#if role}}{{role}}{{else}}navigation{{/if}}"
|
|
396
|
+
aria-label="{{#if ariaLabel}}{{ariaLabel}}{{else}}main navigation{{/if}}"
|
|
397
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
398
|
+
>
|
|
399
|
+
{{yield}}
|
|
400
|
+
</nav>
|
|
401
|
+
`.trim(),
|
|
402
|
+
},
|
|
403
|
+
'navbar-brand': {
|
|
404
|
+
isolated: true,
|
|
405
|
+
template: `<div class="navbar-brand {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
406
|
+
},
|
|
407
|
+
'navbar-burger': {
|
|
408
|
+
isolated: true,
|
|
409
|
+
template: `
|
|
410
|
+
<button
|
|
411
|
+
type="button"
|
|
412
|
+
class="navbar-burger {{#active}}is-active{{/}} {{class}}"
|
|
413
|
+
aria-label="{{#if ariaLabel}}{{ariaLabel}}{{else}}menu{{/if}}"
|
|
414
|
+
aria-expanded="{{#if expanded}}true{{else}}false{{/if}}"
|
|
415
|
+
{{#if dataTarget}}data-target="{{dataTarget}}"{{/if}}
|
|
416
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
417
|
+
{{#if onclick}}on-click="{{onclick}}"{{/if}}
|
|
418
|
+
>{{yield}}</button>
|
|
419
|
+
`.trim(),
|
|
420
|
+
},
|
|
421
|
+
'navbar-menu': {
|
|
422
|
+
isolated: true,
|
|
423
|
+
template: `<div class="navbar-menu {{#active}}is-active{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
424
|
+
},
|
|
425
|
+
'navbar-start': {
|
|
426
|
+
isolated: true,
|
|
427
|
+
template: `<div class="navbar-start {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
428
|
+
},
|
|
429
|
+
'navbar-end': {
|
|
430
|
+
isolated: true,
|
|
431
|
+
template: `<div class="navbar-end {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>`,
|
|
432
|
+
},
|
|
433
|
+
'navbar-item': {
|
|
434
|
+
isolated: true,
|
|
435
|
+
template: `
|
|
436
|
+
{{#if safeHref}}
|
|
437
|
+
<a
|
|
438
|
+
class="navbar-item {{#active}}is-active{{/}} {{class}}"
|
|
439
|
+
href="{{safeHref}}"
|
|
440
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
441
|
+
{{#if onclick}}on-click="{{onclick}}"{{/if}}
|
|
442
|
+
>{{yield}}</a>
|
|
443
|
+
{{else}}
|
|
444
|
+
<div class="navbar-item {{#active}}is-active{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>
|
|
445
|
+
{{/if}}
|
|
446
|
+
`.trim(),
|
|
447
|
+
},
|
|
448
|
+
'navbar-item-dropdown': {
|
|
449
|
+
isolated: true,
|
|
450
|
+
template: `
|
|
451
|
+
<div class="navbar-item has-dropdown {{#active}}is-active{{/}} {{#hoverable}}is-hoverable{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
|
452
|
+
{{yield}}
|
|
453
|
+
</div>
|
|
454
|
+
`.trim(),
|
|
455
|
+
},
|
|
456
|
+
'navbar-link': {
|
|
457
|
+
isolated: true,
|
|
458
|
+
template: `
|
|
459
|
+
{{#if safeHref}}
|
|
460
|
+
<a class="navbar-link {{#active}}is-active{{/}} {{class}}" href="{{safeHref}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</a>
|
|
461
|
+
{{else}}
|
|
462
|
+
<button type="button" class="navbar-link {{#active}}is-active{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</button>
|
|
463
|
+
{{/if}}
|
|
464
|
+
`.trim(),
|
|
465
|
+
},
|
|
466
|
+
'navbar-dropdown': {
|
|
467
|
+
isolated: true,
|
|
468
|
+
template: `
|
|
469
|
+
<div class="navbar-dropdown {{#right}}is-right{{/}} {{#boxed}}is-boxed{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
|
470
|
+
{{yield}}
|
|
471
|
+
</div>
|
|
472
|
+
`.trim(),
|
|
473
|
+
},
|
|
474
|
+
'navbar-divider': {
|
|
475
|
+
isolated: true,
|
|
476
|
+
template: `<hr class="navbar-divider {{class}}" {{#if id}}id="{{id}}"{{/if}} />`,
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
// Pagination
|
|
480
|
+
pagination: {
|
|
481
|
+
isolated: true,
|
|
482
|
+
template: `
|
|
483
|
+
<nav
|
|
484
|
+
class="pagination {{#centered}}is-centered{{/}} {{#right}}is-right{{/}} {{#rounded}}is-rounded{{/}} {{#small}}is-small{{/}} {{#medium}}is-medium{{/}} {{#large}}is-large{{/}} {{class}}"
|
|
485
|
+
role="{{#if role}}{{role}}{{else}}navigation{{/if}}"
|
|
486
|
+
aria-label="{{#if ariaLabel}}{{ariaLabel}}{{else}}pagination{{/if}}"
|
|
487
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
488
|
+
>
|
|
489
|
+
{{yield}}
|
|
490
|
+
</nav>
|
|
491
|
+
`.trim(),
|
|
492
|
+
},
|
|
493
|
+
'pagination-previous': {
|
|
494
|
+
isolated: true,
|
|
495
|
+
template: `
|
|
496
|
+
<a
|
|
497
|
+
class="pagination-previous {{class}}"
|
|
498
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
499
|
+
{{#if title}}title="{{title}}"{{/if}}
|
|
500
|
+
{{#if disabled}}aria-disabled="true" tabindex="-1"{{/if}}
|
|
501
|
+
{{#if safeHref}}{{#if disabled}}{{else}}href="{{safeHref}}"{{/if}}{{/if}}
|
|
502
|
+
>{{yield}}</a>
|
|
503
|
+
`.trim(),
|
|
504
|
+
},
|
|
505
|
+
'pagination-next': {
|
|
506
|
+
isolated: true,
|
|
507
|
+
template: `
|
|
508
|
+
<a
|
|
509
|
+
class="pagination-next {{class}}"
|
|
510
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
511
|
+
{{#if title}}title="{{title}}"{{/if}}
|
|
512
|
+
{{#if disabled}}aria-disabled="true" tabindex="-1"{{/if}}
|
|
513
|
+
{{#if safeHref}}{{#if disabled}}{{else}}href="{{safeHref}}"{{/if}}{{/if}}
|
|
514
|
+
>{{yield}}</a>
|
|
515
|
+
`.trim(),
|
|
516
|
+
},
|
|
517
|
+
'pagination-list': {
|
|
518
|
+
isolated: true,
|
|
519
|
+
template: `<ul class="pagination-list {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</ul>`,
|
|
520
|
+
},
|
|
521
|
+
'pagination-link': {
|
|
522
|
+
isolated: true,
|
|
523
|
+
template: `
|
|
524
|
+
<a
|
|
525
|
+
class="pagination-link {{#current}}is-current{{/}} {{class}}"
|
|
526
|
+
{{#if ariaLabel}}aria-label="{{ariaLabel}}"{{else}}{{#if page}}aria-label="Goto page {{page}}"{{/if}}{{/if}}
|
|
527
|
+
{{#if current}}aria-current="page"{{/if}}
|
|
528
|
+
{{#if id}}id="{{id}}"{{/if}}
|
|
529
|
+
{{#if safeHref}}href="{{safeHref}}"{{/if}}
|
|
530
|
+
>{{yield}}</a>
|
|
531
|
+
`.trim(),
|
|
532
|
+
},
|
|
533
|
+
'pagination-ellipsis': {
|
|
534
|
+
isolated: true,
|
|
535
|
+
template: `<span class="pagination-ellipsis {{class}}" {{#if id}}id="{{id}}"{{/if}}>…</span>`,
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
// Panel
|
|
539
|
+
panel: {
|
|
540
|
+
isolated: true,
|
|
541
|
+
template: `
|
|
542
|
+
<nav class="panel ${panelModifiers} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
|
543
|
+
{{yield}}
|
|
544
|
+
</nav>
|
|
545
|
+
`.trim(),
|
|
546
|
+
},
|
|
547
|
+
'panel-heading': {
|
|
548
|
+
isolated: true,
|
|
549
|
+
template: `<p class="panel-heading {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</p>`,
|
|
550
|
+
},
|
|
551
|
+
'panel-tabs': {
|
|
552
|
+
isolated: true,
|
|
553
|
+
template: `<p class="panel-tabs {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</p>`,
|
|
554
|
+
},
|
|
555
|
+
'panel-block': {
|
|
556
|
+
isolated: true,
|
|
557
|
+
template: `
|
|
558
|
+
{{#if safeHref}}
|
|
559
|
+
<a class="panel-block {{#active}}is-active{{/}} {{class}}" href="{{safeHref}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</a>
|
|
560
|
+
{{else}}
|
|
561
|
+
<div class="panel-block {{#active}}is-active{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</div>
|
|
562
|
+
{{/if}}
|
|
563
|
+
`.trim(),
|
|
564
|
+
},
|
|
565
|
+
'panel-icon': {
|
|
566
|
+
isolated: true,
|
|
567
|
+
template: `<span class="panel-icon {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</span>`,
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
// Tabs
|
|
571
|
+
tabs: {
|
|
572
|
+
isolated: true,
|
|
573
|
+
template: `
|
|
574
|
+
<div class="tabs {{#centered}}is-centered{{/}} {{#right}}is-right{{/}} {{#boxed}}is-boxed{{/}} {{#toggle}}is-toggle{{/}} {{#toggle-rounded}}is-toggle-rounded{{/}} {{#fullwidth}}is-fullwidth{{/}} {{#small}}is-small{{/}} {{#medium}}is-medium{{/}} {{#large}}is-large{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
|
575
|
+
<ul>{{yield}}</ul>
|
|
576
|
+
</div>
|
|
577
|
+
`.trim(),
|
|
578
|
+
},
|
|
579
|
+
tab: {
|
|
580
|
+
isolated: true,
|
|
581
|
+
template: `<li class="{{#active}}is-active{{/}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</li>`,
|
|
582
|
+
},
|
|
583
|
+
'tab-link': {
|
|
584
|
+
isolated: true,
|
|
585
|
+
template: `
|
|
586
|
+
{{#if safeHref}}
|
|
587
|
+
<a href="{{safeHref}}" class="{{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</a>
|
|
588
|
+
{{else}}
|
|
589
|
+
<button type="button" class="{{class}}" {{#if id}}id="{{id}}"{{/if}}>{{yield}}</button>
|
|
590
|
+
{{/if}}
|
|
591
|
+
`.trim(),
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
content: {
|
|
595
|
+
isolated: true,
|
|
596
|
+
template:
|
|
597
|
+
"<div class='content {{#small}}is-small{{/}} {{#medium}}is-medium{{/}} {{#large}}is-large{{/}} {{class}}' {{#if id}} id='{{id}}' {{/if}}>{{yield}}</div>",
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
column: {
|
|
601
|
+
isolated: true,
|
|
602
|
+
computed: {
|
|
603
|
+
/** @this {RactiveInstance} */
|
|
604
|
+
columnModifiers() {
|
|
605
|
+
return buildModifierClassNames(this, columnModifierClassNames);
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
template:
|
|
609
|
+
"<div class='column {{class}} {{columnModifiers}}' {{#if id}} id='{{id}}' {{/if}}>{{yield}}</div>",
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
columns: {
|
|
613
|
+
isolated: true,
|
|
614
|
+
computed: {
|
|
615
|
+
/** @this {RactiveInstance} */
|
|
616
|
+
columnsModifiers() {
|
|
617
|
+
return buildModifierClassNames(this, columnsModifierClassNames);
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
template:
|
|
621
|
+
"<div class='columns {{class}} {{columnsModifiers}}' {{#if id}} id='{{id}}' {{/if}}>{{yield}}</div>",
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
for (const definition of Object.values(componentDefinitions)) Object.freeze(definition);
|
|
626
|
+
Object.freeze(componentDefinitions);
|
|
627
|
+
|
|
628
|
+
const componentEntries = /** @type {Array<[string, RalmaComponentDefinition]>} */ (
|
|
629
|
+
Object.entries(componentDefinitions)
|
|
630
|
+
);
|
|
631
|
+
/** @type {Array<[string, RalmaComponentDefinition]>} */
|
|
632
|
+
const runtimeComponentEntries = componentEntries.map(([name, definition]) => [
|
|
633
|
+
name,
|
|
634
|
+
hrefAwareComponentNames.has(name) ? withSafeHref(definition) : definition,
|
|
635
|
+
]);
|
|
636
|
+
export const componentNames = Object.freeze(componentEntries.map(([name]) => name));
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Removes C0 control characters, space, and DEL.
|
|
640
|
+
*
|
|
641
|
+
* @param {string} value
|
|
642
|
+
* @returns {string}
|
|
643
|
+
*/
|
|
644
|
+
function stripControlCharacters(value) {
|
|
645
|
+
return Array.from(value)
|
|
646
|
+
.filter((character) => {
|
|
647
|
+
const code = character.charCodeAt(0);
|
|
648
|
+
return code > 0x20 && code !== 0x7f;
|
|
649
|
+
})
|
|
650
|
+
.join('');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Returns the href if it is safe to render, or `null` if it must be dropped.
|
|
655
|
+
*
|
|
656
|
+
* @param {unknown} href
|
|
657
|
+
* @returns {string | null}
|
|
658
|
+
*/
|
|
659
|
+
function sanitizeHref(href) {
|
|
660
|
+
if (typeof href !== 'string') return null;
|
|
661
|
+
const trimmedHref = href.trim();
|
|
662
|
+
if (trimmedHref === '') return null;
|
|
663
|
+
// Browsers ignore control characters inside a URL scheme, so "java<TAB>script:" still
|
|
664
|
+
// executes. Drop them before testing, but keep the original as the rendered value.
|
|
665
|
+
if (UNSAFE_HREF_SCHEME_PATTERN.test(stripControlCharacters(trimmedHref))) return null;
|
|
666
|
+
return trimmedHref;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Maps a component's truthy modifier properties to their Bulma class names.
|
|
671
|
+
*
|
|
672
|
+
* @param {{ get: (key: string) => unknown }} component
|
|
673
|
+
* @param {ReadonlyArray<readonly [string, string]>} modifierClassNames
|
|
674
|
+
* @returns {string}
|
|
675
|
+
*/
|
|
676
|
+
function buildModifierClassNames(component, modifierClassNames) {
|
|
677
|
+
return modifierClassNames
|
|
678
|
+
.filter(([key]) => component.get(key))
|
|
679
|
+
.map(([, className]) => className)
|
|
680
|
+
.join(' ');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Wraps a component definition so its `href` is sanitized into `safeHref`, both at init and on
|
|
685
|
+
* every later change. Templates must render `safeHref`, never the raw `href`.
|
|
686
|
+
*
|
|
687
|
+
* @param {RalmaComponentDefinition} definition
|
|
688
|
+
* @returns {RalmaComponentDefinition}
|
|
689
|
+
*/
|
|
690
|
+
function withSafeHref(definition) {
|
|
691
|
+
const existingData = typeof definition.data === 'function' ? definition.data : null;
|
|
692
|
+
const existingOninit = typeof definition.oninit === 'function' ? definition.oninit : null;
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
...definition,
|
|
696
|
+
/** @this {RactiveInstance} */
|
|
697
|
+
data() {
|
|
698
|
+
const baseData = existingData ? existingData.call(this) : {};
|
|
699
|
+
return {
|
|
700
|
+
...baseData,
|
|
701
|
+
safeHref: sanitizeHref(baseData.href),
|
|
702
|
+
};
|
|
703
|
+
},
|
|
704
|
+
/** @this {RactiveInstance} */
|
|
705
|
+
oninit() {
|
|
706
|
+
if (existingOninit) existingOninit.call(this);
|
|
707
|
+
|
|
708
|
+
/** @param {unknown} href */
|
|
709
|
+
const syncSafeHref = (href) => {
|
|
710
|
+
const nextSafeHref = sanitizeHref(href);
|
|
711
|
+
if (this.get('safeHref') !== nextSafeHref) this.set('safeHref', nextSafeHref);
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
syncSafeHref(this.get('href'));
|
|
715
|
+
this.observe(
|
|
716
|
+
'href',
|
|
717
|
+
(nextHref) => {
|
|
718
|
+
syncSafeHref(nextHref);
|
|
719
|
+
},
|
|
720
|
+
{ init: false },
|
|
721
|
+
);
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Registers Ralma components onto a Ractive constructor.
|
|
728
|
+
*
|
|
729
|
+
* @param {unknown} ractive - The `Ractive` constructor (global or imported).
|
|
730
|
+
* @param {{overwrite?: boolean, warnOnCollision?: boolean}} [options]
|
|
731
|
+
* - `overwrite`: overwrite already-registered component names.
|
|
732
|
+
* - `warnOnCollision`: emit console warnings when a name is skipped.
|
|
733
|
+
* @returns {unknown} The same `Ractive` reference.
|
|
734
|
+
*/
|
|
735
|
+
export function registerRalma(ractive, options = {}) {
|
|
736
|
+
if (ractive == null || (typeof ractive !== 'object' && typeof ractive !== 'function')) {
|
|
737
|
+
throw new TypeError('registerRalma(Ractive): Ractive must be an object or function');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// The runtime guards above and below are the real contract; this cast only tells the type
|
|
741
|
+
// checker what shape those guards have already proven.
|
|
742
|
+
const target = /** @type {RactiveLike} */ (ractive);
|
|
743
|
+
|
|
744
|
+
const hasExtend = typeof target.extend === 'function';
|
|
745
|
+
const hasComponents = target.components != null && typeof target.components === 'object';
|
|
746
|
+
if (!hasExtend || !hasComponents) {
|
|
747
|
+
throw new TypeError('registerRalma(Ractive): Ractive must have .extend() and .components');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (options == null || (typeof options !== 'object' && typeof options !== 'function')) {
|
|
751
|
+
throw new TypeError('registerRalma(Ractive, options): options must be an object when provided');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const overwrite = options.overwrite === true;
|
|
755
|
+
const warnOnCollision = options.warnOnCollision === true;
|
|
756
|
+
|
|
757
|
+
const registrationState = target.components[REGISTRY_MARKER];
|
|
758
|
+
if (
|
|
759
|
+
registrationState &&
|
|
760
|
+
typeof registrationState === 'object' &&
|
|
761
|
+
/** @type {{ definitions?: unknown }} */ (registrationState).definitions ===
|
|
762
|
+
componentDefinitions &&
|
|
763
|
+
/** @type {{ overwrite?: unknown }} */ (registrationState).overwrite === overwrite
|
|
764
|
+
) {
|
|
765
|
+
return ractive;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
for (const [name, def] of runtimeComponentEntries) {
|
|
769
|
+
if (!overwrite && Object.prototype.hasOwnProperty.call(target.components, name)) {
|
|
770
|
+
if (warnOnCollision) {
|
|
771
|
+
console.warn(`registerRalma: skipping existing component "${name}"`);
|
|
772
|
+
}
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
target.components[name] = target.extend(def);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
target.components[REGISTRY_MARKER] = Object.freeze({
|
|
779
|
+
definitions: componentDefinitions,
|
|
780
|
+
overwrite,
|
|
781
|
+
});
|
|
782
|
+
return ractive;
|
|
783
|
+
}
|