@cobdfamily/clf-core 7.0.1
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 +250 -0
- package/dist/_variables.scss +973 -0
- package/dist/components/cobd-embed.d.ts +4 -0
- package/dist/components/cobd-embed.js +163 -0
- package/dist/components/cobd-nav.d.ts +1 -0
- package/dist/components/cobd-nav.js +383 -0
- package/dist/components/cobd-support.d.ts +26 -0
- package/dist/components/cobd-support.js +296 -0
- package/dist/components/font-scale-toggle.d.ts +9 -0
- package/dist/components/font-scale-toggle.js +159 -0
- package/dist/components/forms/checkbox.d.ts +1 -0
- package/dist/components/forms/checkbox.js +118 -0
- package/dist/components/forms/common.d.ts +17 -0
- package/dist/components/forms/common.js +137 -0
- package/dist/components/forms/index.d.ts +5 -0
- package/dist/components/forms/index.js +13 -0
- package/dist/components/forms/select.d.ts +1 -0
- package/dist/components/forms/select.js +132 -0
- package/dist/components/forms/submit.d.ts +1 -0
- package/dist/components/forms/submit.js +78 -0
- package/dist/components/forms/textarea.d.ts +1 -0
- package/dist/components/forms/textarea.js +95 -0
- package/dist/components/forms/textfield.d.ts +1 -0
- package/dist/components/forms/textfield.js +125 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.js +33 -0
- package/dist/components/theme-toggle.d.ts +10 -0
- package/dist/components/theme-toggle.js +130 -0
- package/dist/i18n/chrome.json +94 -0
- package/dist/navs/cobd.ca.json +83 -0
- package/dist/navs/more-cobd.json +60 -0
- package/dist/navs.d.ts +27 -0
- package/dist/navs.js +193 -0
- package/dist/theming/font-scale-paint.d.ts +0 -0
- package/dist/theming/font-scale-paint.js +32 -0
- package/dist/theming/init.d.ts +1 -0
- package/dist/theming/init.js +19 -0
- package/dist/theming/runtime.d.ts +22 -0
- package/dist/theming/runtime.js +333 -0
- package/dist/tokens.css +972 -0
- package/dist/tokens.json +97 -0
- package/dist/tokens.scss +95 -0
- package/package.json +123 -0
- package/src/navs/schema.json +64 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Shared helpers for the form custom elements.
|
|
2
|
+
//
|
|
3
|
+
// - ionicPresent() -- same auto-detect as
|
|
4
|
+
// cobd-nav. Looking for <ion-app> in the DOM
|
|
5
|
+
// sidesteps the
|
|
6
|
+
// customElements-not-yet-defined race that an
|
|
7
|
+
// `customElements.get("ion-input")` check would
|
|
8
|
+
// hit under Angular Ionic's deferred bootstrap.
|
|
9
|
+
//
|
|
10
|
+
// - ensureBaseStyle() -- Light-DOM custom elements
|
|
11
|
+
// default to `display: inline`, which would
|
|
12
|
+
// collapse the rendered field labels + slots.
|
|
13
|
+
// Inject one global rule the first time any
|
|
14
|
+
// form element upgrades. Idempotent.
|
|
15
|
+
//
|
|
16
|
+
// - readBoolAttr() / readAttr() -- attribute-
|
|
17
|
+
// reading helpers that respect the HTML
|
|
18
|
+
// convention (presence-as-true for booleans,
|
|
19
|
+
// getAttribute() for strings) and default
|
|
20
|
+
// gracefully.
|
|
21
|
+
export function ionicPresent() {
|
|
22
|
+
return typeof document !== "undefined"
|
|
23
|
+
&& document.querySelector("ion-app") !== null;
|
|
24
|
+
}
|
|
25
|
+
const BASE_STYLE_ID = "cobd-forms-base-style";
|
|
26
|
+
const BASE_STYLE = `
|
|
27
|
+
cobd-textfield, cobd-select, cobd-textarea,
|
|
28
|
+
cobd-checkbox, cobd-submit {
|
|
29
|
+
display: block;
|
|
30
|
+
}
|
|
31
|
+
.cobd-field-row {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: var(--cobd-spacing-xs, 4px);
|
|
35
|
+
}
|
|
36
|
+
.cobd-field-help,
|
|
37
|
+
.cobd-field-error {
|
|
38
|
+
font-size: var(--cobd-typography-size-sm, 0.875rem);
|
|
39
|
+
line-height: var(--cobd-typography-line-default, 1.5);
|
|
40
|
+
}
|
|
41
|
+
.cobd-field-help {
|
|
42
|
+
color: var(--cobd-color-medium, #92949c);
|
|
43
|
+
}
|
|
44
|
+
.cobd-field-error {
|
|
45
|
+
color: var(--cobd-color-danger, #c5283b);
|
|
46
|
+
}
|
|
47
|
+
.cobd-required-asterisk {
|
|
48
|
+
color: var(--cobd-color-danger, #c5283b);
|
|
49
|
+
margin-left: 0.2em;
|
|
50
|
+
aria-hidden: true;
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
export function ensureBaseStyle() {
|
|
54
|
+
if (typeof document === "undefined")
|
|
55
|
+
return;
|
|
56
|
+
if (document.getElementById(BASE_STYLE_ID))
|
|
57
|
+
return;
|
|
58
|
+
const style = document.createElement("style");
|
|
59
|
+
style.id = BASE_STYLE_ID;
|
|
60
|
+
style.textContent = BASE_STYLE;
|
|
61
|
+
document.head.appendChild(style);
|
|
62
|
+
}
|
|
63
|
+
export function readAttr(el, name) {
|
|
64
|
+
return el.hasAttribute(name) ? el.getAttribute(name) : null;
|
|
65
|
+
}
|
|
66
|
+
export function readBoolAttr(el, name) {
|
|
67
|
+
// HTML boolean attrs: present (even empty) =
|
|
68
|
+
// true; absent = false. Matches the rule
|
|
69
|
+
// browsers use for `disabled`, `required`,
|
|
70
|
+
// etc.
|
|
71
|
+
return el.hasAttribute(name);
|
|
72
|
+
}
|
|
73
|
+
export function readFieldDef(el, fallbackIdPrefix) {
|
|
74
|
+
const name = readAttr(el, "name") ?? "";
|
|
75
|
+
const explicitId = readAttr(el, "id");
|
|
76
|
+
const id = explicitId || name
|
|
77
|
+
|| `${fallbackIdPrefix}-${Math.random().toString(36).slice(2, 8)}`;
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
name,
|
|
81
|
+
label: readAttr(el, "label") ?? "",
|
|
82
|
+
help: readAttr(el, "help"),
|
|
83
|
+
error: readAttr(el, "error"),
|
|
84
|
+
required: readBoolAttr(el, "required"),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function buildLabel(def) {
|
|
88
|
+
const lbl = document.createElement("label");
|
|
89
|
+
lbl.setAttribute("for", def.id);
|
|
90
|
+
lbl.className = "cobd-field-label";
|
|
91
|
+
lbl.textContent = def.label;
|
|
92
|
+
if (def.required) {
|
|
93
|
+
const star = document.createElement("span");
|
|
94
|
+
star.className = "cobd-required-asterisk";
|
|
95
|
+
star.setAttribute("aria-hidden", "true");
|
|
96
|
+
star.textContent = "*";
|
|
97
|
+
lbl.appendChild(star);
|
|
98
|
+
const sr = document.createElement("span");
|
|
99
|
+
sr.className = "sr-only";
|
|
100
|
+
sr.textContent = " (required)";
|
|
101
|
+
lbl.appendChild(sr);
|
|
102
|
+
}
|
|
103
|
+
return lbl;
|
|
104
|
+
}
|
|
105
|
+
export function buildHelp(def) {
|
|
106
|
+
if (!def.help)
|
|
107
|
+
return null;
|
|
108
|
+
const help = document.createElement("div");
|
|
109
|
+
help.id = `${def.id}-help`;
|
|
110
|
+
help.className = "cobd-field-help";
|
|
111
|
+
help.innerHTML = def.help;
|
|
112
|
+
return help;
|
|
113
|
+
}
|
|
114
|
+
export function buildError(def) {
|
|
115
|
+
// Always render the error slot (even when
|
|
116
|
+
// empty) so consumers can swap text into it
|
|
117
|
+
// at runtime without restructuring the DOM.
|
|
118
|
+
const err = document.createElement("div");
|
|
119
|
+
err.id = `${def.id}-error`;
|
|
120
|
+
err.className = "cobd-field-error";
|
|
121
|
+
err.setAttribute("role", "alert");
|
|
122
|
+
err.setAttribute("aria-live", "polite");
|
|
123
|
+
if (def.error)
|
|
124
|
+
err.innerHTML = def.error;
|
|
125
|
+
return err;
|
|
126
|
+
}
|
|
127
|
+
// Compose aria-describedby pointing at help +
|
|
128
|
+
// error slots (whichever are present). The
|
|
129
|
+
// control's aria-describedby gets this string
|
|
130
|
+
// so screen readers announce them in order.
|
|
131
|
+
export function describedByValue(def) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
if (def.help)
|
|
134
|
+
parts.push(`${def.id}-help`);
|
|
135
|
+
parts.push(`${def.id}-error`);
|
|
136
|
+
return parts.join(" ");
|
|
137
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Aggregate entry: importing this module registers
|
|
2
|
+
// every cobd-* form element as a side effect. For
|
|
3
|
+
// tree-shaking, each element is also exported on
|
|
4
|
+
// its own subpath:
|
|
5
|
+
//
|
|
6
|
+
// import "@cobdfamily/clf-core/components/forms/textfield";
|
|
7
|
+
// import "@cobdfamily/clf-core/components/forms/select";
|
|
8
|
+
// ...
|
|
9
|
+
import "./textfield.js";
|
|
10
|
+
import "./textarea.js";
|
|
11
|
+
import "./select.js";
|
|
12
|
+
import "./checkbox.js";
|
|
13
|
+
import "./submit.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// <cobd-select name="..." label="..." [value="..."]
|
|
2
|
+
// [help="..."] [error="..."] [required]>
|
|
3
|
+
// <option value="ferry">Ferry</option>
|
|
4
|
+
// <option value="lift">Lift</option>
|
|
5
|
+
// </cobd-select>
|
|
6
|
+
//
|
|
7
|
+
// Select with options as Light-DOM children. The
|
|
8
|
+
// child <option> elements are read on render and
|
|
9
|
+
// translated to <ion-select-option> when Ionic is
|
|
10
|
+
// present, kept as <option> when not.
|
|
11
|
+
//
|
|
12
|
+
// Form-associated like cobd-textfield.
|
|
13
|
+
import { ionicPresent, ensureBaseStyle, readAttr, readFieldDef, buildLabel, buildHelp, buildError, describedByValue, } from "./common.js";
|
|
14
|
+
function readOptions(host, initialValue) {
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const opt of Array.from(host.querySelectorAll("option"))) {
|
|
17
|
+
const v = opt.getAttribute("value") ?? opt.textContent ?? "";
|
|
18
|
+
out.push({
|
|
19
|
+
value: v,
|
|
20
|
+
label: opt.textContent?.trim() ?? v,
|
|
21
|
+
selected: opt.hasAttribute("selected")
|
|
22
|
+
|| v === initialValue,
|
|
23
|
+
disabled: opt.hasAttribute("disabled"),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
class CobdSelect extends HTMLElement {
|
|
29
|
+
static get observedAttributes() {
|
|
30
|
+
return [
|
|
31
|
+
"name", "label", "value", "help",
|
|
32
|
+
"error", "required", "id",
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
constructor() {
|
|
36
|
+
super();
|
|
37
|
+
this.snapshot = [];
|
|
38
|
+
this.rendered = false;
|
|
39
|
+
this.internals = this.attachInternals();
|
|
40
|
+
}
|
|
41
|
+
connectedCallback() {
|
|
42
|
+
ensureBaseStyle();
|
|
43
|
+
// Snapshot the option children once -- the
|
|
44
|
+
// render path replaceChildren()s the host,
|
|
45
|
+
// which would otherwise wipe them.
|
|
46
|
+
if (!this.rendered) {
|
|
47
|
+
this.snapshot = readOptions(this, readAttr(this, "value") ?? "");
|
|
48
|
+
this.render();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
attributeChangedCallback() {
|
|
52
|
+
if (this.isConnected && this.rendered)
|
|
53
|
+
this.render();
|
|
54
|
+
}
|
|
55
|
+
render() {
|
|
56
|
+
const def = readFieldDef(this, "cobd-select");
|
|
57
|
+
const value = readAttr(this, "value") ?? "";
|
|
58
|
+
const options = this.snapshot.map(o => ({
|
|
59
|
+
...o,
|
|
60
|
+
selected: o.value === value || o.selected,
|
|
61
|
+
}));
|
|
62
|
+
const row = document.createElement("div");
|
|
63
|
+
row.className = "cobd-field-row";
|
|
64
|
+
row.appendChild(buildLabel(def));
|
|
65
|
+
const control = ionicPresent()
|
|
66
|
+
? this.buildIonSelect(def, options)
|
|
67
|
+
: this.buildPlainSelect(def, options);
|
|
68
|
+
row.appendChild(control);
|
|
69
|
+
const help = buildHelp(def);
|
|
70
|
+
if (help)
|
|
71
|
+
row.appendChild(help);
|
|
72
|
+
row.appendChild(buildError(def));
|
|
73
|
+
this.replaceChildren(row);
|
|
74
|
+
const initial = options.find(o => o.selected)?.value
|
|
75
|
+
?? options[0]?.value ?? "";
|
|
76
|
+
this.internals.setFormValue(initial);
|
|
77
|
+
this.rendered = true;
|
|
78
|
+
}
|
|
79
|
+
buildIonSelect(def, options) {
|
|
80
|
+
const it = document.createElement("ion-select");
|
|
81
|
+
it.setAttribute("id", def.id);
|
|
82
|
+
if (def.name)
|
|
83
|
+
it.setAttribute("name", def.name);
|
|
84
|
+
it.setAttribute("label-placement", "stacked");
|
|
85
|
+
if (def.required)
|
|
86
|
+
it.setAttribute("required", "");
|
|
87
|
+
it.setAttribute("aria-describedby", describedByValue(def));
|
|
88
|
+
for (const opt of options) {
|
|
89
|
+
const o = document.createElement("ion-select-option");
|
|
90
|
+
o.setAttribute("value", opt.value);
|
|
91
|
+
if (opt.disabled)
|
|
92
|
+
o.setAttribute("disabled", "");
|
|
93
|
+
o.textContent = opt.label;
|
|
94
|
+
if (opt.selected) {
|
|
95
|
+
it.setAttribute("value", opt.value);
|
|
96
|
+
}
|
|
97
|
+
it.appendChild(o);
|
|
98
|
+
}
|
|
99
|
+
it.addEventListener("ionChange", () => this.syncFrom(it));
|
|
100
|
+
return it;
|
|
101
|
+
}
|
|
102
|
+
buildPlainSelect(def, options) {
|
|
103
|
+
const it = document.createElement("select");
|
|
104
|
+
it.id = def.id;
|
|
105
|
+
if (def.name)
|
|
106
|
+
it.name = def.name;
|
|
107
|
+
if (def.required)
|
|
108
|
+
it.required = true;
|
|
109
|
+
it.setAttribute("aria-describedby", describedByValue(def));
|
|
110
|
+
for (const opt of options) {
|
|
111
|
+
const o = document.createElement("option");
|
|
112
|
+
o.value = opt.value;
|
|
113
|
+
if (opt.disabled)
|
|
114
|
+
o.disabled = true;
|
|
115
|
+
o.textContent = opt.label;
|
|
116
|
+
if (opt.selected)
|
|
117
|
+
o.selected = true;
|
|
118
|
+
it.appendChild(o);
|
|
119
|
+
}
|
|
120
|
+
it.addEventListener("change", () => this.syncFrom(it));
|
|
121
|
+
return it;
|
|
122
|
+
}
|
|
123
|
+
syncFrom(el) {
|
|
124
|
+
const v = el.value;
|
|
125
|
+
const str = v == null ? "" : String(v);
|
|
126
|
+
this.internals.setFormValue(str);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
CobdSelect.formAssociated = true;
|
|
130
|
+
if (!customElements.get("cobd-select")) {
|
|
131
|
+
customElements.define("cobd-select", CobdSelect);
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// <cobd-submit label="Save"
|
|
2
|
+
// [action="/api/save"]
|
|
3
|
+
// [method="post"]>
|
|
4
|
+
//
|
|
5
|
+
// Form submit button. When the host wraps it in a
|
|
6
|
+
// <form>, the button submits that form (browser
|
|
7
|
+
// default). When the action/method attrs are set,
|
|
8
|
+
// the element wraps itself in its own <form> so
|
|
9
|
+
// the button stands alone -- useful for one-off
|
|
10
|
+
// "post and go" actions where the surrounding
|
|
11
|
+
// markup isn't already a form.
|
|
12
|
+
//
|
|
13
|
+
// Renders <ion-button> when Ionic is present, a
|
|
14
|
+
// plain <button> otherwise. The element is NOT
|
|
15
|
+
// form-associated for value purposes (a submit
|
|
16
|
+
// button has no submittable value of its own);
|
|
17
|
+
// it just provides the trigger.
|
|
18
|
+
import { ionicPresent, ensureBaseStyle, readAttr, } from "./common.js";
|
|
19
|
+
class CobdSubmit extends HTMLElement {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
this.rendered = false;
|
|
23
|
+
}
|
|
24
|
+
static get observedAttributes() {
|
|
25
|
+
return ["label", "action", "method", "color"];
|
|
26
|
+
}
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
ensureBaseStyle();
|
|
29
|
+
if (!this.rendered)
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
attributeChangedCallback() {
|
|
33
|
+
if (this.isConnected && this.rendered)
|
|
34
|
+
this.render();
|
|
35
|
+
}
|
|
36
|
+
render() {
|
|
37
|
+
const label = readAttr(this, "label") ?? "Submit";
|
|
38
|
+
const action = readAttr(this, "action");
|
|
39
|
+
const method = (readAttr(this, "method")
|
|
40
|
+
?? "post").toLowerCase();
|
|
41
|
+
const color = readAttr(this, "color") ?? "primary";
|
|
42
|
+
const button = ionicPresent()
|
|
43
|
+
? this.buildIonButton(label, color)
|
|
44
|
+
: this.buildPlainButton(label);
|
|
45
|
+
// Standalone-form mode: if `action` is set,
|
|
46
|
+
// the consumer wants this button to submit
|
|
47
|
+
// its own form (rather than a surrounding
|
|
48
|
+
// one). Wrap the button in a <form>.
|
|
49
|
+
if (action) {
|
|
50
|
+
const form = document.createElement("form");
|
|
51
|
+
form.action = action;
|
|
52
|
+
form.method = method;
|
|
53
|
+
form.appendChild(button);
|
|
54
|
+
this.replaceChildren(form);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.replaceChildren(button);
|
|
58
|
+
}
|
|
59
|
+
this.rendered = true;
|
|
60
|
+
}
|
|
61
|
+
buildIonButton(label, color) {
|
|
62
|
+
const it = document.createElement("ion-button");
|
|
63
|
+
it.setAttribute("type", "submit");
|
|
64
|
+
it.setAttribute("color", color);
|
|
65
|
+
it.setAttribute("expand", "block");
|
|
66
|
+
it.textContent = label;
|
|
67
|
+
return it;
|
|
68
|
+
}
|
|
69
|
+
buildPlainButton(label) {
|
|
70
|
+
const it = document.createElement("button");
|
|
71
|
+
it.type = "submit";
|
|
72
|
+
it.textContent = label;
|
|
73
|
+
return it;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!customElements.get("cobd-submit")) {
|
|
77
|
+
customElements.define("cobd-submit", CobdSubmit);
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// <cobd-textarea name="..." label="..."
|
|
2
|
+
// [value="..."] [placeholder="..."]
|
|
3
|
+
// [help="..."] [error="..."]
|
|
4
|
+
// [required] [rows="..."]>
|
|
5
|
+
//
|
|
6
|
+
// Same shape as <cobd-textfield>, multi-line.
|
|
7
|
+
// Form-associated. Ionic auto-detect.
|
|
8
|
+
import { ionicPresent, ensureBaseStyle, readAttr, readFieldDef, buildLabel, buildHelp, buildError, describedByValue, } from "./common.js";
|
|
9
|
+
class CobdTextarea extends HTMLElement {
|
|
10
|
+
static get observedAttributes() {
|
|
11
|
+
return [
|
|
12
|
+
"name", "label", "value", "placeholder",
|
|
13
|
+
"help", "error", "required", "rows", "id",
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.rendered = false;
|
|
19
|
+
this.internals = this.attachInternals();
|
|
20
|
+
}
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
ensureBaseStyle();
|
|
23
|
+
if (!this.rendered)
|
|
24
|
+
this.render();
|
|
25
|
+
}
|
|
26
|
+
attributeChangedCallback() {
|
|
27
|
+
if (this.isConnected && this.rendered)
|
|
28
|
+
this.render();
|
|
29
|
+
}
|
|
30
|
+
render() {
|
|
31
|
+
const def = readFieldDef(this, "cobd-textarea");
|
|
32
|
+
const value = readAttr(this, "value") ?? "";
|
|
33
|
+
const placeholder = readAttr(this, "placeholder") ?? "";
|
|
34
|
+
const rows = readAttr(this, "rows") ?? "4";
|
|
35
|
+
const row = document.createElement("div");
|
|
36
|
+
row.className = "cobd-field-row";
|
|
37
|
+
row.appendChild(buildLabel(def));
|
|
38
|
+
const control = ionicPresent()
|
|
39
|
+
? this.buildIonTextarea(def, value, placeholder, rows)
|
|
40
|
+
: this.buildPlainTextarea(def, value, placeholder, rows);
|
|
41
|
+
row.appendChild(control);
|
|
42
|
+
const help = buildHelp(def);
|
|
43
|
+
if (help)
|
|
44
|
+
row.appendChild(help);
|
|
45
|
+
row.appendChild(buildError(def));
|
|
46
|
+
this.replaceChildren(row);
|
|
47
|
+
this.internals.setFormValue(value);
|
|
48
|
+
this.rendered = true;
|
|
49
|
+
}
|
|
50
|
+
buildIonTextarea(def, value, placeholder, rows) {
|
|
51
|
+
const it = document.createElement("ion-textarea");
|
|
52
|
+
it.setAttribute("id", def.id);
|
|
53
|
+
if (def.name)
|
|
54
|
+
it.setAttribute("name", def.name);
|
|
55
|
+
it.setAttribute("label-placement", "stacked");
|
|
56
|
+
if (value)
|
|
57
|
+
it.setAttribute("value", value);
|
|
58
|
+
if (placeholder)
|
|
59
|
+
it.setAttribute("placeholder", placeholder);
|
|
60
|
+
if (rows)
|
|
61
|
+
it.setAttribute("rows", rows);
|
|
62
|
+
if (def.required)
|
|
63
|
+
it.setAttribute("required", "");
|
|
64
|
+
it.setAttribute("aria-describedby", describedByValue(def));
|
|
65
|
+
it.addEventListener("ionInput", () => this.syncFrom(it));
|
|
66
|
+
it.addEventListener("ionChange", () => this.syncFrom(it));
|
|
67
|
+
return it;
|
|
68
|
+
}
|
|
69
|
+
buildPlainTextarea(def, value, placeholder, rows) {
|
|
70
|
+
const it = document.createElement("textarea");
|
|
71
|
+
it.id = def.id;
|
|
72
|
+
if (def.name)
|
|
73
|
+
it.name = def.name;
|
|
74
|
+
if (placeholder)
|
|
75
|
+
it.placeholder = placeholder;
|
|
76
|
+
if (rows)
|
|
77
|
+
it.rows = Number(rows);
|
|
78
|
+
if (def.required)
|
|
79
|
+
it.required = true;
|
|
80
|
+
it.value = value;
|
|
81
|
+
it.setAttribute("aria-describedby", describedByValue(def));
|
|
82
|
+
it.addEventListener("input", () => this.syncFrom(it));
|
|
83
|
+
it.addEventListener("change", () => this.syncFrom(it));
|
|
84
|
+
return it;
|
|
85
|
+
}
|
|
86
|
+
syncFrom(el) {
|
|
87
|
+
const v = el.value;
|
|
88
|
+
const str = v == null ? "" : String(v);
|
|
89
|
+
this.internals.setFormValue(str);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
CobdTextarea.formAssociated = true;
|
|
93
|
+
if (!customElements.get("cobd-textarea")) {
|
|
94
|
+
customElements.define("cobd-textarea", CobdTextarea);
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// <cobd-textfield name="..." label="..." [type="..."]
|
|
2
|
+
// [value="..."] [placeholder="..."]
|
|
3
|
+
// [help="..."] [error="..."]
|
|
4
|
+
// [required] [maxlength="..."]>
|
|
5
|
+
//
|
|
6
|
+
// Attribute-driven, form-associated text input.
|
|
7
|
+
// Form-Associated Custom Elements API (ElementInternals)
|
|
8
|
+
// makes the element participate in <form> submission
|
|
9
|
+
// natively -- the form sees `name=value` on POST as if
|
|
10
|
+
// the element were a plain <input>.
|
|
11
|
+
//
|
|
12
|
+
// Render mode:
|
|
13
|
+
//
|
|
14
|
+
// Ionic present -> <ion-input> + custom label,
|
|
15
|
+
// help, error slots around it.
|
|
16
|
+
// Ionic absent -> plain <input> with the same
|
|
17
|
+
// semantic surround.
|
|
18
|
+
//
|
|
19
|
+
// In both modes the element follows the cobd-* design
|
|
20
|
+
// tokens via CSS variables on <html>; no inline
|
|
21
|
+
// colours.
|
|
22
|
+
import { ionicPresent, ensureBaseStyle, readAttr, readFieldDef, buildLabel, buildHelp, buildError, describedByValue, } from "./common.js";
|
|
23
|
+
const KNOWN_TYPES = new Set([
|
|
24
|
+
"text", "email", "password", "tel", "url",
|
|
25
|
+
"number", "search",
|
|
26
|
+
]);
|
|
27
|
+
class CobdTextfield extends HTMLElement {
|
|
28
|
+
static get observedAttributes() {
|
|
29
|
+
return [
|
|
30
|
+
"name", "label", "type", "value",
|
|
31
|
+
"placeholder", "help", "error",
|
|
32
|
+
"required", "maxlength", "id",
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
constructor() {
|
|
36
|
+
super();
|
|
37
|
+
this.rendered = false;
|
|
38
|
+
this.internals = this.attachInternals();
|
|
39
|
+
}
|
|
40
|
+
connectedCallback() {
|
|
41
|
+
ensureBaseStyle();
|
|
42
|
+
if (!this.rendered)
|
|
43
|
+
this.render();
|
|
44
|
+
}
|
|
45
|
+
attributeChangedCallback() {
|
|
46
|
+
if (this.isConnected && this.rendered)
|
|
47
|
+
this.render();
|
|
48
|
+
}
|
|
49
|
+
render() {
|
|
50
|
+
const def = readFieldDef(this, "cobd-textfield");
|
|
51
|
+
const type = (readAttr(this, "type") ?? "text").toLowerCase();
|
|
52
|
+
const safeType = KNOWN_TYPES.has(type) ? type : "text";
|
|
53
|
+
const value = readAttr(this, "value") ?? "";
|
|
54
|
+
const placeholder = readAttr(this, "placeholder") ?? "";
|
|
55
|
+
const maxlength = readAttr(this, "maxlength");
|
|
56
|
+
const row = document.createElement("div");
|
|
57
|
+
row.className = "cobd-field-row";
|
|
58
|
+
row.appendChild(buildLabel(def));
|
|
59
|
+
const control = ionicPresent()
|
|
60
|
+
? this.buildIonInput(def, safeType, value, placeholder, maxlength)
|
|
61
|
+
: this.buildPlainInput(def, safeType, value, placeholder, maxlength);
|
|
62
|
+
row.appendChild(control);
|
|
63
|
+
const help = buildHelp(def);
|
|
64
|
+
if (help)
|
|
65
|
+
row.appendChild(help);
|
|
66
|
+
row.appendChild(buildError(def));
|
|
67
|
+
this.replaceChildren(row);
|
|
68
|
+
this.internals.setFormValue(value);
|
|
69
|
+
this.rendered = true;
|
|
70
|
+
}
|
|
71
|
+
buildIonInput(def, type, value, placeholder, maxlength) {
|
|
72
|
+
const it = document.createElement("ion-input");
|
|
73
|
+
it.setAttribute("id", def.id);
|
|
74
|
+
if (def.name)
|
|
75
|
+
it.setAttribute("name", def.name);
|
|
76
|
+
it.setAttribute("type", type);
|
|
77
|
+
it.setAttribute("label-placement", "stacked");
|
|
78
|
+
if (value)
|
|
79
|
+
it.setAttribute("value", value);
|
|
80
|
+
if (placeholder)
|
|
81
|
+
it.setAttribute("placeholder", placeholder);
|
|
82
|
+
if (maxlength)
|
|
83
|
+
it.setAttribute("maxlength", maxlength);
|
|
84
|
+
if (def.required)
|
|
85
|
+
it.setAttribute("required", "");
|
|
86
|
+
it.setAttribute("aria-describedby", describedByValue(def));
|
|
87
|
+
// ion-input fires ionInput / ionChange; mirror
|
|
88
|
+
// both to ElementInternals so <form> POST sees
|
|
89
|
+
// the latest value.
|
|
90
|
+
it.addEventListener("ionInput", () => this.syncValueFrom(it));
|
|
91
|
+
it.addEventListener("ionChange", () => this.syncValueFrom(it));
|
|
92
|
+
return it;
|
|
93
|
+
}
|
|
94
|
+
buildPlainInput(def, type, value, placeholder, maxlength) {
|
|
95
|
+
const it = document.createElement("input");
|
|
96
|
+
it.id = def.id;
|
|
97
|
+
if (def.name)
|
|
98
|
+
it.name = def.name;
|
|
99
|
+
it.type = type;
|
|
100
|
+
if (value)
|
|
101
|
+
it.value = value;
|
|
102
|
+
if (placeholder)
|
|
103
|
+
it.placeholder = placeholder;
|
|
104
|
+
if (maxlength)
|
|
105
|
+
it.maxLength = Number(maxlength);
|
|
106
|
+
if (def.required)
|
|
107
|
+
it.required = true;
|
|
108
|
+
it.setAttribute("aria-describedby", describedByValue(def));
|
|
109
|
+
it.addEventListener("input", () => this.syncValueFrom(it));
|
|
110
|
+
it.addEventListener("change", () => this.syncValueFrom(it));
|
|
111
|
+
return it;
|
|
112
|
+
}
|
|
113
|
+
syncValueFrom(el) {
|
|
114
|
+
// ion-input has its own `value` getter (number
|
|
115
|
+
// | string | null); HTMLInputElement is just
|
|
116
|
+
// string. Coerce uniformly.
|
|
117
|
+
const v = el.value;
|
|
118
|
+
const str = v == null ? "" : String(v);
|
|
119
|
+
this.internals.setFormValue(str);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
CobdTextfield.formAssociated = true;
|
|
123
|
+
if (!customElements.get("cobd-textfield")) {
|
|
124
|
+
customElements.define("cobd-textfield", CobdTextfield);
|
|
125
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Side-effect bundle: importing this module
|
|
2
|
+
// registers every clf-core custom element AND
|
|
3
|
+
// boots the theming runtime. One import = one
|
|
4
|
+
// `<script type="module" src=".../components/
|
|
5
|
+
// index.js">` covers the whole element surface.
|
|
6
|
+
//
|
|
7
|
+
// Why this exists: most CLF consumers (Hugo, WP,
|
|
8
|
+
// Keycloak, Moodle, CiviCRM, static HTML) don't
|
|
9
|
+
// ship a JS module pipeline. The clf-factory
|
|
10
|
+
// chrome auto-loads this one bundle from the
|
|
11
|
+
// CDN, with one SRI integrity attr, and every
|
|
12
|
+
// element is then available globally inside
|
|
13
|
+
// authored body content. Consumers with a real
|
|
14
|
+
// bundler can keep importing per-element entry
|
|
15
|
+
// points (`@cobdfamily/clf-core/components/
|
|
16
|
+
// cobd-embed`, etc.) for tree-shaking; that
|
|
17
|
+
// surface is unchanged.
|
|
18
|
+
//
|
|
19
|
+
// Order matters for theming/init: it has to
|
|
20
|
+
// run before the toggles register, otherwise a
|
|
21
|
+
// toggle that ticks at definition time
|
|
22
|
+
// (touching the runtime singleton) wouldn't
|
|
23
|
+
// find the state machine initialized. Putting
|
|
24
|
+
// it first here is enough; everything below is
|
|
25
|
+
// a side-effect import so the order of
|
|
26
|
+
// definitions matches the order of imports.
|
|
27
|
+
import "../theming/init.js";
|
|
28
|
+
import "./theme-toggle.js";
|
|
29
|
+
import "./font-scale-toggle.js";
|
|
30
|
+
import "./cobd-nav.js";
|
|
31
|
+
import "./cobd-support.js";
|
|
32
|
+
import "./cobd-embed.js";
|
|
33
|
+
import "./forms/index.js";
|