ckeditor5-symfony 1.0.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.
Files changed (174) hide show
  1. package/dist/ckeditor5-symfony-error.d.ts +7 -0
  2. package/dist/ckeditor5-symfony-error.d.ts.map +1 -0
  3. package/dist/elements/context/context.d.ts +18 -0
  4. package/dist/elements/context/context.d.ts.map +1 -0
  5. package/dist/elements/context/contexts-registry.d.ts +9 -0
  6. package/dist/elements/context/contexts-registry.d.ts.map +1 -0
  7. package/dist/elements/context/index.d.ts +4 -0
  8. package/dist/elements/context/index.d.ts.map +1 -0
  9. package/dist/elements/context/typings.d.ts +34 -0
  10. package/dist/elements/context/typings.d.ts.map +1 -0
  11. package/dist/elements/editable.d.ts +18 -0
  12. package/dist/elements/editable.d.ts.map +1 -0
  13. package/dist/elements/editor/custom-editor-plugins.d.ts +54 -0
  14. package/dist/elements/editor/custom-editor-plugins.d.ts.map +1 -0
  15. package/dist/elements/editor/editor.d.ts +23 -0
  16. package/dist/elements/editor/editor.d.ts.map +1 -0
  17. package/dist/elements/editor/editors-registry.d.ts +9 -0
  18. package/dist/elements/editor/editors-registry.d.ts.map +1 -0
  19. package/dist/elements/editor/index.d.ts +3 -0
  20. package/dist/elements/editor/index.d.ts.map +1 -0
  21. package/dist/elements/editor/plugins/index.d.ts +2 -0
  22. package/dist/elements/editor/plugins/index.d.ts.map +1 -0
  23. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts +6 -0
  24. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
  25. package/dist/elements/editor/typings.d.ts +99 -0
  26. package/dist/elements/editor/typings.d.ts.map +1 -0
  27. package/dist/elements/editor/utils/create-editor-in-context.d.ts +44 -0
  28. package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -0
  29. package/dist/elements/editor/utils/index.d.ts +12 -0
  30. package/dist/elements/editor/utils/index.d.ts.map +1 -0
  31. package/dist/elements/editor/utils/is-single-root-editor.d.ts +9 -0
  32. package/dist/elements/editor/utils/is-single-root-editor.d.ts.map +1 -0
  33. package/dist/elements/editor/utils/load-editor-constructor.d.ts +9 -0
  34. package/dist/elements/editor/utils/load-editor-constructor.d.ts.map +1 -0
  35. package/dist/elements/editor/utils/load-editor-plugins.d.ts +20 -0
  36. package/dist/elements/editor/utils/load-editor-plugins.d.ts.map +1 -0
  37. package/dist/elements/editor/utils/load-editor-translations.d.ts +14 -0
  38. package/dist/elements/editor/utils/load-editor-translations.d.ts.map +1 -0
  39. package/dist/elements/editor/utils/normalize-custom-translations.d.ts +11 -0
  40. package/dist/elements/editor/utils/normalize-custom-translations.d.ts.map +1 -0
  41. package/dist/elements/editor/utils/query-all-editor-ids.d.ts +5 -0
  42. package/dist/elements/editor/utils/query-all-editor-ids.d.ts.map +1 -0
  43. package/dist/elements/editor/utils/query-editor-editables.d.ts +25 -0
  44. package/dist/elements/editor/utils/query-editor-editables.d.ts.map +1 -0
  45. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
  46. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
  47. package/dist/elements/editor/utils/set-editor-editable-height.d.ts +9 -0
  48. package/dist/elements/editor/utils/set-editor-editable-height.d.ts.map +1 -0
  49. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +24 -0
  50. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
  51. package/dist/elements/index.d.ts +6 -0
  52. package/dist/elements/index.d.ts.map +1 -0
  53. package/dist/elements/register-custom-elements.d.ts +5 -0
  54. package/dist/elements/register-custom-elements.d.ts.map +1 -0
  55. package/dist/elements/ui-part.d.ts +18 -0
  56. package/dist/elements/ui-part.d.ts.map +1 -0
  57. package/dist/index.cjs +5 -0
  58. package/dist/index.cjs.map +1 -0
  59. package/dist/index.d.ts +3 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.mjs +1089 -0
  62. package/dist/index.mjs.map +1 -0
  63. package/dist/shared/async-registry.d.ts +136 -0
  64. package/dist/shared/async-registry.d.ts.map +1 -0
  65. package/dist/shared/camel-case.d.ts +8 -0
  66. package/dist/shared/camel-case.d.ts.map +1 -0
  67. package/dist/shared/debounce.d.ts +2 -0
  68. package/dist/shared/debounce.d.ts.map +1 -0
  69. package/dist/shared/deep-camel-case-keys.d.ts +8 -0
  70. package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
  71. package/dist/shared/filter-object-values.d.ts +9 -0
  72. package/dist/shared/filter-object-values.d.ts.map +1 -0
  73. package/dist/shared/index.d.ts +15 -0
  74. package/dist/shared/index.d.ts.map +1 -0
  75. package/dist/shared/is-empty-object.d.ts +2 -0
  76. package/dist/shared/is-empty-object.d.ts.map +1 -0
  77. package/dist/shared/is-plain-object.d.ts +8 -0
  78. package/dist/shared/is-plain-object.d.ts.map +1 -0
  79. package/dist/shared/map-object-values.d.ts +11 -0
  80. package/dist/shared/map-object-values.d.ts.map +1 -0
  81. package/dist/shared/once.d.ts +2 -0
  82. package/dist/shared/once.d.ts.map +1 -0
  83. package/dist/shared/shallow-equal.d.ts +9 -0
  84. package/dist/shared/shallow-equal.d.ts.map +1 -0
  85. package/dist/shared/timeout.d.ts +8 -0
  86. package/dist/shared/timeout.d.ts.map +1 -0
  87. package/dist/shared/uid.d.ts +7 -0
  88. package/dist/shared/uid.d.ts.map +1 -0
  89. package/dist/shared/wait-for-dom-ready.d.ts +5 -0
  90. package/dist/shared/wait-for-dom-ready.d.ts.map +1 -0
  91. package/dist/shared/wait-for.d.ts +20 -0
  92. package/dist/shared/wait-for.d.ts.map +1 -0
  93. package/dist/types/can-be-promise.type.d.ts +2 -0
  94. package/dist/types/can-be-promise.type.d.ts.map +1 -0
  95. package/dist/types/index.d.ts +3 -0
  96. package/dist/types/index.d.ts.map +1 -0
  97. package/dist/types/required-by.type.d.ts +2 -0
  98. package/dist/types/required-by.type.d.ts.map +1 -0
  99. package/package.json +40 -0
  100. package/src/ckeditor5-symfony-error.ts +9 -0
  101. package/src/elements/context/context.test.ts +291 -0
  102. package/src/elements/context/context.ts +99 -0
  103. package/src/elements/context/contexts-registry.test.ts +10 -0
  104. package/src/elements/context/contexts-registry.ts +10 -0
  105. package/src/elements/context/index.ts +3 -0
  106. package/src/elements/context/typings.ts +39 -0
  107. package/src/elements/editable.test.ts +334 -0
  108. package/src/elements/editable.ts +114 -0
  109. package/src/elements/editor/custom-editor-plugins.test.ts +103 -0
  110. package/src/elements/editor/custom-editor-plugins.ts +86 -0
  111. package/src/elements/editor/editor.test.ts +438 -0
  112. package/src/elements/editor/editor.ts +279 -0
  113. package/src/elements/editor/editors-registry.test.ts +10 -0
  114. package/src/elements/editor/editors-registry.ts +10 -0
  115. package/src/elements/editor/index.ts +2 -0
  116. package/src/elements/editor/plugins/index.ts +1 -0
  117. package/src/elements/editor/plugins/sync-editor-with-input.ts +78 -0
  118. package/src/elements/editor/typings.ts +114 -0
  119. package/src/elements/editor/utils/create-editor-in-context.ts +90 -0
  120. package/src/elements/editor/utils/index.ts +11 -0
  121. package/src/elements/editor/utils/is-single-root-editor.test.ts +40 -0
  122. package/src/elements/editor/utils/is-single-root-editor.ts +11 -0
  123. package/src/elements/editor/utils/load-editor-constructor.test.ts +62 -0
  124. package/src/elements/editor/utils/load-editor-constructor.ts +29 -0
  125. package/src/elements/editor/utils/load-editor-plugins.test.ts +100 -0
  126. package/src/elements/editor/utils/load-editor-plugins.ts +73 -0
  127. package/src/elements/editor/utils/load-editor-translations.ts +233 -0
  128. package/src/elements/editor/utils/normalize-custom-translations.test.ts +152 -0
  129. package/src/elements/editor/utils/normalize-custom-translations.ts +18 -0
  130. package/src/elements/editor/utils/query-all-editor-ids.ts +9 -0
  131. package/src/elements/editor/utils/query-editor-editables.ts +101 -0
  132. package/src/elements/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
  133. package/src/elements/editor/utils/resolve-editor-config-elements-references.ts +36 -0
  134. package/src/elements/editor/utils/set-editor-editable-height.test.ts +131 -0
  135. package/src/elements/editor/utils/set-editor-editable-height.ts +15 -0
  136. package/src/elements/editor/utils/wrap-with-watchdog.test.ts +45 -0
  137. package/src/elements/editor/utils/wrap-with-watchdog.ts +51 -0
  138. package/src/elements/index.ts +14 -0
  139. package/src/elements/register-custom-elements.ts +24 -0
  140. package/src/elements/ui-part.test.ts +142 -0
  141. package/src/elements/ui-part.ts +80 -0
  142. package/src/index.ts +6 -0
  143. package/src/shared/async-registry.test.ts +737 -0
  144. package/src/shared/async-registry.ts +353 -0
  145. package/src/shared/camel-case.test.ts +35 -0
  146. package/src/shared/camel-case.ts +11 -0
  147. package/src/shared/debounce.test.ts +72 -0
  148. package/src/shared/debounce.ts +16 -0
  149. package/src/shared/deep-camel-case-keys.test.ts +34 -0
  150. package/src/shared/deep-camel-case-keys.ts +26 -0
  151. package/src/shared/filter-object-values.test.ts +25 -0
  152. package/src/shared/filter-object-values.ts +17 -0
  153. package/src/shared/index.ts +14 -0
  154. package/src/shared/is-empty-object.test.ts +78 -0
  155. package/src/shared/is-empty-object.ts +3 -0
  156. package/src/shared/is-plain-object.test.ts +38 -0
  157. package/src/shared/is-plain-object.ts +15 -0
  158. package/src/shared/map-object-values.test.ts +29 -0
  159. package/src/shared/map-object-values.ts +19 -0
  160. package/src/shared/once.test.ts +116 -0
  161. package/src/shared/once.ts +12 -0
  162. package/src/shared/shallow-equal.test.ts +51 -0
  163. package/src/shared/shallow-equal.ts +30 -0
  164. package/src/shared/timeout.test.ts +65 -0
  165. package/src/shared/timeout.ts +13 -0
  166. package/src/shared/uid.test.ts +25 -0
  167. package/src/shared/uid.ts +8 -0
  168. package/src/shared/wait-for-dom-ready.test.ts +87 -0
  169. package/src/shared/wait-for-dom-ready.ts +21 -0
  170. package/src/shared/wait-for.test.ts +24 -0
  171. package/src/shared/wait-for.ts +56 -0
  172. package/src/types/can-be-promise.type.ts +1 -0
  173. package/src/types/index.ts +2 -0
  174. package/src/types/required-by.type.ts +1 -0
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Generic async registry for objects with an async destroy method.
3
+ * Provides a way to register, unregister, and execute callbacks on objects by ID.
4
+ */
5
+ export class AsyncRegistry<T extends Destructible> {
6
+ /**
7
+ * Map of registered items.
8
+ */
9
+ private readonly items = new Map<RegistryId | null, T>();
10
+
11
+ /**
12
+ * Map of initialization errors for items that failed to register.
13
+ */
14
+ private readonly initializationErrors = new Map<RegistryId | null, any>();
15
+
16
+ /**
17
+ * Map of pending callbacks waiting for items to be registered or fail.
18
+ */
19
+ private readonly pendingCallbacks = new Map<RegistryId | null, PendingCallbacks<T>>();
20
+
21
+ /**
22
+ * Set of watchers that observe changes to the registry.
23
+ */
24
+ private readonly watchers = new Set<RegistryWatcher<T>>();
25
+
26
+ /**
27
+ * Executes a function on an item.
28
+ * If the item is not yet registered, it will wait for it to be registered.
29
+ *
30
+ * @param id The ID of the item.
31
+ * @param onSuccess The function to execute.
32
+ * @param onError Optional error callback.
33
+ * @returns A promise that resolves with the result of the function.
34
+ */
35
+ execute<R, E extends T = T>(
36
+ id: RegistryId | null,
37
+ onSuccess: (item: E) => R,
38
+ onError?: (error: any) => void,
39
+ ): Promise<Awaited<R>> {
40
+ const item = this.items.get(id);
41
+ const error = this.initializationErrors.get(id);
42
+
43
+ // If error exists and callback provided, invoke it immediately.
44
+ if (error) {
45
+ onError?.(error);
46
+ return Promise.reject(error);
47
+ }
48
+
49
+ // If item exists, invoke callback immediately (synchronously via Promise.resolve).
50
+ if (item) {
51
+ return Promise.resolve(onSuccess(item as E));
52
+ }
53
+
54
+ // Item not ready yet - queue the callbacks.
55
+ return new Promise((resolve, reject) => {
56
+ const pending = this.getPendingCallbacks(id);
57
+
58
+ pending.success.push(async (item: T) => {
59
+ resolve(await onSuccess(item as E));
60
+ });
61
+
62
+ if (onError) {
63
+ pending.error.push(onError);
64
+ }
65
+ else {
66
+ pending.error.push(reject);
67
+ }
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Registers an item.
73
+ *
74
+ * @param id The ID of the item.
75
+ * @param item The item instance.
76
+ */
77
+ register(id: RegistryId | null, item: T): void {
78
+ if (this.items.has(id)) {
79
+ throw new Error(`Item with ID "${id}" is already registered.`);
80
+ }
81
+
82
+ this.resetErrors(id);
83
+ this.items.set(id, item);
84
+
85
+ // Execute all pending callbacks for this item (synchronously).
86
+ const pending = this.pendingCallbacks.get(id);
87
+
88
+ if (pending) {
89
+ pending.success.forEach(callback => callback(item));
90
+ this.pendingCallbacks.delete(id);
91
+ }
92
+
93
+ // Register the first item as the default item (null ID).
94
+ this.registerAsDefault(id, item);
95
+ this.notifyWatchers();
96
+ }
97
+
98
+ /**
99
+ * Registers an error for an item.
100
+ *
101
+ * @param id The ID of the item.
102
+ * @param error The error to register.
103
+ */
104
+ error(id: RegistryId | null, error: any): void {
105
+ this.items.delete(id);
106
+ this.initializationErrors.set(id, error);
107
+
108
+ // Execute all pending error callbacks for this item.
109
+ const pending = this.pendingCallbacks.get(id);
110
+
111
+ if (pending) {
112
+ pending.error.forEach(callback => callback(error));
113
+ this.pendingCallbacks.delete(id);
114
+ }
115
+
116
+ // Set as default error if this is the first error and no items exist.
117
+ if (this.initializationErrors.size === 1 && !this.items.size) {
118
+ this.error(null, error);
119
+ }
120
+
121
+ // Notify watchers about the error state.
122
+ this.notifyWatchers();
123
+ }
124
+
125
+ /**
126
+ * Resets errors for an item.
127
+ *
128
+ * @param id The ID of the item.
129
+ */
130
+ resetErrors(id: RegistryId | null): void {
131
+ const { initializationErrors } = this;
132
+
133
+ // Clear default error if it's the same as the specific error.
134
+ if (initializationErrors.has(null) && initializationErrors.get(null) === initializationErrors.get(id)) {
135
+ initializationErrors.delete(null);
136
+ }
137
+
138
+ initializationErrors.delete(id);
139
+ }
140
+
141
+ /**
142
+ * Un-registers an item.
143
+ *
144
+ * @param id The ID of the item.
145
+ */
146
+ unregister(id: RegistryId | null): void {
147
+ if (!this.items.has(id)) {
148
+ throw new Error(`Item with ID "${id}" is not registered.`);
149
+ }
150
+
151
+ // If unregistering the default item, clear it.
152
+ if (id && this.items.get(null) === this.items.get(id)) {
153
+ this.unregister(null);
154
+ }
155
+
156
+ this.items.delete(id);
157
+ this.pendingCallbacks.delete(id);
158
+
159
+ this.notifyWatchers();
160
+ }
161
+
162
+ /**
163
+ * Gets all registered items.
164
+ *
165
+ * @returns An array of all registered items.
166
+ */
167
+ getItems(): T[] {
168
+ return Array.from(this.items.values());
169
+ }
170
+
171
+ /**
172
+ * Checks if an item with the given ID is registered.
173
+ *
174
+ * @param id The ID of the item.
175
+ * @returns `true` if the item is registered, `false` otherwise.
176
+ */
177
+ hasItem(id: RegistryId | null): boolean {
178
+ return this.items.has(id);
179
+ }
180
+
181
+ /**
182
+ * Gets a promise that resolves with the item instance for the given ID.
183
+ * If the item is not registered yet, it will wait for it to be registered.
184
+ *
185
+ * @param id The ID of the item.
186
+ * @param timeout Optional timeout in milliseconds.
187
+ * @returns A promise that resolves with the item instance.
188
+ */
189
+ waitFor<E extends T = T>(id: RegistryId | null, timeout?: number): Promise<E> {
190
+ return new Promise<E>((resolve, reject) => {
191
+ let exceedTimeout = false;
192
+ let timer: ReturnType<typeof setTimeout> | null = null;
193
+
194
+ void this.execute(
195
+ id,
196
+ (value: E) => {
197
+ if (exceedTimeout) {
198
+ return;
199
+ }
200
+
201
+ if (timer !== null) {
202
+ clearTimeout(timer!);
203
+ }
204
+
205
+ (resolve as (value: E) => void)(value);
206
+ },
207
+ (error: any) => {
208
+ if (exceedTimeout) {
209
+ return;
210
+ }
211
+
212
+ if (timer !== null) {
213
+ clearTimeout(timer!);
214
+ }
215
+
216
+ reject(error);
217
+ },
218
+ );
219
+
220
+ if (timeout) {
221
+ timer = setTimeout(() => {
222
+ exceedTimeout = true;
223
+ reject(new Error(`Timeout waiting for item with ID "${id}" to be registered.`));
224
+ }, timeout);
225
+ }
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Destroys all registered items and clears the registry.
231
+ * This will call the `destroy` method on each item.
232
+ */
233
+ async destroyAll() {
234
+ const promises = (
235
+ Array
236
+ .from(new Set(this.items.values()))
237
+ .map(item => item.destroy())
238
+ );
239
+
240
+ this.items.clear();
241
+ this.pendingCallbacks.clear();
242
+
243
+ await Promise.all(promises);
244
+
245
+ this.notifyWatchers();
246
+ }
247
+
248
+ /**
249
+ * Registers a watcher that will be called whenever the registry changes.
250
+ *
251
+ * @param watcher The watcher function to register.
252
+ * @returns A function to unregister the watcher.
253
+ */
254
+ watch(watcher: RegistryWatcher<T>): () => void {
255
+ this.watchers.add(watcher);
256
+
257
+ // Call the watcher immediately with the current state.
258
+ watcher(
259
+ new Map(this.items),
260
+ new Map(this.initializationErrors),
261
+ );
262
+
263
+ return this.unwatch.bind(this, watcher);
264
+ }
265
+
266
+ /**
267
+ * Un-registers a watcher.
268
+ *
269
+ * @param watcher The watcher function to unregister.
270
+ */
271
+ unwatch(watcher: RegistryWatcher<T>): void {
272
+ this.watchers.delete(watcher);
273
+ }
274
+
275
+ /**
276
+ * Resets the registry by clearing all items, errors, and pending callbacks.
277
+ */
278
+ reset(): void {
279
+ this.items.clear();
280
+ this.initializationErrors.clear();
281
+ this.pendingCallbacks.clear();
282
+ this.notifyWatchers();
283
+ }
284
+
285
+ /**
286
+ * Notifies all watchers about changes to the registry.
287
+ */
288
+ private notifyWatchers(): void {
289
+ this.watchers.forEach(
290
+ watcher => watcher(
291
+ new Map(this.items),
292
+ new Map(this.initializationErrors),
293
+ ),
294
+ );
295
+ }
296
+
297
+ /**
298
+ * Gets or creates pending callbacks for a specific ID.
299
+ *
300
+ * @param id The ID of the item.
301
+ * @returns The pending callbacks structure.
302
+ */
303
+ private getPendingCallbacks(id: RegistryId | null): PendingCallbacks<T> {
304
+ let pending = this.pendingCallbacks.get(id);
305
+
306
+ if (!pending) {
307
+ pending = { success: [], error: [] };
308
+ this.pendingCallbacks.set(id, pending);
309
+ }
310
+
311
+ return pending;
312
+ }
313
+
314
+ /**
315
+ * Registers an item as the default (null ID) item if it's the first one.
316
+ *
317
+ * @param id The ID of the item being registered.
318
+ * @param item The item instance.
319
+ */
320
+ private registerAsDefault(id: RegistryId | null, item: T): void {
321
+ if (this.items.size === 1 && id !== null) {
322
+ this.register(null, item);
323
+ }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Interface for objects that can be destroyed.
329
+ */
330
+ export type Destructible = {
331
+ destroy: () => Promise<any>;
332
+ };
333
+
334
+ /**
335
+ * Identifier of the registry item.
336
+ */
337
+ type RegistryId = string;
338
+
339
+ /**
340
+ * Structure holding pending success and error callbacks for an item.
341
+ */
342
+ type PendingCallbacks<T> = {
343
+ success: Array<(item: T) => void>;
344
+ error: Array<(error: Error) => void>;
345
+ };
346
+
347
+ /**
348
+ * Callback type for watching registry changes.
349
+ */
350
+ type RegistryWatcher<T> = (
351
+ items: Map<RegistryId | null, T>,
352
+ errors: Map<RegistryId | null, Error>,
353
+ ) => void;
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { camelCase } from './camel-case';
4
+
5
+ describe('camelCase', () => {
6
+ it('converts kebab-case to camelCase', () => {
7
+ expect(camelCase('foo-bar-baz')).toBe('fooBarBaz');
8
+ });
9
+
10
+ it('converts snake_case to camelCase', () => {
11
+ expect(camelCase('foo_bar_baz')).toBe('fooBarBaz');
12
+ });
13
+
14
+ it('converts space separated to camelCase', () => {
15
+ expect(camelCase('foo bar baz')).toBe('fooBarBaz');
16
+ });
17
+
18
+ it('returns already camelCase string unchanged', () => {
19
+ expect(camelCase('fooBarBaz')).toBe('fooBarBaz');
20
+ });
21
+
22
+ it('handles single word', () => {
23
+ expect(camelCase('foo')).toBe('foo');
24
+ });
25
+
26
+ it('handles empty string', () => {
27
+ expect(camelCase('')).toBe('');
28
+ });
29
+
30
+ it('handles leading/trailing separators', () => {
31
+ expect(camelCase('-foo-bar-')).toBe('fooBar');
32
+ expect(camelCase('_foo_bar_')).toBe('fooBar');
33
+ expect(camelCase(' foo bar ')).toBe('fooBar');
34
+ });
35
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Converts a string to camelCase.
3
+ *
4
+ * @param str The string to convert
5
+ * @returns The camelCased string
6
+ */
7
+ export function camelCase(str: string): string {
8
+ return str
9
+ .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
10
+ .replace(/^./, m => m.toLowerCase());
11
+ }
@@ -0,0 +1,72 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { debounce } from './debounce';
4
+
5
+ describe('debounce', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ });
13
+
14
+ it('should not call the callback immediately', () => {
15
+ const callback = vi.fn();
16
+ const debounced = debounce(100, callback);
17
+
18
+ debounced();
19
+
20
+ expect(callback).not.toHaveBeenCalled();
21
+ });
22
+
23
+ it('should call the callback after the specified delay', () => {
24
+ const callback = vi.fn();
25
+ const debounced = debounce(100, callback);
26
+
27
+ debounced();
28
+
29
+ vi.advanceTimersByTime(100);
30
+
31
+ expect(callback).toHaveBeenCalledTimes(1);
32
+ });
33
+
34
+ it('should call the callback only once for multiple calls within the delay period', () => {
35
+ const callback = vi.fn();
36
+ const debounced = debounce(100, callback);
37
+
38
+ debounced();
39
+ debounced();
40
+ debounced();
41
+
42
+ vi.advanceTimersByTime(100);
43
+
44
+ expect(callback).toHaveBeenCalledTimes(1);
45
+ });
46
+
47
+ it('should reset the timeout on subsequent calls', () => {
48
+ const callback = vi.fn();
49
+ const debounced = debounce(100, callback);
50
+
51
+ debounced();
52
+ vi.advanceTimersByTime(50);
53
+ debounced();
54
+ vi.advanceTimersByTime(50);
55
+ expect(callback).not.toHaveBeenCalled();
56
+
57
+ vi.advanceTimersByTime(50);
58
+ expect(callback).toHaveBeenCalledTimes(1);
59
+ });
60
+
61
+ it('should pass the arguments to the callback', () => {
62
+ const callback = vi.fn();
63
+ const debounced = debounce(100, callback);
64
+ const args = [1, 'test', { a: 2 }];
65
+
66
+ debounced(...args);
67
+
68
+ vi.advanceTimersByTime(100);
69
+
70
+ expect(callback).toHaveBeenCalledWith(...args);
71
+ });
72
+ });
@@ -0,0 +1,16 @@
1
+ export function debounce<T extends (...args: any[]) => any>(
2
+ delay: number,
3
+ callback: T,
4
+ ): (...args: Parameters<T>) => void {
5
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
6
+
7
+ return (...args: Parameters<T>): void => {
8
+ if (timeoutId) {
9
+ clearTimeout(timeoutId);
10
+ }
11
+
12
+ timeoutId = setTimeout(() => {
13
+ callback(...args);
14
+ }, delay);
15
+ };
16
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { deepCamelCaseKeys } from './deep-camel-case-keys';
4
+
5
+ describe('deepCamelCaseKeys', () => {
6
+ it('converts keys of a flat object', () => {
7
+ expect(deepCamelCaseKeys({ 'foo_bar': 1, 'baz-qux': 2 })).toEqual({ fooBar: 1, bazQux: 2 });
8
+ });
9
+
10
+ it('converts keys deeply in nested objects', () => {
11
+ const input = { foo_bar: { nested_key: 1 }, arr: [{ snake_case: 2 }] };
12
+ const expected = { fooBar: { nestedKey: 1 }, arr: [{ snakeCase: 2 }] };
13
+ expect(deepCamelCaseKeys(input)).toEqual(expected);
14
+ });
15
+
16
+ it('converts keys in arrays of objects', () => {
17
+ const input = [{ foo_bar: 1 }, { baz_qux: 2 }];
18
+ const expected = [{ fooBar: 1 }, { bazQux: 2 }];
19
+ expect(deepCamelCaseKeys(input)).toEqual(expected);
20
+ });
21
+
22
+ it('leaves primitives untouched', () => {
23
+ expect(deepCamelCaseKeys(42)).toBe(42);
24
+ expect(deepCamelCaseKeys('string')).toBe('string');
25
+ expect(deepCamelCaseKeys(null)).toBe(null);
26
+ expect(deepCamelCaseKeys(undefined)).toBe(undefined);
27
+ });
28
+
29
+ it('leaves class instances untouched', () => {
30
+ class MyClass { a = 1; }
31
+ const instance = new MyClass();
32
+ expect(deepCamelCaseKeys(instance)).toBe(instance);
33
+ });
34
+ });
@@ -0,0 +1,26 @@
1
+ import { camelCase } from './camel-case';
2
+ import { isPlainObject } from './is-plain-object';
3
+
4
+ /**
5
+ * Recursively converts all keys of a plain object or array to camelCase.
6
+ * Skips class instances and leaves them untouched.
7
+ *
8
+ * @param input The object or array to process
9
+ */
10
+ export function deepCamelCaseKeys<T>(input: T): T {
11
+ if (Array.isArray(input)) {
12
+ return input.map(deepCamelCaseKeys) as unknown as T;
13
+ }
14
+
15
+ if (isPlainObject(input)) {
16
+ const result: Record<string, unknown> = Object.create(null);
17
+
18
+ for (const [key, value] of Object.entries(input)) {
19
+ result[camelCase(key)] = deepCamelCaseKeys(value);
20
+ }
21
+
22
+ return result as T;
23
+ }
24
+
25
+ return input;
26
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { filterObjectValues } from './filter-object-values';
4
+
5
+ describe('filterObjectValues', () => {
6
+ it('should filter object values using the provided filter function', () => {
7
+ const obj = {
8
+ a: 1,
9
+ b: null,
10
+ c: undefined,
11
+ d: [],
12
+ };
13
+
14
+ const filteredObj = filterObjectValues(
15
+ obj,
16
+ value => value !== null,
17
+ );
18
+
19
+ expect(filteredObj).toEqual({
20
+ a: 1,
21
+ c: undefined,
22
+ d: [],
23
+ });
24
+ });
25
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Filters the values of an object based on a provided filter function.
3
+ *
4
+ * @param obj The object to filter.
5
+ * @param filter The filter function that determines whether a value should be included.
6
+ * @returns A new object containing only the key-value pairs that passed the filter.
7
+ */
8
+ export function filterObjectValues<T>(
9
+ obj: Record<string, T>,
10
+ filter: (value: T, key: string) => boolean,
11
+ ): Record<string, T> {
12
+ const filteredEntries = Object
13
+ .entries(obj)
14
+ .filter(([key, value]) => filter(value, key));
15
+
16
+ return Object.fromEntries(filteredEntries);
17
+ }
@@ -0,0 +1,14 @@
1
+ export * from './async-registry';
2
+ export * from './camel-case';
3
+ export * from './debounce';
4
+ export * from './deep-camel-case-keys';
5
+ export * from './filter-object-values';
6
+ export * from './is-empty-object';
7
+ export * from './is-plain-object';
8
+ export * from './map-object-values';
9
+ export * from './once';
10
+ export * from './shallow-equal';
11
+ export * from './timeout';
12
+ export * from './uid';
13
+ export * from './wait-for';
14
+ export * from './wait-for-dom-ready';
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { isEmptyObject } from './is-empty-object';
4
+
5
+ describe('isEmptyObject', () => {
6
+ it('should return true for empty object', () => {
7
+ const result = isEmptyObject({});
8
+ expect(result).toBe(true);
9
+ });
10
+
11
+ it('should return false for object with properties', () => {
12
+ const result = isEmptyObject({ key: 'value' });
13
+ expect(result).toBe(false);
14
+ });
15
+
16
+ it('should return false for object with multiple properties', () => {
17
+ const result = isEmptyObject({
18
+ name: 'test',
19
+ age: 25,
20
+ active: true,
21
+ });
22
+
23
+ expect(result).toBe(false);
24
+ });
25
+
26
+ it('should return false for object with undefined values', () => {
27
+ const result = isEmptyObject({ key: undefined });
28
+ expect(result).toBe(false);
29
+ });
30
+
31
+ it('should return false for object with null values', () => {
32
+ const result = isEmptyObject({ key: null });
33
+ expect(result).toBe(false);
34
+ });
35
+
36
+ it('should return false for object with empty string values', () => {
37
+ const result = isEmptyObject({ key: '' });
38
+ expect(result).toBe(false);
39
+ });
40
+
41
+ it('should return false for object with numeric keys', () => {
42
+ const result = isEmptyObject({ 0: 'value' });
43
+ expect(result).toBe(false);
44
+ });
45
+
46
+ it('should return false for arrays', () => {
47
+ const result = isEmptyObject([] as any);
48
+ expect(result).toBe(false);
49
+ });
50
+
51
+ it('should return false for arrays with elements', () => {
52
+ const result = isEmptyObject([1, 2, 3] as any);
53
+ expect(result).toBe(false);
54
+ });
55
+
56
+ it('should return false for Date objects', () => {
57
+ const result = isEmptyObject(new Date() as any);
58
+ expect(result).toBe(false);
59
+ });
60
+
61
+ it('should return false for custom class instances', () => {
62
+ class TestClass {}
63
+ const result = isEmptyObject(new TestClass() as any);
64
+ expect(result).toBe(false);
65
+ });
66
+
67
+ it('should return false for objects created with Object.create', () => {
68
+ const result = isEmptyObject(Object.create(null));
69
+ expect(result).toBe(false);
70
+ });
71
+
72
+ it('should return true for objects with inherited properties only', () => {
73
+ const proto = { inheritedProp: 'value' };
74
+ const obj = Object.create(proto);
75
+ const result = isEmptyObject(obj);
76
+ expect(result).toBe(true);
77
+ });
78
+ });
@@ -0,0 +1,3 @@
1
+ export function isEmptyObject(obj: Record<string, unknown>): boolean {
2
+ return Object.keys(obj).length === 0 && obj.constructor === Object;
3
+ }