@stanko/ctrls 0.3.4 → 0.4.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 +81 -5
- package/dist/ctrls/ctrl-boolean.d.ts +2 -2
- package/dist/ctrls/ctrl-boolean.js +1 -1
- package/dist/ctrls/ctrl-dual-range.d.ts +2 -2
- package/dist/ctrls/ctrl-dual-range.js +1 -1
- package/dist/ctrls/ctrl-easing.d.ts +2 -2
- package/dist/ctrls/ctrl-easing.js +1 -1
- package/dist/ctrls/ctrl-file.d.ts +27 -0
- package/dist/ctrls/ctrl-file.js +99 -0
- package/dist/ctrls/ctrl-html.d.ts +2 -0
- package/dist/ctrls/ctrl-html.js +13 -0
- package/dist/ctrls/ctrl-radio.d.ts +2 -2
- package/dist/ctrls/ctrl-range.d.ts +2 -2
- package/dist/ctrls/ctrl-range.js +1 -1
- package/dist/ctrls/ctrl-seed.d.ts +2 -2
- package/dist/ctrls/ctrl-seed.js +1 -1
- package/dist/ctrls/dom.d.ts +12 -0
- package/dist/ctrls/dom.js +16 -0
- package/dist/ctrls/index.d.ts +3 -127
- package/dist/ctrls/index.js +93 -85
- package/dist/ctrls/types.d.ts +146 -0
- package/dist/ctrls/types.js +1 -0
- package/dist/ctrls.css +198 -89
- package/dist/ctrls.css.map +1 -1
- package/dist/utils/icons.d.ts +2 -0
- package/dist/utils/icons.js +2 -0
- package/dist/utils/random.d.ts +1 -1
- package/package.json +5 -5
package/dist/ctrls/index.js
CHANGED
|
@@ -6,8 +6,10 @@ import { EasingCtrl } from "./ctrl-easing";
|
|
|
6
6
|
import { RadioCtrl } from "./ctrl-radio";
|
|
7
7
|
import { RangeCtrl } from "./ctrl-range";
|
|
8
8
|
import { SeedCtrl } from "./ctrl-seed";
|
|
9
|
+
import { FileCtrl } from "./ctrl-file";
|
|
9
10
|
import Alea from "../utils/alea";
|
|
10
|
-
import { diceIcon } from "../utils/icons";
|
|
11
|
+
import { chevronUpIcon, diceIcon } from "../utils/icons";
|
|
12
|
+
import { getHTMLControlElement } from "./ctrl-html";
|
|
11
13
|
const controlMap = {
|
|
12
14
|
boolean: BooleanCtrl,
|
|
13
15
|
range: RangeCtrl,
|
|
@@ -15,84 +17,82 @@ const controlMap = {
|
|
|
15
17
|
seed: SeedCtrl,
|
|
16
18
|
easing: EasingCtrl,
|
|
17
19
|
"dual-range": DualRangeCtrl,
|
|
20
|
+
file: FileCtrl,
|
|
18
21
|
};
|
|
19
22
|
export class Ctrls {
|
|
20
23
|
constructor(configs, options) {
|
|
21
24
|
this.controls = [];
|
|
22
25
|
this.controlsMap = {};
|
|
26
|
+
this.processControls = (configs) => {
|
|
27
|
+
const elements = [];
|
|
28
|
+
// Handlers shared by all controls
|
|
29
|
+
const onChangeControlHandler = (control) => {
|
|
30
|
+
const updatedValues = this.updateValuesObject({}, control);
|
|
31
|
+
this.onChange?.(updatedValues);
|
|
32
|
+
if (this.options.storage === "hash") {
|
|
33
|
+
this.setHash();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const onInputControlHandler = (control) => {
|
|
37
|
+
const updatedValues = this.updateValuesObject({}, control);
|
|
38
|
+
this.onInput?.(updatedValues);
|
|
39
|
+
};
|
|
40
|
+
// Processing configs and creating component instances and HTML elements
|
|
41
|
+
configs.map((config) => {
|
|
42
|
+
if (config.type === "group") {
|
|
43
|
+
// Create group element
|
|
44
|
+
const groupElement = document.createElement("div");
|
|
45
|
+
groupElement.classList.add("ctrls__group");
|
|
46
|
+
if (config.isCollapsed) {
|
|
47
|
+
groupElement.classList.add("ctrls__group--hidden");
|
|
48
|
+
}
|
|
49
|
+
const groupTitle = document.createElement("button");
|
|
50
|
+
groupTitle.classList.add("ctrls__group-title");
|
|
51
|
+
groupTitle.innerHTML =
|
|
52
|
+
(config.label || toSpaceCase(config.name)) + chevronUpIcon;
|
|
53
|
+
groupTitle.addEventListener("click", () => {
|
|
54
|
+
groupTitle.parentElement?.classList.toggle("ctrls__group--hidden");
|
|
55
|
+
});
|
|
56
|
+
// Add title
|
|
57
|
+
groupElement.append(groupTitle);
|
|
58
|
+
config.controls.forEach((itemConfig) => {
|
|
59
|
+
const control = this.registerControl(itemConfig, onChangeControlHandler, onInputControlHandler, toCamelCase(config.name));
|
|
60
|
+
// Add the control elements to the group element
|
|
61
|
+
groupElement.append(control?.element);
|
|
62
|
+
});
|
|
63
|
+
// Add the group element
|
|
64
|
+
elements.push(groupElement);
|
|
65
|
+
}
|
|
66
|
+
else if (config.type === "html") {
|
|
67
|
+
// HTML controls are just rendered on their own
|
|
68
|
+
elements.push(getHTMLControlElement(config));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const control = this.registerControl(config, onChangeControlHandler, onInputControlHandler);
|
|
72
|
+
// Add the control element
|
|
73
|
+
elements.push(control.element);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return elements;
|
|
77
|
+
};
|
|
23
78
|
this.registerControl = (config, onChangeControlHandler, onInputControlHandler, group = "") => {
|
|
24
|
-
//
|
|
25
|
-
if (config.type === "group") {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
// TODO
|
|
29
|
-
// Again, document as it is my personal preference
|
|
79
|
+
// It is my personal preference is to use space case for labels
|
|
30
80
|
if (!config.label) {
|
|
31
81
|
config.label = toSpaceCase(config.name);
|
|
32
82
|
}
|
|
33
|
-
//
|
|
34
|
-
// Document this behaviour
|
|
35
|
-
// This might counter-intuitive for some people,
|
|
36
|
-
// but it is my personal preference to have properties named in camel case
|
|
37
|
-
// when using them in code
|
|
38
|
-
//
|
|
39
|
-
// However, they are going to be converted to kebab case when used in the hash,
|
|
40
|
-
// because it is nicer that URL be all lowercase
|
|
83
|
+
// Another personal preference of mine is to have properties named in camel case when using them in code
|
|
41
84
|
config.name = toCamelCase(config.name);
|
|
85
|
+
// Add group properties to the control config
|
|
42
86
|
if (group) {
|
|
43
87
|
config.group = group;
|
|
44
88
|
config.id = toCamelCase(`${group}-${config.name}`);
|
|
45
89
|
}
|
|
90
|
+
// Instantiate the control component
|
|
46
91
|
const ControlComponent = controlMap[config.type];
|
|
47
92
|
const control = new ControlComponent(config, onChangeControlHandler, onInputControlHandler);
|
|
48
93
|
this.controlsMap[control.id] = control;
|
|
49
94
|
this.controls.push(control);
|
|
50
|
-
|
|
51
|
-
this.buildUI = () => {
|
|
52
|
-
const element = document.createElement("div");
|
|
53
|
-
element.classList.add("ctrls");
|
|
54
|
-
element.classList.add(`ctrls--${this.options.theme}-theme`);
|
|
55
|
-
const controlsContainer = document.createElement("div");
|
|
56
|
-
controlsContainer.classList.add("ctrls__controls");
|
|
57
|
-
let group = "";
|
|
58
|
-
let groupElement;
|
|
59
|
-
this.controls.forEach((control) => {
|
|
60
|
-
if (control.group) {
|
|
61
|
-
if (control.group !== group) {
|
|
62
|
-
group = control.group;
|
|
63
|
-
groupElement = document.createElement("div");
|
|
64
|
-
groupElement.classList.add("ctrls__group");
|
|
65
|
-
const groupTitle = document.createElement("button");
|
|
66
|
-
groupTitle.classList.add("ctrls__group-title");
|
|
67
|
-
groupTitle.innerText = control.group;
|
|
68
|
-
groupTitle.addEventListener("click", () => {
|
|
69
|
-
groupTitle.parentElement?.classList.toggle("ctrls__group--hidden");
|
|
70
|
-
});
|
|
71
|
-
groupElement.append(groupTitle);
|
|
72
|
-
controlsContainer.appendChild(groupElement);
|
|
73
|
-
}
|
|
74
|
-
groupElement.append(control.element);
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
controlsContainer.appendChild(control.element);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
if (this.options.showRandomizeButton) {
|
|
81
|
-
const randomizeButton = document.createElement("button");
|
|
82
|
-
randomizeButton.classList.add("ctrls__randomize", "ctrls__btn", "ctrls__btn--lg");
|
|
83
|
-
randomizeButton.innerHTML = `Randomize ${diceIcon}`;
|
|
84
|
-
randomizeButton.addEventListener("click", this.randomize);
|
|
85
|
-
controlsContainer.appendChild(randomizeButton);
|
|
86
|
-
}
|
|
87
|
-
if (this.options.title) {
|
|
88
|
-
const titleButton = document.createElement("button");
|
|
89
|
-
titleButton.classList.add("ctrls__title");
|
|
90
|
-
titleButton.innerText = this.options.title;
|
|
91
|
-
titleButton.addEventListener("click", this.toggleVisibility);
|
|
92
|
-
element.appendChild(titleButton);
|
|
93
|
-
}
|
|
94
|
-
element.appendChild(controlsContainer);
|
|
95
|
-
return element;
|
|
95
|
+
return control;
|
|
96
96
|
};
|
|
97
97
|
this.toggleVisibility = () => {
|
|
98
98
|
this.element.classList.toggle("ctrls--hidden");
|
|
@@ -107,6 +107,7 @@ export class Ctrls {
|
|
|
107
107
|
};
|
|
108
108
|
this.getHash = () => {
|
|
109
109
|
const values = this.controls
|
|
110
|
+
.filter((control) => control.type !== "file")
|
|
110
111
|
.map((control) => {
|
|
111
112
|
return `${toKebabCase(control.id)}:${control.valueToString()}`;
|
|
112
113
|
})
|
|
@@ -170,34 +171,41 @@ export class Ctrls {
|
|
|
170
171
|
theme: "system",
|
|
171
172
|
...options,
|
|
172
173
|
};
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
configs.map((config) => {
|
|
185
|
-
if (config.type === "group") {
|
|
186
|
-
config.controls.forEach((groupConfig) => {
|
|
187
|
-
this.registerControl(groupConfig, onChangeControlHandler, onInputControlHandler, toCamelCase(config.name));
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
this.registerControl(config, onChangeControlHandler, onInputControlHandler);
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
this.element = this.buildUI();
|
|
195
|
-
if (this.options.storage === "hash") {
|
|
196
|
-
this.addHashListeners();
|
|
174
|
+
// Main element
|
|
175
|
+
this.element = document.createElement("div");
|
|
176
|
+
this.element.classList.add("ctrls");
|
|
177
|
+
this.element.classList.add(`ctrls--${this.options.theme}-theme`);
|
|
178
|
+
// Title
|
|
179
|
+
if (this.options.title) {
|
|
180
|
+
const titleButton = document.createElement("button");
|
|
181
|
+
titleButton.classList.add("ctrls__title");
|
|
182
|
+
titleButton.innerHTML = this.options.title + chevronUpIcon;
|
|
183
|
+
titleButton.addEventListener("click", this.toggleVisibility);
|
|
184
|
+
this.element.appendChild(titleButton);
|
|
197
185
|
}
|
|
186
|
+
// Controls wrapper
|
|
187
|
+
const controlsContainer = document.createElement("div");
|
|
188
|
+
controlsContainer.classList.add("ctrls__controls");
|
|
189
|
+
this.element.appendChild(controlsContainer);
|
|
190
|
+
// Controls
|
|
191
|
+
const controlElements = this.processControls(configs);
|
|
192
|
+
controlsContainer.append(...controlElements);
|
|
193
|
+
// Randomize button
|
|
194
|
+
if (this.options.showRandomizeButton) {
|
|
195
|
+
const randomizeButton = document.createElement("button");
|
|
196
|
+
randomizeButton.classList.add("ctrls__randomize", "ctrls__btn", "ctrls__btn--lg");
|
|
197
|
+
randomizeButton.innerHTML = `Randomize ${diceIcon}`;
|
|
198
|
+
randomizeButton.addEventListener("click", this.randomize);
|
|
199
|
+
controlsContainer.appendChild(randomizeButton);
|
|
200
|
+
}
|
|
201
|
+
// Append the Ctrls element to the provided parent element
|
|
198
202
|
if (this.options.parent) {
|
|
199
203
|
this.options.parent.appendChild(this.element);
|
|
200
204
|
}
|
|
205
|
+
// Hash storage
|
|
206
|
+
if (this.options.storage === "hash") {
|
|
207
|
+
this.addHashListeners();
|
|
208
|
+
}
|
|
201
209
|
}
|
|
202
210
|
updateValuesObject(values, control) {
|
|
203
211
|
let objectToUpdate = values;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type BezierEasing from "bezier-easing";
|
|
2
|
+
import type { BooleanCtrl } from "./ctrl-boolean";
|
|
3
|
+
import type { DualRangeValue, DualRangeCtrl } from "./ctrl-dual-range";
|
|
4
|
+
import type { Easing, EasingCtrl } from "./ctrl-easing";
|
|
5
|
+
import type { RadioCtrl } from "./ctrl-radio";
|
|
6
|
+
import type { RangeCtrl } from "./ctrl-range";
|
|
7
|
+
import type { SeedCtrl } from "./ctrl-seed";
|
|
8
|
+
import type { FileCtrl } from "./ctrl-file";
|
|
9
|
+
export interface PRNG {
|
|
10
|
+
(): number;
|
|
11
|
+
}
|
|
12
|
+
export type HashItem = {
|
|
13
|
+
id: string;
|
|
14
|
+
value: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type CtrlControlType = "boolean" | "range" | "radio" | "seed" | "easing" | "dual-range" | "file";
|
|
17
|
+
export type CtrlItemType = CtrlControlType | "group" | "html";
|
|
18
|
+
export type CtrlChangeHandler = (control: CtrlComponent) => void;
|
|
19
|
+
export type CtrlConfig<T = unknown> = {
|
|
20
|
+
type: CtrlItemType;
|
|
21
|
+
id?: string;
|
|
22
|
+
name: string;
|
|
23
|
+
accept?: string;
|
|
24
|
+
group?: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
defaultValue?: T;
|
|
27
|
+
isRandomizationDisabled?: boolean;
|
|
28
|
+
};
|
|
29
|
+
export interface Ctrl<T> {
|
|
30
|
+
id: string;
|
|
31
|
+
group?: string;
|
|
32
|
+
name: string;
|
|
33
|
+
label?: string;
|
|
34
|
+
type: CtrlItemType;
|
|
35
|
+
isRandomizationDisabled: boolean;
|
|
36
|
+
onChange: CtrlChangeHandler;
|
|
37
|
+
parse: (value: string) => T;
|
|
38
|
+
getRandomValue: () => T;
|
|
39
|
+
getDefaultValue: () => T;
|
|
40
|
+
buildUI: () => unknown;
|
|
41
|
+
valueToString: (value?: T) => string;
|
|
42
|
+
update: (value: T) => void;
|
|
43
|
+
element: HTMLElement;
|
|
44
|
+
}
|
|
45
|
+
export interface CtrlTypeMap {
|
|
46
|
+
boolean: {
|
|
47
|
+
value: boolean;
|
|
48
|
+
};
|
|
49
|
+
range: {
|
|
50
|
+
value: number;
|
|
51
|
+
min: number;
|
|
52
|
+
max: number;
|
|
53
|
+
step?: number;
|
|
54
|
+
};
|
|
55
|
+
radio: {
|
|
56
|
+
value: string;
|
|
57
|
+
items: Record<string, string>;
|
|
58
|
+
columns?: 1 | 2 | 3 | 4 | 5;
|
|
59
|
+
};
|
|
60
|
+
seed: {
|
|
61
|
+
value: string;
|
|
62
|
+
};
|
|
63
|
+
easing: {
|
|
64
|
+
value: Easing;
|
|
65
|
+
presets?: Record<string, Easing>;
|
|
66
|
+
};
|
|
67
|
+
"dual-range": {
|
|
68
|
+
value: DualRangeValue;
|
|
69
|
+
min: number;
|
|
70
|
+
max: number;
|
|
71
|
+
step?: number;
|
|
72
|
+
};
|
|
73
|
+
group: {
|
|
74
|
+
value: Record<string, unknown>;
|
|
75
|
+
controls: readonly TypedControlConfig[];
|
|
76
|
+
isRandomizationDisabled?: boolean;
|
|
77
|
+
};
|
|
78
|
+
html: {
|
|
79
|
+
html: HTMLElement;
|
|
80
|
+
};
|
|
81
|
+
file: {
|
|
82
|
+
value: File | null;
|
|
83
|
+
accept?: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export type TypedControlConfig = {
|
|
87
|
+
[K in CtrlControlType]: {
|
|
88
|
+
type: K;
|
|
89
|
+
id?: string;
|
|
90
|
+
name: string;
|
|
91
|
+
group?: string;
|
|
92
|
+
label?: string;
|
|
93
|
+
defaultValue?: CtrlTypeMap[K]["value"];
|
|
94
|
+
isRandomizationDisabled?: boolean;
|
|
95
|
+
} & Omit<CtrlTypeMap[K], "value">;
|
|
96
|
+
}[CtrlControlType];
|
|
97
|
+
export type GroupConfig = {
|
|
98
|
+
type: "group";
|
|
99
|
+
name: string;
|
|
100
|
+
label?: string;
|
|
101
|
+
controls: readonly TypedControlConfig[];
|
|
102
|
+
isCollapsed?: boolean;
|
|
103
|
+
};
|
|
104
|
+
export type HTMLConfig = {
|
|
105
|
+
type: "html";
|
|
106
|
+
name: string;
|
|
107
|
+
label?: string;
|
|
108
|
+
html: HTMLElement;
|
|
109
|
+
};
|
|
110
|
+
export type ConfigItem = TypedControlConfig | GroupConfig | HTMLConfig;
|
|
111
|
+
export type ConfigFor<T extends CtrlItemType> = Extract<TypedControlConfig, {
|
|
112
|
+
type: T;
|
|
113
|
+
}>;
|
|
114
|
+
type ExtractValues<Configs extends readonly ConfigItem[]> = {
|
|
115
|
+
[C in Extract<Configs[number], {
|
|
116
|
+
type: CtrlControlType;
|
|
117
|
+
}> as C["name"]]: CtrlTypeMap[C["type"]]["value"];
|
|
118
|
+
} & {
|
|
119
|
+
[C in Extract<Configs[number], {
|
|
120
|
+
type: "group";
|
|
121
|
+
}> as C["name"]]: OptionsMap<C["controls"]>;
|
|
122
|
+
};
|
|
123
|
+
type DerivedProps<Configs extends readonly ConfigItem[]> = {
|
|
124
|
+
[C in Extract<Configs[number], {
|
|
125
|
+
type: "easing";
|
|
126
|
+
}> as `${C["name"]}Easing`]: ReturnType<typeof BezierEasing>;
|
|
127
|
+
} & {
|
|
128
|
+
[C in Extract<Configs[number], {
|
|
129
|
+
type: "seed";
|
|
130
|
+
}> as `${C["name"]}Rng`]: PRNG;
|
|
131
|
+
} & {
|
|
132
|
+
[C in Extract<Configs[number], {
|
|
133
|
+
type: "group";
|
|
134
|
+
}> as C["name"]]: DerivedProps<C["controls"]>;
|
|
135
|
+
};
|
|
136
|
+
export type OptionsMap<Configs extends readonly ConfigItem[]> = ExtractValues<Configs> & DerivedProps<Configs>;
|
|
137
|
+
export type ControlsOptions = {
|
|
138
|
+
showRandomizeButton?: boolean;
|
|
139
|
+
storage?: "hash" | "none";
|
|
140
|
+
theme?: "system" | "light" | "dark";
|
|
141
|
+
parent?: Element;
|
|
142
|
+
title?: string;
|
|
143
|
+
};
|
|
144
|
+
export type CtrlComponent = BooleanCtrl | RangeCtrl | RadioCtrl | SeedCtrl | EasingCtrl | DualRangeCtrl | FileCtrl;
|
|
145
|
+
export type ControlConstructor<T> = new (...args: any[]) => T;
|
|
146
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|