@markout-lang/bootstrap-kit 0.2.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 +331 -0
- package/all.htm +44 -0
- package/package.json +33 -0
- package/parts/accordion.htm +81 -0
- package/parts/alert.htm +48 -0
- package/parts/badge.htm +33 -0
- package/parts/base.htm +71 -0
- package/parts/breadcrumb.htm +44 -0
- package/parts/button.htm +162 -0
- package/parts/card.htm +103 -0
- package/parts/carousel.htm +87 -0
- package/parts/check.htm +164 -0
- package/parts/collapse.htm +31 -0
- package/parts/dropdown.htm +86 -0
- package/parts/image.htm +46 -0
- package/parts/input.htm +178 -0
- package/parts/list-group.htm +58 -0
- package/parts/modal.htm +119 -0
- package/parts/nav.htm +89 -0
- package/parts/navbar.htm +82 -0
- package/parts/offcanvas.htm +90 -0
- package/parts/pagination.htm +73 -0
- package/parts/placeholder.htm +34 -0
- package/parts/popover.htm +43 -0
- package/parts/progress.htm +54 -0
- package/parts/scrollspy.htm +33 -0
- package/parts/select.htm +72 -0
- package/parts/spinner.htm +32 -0
- package/parts/table.htm +70 -0
- package/parts/theme.htm +70 -0
- package/parts/toast.htm +115 -0
- package/parts/tooltip.htm +57 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
Pagination driven by a page number rather than by markup: give it the
|
|
6
|
+
`:current` page and the total `:pages` and it works out which entry is
|
|
7
|
+
active and when the arrows are dead.
|
|
8
|
+
|
|
9
|
+
The current page is `:current` rather than `:page`, because `page` is
|
|
10
|
+
already the name of `<html>`'s own scope — a parameter of that name would
|
|
11
|
+
resolve to the scope instead of to the number.
|
|
12
|
+
|
|
13
|
+
`:link` and `:select` are the two ways to answer "what does a click do".
|
|
14
|
+
`:link` builds an href per page for a server-rendered list; `:select` is
|
|
15
|
+
called with the page number for one that stays on the page. Both are
|
|
16
|
+
ordinary function parameters — a callback the component calls, not a DOM
|
|
17
|
+
event, so it is a value and not an `:on-` handler.
|
|
18
|
+
-->
|
|
19
|
+
<:define tag="bs-pagination:nav"
|
|
20
|
+
|
|
21
|
+
// parameters
|
|
22
|
+
:extra=${null} // extra classes for this instance
|
|
23
|
+
:current=${1}
|
|
24
|
+
:pages=${5}
|
|
25
|
+
:size=${null} // 'sm' | 'lg'
|
|
26
|
+
:align=${'start'} // 'start' | 'center' | 'end'
|
|
27
|
+
:prev=${'«'}
|
|
28
|
+
:next=${'»'}
|
|
29
|
+
:link=${(n) => null}
|
|
30
|
+
:select=${(n) => {}}
|
|
31
|
+
:label=${'Page navigation'}
|
|
32
|
+
|
|
33
|
+
// private
|
|
34
|
+
:_class=${[
|
|
35
|
+
'pagination mb-0',
|
|
36
|
+
size ? `pagination-${size}` : '',
|
|
37
|
+
align === 'center' ? 'justify-content-center' : '',
|
|
38
|
+
align === 'end' ? 'justify-content-end' : '',
|
|
39
|
+
].filter(s => s).join(' ')}
|
|
40
|
+
:_numbers=${Array.from({ length: pages }, (_, i) => i + 1)}
|
|
41
|
+
|
|
42
|
+
class=${extra}
|
|
43
|
+
aria-label=${label}
|
|
44
|
+
>
|
|
45
|
+
<ul class=${_class}>
|
|
46
|
+
<li class="page-item" :class-disabled=${current <= 1}>
|
|
47
|
+
<a class="page-link"
|
|
48
|
+
href=${link(current - 1)}
|
|
49
|
+
aria-label="Previous"
|
|
50
|
+
:on-click=${() => current > 1 && select(current - 1)}>${prev}</a>
|
|
51
|
+
</li>
|
|
52
|
+
|
|
53
|
+
<li class="page-item"
|
|
54
|
+
:for-each=${_numbers}
|
|
55
|
+
:for-as="n"
|
|
56
|
+
:for-key=${n}
|
|
57
|
+
:class-active=${n === current}
|
|
58
|
+
aria-current=${n === current ? 'page' : null}>
|
|
59
|
+
<a class="page-link"
|
|
60
|
+
href=${link(n)}
|
|
61
|
+
:on-click=${() => select(n)}>${n}</a>
|
|
62
|
+
</li>
|
|
63
|
+
|
|
64
|
+
<li class="page-item" :class-disabled=${current >= pages}>
|
|
65
|
+
<a class="page-link"
|
|
66
|
+
href=${link(current + 1)}
|
|
67
|
+
aria-label="Next"
|
|
68
|
+
:on-click=${() => current < pages && select(current + 1)}>${next}</a>
|
|
69
|
+
</li>
|
|
70
|
+
</ul>
|
|
71
|
+
</:define>
|
|
72
|
+
|
|
73
|
+
</lib>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
A loading placeholder. Bootstrap's animation classes are descendant
|
|
6
|
+
selectors (`.placeholder-glow .placeholder`), so the component wraps its
|
|
7
|
+
own bar and is self-contained — no animation wrapper to remember.
|
|
8
|
+
-->
|
|
9
|
+
<:define tag="bs-placeholder:span"
|
|
10
|
+
|
|
11
|
+
// parameters
|
|
12
|
+
:extra=${null} // extra classes for this instance
|
|
13
|
+
:cols=${6}
|
|
14
|
+
:size=${null} // 'xs' | 'sm' | 'lg'
|
|
15
|
+
:variant=${null}
|
|
16
|
+
:animation=${'glow'} // 'glow' | 'wave' | null
|
|
17
|
+
|
|
18
|
+
// private
|
|
19
|
+
:_class=${[animation ? `placeholder-${animation}` : '', extra]
|
|
20
|
+
.filter(s => s).join(' ') || null}
|
|
21
|
+
:_barClass=${[
|
|
22
|
+
'placeholder',
|
|
23
|
+
`col-${cols}`,
|
|
24
|
+
size ? `placeholder-${size}` : '',
|
|
25
|
+
variant ? `bg-${variant}` : '',
|
|
26
|
+
].filter(s => s).join(' ')}
|
|
27
|
+
|
|
28
|
+
class=${_class}
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
>
|
|
31
|
+
<span class=${_barClass}></span>
|
|
32
|
+
</:define>
|
|
33
|
+
|
|
34
|
+
</lib>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
A popover: a tooltip with a heading and a body, and click to open rather
|
|
6
|
+
than hover. Built, rebuilt and disposed on the same three moments, and
|
|
7
|
+
for the same reasons — see `tooltip.htm`.
|
|
8
|
+
|
|
9
|
+
`:handle-content` rather than `:handle-title` because the body is the
|
|
10
|
+
part that usually changes; both are read when it runs.
|
|
11
|
+
-->
|
|
12
|
+
<:define tag="bs-popover:span"
|
|
13
|
+
class="d-inline-block ${extra ?? ''}"
|
|
14
|
+
|
|
15
|
+
// parameters
|
|
16
|
+
:extra=${null} // extra classes for this instance
|
|
17
|
+
:title=${''}
|
|
18
|
+
:content=${''}
|
|
19
|
+
:placement=${'right'}
|
|
20
|
+
:trigger=${'click'}
|
|
21
|
+
:html=${false}
|
|
22
|
+
|
|
23
|
+
// private
|
|
24
|
+
:_build=${() => {
|
|
25
|
+
const bs = globalThis.bootstrap;
|
|
26
|
+
bs.Popover.getInstance($dom)?.dispose();
|
|
27
|
+
new bs.Popover($dom, {
|
|
28
|
+
title: title,
|
|
29
|
+
content: content,
|
|
30
|
+
placement: placement,
|
|
31
|
+
trigger: trigger,
|
|
32
|
+
html: html,
|
|
33
|
+
});
|
|
34
|
+
}}
|
|
35
|
+
|
|
36
|
+
:did-attach=${() => _build()}
|
|
37
|
+
:handle-content=${() => _build()}
|
|
38
|
+
:will-detach=${() => globalThis.bootstrap.Popover.getInstance($dom)?.dispose()}
|
|
39
|
+
>
|
|
40
|
+
<:slot />
|
|
41
|
+
</:define>
|
|
42
|
+
|
|
43
|
+
</lib>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
Bootstrap 5.3 moved the ARIA onto the outer `.progress` and left
|
|
6
|
+
`.progress-bar` as the visual fill, so a progress bar is two elements
|
|
7
|
+
whose values have to agree. Here they are derived from one `:value`.
|
|
8
|
+
|
|
9
|
+
`:label` is the text drawn inside the bar; leave it off for a bare bar.
|
|
10
|
+
-->
|
|
11
|
+
<:define tag="bs-progress:div"
|
|
12
|
+
class="progress ${extra ?? ''}"
|
|
13
|
+
|
|
14
|
+
// parameters
|
|
15
|
+
:extra=${null} // extra classes for this instance
|
|
16
|
+
:value=${0}
|
|
17
|
+
:min=${0}
|
|
18
|
+
:max=${100}
|
|
19
|
+
:label=${null}
|
|
20
|
+
:variant=${null}
|
|
21
|
+
:striped=${false}
|
|
22
|
+
:animated=${false}
|
|
23
|
+
:height=${null}
|
|
24
|
+
:name=${'Progress'}
|
|
25
|
+
|
|
26
|
+
// private
|
|
27
|
+
:_percent=${Math.max(0, Math.min(100, ((value - min) / (max - min || 1)) * 100))}
|
|
28
|
+
:_class=${[
|
|
29
|
+
'progress-bar',
|
|
30
|
+
variant ? `text-bg-${variant}` : '',
|
|
31
|
+
striped || animated ? 'progress-bar-striped' : '',
|
|
32
|
+
animated ? 'progress-bar-animated' : '',
|
|
33
|
+
].filter(s => s).join(' ')}
|
|
34
|
+
|
|
35
|
+
role="progressbar"
|
|
36
|
+
aria-label=${name}
|
|
37
|
+
aria-valuenow=${value}
|
|
38
|
+
aria-valuemin=${min}
|
|
39
|
+
aria-valuemax=${max}
|
|
40
|
+
:style-height=${height}
|
|
41
|
+
>
|
|
42
|
+
<div class=${_class}
|
|
43
|
+
:style-width="${_percent}%">${label}</div>
|
|
44
|
+
</:define>
|
|
45
|
+
|
|
46
|
+
<!---
|
|
47
|
+
Several bars sharing one track. Each child is a `bs-progress` whose
|
|
48
|
+
percentages add up to 100.
|
|
49
|
+
-->
|
|
50
|
+
<:define tag="bs-progress-stacked:div" class="progress-stacked">
|
|
51
|
+
<:slot />
|
|
52
|
+
</:define>
|
|
53
|
+
|
|
54
|
+
</lib>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
Scrollspy is behaviour rather than appearance: it marks the entry in a
|
|
6
|
+
nav that matches what is currently on screen. `:target` is the selector
|
|
7
|
+
of that nav, and the element this sits on is the thing being scrolled.
|
|
8
|
+
|
|
9
|
+
<bs-nav :vertical id="toc" :items=${sections} />
|
|
10
|
+
<bs-scrollspy :target="#toc" :height="300">…</bs-scrollspy>
|
|
11
|
+
-->
|
|
12
|
+
<:define tag="bs-scrollspy:div"
|
|
13
|
+
|
|
14
|
+
// parameters
|
|
15
|
+
:extra=${null} // extra classes for this instance
|
|
16
|
+
:target=${null}
|
|
17
|
+
:offset=${0}
|
|
18
|
+
:smooth=${false}
|
|
19
|
+
:height=${null}
|
|
20
|
+
|
|
21
|
+
class=${extra}
|
|
22
|
+
data-bs-spy="scroll"
|
|
23
|
+
data-bs-target=${target}
|
|
24
|
+
data-bs-root-margin="${offset}px 0px -40%"
|
|
25
|
+
data-bs-smooth-scroll=${smooth ? 'true' : null}
|
|
26
|
+
tabindex="0"
|
|
27
|
+
:style-height=${height ? `${height}px` : null}
|
|
28
|
+
:style-overflow-y=${height ? 'auto' : null}
|
|
29
|
+
>
|
|
30
|
+
<:slot />
|
|
31
|
+
</:define>
|
|
32
|
+
|
|
33
|
+
</lib>
|
package/parts/select.htm
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
A select whose options come from an array. An entry is either a string or
|
|
6
|
+
`{ name, value, disabled }`, so the quick case stays quick:
|
|
7
|
+
|
|
8
|
+
<bs-select :label="Size" :options=${['S', 'M', 'L']} />
|
|
9
|
+
-->
|
|
10
|
+
<:define tag="bs-select:div"
|
|
11
|
+
class="mb-3 ${extra ?? ''}"
|
|
12
|
+
|
|
13
|
+
// parameters
|
|
14
|
+
:extra=${null} // extra classes for this instance
|
|
15
|
+
:label=${null}
|
|
16
|
+
:name=${null}
|
|
17
|
+
:options=${[]}
|
|
18
|
+
:value=${null}
|
|
19
|
+
:placeholder=${null}
|
|
20
|
+
:help=${null}
|
|
21
|
+
:size=${null} // 'sm' | 'lg'
|
|
22
|
+
:multiple=${false}
|
|
23
|
+
:disabled=${false}
|
|
24
|
+
:required=${false}
|
|
25
|
+
:floating=${false}
|
|
26
|
+
|
|
27
|
+
// private
|
|
28
|
+
:_id=${`bs-select-${$id}`}
|
|
29
|
+
:_helpId=${`bs-select-help-${$id}`}
|
|
30
|
+
:_class=${['form-select', size ? `form-select-${size}` : '']
|
|
31
|
+
.filter(s => s).join(' ')}
|
|
32
|
+
:_options=${options.map(o =>
|
|
33
|
+
typeof o === 'object' && o !== null
|
|
34
|
+
? { name: o.name, value: o.value ?? o.name, disabled: !!o.disabled }
|
|
35
|
+
: { name: o, value: o, disabled: false })}
|
|
36
|
+
>
|
|
37
|
+
<label class="form-label"
|
|
38
|
+
for=${_id}
|
|
39
|
+
:if=${!(floating || !label)}>${label}</label>
|
|
40
|
+
|
|
41
|
+
<div :class-form-floating=${floating}>
|
|
42
|
+
<select class=${_class}
|
|
43
|
+
id=${_id}
|
|
44
|
+
name=${name}
|
|
45
|
+
aria-label=${label || null}
|
|
46
|
+
aria-describedby=${help ? _helpId : null}
|
|
47
|
+
:attr-multiple=${multiple}
|
|
48
|
+
:attr-disabled=${disabled}
|
|
49
|
+
:attr-required=${required}
|
|
50
|
+
:on-change=${ev => value = ev.target.value}>
|
|
51
|
+
|
|
52
|
+
<option value=""
|
|
53
|
+
:if=${placeholder}
|
|
54
|
+
:attr-selected=${value == null}>${placeholder}</option>
|
|
55
|
+
|
|
56
|
+
<option :for-each=${_options}
|
|
57
|
+
:for-as="option"
|
|
58
|
+
value=${option.value}
|
|
59
|
+
:attr-disabled=${option.disabled}
|
|
60
|
+
:attr-selected=${option.value === value}>${option.name}</option>
|
|
61
|
+
</select>
|
|
62
|
+
|
|
63
|
+
<label for=${_id}
|
|
64
|
+
:if=${floating}>${label}</label>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="form-text"
|
|
68
|
+
id=${_helpId}
|
|
69
|
+
:if=${help}>${help}</div>
|
|
70
|
+
</:define>
|
|
71
|
+
|
|
72
|
+
</lib>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
A spinner is a loading state, so it needs to say so to a screen reader as
|
|
6
|
+
well as to the eye: `role="status"` plus visually hidden text. That text
|
|
7
|
+
is the reason this is a component rather than a class.
|
|
8
|
+
-->
|
|
9
|
+
<:define tag="bs-spinner:div"
|
|
10
|
+
role="status"
|
|
11
|
+
|
|
12
|
+
// parameters
|
|
13
|
+
:extra=${null} // extra classes for this instance
|
|
14
|
+
:variant=${null}
|
|
15
|
+
:type=${'border'} // 'border' | 'grow'
|
|
16
|
+
:small=${false}
|
|
17
|
+
:label=${'Loading…'}
|
|
18
|
+
|
|
19
|
+
// private
|
|
20
|
+
:_class=${[
|
|
21
|
+
`spinner-${type}`,
|
|
22
|
+
small ? `spinner-${type}-sm` : '',
|
|
23
|
+
variant ? `text-${variant}` : '',
|
|
24
|
+
extra,
|
|
25
|
+
].filter(s => s).join(' ')}
|
|
26
|
+
|
|
27
|
+
class=${_class}
|
|
28
|
+
>
|
|
29
|
+
<span class="visually-hidden">${label}</span>
|
|
30
|
+
</:define>
|
|
31
|
+
|
|
32
|
+
</lib>
|
package/parts/table.htm
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
A table from `:columns` and `:rows`.
|
|
6
|
+
|
|
7
|
+
A column is `{ key, name, class }`; a row is any object the columns can
|
|
8
|
+
be read out of. That is the one place in this kit where markup is
|
|
9
|
+
generated from data twice over — once per column, once per row — and it
|
|
10
|
+
is the case where writing it by hand hurts most.
|
|
11
|
+
|
|
12
|
+
`:responsive` is a class on the same element rather than an extra
|
|
13
|
+
wrapper: `.table-responsive` only sets `overflow-x`, so it costs nothing
|
|
14
|
+
when off.
|
|
15
|
+
-->
|
|
16
|
+
<:define tag="bs-table:div"
|
|
17
|
+
|
|
18
|
+
// parameters
|
|
19
|
+
:extra=${null} // extra classes for this instance
|
|
20
|
+
:columns=${[]}
|
|
21
|
+
:rows=${[]}
|
|
22
|
+
:caption=${null}
|
|
23
|
+
:striped=${false}
|
|
24
|
+
:hover=${false}
|
|
25
|
+
:bordered=${false}
|
|
26
|
+
:borderless=${false}
|
|
27
|
+
:small=${false}
|
|
28
|
+
:variant=${null}
|
|
29
|
+
:headVariant=${null}
|
|
30
|
+
:align=${null} // 'middle' | 'bottom'
|
|
31
|
+
:responsive=${false}
|
|
32
|
+
|
|
33
|
+
// private
|
|
34
|
+
:_class=${[
|
|
35
|
+
'table',
|
|
36
|
+
striped ? 'table-striped' : '',
|
|
37
|
+
hover ? 'table-hover' : '',
|
|
38
|
+
bordered ? 'table-bordered' : '',
|
|
39
|
+
borderless ? 'table-borderless' : '',
|
|
40
|
+
small ? 'table-sm' : '',
|
|
41
|
+
variant ? `table-${variant}` : '',
|
|
42
|
+
align ? `align-${align}` : '',
|
|
43
|
+
].filter(s => s).join(' ')}
|
|
44
|
+
|
|
45
|
+
class=${extra}
|
|
46
|
+
:class-table-responsive=${responsive}
|
|
47
|
+
>
|
|
48
|
+
<table class=${_class}>
|
|
49
|
+
<caption :if=${caption}>${caption}</caption>
|
|
50
|
+
|
|
51
|
+
<thead class=${headVariant ? `table-${headVariant}` : null}>
|
|
52
|
+
<tr>
|
|
53
|
+
<th scope="col"
|
|
54
|
+
:for-each=${columns}
|
|
55
|
+
:for-as="column"
|
|
56
|
+
class=${column.class ?? null}>${column.name ?? column.key}</th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
|
|
60
|
+
<tbody>
|
|
61
|
+
<tr :for-each=${rows} :for-as="row" class=${row.class ?? null}>
|
|
62
|
+
<td :for-each=${columns}
|
|
63
|
+
:for-as="column"
|
|
64
|
+
class=${column.class ?? null}>${row[column.key]}</td>
|
|
65
|
+
</tr>
|
|
66
|
+
</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
</:define>
|
|
69
|
+
|
|
70
|
+
</lib>
|
package/parts/theme.htm
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
Bootstrap 5.3's colour modes, as a value.
|
|
6
|
+
|
|
7
|
+
The inline script runs before first paint, which is the whole point of
|
|
8
|
+
putting it here rather than in a handler: the mode has to be on `<html>`
|
|
9
|
+
before the first frame or the page flashes the wrong one. It is the one
|
|
10
|
+
piece of this kit that cannot be reactive, because it has to happen
|
|
11
|
+
before Markout exists.
|
|
12
|
+
|
|
13
|
+
From there `_theme` is an ordinary value, and it starts as whatever that
|
|
14
|
+
script decided rather than as a literal: a served page knows nothing
|
|
15
|
+
about this visitor, so a default written here would be a guess that the
|
|
16
|
+
handler then wrote over the truth. In the browser the expression finds
|
|
17
|
+
the attribute already set; on the server there is no document and the
|
|
18
|
+
fallback stands, which is only ever what the markup is stamped with.
|
|
19
|
+
|
|
20
|
+
`:handle-_theme` writes every change out, and a handler runs once at
|
|
21
|
+
start as well as on every change — so it asks first. On that first run
|
|
22
|
+
the page already shows `v`, and there is nothing to do; leaving storage
|
|
23
|
+
alone there is what keeps a visitor who has never chosen following their
|
|
24
|
+
system rather than being pinned to whatever it said the first time.
|
|
25
|
+
|
|
26
|
+
Everything goes through `globalThis`, because `document` and
|
|
27
|
+
`localStorage` are browser globals rather than standard library, and this
|
|
28
|
+
kit would rather say so at the point of use than pretend a server has
|
|
29
|
+
them.
|
|
30
|
+
-->
|
|
31
|
+
<script>
|
|
32
|
+
(() => {
|
|
33
|
+
const stored = localStorage.getItem('bs-theme');
|
|
34
|
+
const theme = stored
|
|
35
|
+
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
36
|
+
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
37
|
+
})();
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<:define tag="bs-theme-toggle:button"
|
|
41
|
+
type="button"
|
|
42
|
+
|
|
43
|
+
// parameters
|
|
44
|
+
:extra=${null} // extra classes for this instance
|
|
45
|
+
:variant=${'secondary'}
|
|
46
|
+
:outline=${true}
|
|
47
|
+
:size=${null}
|
|
48
|
+
|
|
49
|
+
// private
|
|
50
|
+
:_theme=${globalThis.document?.documentElement.getAttribute('data-bs-theme') ?? 'light'}
|
|
51
|
+
:_class=${[
|
|
52
|
+
'btn',
|
|
53
|
+
`btn-${outline ? 'outline-' : ''}${variant}`,
|
|
54
|
+
size ? `btn-${size}` : '',
|
|
55
|
+
extra,
|
|
56
|
+
].filter(s => s).join(' ')}
|
|
57
|
+
|
|
58
|
+
class=${_class}
|
|
59
|
+
aria-label="Toggle colour mode"
|
|
60
|
+
|
|
61
|
+
:handle-_theme=${(v) => {
|
|
62
|
+
const html = globalThis.document.documentElement;
|
|
63
|
+
if (html.getAttribute('data-bs-theme') === v) return;
|
|
64
|
+
html.setAttribute('data-bs-theme', v);
|
|
65
|
+
globalThis.localStorage.setItem('bs-theme', v);
|
|
66
|
+
}}
|
|
67
|
+
:on-click=${() => _theme = _theme === 'dark' ? 'light' : 'dark'}
|
|
68
|
+
>${_theme === 'dark' ? '☀' : '☾'}</:define>
|
|
69
|
+
|
|
70
|
+
</lib>
|
package/parts/toast.htm
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
<:import src="button.htm" />
|
|
4
|
+
|
|
5
|
+
<!---
|
|
6
|
+
A toast, shown by setting a value rather than by calling anything:
|
|
7
|
+
|
|
8
|
+
<bs-toast :aka="saved" :title="Saved">Your changes are safe.</bs-toast>
|
|
9
|
+
<bs-button :on-click=${() => saved.open = true}>Save</bs-button>
|
|
10
|
+
|
|
11
|
+
`:handle-open` turns that value into the `show()` / `hide()` the plugin
|
|
12
|
+
wants, and Bootstrap's own `hidden.bs.toast` puts the value back when the
|
|
13
|
+
toast times out — so the value stays true to what is on screen however it
|
|
14
|
+
got closed.
|
|
15
|
+
-->
|
|
16
|
+
<:define tag="bs-toast:div"
|
|
17
|
+
class="toast"
|
|
18
|
+
role="alert"
|
|
19
|
+
aria-live="assertive"
|
|
20
|
+
aria-atomic="true"
|
|
21
|
+
|
|
22
|
+
// parameters
|
|
23
|
+
:extra=${null} // extra classes for this instance
|
|
24
|
+
:title=${null}
|
|
25
|
+
:time=${null}
|
|
26
|
+
:variant=${null}
|
|
27
|
+
:open=${false}
|
|
28
|
+
:autohide=${true}
|
|
29
|
+
:delay=${5000}
|
|
30
|
+
|
|
31
|
+
// private
|
|
32
|
+
:_class=${['toast', variant ? `text-bg-${variant} border-0` : '', extra]
|
|
33
|
+
.filter(s => s).join(' ')}
|
|
34
|
+
|
|
35
|
+
class=${_class}
|
|
36
|
+
data-bs-autohide=${autohide ? null : 'false'}
|
|
37
|
+
data-bs-delay=${delay}
|
|
38
|
+
|
|
39
|
+
:handle-open=${(v) => {
|
|
40
|
+
// only while this markup is in the page: an `:if`/`:for-data` region
|
|
41
|
+
// can take it out with `open` still true, and Bootstrap asked
|
|
42
|
+
// to show a detached element walks into its own internals
|
|
43
|
+
if (!$dom.isConnected) return;
|
|
44
|
+
const toast = globalThis.bootstrap.Toast.getOrCreateInstance($dom);
|
|
45
|
+
v ? toast.show() : toast.hide();
|
|
46
|
+
}}
|
|
47
|
+
/*
|
|
48
|
+
Bootstrap's plugins own DOM outside this element -- a backdrop,
|
|
49
|
+
a popper, a body class -- so an element removed without a
|
|
50
|
+
dispose() leaves it behind. `:will-detach` rather than
|
|
51
|
+
`:will-dispose`: an `:if`/`:for-data` region takes its markup out of
|
|
52
|
+
the page without its scope going anywhere, and the leak is the
|
|
53
|
+
same either way
|
|
54
|
+
*/
|
|
55
|
+
:will-detach=${() => globalThis.bootstrap.Toast.getInstance($dom)?.dispose()}
|
|
56
|
+
:on-hidden.bs.toast=${() => open = false}
|
|
57
|
+
>
|
|
58
|
+
<!--- with a title, Bootstrap's header form: the close button lives in it -->
|
|
59
|
+
<div class="toast-header" :if=${title}>
|
|
60
|
+
<strong class="me-auto">${title}</strong>
|
|
61
|
+
<small class="text-body-secondary"
|
|
62
|
+
:if=${time}>${time}</small>
|
|
63
|
+
<bs-close :dismiss="toast" :label="Close notification" />
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!---
|
|
67
|
+
Without one, Bootstrap's flex form, because a toast that cannot be
|
|
68
|
+
dismissed is a real outcome here: `:autohide=${false}` and no title
|
|
69
|
+
used to leave no close button anywhere on it.
|
|
70
|
+
|
|
71
|
+
The wrapper is what carries `d-flex`, not the toast: `.d-flex` is
|
|
72
|
+
`display: flex !important`, which would beat Bootstrap's own
|
|
73
|
+
`.toast:not(.show) { display: none }` and leave the toast on screen
|
|
74
|
+
for good. The class goes on rather than off so a titled toast keeps
|
|
75
|
+
the plain block body it has always had.
|
|
76
|
+
-->
|
|
77
|
+
<div class=${title ? null : 'd-flex'}>
|
|
78
|
+
<div class="toast-body">
|
|
79
|
+
<:slot />
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<bs-close :dismiss="toast"
|
|
83
|
+
:label="Close notification"
|
|
84
|
+
:extra="me-2 m-auto"
|
|
85
|
+
:if=${!title} />
|
|
86
|
+
</div>
|
|
87
|
+
</:define>
|
|
88
|
+
|
|
89
|
+
<!---
|
|
90
|
+
Where toasts stack. Bootstrap positions the container, not the toasts, so
|
|
91
|
+
this is the piece that decides the corner they appear in.
|
|
92
|
+
-->
|
|
93
|
+
<:define tag="bs-toast-container:div"
|
|
94
|
+
|
|
95
|
+
// parameters
|
|
96
|
+
:extra=${null} // extra classes for this instance
|
|
97
|
+
:placement=${'bottom-end'} // 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'top-center'
|
|
98
|
+
|
|
99
|
+
// private
|
|
100
|
+
:_class=${[
|
|
101
|
+
'toast-container position-fixed p-3',
|
|
102
|
+
placement.startsWith('top') ? 'top-0' : 'bottom-0',
|
|
103
|
+
placement.endsWith('start') ? 'start-0' : '',
|
|
104
|
+
placement.endsWith('end') ? 'end-0' : '',
|
|
105
|
+
placement.endsWith('center') ? 'start-50 translate-middle-x' : '',
|
|
106
|
+
extra,
|
|
107
|
+
].filter(s => s).join(' ')}
|
|
108
|
+
|
|
109
|
+
class=${_class}
|
|
110
|
+
style="z-index: 1090"
|
|
111
|
+
>
|
|
112
|
+
<:slot />
|
|
113
|
+
</:define>
|
|
114
|
+
|
|
115
|
+
</lib>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
Tooltips and popovers are the two components Bootstrap does not start on
|
|
6
|
+
its own: every other one wakes up from a `data-bs-*` attribute, these two
|
|
7
|
+
have to be constructed. The usual answer is a page-level loop over
|
|
8
|
+
`[data-bs-toggle="tooltip"]`, which then misses anything added later.
|
|
9
|
+
|
|
10
|
+
Three moments instead, which between them cover everything a tooltip can
|
|
11
|
+
live through:
|
|
12
|
+
|
|
13
|
+
- `:did-attach` builds it, every time the element enters the page — so
|
|
14
|
+
one inside an `:if`/`:for-data` region that comes back has a working tooltip
|
|
15
|
+
rather than the memory of one.
|
|
16
|
+
- `:will-detach` disposes it before the element leaves. Bootstrap parks
|
|
17
|
+
the popper on `<body>`, so without this, hiding the region leaves a
|
|
18
|
+
tooltip floating over the page for good.
|
|
19
|
+
- `:handle-title` rebuilds it when the text changes, since Bootstrap
|
|
20
|
+
reads its options at construction and never again.
|
|
21
|
+
|
|
22
|
+
All three go through the same `_build`, which disposes first and is
|
|
23
|
+
therefore safe to call more than once.
|
|
24
|
+
|
|
25
|
+
This wraps its content in a `<span>`, since the tooltip needs an element
|
|
26
|
+
of its own to hang off; wrap the trigger, not the page around it.
|
|
27
|
+
-->
|
|
28
|
+
<:define tag="bs-tooltip:span"
|
|
29
|
+
class="d-inline-block ${extra ?? ''}"
|
|
30
|
+
|
|
31
|
+
// parameters
|
|
32
|
+
:extra=${null} // extra classes for this instance
|
|
33
|
+
:title=${''}
|
|
34
|
+
:placement=${'top'}
|
|
35
|
+
:html=${false}
|
|
36
|
+
:trigger=${'hover focus'}
|
|
37
|
+
|
|
38
|
+
// private
|
|
39
|
+
:_build=${() => {
|
|
40
|
+
const bs = globalThis.bootstrap;
|
|
41
|
+
bs.Tooltip.getInstance($dom)?.dispose();
|
|
42
|
+
new bs.Tooltip($dom, {
|
|
43
|
+
title: title,
|
|
44
|
+
placement: placement,
|
|
45
|
+
html: html,
|
|
46
|
+
trigger: trigger,
|
|
47
|
+
});
|
|
48
|
+
}}
|
|
49
|
+
|
|
50
|
+
:did-attach=${() => _build()}
|
|
51
|
+
:handle-title=${() => _build()}
|
|
52
|
+
:will-detach=${() => globalThis.bootstrap.Tooltip.getInstance($dom)?.dispose()}
|
|
53
|
+
>
|
|
54
|
+
<:slot />
|
|
55
|
+
</:define>
|
|
56
|
+
|
|
57
|
+
</lib>
|