@pairbo/ui-kit 0.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/.husky/pre-commit +1 -0
- package/.prettierignore +16 -0
- package/.prettierrc.json +17 -0
- package/README.md +61 -0
- package/cspell.json +9 -0
- package/dev.html +101 -0
- package/docs/README.md +1 -0
- package/docs/_includes/component.njk +16 -0
- package/docs/_includes/default.njk +39 -0
- package/docs/_includes/sidebar.njk +16 -0
- package/docs/eleventy.config.mjs +72 -0
- package/docs/pages/components/message-selector.md +17 -0
- package/docs/pages/fabric-example.html +46 -0
- package/docs/pages/fabric-example.js +28 -0
- package/docs/pages/index.md +76 -0
- package/eslint.config.mjs +32 -0
- package/ignote_temp +3 -0
- package/index.html +162 -0
- package/lint-stage.confg.js +6 -0
- package/package.json +66 -0
- package/pages/card-selection.html +65 -0
- package/pages/drawer.html +47 -0
- package/pages/editor.html +45 -0
- package/pages/page-mgn.html +51 -0
- package/pages/test_build.html +47 -0
- package/public/Greeting Card from Pairbo.png +0 -0
- package/scripts/plop/plopfile.js +51 -0
- package/scripts/plop/templates/components/component.hbs +34 -0
- package/scripts/plop/templates/components/define.hbs +10 -0
- package/scripts/plop/templates/components/styles.hbs +7 -0
- package/src/components/button/button.component.ts +93 -0
- package/src/components/button/button.styles.ts +273 -0
- package/src/components/button/button.ts +10 -0
- package/src/components/button-group/button-group.component.ts +36 -0
- package/src/components/button-group/button-group.styles.ts +7 -0
- package/src/components/button-group/button-group.ts +10 -0
- package/src/components/card-selection/card-selection.component.ts +43 -0
- package/src/components/card-selection/card-selection.styles.ts +7 -0
- package/src/components/card-selection/card-selection.ts +10 -0
- package/src/components/category/category.component.ts +91 -0
- package/src/components/category/category.styles.ts +27 -0
- package/src/components/category/category.ts +10 -0
- package/src/components/category-image/category-image.component.ts +38 -0
- package/src/components/category-image/category-image.styles.ts +11 -0
- package/src/components/category-image/category-image.ts +10 -0
- package/src/components/drawer/drawer.component.ts +82 -0
- package/src/components/drawer/drawer.styles.ts +54 -0
- package/src/components/drawer/drawer.ts +10 -0
- package/src/components/editor/editor.component.ts +135 -0
- package/src/components/editor/editor.styles.ts +13 -0
- package/src/components/editor/editor.ts +10 -0
- package/src/components/fabric-example/fabric-example.component.ts +268 -0
- package/src/components/fabric-example/fabric-example.styles.ts +23 -0
- package/src/components/fabric-example/fabric-example.test.ts +0 -0
- package/src/components/fabric-example/fabric-example.ts +12 -0
- package/src/components/image-slider/editor-card-slider.component.ts +136 -0
- package/src/components/image-slider/editor-card-slider.styles.ts +46 -0
- package/src/components/image-slider/editor-card-slider.ts +9 -0
- package/src/components/main.ts +17 -0
- package/src/components/message-selector/message-selector.component.ts +154 -0
- package/src/components/message-selector/message-selector.styles.ts +16 -0
- package/src/components/message-selector/message-selector.test.ts +64 -0
- package/src/components/message-selector/message-selector.ts +13 -0
- package/src/components/page-manager/page-manager.component.ts +228 -0
- package/src/components/page-manager/page-manager.styles.ts +9 -0
- package/src/components/page-manager/page-manager.ts +10 -0
- package/src/components/radio-button/radio-button.component.ts +118 -0
- package/src/components/radio-button/radio-button.styles.ts +13 -0
- package/src/components/radio-button/radio-button.ts +10 -0
- package/src/components/radio-group/radio-group.component.ts +203 -0
- package/src/components/radio-group/radio-group.styles.ts +19 -0
- package/src/components/radio-group/radio-group.ts +10 -0
- package/src/components/selector/selector.component.ts +115 -0
- package/src/components/selector/selector.styles.ts +9 -0
- package/src/components/selector/selector.ts +10 -0
- package/src/components/textarea/textarea.component.ts +234 -0
- package/src/components/textarea/textarea.styles.ts +178 -0
- package/src/components/textarea/textarea.ts +10 -0
- package/src/components/type-form/type-form.component.ts +121 -0
- package/src/components/type-form/type-form.styles.ts +7 -0
- package/src/components/type-form/type-form.ts +10 -0
- package/src/declaration.d.ts +44 -0
- package/src/events/events.ts +1 -0
- package/src/events/pbo-category-card-select.ts +7 -0
- package/src/internal/form.ts +376 -0
- package/src/internal/pairbo-element.ts +85 -0
- package/src/internal/slots.ts +54 -0
- package/src/internal/watch.ts +79 -0
- package/src/styles/component.styles.ts +17 -0
- package/src/styles/form-control.styles.ts +59 -0
- package/src/themes/default.css +414 -0
- package/temp +20 -0
- package/tsconfig.json +28 -0
- package/vite.config.ts +26 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
declare module "*.css" {
|
|
2
|
+
const styles: string;
|
|
3
|
+
export default styles;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Declare that CSS files can be imported as a string
|
|
7
|
+
declare module "*.css?inline" {
|
|
8
|
+
const content: string;
|
|
9
|
+
export default content;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare interface Card {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
category: string | string[];
|
|
16
|
+
medias: {
|
|
17
|
+
cover: {
|
|
18
|
+
url: string;
|
|
19
|
+
alt: string;
|
|
20
|
+
};
|
|
21
|
+
back: {
|
|
22
|
+
url: string;
|
|
23
|
+
alt: string;
|
|
24
|
+
};
|
|
25
|
+
inner: {
|
|
26
|
+
url: string;
|
|
27
|
+
alt: string;
|
|
28
|
+
};
|
|
29
|
+
render_1: {
|
|
30
|
+
url: string;
|
|
31
|
+
alt: string;
|
|
32
|
+
};
|
|
33
|
+
render_2: {
|
|
34
|
+
url: string;
|
|
35
|
+
alt: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare interface Category {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
cards: Card[];
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { PboCategoryCardSelectEvent } from "./pbo-category-card-select.js";
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { ReactiveController, ReactiveControllerHost } from "lit";
|
|
2
|
+
import { PairboFormControl } from "./pairbo-element";
|
|
3
|
+
import { PboButton } from "../components/main";
|
|
4
|
+
|
|
5
|
+
export const formCollections: WeakMap<HTMLFormElement, Set<PairboFormControl>> = new WeakMap();
|
|
6
|
+
|
|
7
|
+
// We store a WeakMap of reportValidity() overloads so we can override it when form controls connect to the DOM and
|
|
8
|
+
// restore the original behavior when they disconnect.
|
|
9
|
+
//
|
|
10
|
+
const reportValidityOverloads: WeakMap<HTMLFormElement, () => boolean> = new WeakMap();
|
|
11
|
+
const checkValidityOverloads: WeakMap<HTMLFormElement, () => boolean> = new WeakMap();
|
|
12
|
+
|
|
13
|
+
//
|
|
14
|
+
// We store a Set of controls that users have interacted with. This allows us to determine the interaction state
|
|
15
|
+
// without littering the DOM with additional data attributes.
|
|
16
|
+
//
|
|
17
|
+
const userInteractedControls: WeakSet<PairboFormControl> = new WeakSet();
|
|
18
|
+
|
|
19
|
+
//
|
|
20
|
+
// We store a WeakMap of interactions for each form control so we can track when all conditions are met for validation.
|
|
21
|
+
//
|
|
22
|
+
const interactions = new WeakMap<PairboFormControl, string[]>();
|
|
23
|
+
|
|
24
|
+
export interface FormControlControllerOptions {
|
|
25
|
+
// The form element that the control is associated with.
|
|
26
|
+
form: (input: PairboFormControl) => HTMLFormElement | null;
|
|
27
|
+
// The name of the control.
|
|
28
|
+
name: (input: PairboFormControl) => string;
|
|
29
|
+
// The default value of the control.
|
|
30
|
+
defaultValue: (input: PairboFormControl) => unknown;
|
|
31
|
+
// The value of the control.
|
|
32
|
+
value: (input: PairboFormControl) => unknown | unknown[];
|
|
33
|
+
// Whether the control is disabled.
|
|
34
|
+
disabled: (input: PairboFormControl) => boolean;
|
|
35
|
+
// When the control is invalid, the error message that should be shown to the user.
|
|
36
|
+
reportValidity: (input: PairboFormControl) => boolean;
|
|
37
|
+
//check if the control is valid
|
|
38
|
+
checkValidity: (input: PairboFormControl) => boolean;
|
|
39
|
+
// set value of the control
|
|
40
|
+
setValue: (input: PairboFormControl, value: unknown) => void;
|
|
41
|
+
// An array of event names to listen to.
|
|
42
|
+
assumeInteractionOn: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class FormControlController implements ReactiveController {
|
|
46
|
+
host: PairboFormControl & ReactiveControllerHost;
|
|
47
|
+
form?: HTMLFormElement | null;
|
|
48
|
+
options: FormControlControllerOptions;
|
|
49
|
+
|
|
50
|
+
constructor(host: PairboFormControl & ReactiveControllerHost, options?: Partial<FormControlControllerOptions>) {
|
|
51
|
+
console.log({ host });
|
|
52
|
+
(this.host = host).addController(this);
|
|
53
|
+
this.options = {
|
|
54
|
+
form: input => {
|
|
55
|
+
const formId = input.form;
|
|
56
|
+
|
|
57
|
+
if (formId) {
|
|
58
|
+
const root = input.getRootNode() as Document | ShadowRoot | HTMLElement;
|
|
59
|
+
const form = root.querySelector(`#${formId}`);
|
|
60
|
+
|
|
61
|
+
if (form) {
|
|
62
|
+
return form as HTMLFormElement;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return input.closest("form");
|
|
66
|
+
},
|
|
67
|
+
name: input => input.name,
|
|
68
|
+
value: input => input.value,
|
|
69
|
+
defaultValue: input => input.defaultValue,
|
|
70
|
+
disabled: input => input.disabled || false,
|
|
71
|
+
reportValidity: input => (typeof input.reportValidity === "function" ? input.reportValidity() : true),
|
|
72
|
+
checkValidity: input => (typeof input.checkValidity === "function" ? input.checkValidity() : true),
|
|
73
|
+
setValue: (input, value: string) => (input.value = value),
|
|
74
|
+
assumeInteractionOn: ["pbo-input"],
|
|
75
|
+
...options,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
hostConnected() {
|
|
80
|
+
const form = this.options.form(this.host);
|
|
81
|
+
|
|
82
|
+
if (form) {
|
|
83
|
+
this.attachForm(form);
|
|
84
|
+
}
|
|
85
|
+
// Listen for interactions
|
|
86
|
+
interactions.set(this.host, []);
|
|
87
|
+
console.log({ interactions });
|
|
88
|
+
|
|
89
|
+
this.options.assumeInteractionOn.forEach(event => {
|
|
90
|
+
this.host.addEventListener(event, this.handleInteraction);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
hostDisconnected() {
|
|
94
|
+
this.detachForm();
|
|
95
|
+
|
|
96
|
+
// Clean up interactions
|
|
97
|
+
interactions.delete(this.host);
|
|
98
|
+
this.options.assumeInteractionOn.forEach(event => {
|
|
99
|
+
this.host.removeEventListener(event, this.handleInteraction);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
hostUpdated() {
|
|
104
|
+
const form = this.options.form(this.host);
|
|
105
|
+
|
|
106
|
+
// Detach if the form no longer exists
|
|
107
|
+
if (!form) {
|
|
108
|
+
this.detachForm();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If the form has changed, reattach it
|
|
112
|
+
if (form && this.form !== form) {
|
|
113
|
+
this.detachForm();
|
|
114
|
+
this.attachForm(form);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.host.hasUpdated) {
|
|
118
|
+
this.setValidity(this.host.validity.valid);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private attachForm(form?: HTMLFormElement) {
|
|
123
|
+
if (form) {
|
|
124
|
+
this.form = form;
|
|
125
|
+
|
|
126
|
+
// Add this element to the form's collection
|
|
127
|
+
if (formCollections.has(this.form)) {
|
|
128
|
+
formCollections.get(this.form)!.add(this.host);
|
|
129
|
+
} else {
|
|
130
|
+
formCollections.set(this.form, new Set<PairboFormControl>([this.host]));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.form.addEventListener("formdata", this.handleFormData);
|
|
134
|
+
this.form.addEventListener("submit", this.handleFormSubmit);
|
|
135
|
+
this.form.addEventListener("reset", this.handleFormReset);
|
|
136
|
+
|
|
137
|
+
// Overload the form's reportValidity() method so it looks at Shoelace form controls
|
|
138
|
+
if (!reportValidityOverloads.has(this.form)) {
|
|
139
|
+
reportValidityOverloads.set(this.form, this.form.reportValidity);
|
|
140
|
+
this.form.reportValidity = () => this.reportFormValidity();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Overload the form's checkValidity() method so it looks at Shoelace form controls
|
|
144
|
+
if (!checkValidityOverloads.has(this.form)) {
|
|
145
|
+
checkValidityOverloads.set(this.form, this.form.checkValidity);
|
|
146
|
+
this.form.checkValidity = () => this.checkFormValidity();
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
this.form = undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
private detachForm() {
|
|
153
|
+
if (!this.form) return;
|
|
154
|
+
|
|
155
|
+
const formCollection = formCollections.get(this.form);
|
|
156
|
+
|
|
157
|
+
if (!formCollection) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Remove this host from the form's collection
|
|
162
|
+
formCollection.delete(this.host);
|
|
163
|
+
|
|
164
|
+
// Check to make sure there's no other form controls in the collection. If we do this
|
|
165
|
+
// without checking if any other controls are still in the collection, then we will wipe out the
|
|
166
|
+
// validity checks for all other elements.
|
|
167
|
+
// see: https://github.com/shoelace-style/shoelace/issues/1703
|
|
168
|
+
if (formCollection.size <= 0) {
|
|
169
|
+
this.form.removeEventListener("formdata", this.handleFormData);
|
|
170
|
+
this.form.removeEventListener("submit", this.handleFormSubmit);
|
|
171
|
+
this.form.removeEventListener("reset", this.handleFormReset);
|
|
172
|
+
|
|
173
|
+
// Remove the overload and restore the original method
|
|
174
|
+
if (reportValidityOverloads.has(this.form)) {
|
|
175
|
+
this.form.reportValidity = reportValidityOverloads.get(this.form)!;
|
|
176
|
+
reportValidityOverloads.delete(this.form);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (checkValidityOverloads.has(this.form)) {
|
|
180
|
+
this.form.checkValidity = checkValidityOverloads.get(this.form)!;
|
|
181
|
+
checkValidityOverloads.delete(this.form);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// So it looks weird here to not always set the form to undefined. But I _think_ if we unattach this.form here,
|
|
185
|
+
// we end up in this fun spot where future validity checks don't have a reference to the form validity handler.
|
|
186
|
+
// First form element in sets the validity handler. So we can't clean up `this.form` until there are no other form elements in the form.
|
|
187
|
+
this.form = undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Append the control's name and value to the given FormData object.
|
|
192
|
+
private handleFormData = (event: FormDataEvent) => {
|
|
193
|
+
const disabled = this.options.disabled(this.host);
|
|
194
|
+
const name = this.options.name(this.host);
|
|
195
|
+
const value = this.options.value(this.host);
|
|
196
|
+
if (
|
|
197
|
+
this.host.isConnected &&
|
|
198
|
+
!disabled &&
|
|
199
|
+
typeof name === "string" &&
|
|
200
|
+
name.length > 0 &&
|
|
201
|
+
typeof value !== "undefined"
|
|
202
|
+
) {
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
(value as unknown[]).forEach(val => {
|
|
205
|
+
event.formData.append(name, (val as string | number | boolean).toString());
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
event.formData.append(name, (value as string | number | boolean).toString());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
private handleFormSubmit = (event: Event) => {
|
|
214
|
+
const disabled = this.options.disabled(this.host);
|
|
215
|
+
const reportValidity = this.options.reportValidity;
|
|
216
|
+
if (this.form && !this.form.noValidate) {
|
|
217
|
+
formCollections.get(this.form)?.forEach(control => {
|
|
218
|
+
this.setUserInteracted(control, true);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.form && !this.form.noValidate && !disabled && !reportValidity(this.host)) {
|
|
223
|
+
event?.preventDefault();
|
|
224
|
+
event?.stopImmediatePropagation();
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
private handleFormReset = () => {
|
|
229
|
+
this.options.setValue(this.host, this.options.defaultValue(this.host));
|
|
230
|
+
this.setUserInteracted(this.host, false);
|
|
231
|
+
interactions.set(this.host, []);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
private handleInteraction = (event: Event) => {
|
|
235
|
+
const emittedEvents = interactions.get(this.host)!;
|
|
236
|
+
if (!emittedEvents.includes(event.type)) {
|
|
237
|
+
emittedEvents.push(event.type);
|
|
238
|
+
}
|
|
239
|
+
if (emittedEvents.length === this.options.assumeInteractionOn.length) {
|
|
240
|
+
this.setUserInteracted(this.host, true);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
private checkFormValidity = () => {
|
|
245
|
+
if (this.form && !this.form.noValidate) {
|
|
246
|
+
const elements = this.form.querySelectorAll<HTMLInputElement>("*");
|
|
247
|
+
for (const element of elements) {
|
|
248
|
+
if (typeof element.checkValidity === "function") {
|
|
249
|
+
if (!element.checkValidity()) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
};
|
|
257
|
+
private reportFormValidity() {
|
|
258
|
+
if (this.form && !this.form.noValidate) {
|
|
259
|
+
const elements = this.form.querySelectorAll<HTMLInputElement>("*");
|
|
260
|
+
for (const element of elements) {
|
|
261
|
+
if (typeof element.reportValidity === "function") {
|
|
262
|
+
if (!element.reportValidity()) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private setUserInteracted(el: PairboFormControl, hasInteracted: boolean) {
|
|
272
|
+
if (hasInteracted) {
|
|
273
|
+
userInteractedControls.add(el);
|
|
274
|
+
} else {
|
|
275
|
+
userInteractedControls.delete(el);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
el.requestUpdate();
|
|
279
|
+
}
|
|
280
|
+
private doAction = (type: "submit" | "reset", submitter?: HTMLInputElement | PboButton) => {
|
|
281
|
+
if (this.form) {
|
|
282
|
+
const button = document.createElement("button");
|
|
283
|
+
button.type = type;
|
|
284
|
+
button.style.position = "absolute";
|
|
285
|
+
button.style.width = "0";
|
|
286
|
+
button.style.height = "0";
|
|
287
|
+
button.style.clipPath = "inset(50%)";
|
|
288
|
+
button.style.overflow = "hidden";
|
|
289
|
+
button.style.whiteSpace = "nowrap";
|
|
290
|
+
if (submitter) {
|
|
291
|
+
button.name = submitter.name;
|
|
292
|
+
button.value = submitter.value;
|
|
293
|
+
|
|
294
|
+
["formation", "formenctype", "formmethod", "formnovalidate", "formtarget"].forEach(attr => {
|
|
295
|
+
if (submitter.hasAttribute(attr)) {
|
|
296
|
+
button.setAttribute(attr, submitter.getAttribute(attr)!);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.form.append(button);
|
|
302
|
+
button.click();
|
|
303
|
+
button.remove();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
getForm() {
|
|
308
|
+
return this.form ?? null;
|
|
309
|
+
}
|
|
310
|
+
reset(submitter?: HTMLInputElement | PboButton) {
|
|
311
|
+
this.doAction("reset", submitter);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
submit(submitter?: HTMLInputElement | PboButton) {
|
|
315
|
+
this.doAction("submit", submitter);
|
|
316
|
+
}
|
|
317
|
+
setValidity(isValid: boolean) {
|
|
318
|
+
const host = this.host;
|
|
319
|
+
const hasInteracted = Boolean(userInteractedControls.has(host));
|
|
320
|
+
const required = Boolean(host.required);
|
|
321
|
+
|
|
322
|
+
host.toggleAttribute("data-required", required);
|
|
323
|
+
host.toggleAttribute("data-optional", !required);
|
|
324
|
+
host.toggleAttribute("data-invalid", !isValid);
|
|
325
|
+
host.toggleAttribute("data-valid", isValid);
|
|
326
|
+
host.toggleAttribute("data-user-invalid", hasInteracted && !isValid);
|
|
327
|
+
host.toggleAttribute("data-user-valid", hasInteracted && isValid);
|
|
328
|
+
}
|
|
329
|
+
updateValidity() {
|
|
330
|
+
const host = this.host;
|
|
331
|
+
this.setValidity(host.validity.valid);
|
|
332
|
+
}
|
|
333
|
+
emitInvalidEvent(originalInvalidEvent?: Event) {
|
|
334
|
+
const pboInvalidEvent = new CustomEvent<Record<PropertyKey, never>>("pbo-invalid", {
|
|
335
|
+
bubbles: false,
|
|
336
|
+
composed: false,
|
|
337
|
+
cancelable: true,
|
|
338
|
+
detail: {},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (!originalInvalidEvent) {
|
|
342
|
+
pboInvalidEvent.preventDefault();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!this.host.dispatchEvent(pboInvalidEvent)) {
|
|
346
|
+
originalInvalidEvent?.preventDefault();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export const validValidityState: ValidityState = Object.freeze({
|
|
352
|
+
badInput: false,
|
|
353
|
+
customError: false,
|
|
354
|
+
patternMismatch: false,
|
|
355
|
+
rangeOverflow: false,
|
|
356
|
+
rangeUnderflow: false,
|
|
357
|
+
stepMismatch: false,
|
|
358
|
+
tooLong: false,
|
|
359
|
+
tooShort: false,
|
|
360
|
+
typeMismatch: false,
|
|
361
|
+
valid: true,
|
|
362
|
+
valueMissing: false,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// A validity state object that represents `value missing`
|
|
366
|
+
export const valueMissingValidityState: ValidityState = Object.freeze({
|
|
367
|
+
...validValidityState,
|
|
368
|
+
valid: false,
|
|
369
|
+
valueMissing: false,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
export const customErrorValidityState: ValidityState = Object.freeze({
|
|
373
|
+
...validValidityState,
|
|
374
|
+
valid: false,
|
|
375
|
+
customError: true,
|
|
376
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interface representing a Pairbo form control element.
|
|
5
|
+
* Extends LitElement to provide form control functionality.
|
|
6
|
+
*
|
|
7
|
+
* @interface PairboFormControl
|
|
8
|
+
* @extends {LitElement}
|
|
9
|
+
*
|
|
10
|
+
* @property {string} name - The name of the form control
|
|
11
|
+
* @property {unknown} value - The current value of the form control
|
|
12
|
+
* @property {boolean} [disabled] - Whether the form control is disabled
|
|
13
|
+
* @property {unknown} [defaultValue] - The default value of the form control
|
|
14
|
+
* @property {boolean} [defaultChecked] - The default checked state for checkable controls
|
|
15
|
+
* @property {string} [form] - The id of the form this control belongs to
|
|
16
|
+
* @property {string} [pattern] - Regular expression pattern for validation
|
|
17
|
+
* @property {number|string|Date} [min] - Minimum allowed value
|
|
18
|
+
* @property {number|string|Date} [max] - Maximum allowed value
|
|
19
|
+
* @property {number|"any"} [step] - Step increment value
|
|
20
|
+
* @property {boolean} [required] - Whether the field is required
|
|
21
|
+
* @property {number} [minLength] - Minimum length for text input
|
|
22
|
+
* @property {number} [maxLength] - Maximum length for text input
|
|
23
|
+
* @property {ValidityState} validity - The ValidityState object representing validation state
|
|
24
|
+
* @property {string} validationMessage - The validation message
|
|
25
|
+
*
|
|
26
|
+
* @method checkValidity - Checks if the element's value satisfies validation constraints
|
|
27
|
+
* @returns {boolean} True if the element's value is valid, false otherwise
|
|
28
|
+
*
|
|
29
|
+
* @method getForm - Gets the form element that contains this control
|
|
30
|
+
* @returns {HTMLFormElement|null} The parent form element or null if not found
|
|
31
|
+
*
|
|
32
|
+
* @method reportValidity - Reports validity to the user through UI feedback
|
|
33
|
+
* @returns {boolean} True if the element's value is valid, false otherwise
|
|
34
|
+
*
|
|
35
|
+
* @method setCustomValidity - Sets a custom validation message
|
|
36
|
+
* @param {string} error - The custom error message to display
|
|
37
|
+
*/
|
|
38
|
+
export interface PairboFormControl extends LitElement {
|
|
39
|
+
name: string;
|
|
40
|
+
value: unknown;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
defaultValue?: unknown;
|
|
43
|
+
defaultChecked?: boolean;
|
|
44
|
+
form?: string;
|
|
45
|
+
|
|
46
|
+
pattern?: string;
|
|
47
|
+
min?: number | string | Date;
|
|
48
|
+
max?: number | string | Date;
|
|
49
|
+
step?: number | "any";
|
|
50
|
+
required?: boolean;
|
|
51
|
+
minlength?: number;
|
|
52
|
+
maxlength?: number;
|
|
53
|
+
|
|
54
|
+
readonly validity: ValidityState;
|
|
55
|
+
readonly validationMessage: string;
|
|
56
|
+
|
|
57
|
+
checkValidity: () => boolean;
|
|
58
|
+
getForm: () => HTMLFormElement | null;
|
|
59
|
+
reportValidity: () => boolean;
|
|
60
|
+
setCustomValidity: (error: string) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default class PairboElement extends LitElement {
|
|
64
|
+
/* --------------------------- Emits custom events -------------------------- */
|
|
65
|
+
emit(name: string, options?: CustomEventInit<unknown> | undefined) {
|
|
66
|
+
/**
|
|
67
|
+
* Creates a new CustomEvent instance with specified event name and options.
|
|
68
|
+
* @property {boolean} bubbles - Whether the event can bubble up through the Dom
|
|
69
|
+
* @property {boolean} cancelable - Indicates whether the event is cancelable and therefore prevented as if the event never happened. To Cancel the event, call preventDefault() on the event.
|
|
70
|
+
* @property {boolean} composed - Indicates whether or not the event will propagate across the shadow Dom boundary into the standard DOM.
|
|
71
|
+
* @property {Object} detail - Returns any custom data event was created with. Typically used for synthetic events. For the custom event.
|
|
72
|
+
*/
|
|
73
|
+
const event = new CustomEvent(name, {
|
|
74
|
+
bubbles: true,
|
|
75
|
+
cancelable: true,
|
|
76
|
+
composed: true,
|
|
77
|
+
detail: {},
|
|
78
|
+
...options,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.dispatchEvent(event);
|
|
82
|
+
|
|
83
|
+
return event;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
|
2
|
+
|
|
3
|
+
export class HasSlotController implements ReactiveController {
|
|
4
|
+
host: ReactiveControllerHost & Element;
|
|
5
|
+
slotNames: string[] = [];
|
|
6
|
+
constructor(host: ReactiveControllerHost & Element, ...slotNames: string[]) {
|
|
7
|
+
(this.host = host).addController(this);
|
|
8
|
+
this.slotNames = slotNames;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private hasDefaultSlot() {
|
|
12
|
+
return [...this.host.childNodes].some(node => {
|
|
13
|
+
// The node is a text node and it's not empty
|
|
14
|
+
if (node.nodeType === node.TEXT_NODE && node.textContent?.trim() !== "") {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The node is an element node
|
|
19
|
+
if (node.nodeType === node.ELEMENT_NODE) {
|
|
20
|
+
const element = node as Element;
|
|
21
|
+
const tagName = element.tagName.toLowerCase();
|
|
22
|
+
// If the tag is visually hidden, ignore it
|
|
23
|
+
if (tagName === "pbo-visually-hidden") {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// So if the element has no slot attribute, it's in the default slot
|
|
27
|
+
if (!element.hasAttribute("slot")) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private hasNamedSlot(name: string) {
|
|
36
|
+
return this.host.querySelector(`:scope > [slot="${name}]`) !== null;
|
|
37
|
+
}
|
|
38
|
+
test(slotName: string) {
|
|
39
|
+
return slotName === "[default]" ? this.hasDefaultSlot() : this.hasNamedSlot(slotName);
|
|
40
|
+
}
|
|
41
|
+
hostConnected() {
|
|
42
|
+
this.host.shadowRoot!.addEventListener("slotchange", this.handleSlotChange);
|
|
43
|
+
}
|
|
44
|
+
hostDisconnected() {
|
|
45
|
+
this.host.shadowRoot!.removeEventListener("slotchange", this.handleSlotChange);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private handleSlotChange = (event: Event) => {
|
|
49
|
+
const slot = event.target as HTMLSlotElement;
|
|
50
|
+
if ((this.slotNames.includes("[default]") && !slot.name) || (slot.name && this.slotNames.includes(slot.name))) {
|
|
51
|
+
this.host.requestUpdate();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { waitUntil } from "@open-wc/testing";
|
|
2
|
+
import { LitElement } from "lit";
|
|
3
|
+
// Define the handler function type
|
|
4
|
+
type UpdateHandler = (prev?: unknown, next?: unknown) => void;
|
|
5
|
+
|
|
6
|
+
// Check the generic type, remove undefined
|
|
7
|
+
type NonUndefined<A> = A extends undefined ? never : A;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the keys of the updates handler inside one class.
|
|
11
|
+
*
|
|
12
|
+
* keyof will only extract the keys which are non-never
|
|
13
|
+
*
|
|
14
|
+
* The modified object
|
|
15
|
+
* { [key in keyof T] -?: NonUndefined<T[key]> extends UpdateHandler ? key : never; }
|
|
16
|
+
*
|
|
17
|
+
* Get the keys in the object: [key in keyof T]
|
|
18
|
+
*
|
|
19
|
+
* Remove the undefined keys: -?
|
|
20
|
+
*
|
|
21
|
+
* Ternary operator A ? B : C - Check if the type is UpdateHandler: NonUndefined<T[Key]> extends UpdateHandler
|
|
22
|
+
* If true, set the value of current key to the key name
|
|
23
|
+
* Else set the value of current key to never
|
|
24
|
+
* After that, we use the mapped type to get the keys of the object ExampleObject[keyof ExampleObject] will return an union of they key values, and the never value will be removed
|
|
25
|
+
**/
|
|
26
|
+
type UpdateHandlerKeys<T extends Object> = {
|
|
27
|
+
[key in keyof T]-?: NonUndefined<T[key]> extends UpdateHandler ? key : never;
|
|
28
|
+
}[keyof T];
|
|
29
|
+
|
|
30
|
+
type Func = (first: string, second: number, last?: boolean) => void;
|
|
31
|
+
type FunctionKeys = keyof (Func extends Object ? Func : never);
|
|
32
|
+
type Option = {
|
|
33
|
+
waitUntilFirstUpdate: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function watch(propertyName: string | string[], options?: Option) {
|
|
37
|
+
const resolvedOptions: Required<Option> = {
|
|
38
|
+
waitUntilFirstUpdate: false,
|
|
39
|
+
...options,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* target: The class to be decorated
|
|
44
|
+
* decoratedFnName: The name of the function to be decorated
|
|
45
|
+
*/
|
|
46
|
+
return function <EleClass extends LitElement>(target: EleClass, decoratedFnName: UpdateHandlerKeys<EleClass>) {
|
|
47
|
+
// If the property name is string, convert it into a string array
|
|
48
|
+
const watchedProperty = Array.isArray(propertyName) ? propertyName : [propertyName];
|
|
49
|
+
|
|
50
|
+
// @ts-ignore update is protected in the LitElement
|
|
51
|
+
// Store the original update function
|
|
52
|
+
const { update } = target;
|
|
53
|
+
|
|
54
|
+
// This will create a new update function, but it will
|
|
55
|
+
// wrap the original update function, so it will not be overridden
|
|
56
|
+
|
|
57
|
+
// @ts-ignore update is protected in the LitElement
|
|
58
|
+
target.update = function (this: EleClass, changeProps: Map<keyof EleClass, EleClass[keyof ElemClass]>) {
|
|
59
|
+
// Loop through the watched properties
|
|
60
|
+
watchedProperty.forEach(property => {
|
|
61
|
+
const key = property as keyof EleClass;
|
|
62
|
+
if (changeProps.has(key)) {
|
|
63
|
+
const oldValue = changeProps.get(key);
|
|
64
|
+
const newValue = this[key];
|
|
65
|
+
|
|
66
|
+
if (oldValue !== newValue) {
|
|
67
|
+
if (!resolvedOptions.waitUntilFirstUpdate || this.hasUpdated) {
|
|
68
|
+
(this[decoratedFnName] as unknown as UpdateHandler)(oldValue, newValue);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// @ts-ignore update is protected in the LitElement
|
|
75
|
+
// Call the original update function
|
|
76
|
+
update.call(this, changeProps);
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|