@oscarpalmer/atoms 0.5.2 → 0.7.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/dist/css/flex.css CHANGED
@@ -45,104 +45,104 @@
45
45
  gap: 0;
46
46
  }
47
47
 
48
- .ac-c {
48
+ .flex-ac-c {
49
49
  align-content: center;
50
50
  }
51
- .ac-fe {
51
+ .flex-ac-fe {
52
52
  align-content: flex-end;
53
53
  }
54
- .ac-fs {
54
+ .flex-ac-fs {
55
55
  align-content: flex-start;
56
56
  }
57
- .ac-s {
57
+ .flex-ac-s {
58
58
  align-content: stretch;
59
59
  }
60
60
 
61
- .ai-c {
61
+ .flex-ai-c {
62
62
  align-items: center;
63
63
  }
64
- .ai-fe {
64
+ .flex-ai-fe {
65
65
  align-items: flex-end;
66
66
  }
67
- .ai-fs {
67
+ .flex-ai-fs {
68
68
  align-items: flex-start;
69
69
  }
70
- .ai-s {
70
+ .flex-ai-s {
71
71
  align-items: stretch;
72
72
  }
73
73
 
74
- .as-c {
74
+ .flex-as-c {
75
75
  align-self: center;
76
76
  }
77
- .as-fe {
77
+ .flex-as-fe {
78
78
  align-self: flex-end;
79
79
  }
80
- .as-fs {
80
+ .flex-as-fs {
81
81
  align-self: flex-start;
82
82
  }
83
- .as-s {
83
+ .flex-as-s {
84
84
  align-self: stretch;
85
85
  }
86
86
 
87
- .ac-sa {
87
+ .flex-ac-sa {
88
88
  align-content: space-around;
89
89
  }
90
90
 
91
- .ac-sb {
91
+ .flex-ac-sb {
92
92
  align-content: space-between;
93
93
  }
94
94
 
95
- .ac-se {
95
+ .flex-ac-se {
96
96
  align-content: space-evenly;
97
97
  }
98
98
 
99
- .jc-c {
99
+ .flex-jc-c {
100
100
  justify-content: center;
101
101
  }
102
- .jc-fe {
102
+ .flex-jc-fe {
103
103
  justify-content: flex-end;
104
104
  }
105
- .jc-fs {
105
+ .flex-jc-fs {
106
106
  justify-content: flex-start;
107
107
  }
108
- .jc-s {
108
+ .flex-jc-s {
109
109
  justify-content: stretch;
110
110
  }
111
111
 
112
- .ji-c {
112
+ .flex-ji-c {
113
113
  justify-items: center;
114
114
  }
115
- .ji-fe {
115
+ .flex-ji-fe {
116
116
  justify-items: flex-end;
117
117
  }
118
- .ji-fs {
118
+ .flex-ji-fs {
119
119
  justify-items: flex-start;
120
120
  }
121
- .ji-s {
121
+ .flex-ji-s {
122
122
  justify-items: stretch;
123
123
  }
124
124
 
125
- .js-c {
125
+ .flex-js-c {
126
126
  justify-self: center;
127
127
  }
128
- .js-fe {
128
+ .flex-js-fe {
129
129
  justify-self: flex-end;
130
130
  }
131
- .js-fs {
131
+ .flex-js-fs {
132
132
  justify-self: flex-start;
133
133
  }
134
- .js-s {
134
+ .flex-js-s {
135
135
  justify-self: stretch;
136
136
  }
137
137
 
138
- .jc-sa {
138
+ .flex-jc-sa {
139
139
  justify-content: space-around;
140
140
  }
141
141
 
142
- .jc-sb {
142
+ .flex-jc-sb {
143
143
  justify-content: space-between;
144
144
  }
145
145
 
146
- .jc-se {
146
+ .flex-jc-se {
147
147
  justify-content: space-evenly;
148
148
  }
package/dist/js/atoms.js CHANGED
@@ -1,36 +1,190 @@
1
- // src/js/value.ts
2
- function getValue(data, key) {
3
- if (typeof data !== "object" || data === null || isNullableOrWhitespace(key)) {
1
+ // src/js/element.ts
2
+ function findParentElement(origin, selector) {
3
+ if (origin == null || selector == null) {
4
4
  return;
5
5
  }
6
- const parts = getString(key).split(".");
7
- const length = parts.length;
8
- let index = 0;
9
- let value = data;
10
- while (typeof value === "object" && value !== null && index < length) {
11
- value = value[parts[index++]];
6
+ function matches(element) {
7
+ return typeof selector === "string" ? element.matches?.(selector) ?? false : typeof selector === "function" ? selector(element) : false;
8
+ }
9
+ if (matches(origin)) {
10
+ return origin;
11
+ }
12
+ let parent = origin.parentElement;
13
+ while (parent != null && !matches(parent)) {
14
+ if (parent === document.body) {
15
+ return;
16
+ }
17
+ parent = parent.parentElement;
18
+ }
19
+ return parent ?? undefined;
20
+ }
21
+ function getElementUnderPointer(all) {
22
+ const elements = Array.from(document.querySelectorAll(":hover")).filter((element) => {
23
+ const style = window.getComputedStyle(element);
24
+ return element.tagName !== "HEAD" && (typeof all === "boolean" && all ? true : style.pointerEvents !== "none" && style.visibility !== "hidden");
25
+ });
26
+ return elements[elements.length - 1];
27
+ }
28
+ // src/js/event.ts
29
+ function getPosition(event) {
30
+ let x;
31
+ let y;
32
+ if (event instanceof MouseEvent) {
33
+ x = event.clientX;
34
+ y = event.clientY;
35
+ } else if (event instanceof TouchEvent) {
36
+ x = event.touches[0]?.clientX;
37
+ y = event.touches[0]?.clientY;
38
+ }
39
+ return typeof x === "number" && typeof y === "number" ? { x, y } : undefined;
40
+ }
41
+ var supportsTouch = (() => {
42
+ let value = false;
43
+ try {
44
+ if ("matchMedia" in window) {
45
+ const media = matchMedia("(pointer: coarse)");
46
+ if (typeof media?.matches === "boolean") {
47
+ value = media.matches;
48
+ }
49
+ }
50
+ if (!value) {
51
+ value = "ontouchstart" in window || navigator.maxTouchPoints > 0 || (navigator.msMaxTouchPoints ?? 0) > 0;
52
+ }
53
+ } catch {
54
+ value = false;
12
55
  }
13
56
  return value;
57
+ })();
58
+ // src/js/number.ts
59
+ function clampNumber(value, min, max) {
60
+ return Math.min(Math.max(getNumber(value), getNumber(min)), getNumber(max));
14
61
  }
15
- function isNullable(value) {
16
- return value === undefined || value === null;
62
+ function getNumber(value) {
63
+ if (typeof value === "number") {
64
+ return value;
65
+ }
66
+ if (typeof value === "symbol") {
67
+ return NaN;
68
+ }
69
+ let parsed = value?.valueOf?.() ?? value;
70
+ if (typeof parsed === "object") {
71
+ parsed = parsed?.toString() ?? parsed;
72
+ }
73
+ if (typeof parsed !== "string") {
74
+ return parsed == null ? NaN : typeof parsed === "number" ? parsed : +parsed;
75
+ }
76
+ if (zeroPattern.test(parsed)) {
77
+ return 0;
78
+ }
79
+ const trimmed = parsed.trim();
80
+ if (trimmed.length === 0) {
81
+ return NaN;
82
+ }
83
+ const isBinary = binaryPattern.test(trimmed);
84
+ if (isBinary || octalPattern.test(trimmed)) {
85
+ return parseInt(trimmed.slice(2), isBinary ? 2 : 8);
86
+ }
87
+ return +(hexadecimalPattern.test(trimmed) ? trimmed : trimmed.replace(separatorPattern, ""));
17
88
  }
18
-
89
+ var binaryPattern = /^0b[01]+$/i;
90
+ var hexadecimalPattern = /^0x[0-9a-f]+$/i;
91
+ var octalPattern = /^0o[0-7]+$/i;
92
+ var separatorPattern = /_/g;
93
+ var zeroPattern = /^\s*0+\s*$/;
19
94
  // src/js/string.ts
20
95
  function createUuid() {
21
96
  return uuidTemplate.replace(/[018]/g, (substring) => (substring ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> substring / 4).toString(16));
22
97
  }
23
- function getString(value2) {
24
- return typeof value2 === "string" ? value2 : String(value2);
98
+ function getString(value) {
99
+ return typeof value === "string" ? value : typeof value?.toString === "function" ? value.toString() : String(value);
25
100
  }
26
- function isNullableOrWhitespace(value2) {
27
- return isNullable(value2) || getString(value2).trim().length === 0;
101
+ function isNullableOrWhitespace(value) {
102
+ return value == null || getString(value).trim().length === 0;
28
103
  }
29
104
  var uuidTemplate = "10000000-1000-4000-8000-100000000000";
105
+ // src/js/value.ts
106
+ var _getValue = function(data, key) {
107
+ if (typeof data !== "object" || data === null || badProperties.has(key)) {
108
+ return;
109
+ }
110
+ if (data instanceof Map) {
111
+ return data.get(key);
112
+ }
113
+ return data[key];
114
+ };
115
+ var _setValue = function(data, key, value) {
116
+ if (typeof data !== "object" || data === null || badProperties.has(key)) {
117
+ return;
118
+ }
119
+ if (data instanceof Map) {
120
+ data.set(key, value);
121
+ } else {
122
+ data[key] = value;
123
+ }
124
+ };
125
+ function getValue(data, key) {
126
+ if (typeof data !== "object" || data === null || isNullableOrWhitespace(key)) {
127
+ return;
128
+ }
129
+ const parts = getString(key).split(".").reverse();
130
+ let position = parts.length;
131
+ let value = data;
132
+ while (position--) {
133
+ value = _getValue(value, parts[position]);
134
+ if (value == null) {
135
+ break;
136
+ }
137
+ }
138
+ return value;
139
+ }
140
+ function isArrayOrObject(value) {
141
+ return constructors.has(value?.constructor?.name);
142
+ }
143
+ function isNullable(value) {
144
+ return value == null;
145
+ }
146
+ function isObject(value) {
147
+ return value?.constructor?.name === objectConstructor;
148
+ }
149
+ function setValue(data, key, value) {
150
+ if (typeof data !== "object" || data === null || isNullableOrWhitespace(key)) {
151
+ return data;
152
+ }
153
+ const parts = getString(key).split(".").reverse();
154
+ let position = parts.length;
155
+ let target = data;
156
+ while (position--) {
157
+ const key2 = parts[position];
158
+ if (position === 0) {
159
+ _setValue(target, key2, value);
160
+ break;
161
+ }
162
+ let next = _getValue(target, key2);
163
+ if (typeof next !== "object" || next === null) {
164
+ next = numberExpression.test(parts[position - 1]) ? [] : {};
165
+ target[key2] = next;
166
+ }
167
+ target = next;
168
+ }
169
+ return data;
170
+ }
171
+ var badProperties = new Set(["__proto__", "constructor", "prototype"]);
172
+ var objectConstructor = "Object";
173
+ var constructors = new Set(["Array", objectConstructor]);
174
+ var numberExpression = /^\d+$/;
30
175
  export {
176
+ supportsTouch,
177
+ setValue,
178
+ isObject,
31
179
  isNullableOrWhitespace,
32
180
  isNullable,
181
+ isArrayOrObject,
33
182
  getValue,
34
183
  getString,
35
- createUuid
184
+ getPosition,
185
+ getNumber,
186
+ getElementUnderPointer,
187
+ findParentElement,
188
+ createUuid,
189
+ clampNumber
36
190
  };
package/package.json CHANGED
@@ -5,13 +5,11 @@
5
5
  },
6
6
  "description": "Sweet little atomic goodies…",
7
7
  "devDependencies": {
8
- "@rollup/plugin-typescript": "^11.1",
8
+ "@biomejs/biome": "^1.5",
9
+ "@happy-dom/global-registrator": "^13.3",
9
10
  "bun": "^1.0",
10
- "prettier": "^3.1",
11
- "rollup": "^4.5",
12
- "sass": "^1.69",
13
- "typescript": "^5.3",
14
- "xo": "^0.56"
11
+ "sass": "^1.70",
12
+ "typescript": "^5.3"
15
13
  },
16
14
  "exports": {
17
15
  ".": {
@@ -20,48 +18,26 @@
20
18
  },
21
19
  "./package.json": "./package.json"
22
20
  },
23
- "files": [
24
- "dist",
25
- "src",
26
- "types"
27
- ],
21
+ "files": ["dist", "src", "types"],
28
22
  "keywords": [],
29
23
  "license": "MIT",
30
24
  "main": "./dist/js/atoms.js",
31
25
  "module": "./dist/js/atoms.js",
32
26
  "name": "@oscarpalmer/atoms",
33
- "prettier": {
34
- "arrowParens": "avoid",
35
- "bracketSpacing": false,
36
- "singleQuote": true,
37
- "switchIndent": true,
38
- "trailingComma": "all",
39
- "useTabs": true
40
- },
41
27
  "repository": {
42
28
  "type": "git",
43
29
  "url": "git+https://github.com/oscarpalmer/atoms.git"
44
30
  },
45
31
  "scripts": {
46
- "build": "npm run build:css && npm run build:js && npm run types",
47
- "build:css": "npx sass ./src/css:./dist/css --no-source-map",
48
- "build:js": "npx bun build ./src/js/index.ts --outfile ./dist/js/atoms.js",
49
- "test": "npx bun test --coverage",
50
- "types": "npx tsc ./src/js/index.ts --declaration --declarationDir ./types --emitDeclarationOnly",
51
- "watch:css": "npx sass ./src/css:./dist/css --no-source-map --watch",
52
- "watch:js": "npx bun build ./src/js/index.ts --outfile ./dist/js/atoms.js --watch"
32
+ "build": "bun run build:css && bun run build:js && bun run types",
33
+ "build:css": "bunx sass ./src/css:./dist/css --no-source-map",
34
+ "build:js": "bunx bun build ./src/js/index.ts --outfile ./dist/js/atoms.js",
35
+ "test": "bun test --preload ./test/_preload.ts --coverage",
36
+ "types": "bunx tsc -p ./tsconfig.json",
37
+ "watch:css": "bunx sass ./src/css:./dist/css --no-source-map --watch",
38
+ "watch:js": "bun build ./src/js/index.ts --outfile ./dist/js/atoms.js --watch"
53
39
  },
54
40
  "type": "module",
55
41
  "types": "./types/index.d.ts",
56
- "version": "0.5.2",
57
- "xo": {
58
- "envs": [
59
- "browser"
60
- ],
61
- "prettier": true,
62
- "rules": {
63
- "import/extensions": "off",
64
- "import/no-cycle": "off"
65
- }
66
- }
67
- }
42
+ "version": "0.7.0"
43
+ }
package/src/css/flex.scss CHANGED
@@ -61,7 +61,7 @@ $types: (a: 'align', j: 'justify');
61
61
 
62
62
  @each $typeKey, $typeValue in $types {
63
63
  @each $alignmentOriginKey, $alignmentOriginValue in $alignmentOrigins {
64
- .#{$typeKey}#{$alignmentOriginKey} {
64
+ .flex-#{$typeKey}#{$alignmentOriginKey} {
65
65
  @each $alignmentValueKey, $alignmentValueValue in $alignmentValues {
66
66
  &-#{$alignmentValueKey} {
67
67
  #{$typeValue}-#{$alignmentOriginValue}: #{$alignmentValueValue}
@@ -71,7 +71,7 @@ $types: (a: 'align', j: 'justify');
71
71
  }
72
72
 
73
73
  @each $justificationValueKey, $justificationValueValue in $justificationValues {
74
- .#{$typeKey}c-#{$justificationValueKey} {
74
+ .flex-#{$typeKey}c-#{$justificationValueKey} {
75
75
  #{$typeValue}-content: #{$justificationValueValue}
76
76
  }
77
77
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * - Find the parent element that matches the selector
3
+ * - Matches may be found by a query string or a callback
4
+ */
5
+ export function findParentElement(
6
+ origin: Element,
7
+ selector: string | ((element: Element) => boolean),
8
+ ): Element | undefined {
9
+ if (origin == null || selector == null) {
10
+ return undefined;
11
+ }
12
+
13
+ function matches(element: Element): boolean {
14
+ return typeof selector === 'string'
15
+ ? element.matches?.(selector) ?? false
16
+ : typeof selector === 'function'
17
+ ? selector(element)
18
+ : false;
19
+ }
20
+
21
+ if (matches(origin)) {
22
+ return origin;
23
+ }
24
+
25
+ let parent: Element | null = origin.parentElement;
26
+
27
+ while (parent != null && !matches(parent)) {
28
+ if (parent === document.body) {
29
+ return undefined;
30
+ }
31
+
32
+ parent = parent.parentElement;
33
+ }
34
+
35
+ return parent ?? undefined;
36
+ }
37
+
38
+ /**
39
+ * - Get the most specific element under the pointer
40
+ * - Ignores elements with `pointer-events: none` and `visibility: hidden`
41
+ * - If `all` is `true`, all elements under the pointer are returned
42
+ */
43
+ export function getElementUnderPointer(all?: boolean): Element | undefined {
44
+ const elements = Array.from(document.querySelectorAll(':hover')).filter(
45
+ element => {
46
+ const style = window.getComputedStyle(element);
47
+
48
+ return (
49
+ element.tagName !== 'HEAD' &&
50
+ (typeof all === 'boolean' && all
51
+ ? true
52
+ : style.pointerEvents !== 'none' && style.visibility !== 'hidden')
53
+ );
54
+ },
55
+ );
56
+
57
+ return elements[elements.length - 1];
58
+ }
@@ -0,0 +1,57 @@
1
+ type NavigatorWithMsMaxTouchPoints = Navigator & {
2
+ msMaxTouchPoints: number;
3
+ };
4
+
5
+ type Position = {
6
+ x: number;
7
+ y: number;
8
+ };
9
+
10
+ /**
11
+ * Does the browser support touch events?
12
+ */
13
+ export const supportsTouch = (() => {
14
+ let value = false;
15
+
16
+ try {
17
+ if ('matchMedia' in window) {
18
+ const media = matchMedia('(pointer: coarse)');
19
+
20
+ if (typeof media?.matches === 'boolean') {
21
+ value = media.matches;
22
+ }
23
+ }
24
+
25
+ if (!value) {
26
+ value =
27
+ 'ontouchstart' in window ||
28
+ navigator.maxTouchPoints > 0 ||
29
+ ((navigator as NavigatorWithMsMaxTouchPoints).msMaxTouchPoints ?? 0) >
30
+ 0;
31
+ }
32
+ } catch {
33
+ value = false;
34
+ }
35
+
36
+ return value;
37
+ })();
38
+
39
+ /**
40
+ * Get the X- and Y-coordinates from a pointer event
41
+ */
42
+ export function getPosition(
43
+ event: MouseEvent | TouchEvent,
44
+ ): Position | undefined {
45
+ let x: number | undefined;
46
+ let y: number | undefined;
47
+
48
+ if (event instanceof MouseEvent) {
49
+ x = event.clientX;
50
+ y = event.clientY;
51
+ } else if (event instanceof TouchEvent) {
52
+ x = event.touches[0]?.clientX;
53
+ y = event.touches[0]?.clientY;
54
+ }
55
+
56
+ return typeof x === 'number' && typeof y === 'number' ? {x, y} : undefined;
57
+ }
package/src/js/index.ts CHANGED
@@ -1,2 +1,5 @@
1
+ export * from './element';
2
+ export * from './event';
3
+ export * from './number';
1
4
  export * from './string';
2
5
  export * from './value';
@@ -0,0 +1,56 @@
1
+ const binaryPattern = /^0b[01]+$/i;
2
+ const hexadecimalPattern = /^0x[0-9a-f]+$/i;
3
+ const octalPattern = /^0o[0-7]+$/i;
4
+ const separatorPattern = /_/g;
5
+ const zeroPattern = /^\s*0+\s*$/;
6
+
7
+ /**
8
+ * Clamps a number between a minimum and maximum value
9
+ */
10
+ export function clampNumber(value: number, min: number, max: number): number {
11
+ return Math.min(Math.max(getNumber(value), getNumber(min)), getNumber(max));
12
+ }
13
+
14
+ /**
15
+ * - Gets the number value from an unknown value
16
+ * - Returns `NaN` if the value is `undefined`, `null`, or cannot be parsed
17
+ */
18
+ export function getNumber(value: unknown): number {
19
+ if (typeof value === 'number') {
20
+ return value;
21
+ }
22
+
23
+ if (typeof value === 'symbol') {
24
+ return NaN;
25
+ }
26
+
27
+ let parsed = value?.valueOf?.() ?? value;
28
+
29
+ if (typeof parsed === 'object') {
30
+ parsed = parsed?.toString() ?? parsed;
31
+ }
32
+
33
+ if (typeof parsed !== 'string') {
34
+ return parsed == null ? NaN : typeof parsed === 'number' ? parsed : +parsed;
35
+ }
36
+
37
+ if (zeroPattern.test(parsed)) {
38
+ return 0;
39
+ }
40
+
41
+ const trimmed = parsed.trim();
42
+
43
+ if (trimmed.length === 0) {
44
+ return NaN;
45
+ }
46
+
47
+ const isBinary = binaryPattern.test(trimmed);
48
+
49
+ if (isBinary || octalPattern.test(trimmed)) {
50
+ return parseInt(trimmed.slice(2), isBinary ? 2 : 8);
51
+ }
52
+
53
+ return +(hexadecimalPattern.test(trimmed)
54
+ ? trimmed
55
+ : trimmed.replace(separatorPattern, ''));
56
+ }
package/src/js/string.ts CHANGED
@@ -1,5 +1,3 @@
1
- import {isNullable} from './value';
2
-
3
1
  const uuidTemplate = '10000000-1000-4000-8000-100000000000';
4
2
 
5
3
  /**
@@ -18,7 +16,11 @@ export function createUuid(): string {
18
16
  * Get the string value from any value
19
17
  */
20
18
  export function getString(value: unknown): string {
21
- return typeof value === 'string' ? value : String(value);
19
+ return typeof value === 'string'
20
+ ? value
21
+ : typeof value?.toString === 'function'
22
+ ? value.toString()
23
+ : String(value);
22
24
  }
23
25
 
24
26
  /**
@@ -27,5 +29,5 @@ export function getString(value: unknown): string {
27
29
  export function isNullableOrWhitespace(
28
30
  value: unknown,
29
31
  ): value is undefined | null | '' {
30
- return isNullable(value) || getString(value).trim().length === 0;
32
+ return value == null || getString(value).trim().length === 0;
31
33
  }
package/src/js/value.ts CHANGED
@@ -1,10 +1,51 @@
1
1
  import {getString, isNullableOrWhitespace} from './string';
2
2
 
3
+ export type ArrayOrObject = unknown[] | GenericObject;
4
+ export type GenericObject = Record<string, unknown>;
5
+ export type Key = number | string;
6
+ export type ValueObject = ArrayOrObject | Map<unknown, unknown> | Set<unknown>;
7
+
8
+ const badProperties = new Set(['__proto__', 'constructor', 'prototype']);
9
+ const objectConstructor = 'Object';
10
+ const constructors = new Set(['Array', objectConstructor]);
11
+ const numberExpression = /^\d+$/;
12
+
13
+ /**
14
+ * Internal function to get a value from an object
15
+ */
16
+ function _getValue(data: ValueObject, key: string): unknown {
17
+ if (typeof data !== 'object' || data === null || badProperties.has(key)) {
18
+ return undefined;
19
+ }
20
+
21
+ if (data instanceof Map) {
22
+ return data.get(key as never);
23
+ }
24
+
25
+ return data[key as never];
26
+ }
27
+
28
+ /**
29
+ * Internal function to set a value in an object
30
+ */
31
+ function _setValue(data: ValueObject, key: string, value: unknown): void {
32
+ if (typeof data !== 'object' || data === null || badProperties.has(key)) {
33
+ return;
34
+ }
35
+
36
+ if (data instanceof Map) {
37
+ data.set(key as never, value);
38
+ } else {
39
+ data[key as never] = value as never;
40
+ }
41
+ }
42
+
3
43
  /**
4
- * - Get the value from an object using a path
5
- * - You can retrieve a nested value by using a dot notation path
44
+ * - Get the value from an object using a key path
45
+ * - You can retrieve a nested value by using dot notation, e.g., `foo.bar.baz`
46
+ * - Returns `undefined` if the value is not found
6
47
  */
7
- export function getValue(data: unknown, key: unknown): unknown {
48
+ export function getValue(data: ValueObject, key: Key): unknown {
8
49
  if (
9
50
  typeof data !== 'object' ||
10
51
  data === null ||
@@ -13,22 +54,86 @@ export function getValue(data: unknown, key: unknown): unknown {
13
54
  return undefined;
14
55
  }
15
56
 
16
- const parts = getString(key).split('.');
17
- const length = parts.length;
57
+ const parts = getString(key).split('.').reverse();
18
58
 
19
- let index = 0;
59
+ let position = parts.length;
20
60
  let value = data;
21
61
 
22
- while (typeof value === 'object' && value !== null && index < length) {
23
- value = value[parts[index++]];
62
+ while (position--) {
63
+ value = _getValue(value, parts[position]) as ValueObject;
64
+
65
+ if (value == null) {
66
+ break;
67
+ }
24
68
  }
25
69
 
26
70
  return value;
27
71
  }
28
72
 
73
+ /**
74
+ * Is the value an array or a generic object?
75
+ */
76
+ export function isArrayOrObject(value: unknown): value is ArrayOrObject {
77
+ return constructors.has((value as ArrayOrObject)?.constructor?.name);
78
+ }
79
+
29
80
  /**
30
81
  * Is the value undefined or null?
31
82
  */
32
83
  export function isNullable(value: unknown): value is undefined | null {
33
- return value === undefined || value === null;
84
+ return value == null;
85
+ }
86
+
87
+ /**
88
+ * Is the value a generic object?
89
+ */
90
+ export function isObject(value: unknown): value is GenericObject {
91
+ return (value as GenericObject)?.constructor?.name === objectConstructor;
92
+ }
93
+
94
+ /**
95
+ * - Set the value in an object using a key path
96
+ * - You can set a nested value by using dot notation, e.g., `foo.bar.baz`
97
+ * - If a part of the path does not exist, it will be created, either as an array or a generic object, depending on the key
98
+ * - Returns the original object
99
+ */
100
+ export function setValue<Model extends ValueObject>(
101
+ data: Model,
102
+ key: Key,
103
+ value: unknown,
104
+ ): Model {
105
+ if (
106
+ typeof data !== 'object' ||
107
+ data === null ||
108
+ isNullableOrWhitespace(key)
109
+ ) {
110
+ return data;
111
+ }
112
+
113
+ const parts = getString(key).split('.').reverse();
114
+
115
+ let position = parts.length;
116
+ let target: ValueObject = data;
117
+
118
+ while (position--) {
119
+ const key = parts[position] as never;
120
+
121
+ if (position === 0) {
122
+ _setValue(target, key, value);
123
+
124
+ break;
125
+ }
126
+
127
+ let next = _getValue(target, key);
128
+
129
+ if (typeof next !== 'object' || next === null) {
130
+ next = numberExpression.test(parts[position - 1]) ? [] : {};
131
+
132
+ target[key] = next as never;
133
+ }
134
+
135
+ target = next as ValueObject;
136
+ }
137
+
138
+ return data;
34
139
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * - Find the parent element that matches the selector
3
+ * - Matches may be found by a query string or a callback
4
+ */
5
+ export declare function findParentElement(origin: Element, selector: string | ((element: Element) => boolean)): Element | undefined;
6
+ /**
7
+ * - Get the most specific element under the pointer
8
+ * - Ignores elements with `pointer-events: none` and `visibility: hidden`
9
+ * - If `all` is `true`, all elements under the pointer are returned
10
+ */
11
+ export declare function getElementUnderPointer(all?: boolean): Element | undefined;
@@ -0,0 +1,13 @@
1
+ type Position = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+ /**
6
+ * Does the browser support touch events?
7
+ */
8
+ export declare const supportsTouch: boolean;
9
+ /**
10
+ * Get the X- and Y-coordinates from a pointer event
11
+ */
12
+ export declare function getPosition(event: MouseEvent | TouchEvent): Position | undefined;
13
+ export {};
package/types/index.d.ts CHANGED
@@ -1,2 +1,5 @@
1
+ export * from './element';
2
+ export * from './event';
3
+ export * from './number';
1
4
  export * from './string';
2
5
  export * from './value';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Clamps a number between a minimum and maximum value
3
+ */
4
+ export declare function clampNumber(value: number, min: number, max: number): number;
5
+ /**
6
+ * - Gets the number value from an unknown value
7
+ * - Returns `NaN` if the value is `undefined`, `null`, or cannot be parsed
8
+ */
9
+ export declare function getNumber(value: unknown): number;
package/types/value.d.ts CHANGED
@@ -1,9 +1,29 @@
1
+ export type ArrayOrObject = unknown[] | GenericObject;
2
+ export type GenericObject = Record<string, unknown>;
3
+ export type Key = number | string;
4
+ export type ValueObject = ArrayOrObject | Map<unknown, unknown> | Set<unknown>;
1
5
  /**
2
- * - Get the value from an object using a path
3
- * - You can retrieve a nested value by using a dot notation path
6
+ * - Get the value from an object using a key path
7
+ * - You can retrieve a nested value by using dot notation, e.g., `foo.bar.baz`
8
+ * - Returns `undefined` if the value is not found
4
9
  */
5
- export declare function getValue(data: unknown, key: unknown): unknown;
10
+ export declare function getValue(data: ValueObject, key: Key): unknown;
11
+ /**
12
+ * Is the value an array or a generic object?
13
+ */
14
+ export declare function isArrayOrObject(value: unknown): value is ArrayOrObject;
6
15
  /**
7
16
  * Is the value undefined or null?
8
17
  */
9
18
  export declare function isNullable(value: unknown): value is undefined | null;
19
+ /**
20
+ * Is the value a generic object?
21
+ */
22
+ export declare function isObject(value: unknown): value is GenericObject;
23
+ /**
24
+ * - Set the value in an object using a key path
25
+ * - You can set a nested value by using dot notation, e.g., `foo.bar.baz`
26
+ * - If a part of the path does not exist, it will be created, either as an array or a generic object, depending on the key
27
+ * - Returns the original object
28
+ */
29
+ export declare function setValue<Model extends ValueObject>(data: Model, key: Key, value: unknown): Model;