@stanko/ctrls 0.3.4 → 0.4.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.
@@ -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
- // To make typescript happy
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
- // TODO
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
- const onChangeControlHandler = (control) => {
174
- const updatedValues = this.updateValuesObject({}, control);
175
- this.onChange?.(updatedValues);
176
- if (this.options.storage === "hash") {
177
- this.setHash();
178
- }
179
- };
180
- const onInputControlHandler = (control) => {
181
- const updatedValues = this.updateValuesObject({}, control);
182
- this.onInput?.(updatedValues);
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 {};