ckeditor5-blazor 0.1.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 (219) hide show
  1. package/dist/ckeditor5-blazor-error.d.ts +7 -0
  2. package/dist/ckeditor5-blazor-error.d.ts.map +1 -0
  3. package/dist/elements/context/context.d.ts +26 -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 +34 -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 +31 -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/dispatch-editor-roots-change-event.d.ts +23 -0
  22. package/dist/elements/editor/plugins/dispatch-editor-roots-change-event.d.ts.map +1 -0
  23. package/dist/elements/editor/plugins/index.d.ts +3 -0
  24. package/dist/elements/editor/plugins/index.d.ts.map +1 -0
  25. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts +6 -0
  26. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
  27. package/dist/elements/editor/typings.d.ts +99 -0
  28. package/dist/elements/editor/typings.d.ts.map +1 -0
  29. package/dist/elements/editor/utils/create-editor-in-context.d.ts +44 -0
  30. package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -0
  31. package/dist/elements/editor/utils/get-editor-roots-values.d.ts +9 -0
  32. package/dist/elements/editor/utils/get-editor-roots-values.d.ts.map +1 -0
  33. package/dist/elements/editor/utils/index.d.ts +14 -0
  34. package/dist/elements/editor/utils/index.d.ts.map +1 -0
  35. package/dist/elements/editor/utils/is-single-root-editor.d.ts +9 -0
  36. package/dist/elements/editor/utils/is-single-root-editor.d.ts.map +1 -0
  37. package/dist/elements/editor/utils/load-editor-constructor.d.ts +9 -0
  38. package/dist/elements/editor/utils/load-editor-constructor.d.ts.map +1 -0
  39. package/dist/elements/editor/utils/load-editor-plugins.d.ts +20 -0
  40. package/dist/elements/editor/utils/load-editor-plugins.d.ts.map +1 -0
  41. package/dist/elements/editor/utils/load-editor-translations.d.ts +14 -0
  42. package/dist/elements/editor/utils/load-editor-translations.d.ts.map +1 -0
  43. package/dist/elements/editor/utils/normalize-custom-translations.d.ts +11 -0
  44. package/dist/elements/editor/utils/normalize-custom-translations.d.ts.map +1 -0
  45. package/dist/elements/editor/utils/query-all-editor-ids.d.ts +5 -0
  46. package/dist/elements/editor/utils/query-all-editor-ids.d.ts.map +1 -0
  47. package/dist/elements/editor/utils/query-editor-editables.d.ts +25 -0
  48. package/dist/elements/editor/utils/query-editor-editables.d.ts.map +1 -0
  49. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
  50. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
  51. package/dist/elements/editor/utils/resolve-editor-config-translations.d.ts +25 -0
  52. package/dist/elements/editor/utils/resolve-editor-config-translations.d.ts.map +1 -0
  53. package/dist/elements/editor/utils/set-editor-editable-height.d.ts +9 -0
  54. package/dist/elements/editor/utils/set-editor-editable-height.d.ts.map +1 -0
  55. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +24 -0
  56. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
  57. package/dist/elements/ensure-editor-elements-registered.d.ts +5 -0
  58. package/dist/elements/ensure-editor-elements-registered.d.ts.map +1 -0
  59. package/dist/elements/index.d.ts +6 -0
  60. package/dist/elements/index.d.ts.map +1 -0
  61. package/dist/elements/ui-part.d.ts +18 -0
  62. package/dist/elements/ui-part.d.ts.map +1 -0
  63. package/dist/index.cjs +5 -0
  64. package/dist/index.cjs.map +1 -0
  65. package/dist/index.d.ts +11 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.mjs +1400 -0
  68. package/dist/index.mjs.map +1 -0
  69. package/dist/interop/create-context-blazor-interop.d.ts +10 -0
  70. package/dist/interop/create-context-blazor-interop.d.ts.map +1 -0
  71. package/dist/interop/create-editable-blazor-interop.d.ts +21 -0
  72. package/dist/interop/create-editable-blazor-interop.d.ts.map +1 -0
  73. package/dist/interop/create-editor-blazor-interop.d.ts +19 -0
  74. package/dist/interop/create-editor-blazor-interop.d.ts.map +1 -0
  75. package/dist/interop/create-ui-part-blazor-interop.d.ts +10 -0
  76. package/dist/interop/create-ui-part-blazor-interop.d.ts.map +1 -0
  77. package/dist/interop/index.d.ts +5 -0
  78. package/dist/interop/index.d.ts.map +1 -0
  79. package/dist/interop/utils/create-editor-value-sync.d.ts +63 -0
  80. package/dist/interop/utils/create-editor-value-sync.d.ts.map +1 -0
  81. package/dist/interop/utils/index.d.ts +2 -0
  82. package/dist/interop/utils/index.d.ts.map +1 -0
  83. package/dist/shared/async-registry.d.ts +136 -0
  84. package/dist/shared/async-registry.d.ts.map +1 -0
  85. package/dist/shared/camel-case.d.ts +8 -0
  86. package/dist/shared/camel-case.d.ts.map +1 -0
  87. package/dist/shared/debounce.d.ts +2 -0
  88. package/dist/shared/debounce.d.ts.map +1 -0
  89. package/dist/shared/deep-camel-case-keys.d.ts +8 -0
  90. package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
  91. package/dist/shared/filter-object-values.d.ts +9 -0
  92. package/dist/shared/filter-object-values.d.ts.map +1 -0
  93. package/dist/shared/index.d.ts +16 -0
  94. package/dist/shared/index.d.ts.map +1 -0
  95. package/dist/shared/is-empty-object.d.ts +2 -0
  96. package/dist/shared/is-empty-object.d.ts.map +1 -0
  97. package/dist/shared/is-plain-object.d.ts +8 -0
  98. package/dist/shared/is-plain-object.d.ts.map +1 -0
  99. package/dist/shared/map-object-values.d.ts +11 -0
  100. package/dist/shared/map-object-values.d.ts.map +1 -0
  101. package/dist/shared/once.d.ts +2 -0
  102. package/dist/shared/once.d.ts.map +1 -0
  103. package/dist/shared/shallow-equal.d.ts +9 -0
  104. package/dist/shared/shallow-equal.d.ts.map +1 -0
  105. package/dist/shared/timeout.d.ts +8 -0
  106. package/dist/shared/timeout.d.ts.map +1 -0
  107. package/dist/shared/uid.d.ts +7 -0
  108. package/dist/shared/uid.d.ts.map +1 -0
  109. package/dist/shared/wait-for-dom-ready.d.ts +5 -0
  110. package/dist/shared/wait-for-dom-ready.d.ts.map +1 -0
  111. package/dist/shared/wait-for-interactive-attribute.d.ts +18 -0
  112. package/dist/shared/wait-for-interactive-attribute.d.ts.map +1 -0
  113. package/dist/shared/wait-for.d.ts +20 -0
  114. package/dist/shared/wait-for.d.ts.map +1 -0
  115. package/dist/types/can-be-promise.type.d.ts +2 -0
  116. package/dist/types/can-be-promise.type.d.ts.map +1 -0
  117. package/dist/types/dot-net-interop.type.d.ts +7 -0
  118. package/dist/types/dot-net-interop.type.d.ts.map +1 -0
  119. package/dist/types/index.d.ts +4 -0
  120. package/dist/types/index.d.ts.map +1 -0
  121. package/dist/types/required-by.type.d.ts +2 -0
  122. package/dist/types/required-by.type.d.ts.map +1 -0
  123. package/package.json +49 -0
  124. package/src/ckeditor5-blazor-error.ts +9 -0
  125. package/src/elements/context/context.test.ts +323 -0
  126. package/src/elements/context/context.ts +128 -0
  127. package/src/elements/context/contexts-registry.test.ts +10 -0
  128. package/src/elements/context/contexts-registry.ts +10 -0
  129. package/src/elements/context/index.ts +3 -0
  130. package/src/elements/context/typings.ts +38 -0
  131. package/src/elements/editable.test.ts +383 -0
  132. package/src/elements/editable.ts +183 -0
  133. package/src/elements/editor/custom-editor-plugins.test.ts +103 -0
  134. package/src/elements/editor/custom-editor-plugins.ts +85 -0
  135. package/src/elements/editor/editor.test.ts +562 -0
  136. package/src/elements/editor/editor.ts +330 -0
  137. package/src/elements/editor/editors-registry.test.ts +10 -0
  138. package/src/elements/editor/editors-registry.ts +10 -0
  139. package/src/elements/editor/index.ts +2 -0
  140. package/src/elements/editor/plugins/dispatch-editor-roots-change-event.ts +76 -0
  141. package/src/elements/editor/plugins/index.ts +2 -0
  142. package/src/elements/editor/plugins/sync-editor-with-input.ts +79 -0
  143. package/src/elements/editor/typings.ts +114 -0
  144. package/src/elements/editor/utils/create-editor-in-context.ts +89 -0
  145. package/src/elements/editor/utils/get-editor-roots-values.test.ts +48 -0
  146. package/src/elements/editor/utils/get-editor-roots-values.ts +21 -0
  147. package/src/elements/editor/utils/index.ts +13 -0
  148. package/src/elements/editor/utils/is-single-root-editor.test.ts +40 -0
  149. package/src/elements/editor/utils/is-single-root-editor.ts +11 -0
  150. package/src/elements/editor/utils/load-editor-constructor.test.ts +62 -0
  151. package/src/elements/editor/utils/load-editor-constructor.ts +29 -0
  152. package/src/elements/editor/utils/load-editor-plugins.test.ts +100 -0
  153. package/src/elements/editor/utils/load-editor-plugins.ts +72 -0
  154. package/src/elements/editor/utils/load-editor-translations.ts +232 -0
  155. package/src/elements/editor/utils/normalize-custom-translations.test.ts +152 -0
  156. package/src/elements/editor/utils/normalize-custom-translations.ts +17 -0
  157. package/src/elements/editor/utils/query-all-editor-ids.ts +9 -0
  158. package/src/elements/editor/utils/query-editor-editables.ts +101 -0
  159. package/src/elements/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
  160. package/src/elements/editor/utils/resolve-editor-config-elements-references.ts +36 -0
  161. package/src/elements/editor/utils/resolve-editor-config-translations.test.ts +131 -0
  162. package/src/elements/editor/utils/resolve-editor-config-translations.ts +77 -0
  163. package/src/elements/editor/utils/set-editor-editable-height.test.ts +131 -0
  164. package/src/elements/editor/utils/set-editor-editable-height.ts +15 -0
  165. package/src/elements/editor/utils/wrap-with-watchdog.test.ts +45 -0
  166. package/src/elements/editor/utils/wrap-with-watchdog.ts +51 -0
  167. package/src/elements/ensure-editor-elements-registered.ts +24 -0
  168. package/src/elements/index.ts +14 -0
  169. package/src/elements/ui-part.test.ts +156 -0
  170. package/src/elements/ui-part.ts +84 -0
  171. package/src/index.ts +15 -0
  172. package/src/interop/create-context-blazor-interop.test.ts +30 -0
  173. package/src/interop/create-context-blazor-interop.ts +15 -0
  174. package/src/interop/create-editable-blazor-interop.test.ts +213 -0
  175. package/src/interop/create-editable-blazor-interop.ts +98 -0
  176. package/src/interop/create-editor-blazor-interop.test.ts +183 -0
  177. package/src/interop/create-editor-blazor-interop.ts +112 -0
  178. package/src/interop/create-ui-part-blazor-interop.test.ts +30 -0
  179. package/src/interop/create-ui-part-blazor-interop.ts +15 -0
  180. package/src/interop/index.ts +4 -0
  181. package/src/interop/utils/create-editor-value-sync.test.ts +302 -0
  182. package/src/interop/utils/create-editor-value-sync.ts +160 -0
  183. package/src/interop/utils/index.ts +1 -0
  184. package/src/shared/async-registry.test.ts +737 -0
  185. package/src/shared/async-registry.ts +353 -0
  186. package/src/shared/camel-case.test.ts +35 -0
  187. package/src/shared/camel-case.ts +11 -0
  188. package/src/shared/debounce.test.ts +72 -0
  189. package/src/shared/debounce.ts +16 -0
  190. package/src/shared/deep-camel-case-keys.test.ts +34 -0
  191. package/src/shared/deep-camel-case-keys.ts +26 -0
  192. package/src/shared/filter-object-values.test.ts +25 -0
  193. package/src/shared/filter-object-values.ts +17 -0
  194. package/src/shared/index.ts +15 -0
  195. package/src/shared/is-empty-object.test.ts +78 -0
  196. package/src/shared/is-empty-object.ts +3 -0
  197. package/src/shared/is-plain-object.test.ts +38 -0
  198. package/src/shared/is-plain-object.ts +15 -0
  199. package/src/shared/map-object-values.test.ts +29 -0
  200. package/src/shared/map-object-values.ts +19 -0
  201. package/src/shared/once.test.ts +116 -0
  202. package/src/shared/once.ts +12 -0
  203. package/src/shared/shallow-equal.test.ts +51 -0
  204. package/src/shared/shallow-equal.ts +30 -0
  205. package/src/shared/timeout.test.ts +65 -0
  206. package/src/shared/timeout.ts +13 -0
  207. package/src/shared/uid.test.ts +25 -0
  208. package/src/shared/uid.ts +8 -0
  209. package/src/shared/wait-for-dom-ready.test.ts +87 -0
  210. package/src/shared/wait-for-dom-ready.ts +21 -0
  211. package/src/shared/wait-for-interactive-attribute.test.ts +93 -0
  212. package/src/shared/wait-for-interactive-attribute.ts +50 -0
  213. package/src/shared/wait-for.test.ts +24 -0
  214. package/src/shared/wait-for.ts +56 -0
  215. package/src/types/can-be-promise.type.ts +1 -0
  216. package/src/types/dot-net-interop.type.ts +6 -0
  217. package/src/types/dotnet-global.d.ts +14 -0
  218. package/src/types/index.ts +3 -0
  219. package/src/types/required-by.type.ts +1 -0
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { isPlainObject } from './is-plain-object';
4
+
5
+ describe('isPlainObject', () => {
6
+ it('returns true for plain objects', () => {
7
+ expect(isPlainObject({})).toBe(true);
8
+ expect(isPlainObject({ a: 1, b: 2 })).toBe(true);
9
+ expect(isPlainObject(Object.create(null))).toBe(true);
10
+ });
11
+
12
+ it('returns false for arrays', () => {
13
+ expect(isPlainObject([])).toBe(false);
14
+ expect(isPlainObject([1, 2, 3])).toBe(false);
15
+ });
16
+
17
+ it('returns false for null', () => {
18
+ expect(isPlainObject(null)).toBe(false);
19
+ });
20
+
21
+ it('returns false for primitives', () => {
22
+ expect(isPlainObject(42)).toBe(false);
23
+ expect(isPlainObject('string')).toBe(false);
24
+ expect(isPlainObject(true)).toBe(false);
25
+ expect(isPlainObject(undefined)).toBe(false);
26
+ expect(isPlainObject(Symbol('sym'))).toBe(false);
27
+ });
28
+
29
+ it('returns false for class instances', () => {
30
+ class MyClass {}
31
+ expect(isPlainObject(new MyClass())).toBe(false);
32
+ });
33
+
34
+ it('returns false for functions', () => {
35
+ expect(isPlainObject(() => {})).toBe(false);
36
+ expect(isPlainObject(() => {})).toBe(false);
37
+ });
38
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Utility to check if a value is a plain object (not an array, not null, not a class instance).
3
+ *
4
+ * @param value The value to check.
5
+ * @returns True if the value is a plain object, false otherwise.
6
+ */
7
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
8
+ if (Object.prototype.toString.call(value) !== '[object Object]') {
9
+ return false;
10
+ }
11
+
12
+ const proto = Object.getPrototypeOf(value);
13
+
14
+ return proto === Object.prototype || proto === null;
15
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { mapObjectValues } from './map-object-values';
4
+
5
+ describe('mapObjectValues', () => {
6
+ it('should map object values using the provided mapper function', () => {
7
+ const input = { a: 1, b: 2, c: 3 };
8
+ const mapper = (value: number) => value * 2;
9
+ const result = mapObjectValues(input, mapper);
10
+
11
+ expect(result).toEqual({ a: 2, b: 4, c: 6 });
12
+ });
13
+
14
+ it('should pass the key to the mapper function', () => {
15
+ const input = { x: 'foo', y: 'bar' };
16
+ const mapper = (value: string, key: string) => `${key}:${value}`;
17
+ const result = mapObjectValues(input, mapper);
18
+
19
+ expect(result).toEqual({ x: 'x:foo', y: 'y:bar' });
20
+ });
21
+
22
+ it('should return an empty object if the input is empty', () => {
23
+ const input: Record<string, number> = {};
24
+ const mapper = (value: number) => value * 2;
25
+ const result = mapObjectValues(input, mapper);
26
+
27
+ expect(result).toEqual({});
28
+ });
29
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Maps the values of an object using a provided mapper function.
3
+ *
4
+ * @param obj The object whose values will be mapped.
5
+ * @param mapper A function that takes a value and its key, and returns a new value.
6
+ * @template T The type of the original values in the object.
7
+ * @template U The type of the new values in the object.
8
+ * @returns A new object with the same keys as the original, but with values transformed by
9
+ */
10
+ export function mapObjectValues<T, U>(
11
+ obj: Record<string, T>,
12
+ mapper: (value: T, key: string) => U,
13
+ ): Record<string, U> {
14
+ const mappedEntries = Object
15
+ .entries(obj)
16
+ .map(([key, value]) => [key, mapper(value, key)] as const);
17
+
18
+ return Object.fromEntries(mappedEntries);
19
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { once } from './once';
4
+
5
+ describe('once', () => {
6
+ it('should call the original function only once', () => {
7
+ const mockFn = vi.fn().mockReturnValue('result');
8
+ const onceFn = once(mockFn);
9
+
10
+ onceFn();
11
+ onceFn();
12
+ onceFn();
13
+
14
+ expect(mockFn).toHaveBeenCalledTimes(1);
15
+ });
16
+
17
+ it('should return the same result on subsequent calls', () => {
18
+ const mockFn = vi.fn().mockReturnValue('test-result');
19
+ const onceFn = once(mockFn);
20
+
21
+ const result1 = onceFn();
22
+ const result2 = onceFn();
23
+ const result3 = onceFn();
24
+
25
+ expect(result1).toBe('test-result');
26
+ expect(result2).toBe('test-result');
27
+ expect(result3).toBe('test-result');
28
+ });
29
+
30
+ it('should pass arguments to the original function', () => {
31
+ const mockFn = vi.fn();
32
+ const onceFn = once(mockFn);
33
+
34
+ onceFn('arg1', 'arg2', 'arg3');
35
+ onceFn('different', 'args');
36
+
37
+ expect(mockFn).toHaveBeenCalledTimes(1);
38
+ expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2', 'arg3');
39
+ });
40
+
41
+ it('should preserve the context (this) when called', () => {
42
+ const context = { value: 'test-context' };
43
+ const mockFn = vi.fn(function (this: typeof context) {
44
+ return this.value;
45
+ });
46
+ const onceFn = once(mockFn);
47
+
48
+ const result1 = onceFn.call(context);
49
+ const result2 = onceFn.call({ value: 'different-context' });
50
+
51
+ expect(result1).toBe('test-context');
52
+ expect(result2).toBe('test-context');
53
+ expect(mockFn).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it('should work with functions that return undefined', () => {
57
+ const mockFn = vi.fn().mockReturnValue(undefined);
58
+ const onceFn = once(mockFn);
59
+
60
+ const result1 = onceFn();
61
+ const result2 = onceFn();
62
+
63
+ expect(result1).toBeUndefined();
64
+ expect(result2).toBeUndefined();
65
+ expect(mockFn).toHaveBeenCalledTimes(1);
66
+ });
67
+
68
+ it('should work with async functions', async () => {
69
+ const mockFn = vi.fn().mockResolvedValue('async-result');
70
+ const onceFn = once(mockFn);
71
+
72
+ const result1 = await onceFn();
73
+ const result2 = await onceFn();
74
+
75
+ expect(result1).toBe('async-result');
76
+ expect(result2).toBe('async-result');
77
+ expect(mockFn).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it('should maintain function signature for type safety', () => {
81
+ const originalFn = (a: string, b: number): string => `${a}-${b}`;
82
+ const onceFn = once(originalFn);
83
+
84
+ const result = onceFn('test', 42);
85
+
86
+ expect(result).toBe('test-42');
87
+ expect(typeof result).toBe('string');
88
+ });
89
+
90
+ it('should work with functions that have no parameters', () => {
91
+ let counter = 0;
92
+ const mockFn = vi.fn(() => ++counter);
93
+ const onceFn = once(mockFn);
94
+
95
+ const result1 = onceFn();
96
+ const result2 = onceFn();
97
+
98
+ expect(result1).toBe(1);
99
+ expect(result2).toBe(1);
100
+ expect(mockFn).toHaveBeenCalledTimes(1);
101
+ });
102
+
103
+ it('should work with functions that return objects', () => {
104
+ const returnValue = { id: 1, name: 'test' };
105
+ const mockFn = vi.fn().mockReturnValue(returnValue);
106
+ const onceFn = once(mockFn);
107
+
108
+ const result1 = onceFn();
109
+ const result2 = onceFn();
110
+
111
+ expect(result1).toBe(returnValue);
112
+ expect(result2).toBe(returnValue);
113
+ expect(result1).toEqual({ id: 1, name: 'test' });
114
+ expect(mockFn).toHaveBeenCalledTimes(1);
115
+ });
116
+ });
@@ -0,0 +1,12 @@
1
+ export function once<T extends (...args: any[]) => any>(fn: T): T {
2
+ let called = false;
3
+ let result: ReturnType<T>;
4
+
5
+ return function (this: any, ...args: Parameters<T>): ReturnType<T> {
6
+ if (!called) {
7
+ called = true;
8
+ result = fn.apply(this, args);
9
+ }
10
+ return result;
11
+ } as T;
12
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { shallowEqual } from './shallow-equal';
4
+
5
+ describe('shallowEqual', () => {
6
+ it('should return true for identical objects', () => {
7
+ const obj = { a: 1, b: 2 };
8
+ expect(shallowEqual(obj, obj)).toBe(true);
9
+ });
10
+
11
+ it('should return true for objects with same primitive values', () => {
12
+ const objA = { a: 1, b: 'test', c: true };
13
+ const objB = { a: 1, b: 'test', c: true };
14
+ expect(shallowEqual(objA, objB)).toBe(true);
15
+ });
16
+
17
+ it('should return false for objects with different values', () => {
18
+ const objA = { a: 1, b: 2 };
19
+ const objB = { a: 1, b: 3 };
20
+ expect(shallowEqual(objA, objB)).toBe(false);
21
+ });
22
+
23
+ it('should return false for objects with different keys', () => {
24
+ const objA = { a: 1, b: 2 };
25
+ const objB = { a: 1, c: 2 } as any;
26
+ expect(shallowEqual(objA, objB)).toBe(false);
27
+ });
28
+
29
+ it('should return false for objects with different number of keys', () => {
30
+ const objA = { a: 1, b: 2 };
31
+ const objB = { a: 1 };
32
+ expect(shallowEqual(objA, objB)).toBe(false);
33
+ });
34
+
35
+ it('should return true for empty objects', () => {
36
+ expect(shallowEqual({}, {})).toBe(true);
37
+ });
38
+
39
+ it('should return false for nested objects with same structure but different references', () => {
40
+ const objA = { a: { nested: 1 } };
41
+ const objB = { a: { nested: 1 } };
42
+ expect(shallowEqual(objA, objB)).toBe(false);
43
+ });
44
+
45
+ it('should return true for nested objects with same reference', () => {
46
+ const nested = { value: 1 };
47
+ const objA = { a: nested };
48
+ const objB = { a: nested };
49
+ expect(shallowEqual(objA, objB)).toBe(true);
50
+ });
51
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Performs a shallow comparison of two objects.
3
+ *
4
+ * @param objA - The first object to compare.
5
+ * @param objB - The second object to compare.
6
+ * @returns True if the objects are shallowly equal, false otherwise.
7
+ */
8
+ export function shallowEqual<T extends Record<string, unknown>>(
9
+ objA: T,
10
+ objB: T,
11
+ ): boolean {
12
+ if (objA === objB) {
13
+ return true;
14
+ }
15
+
16
+ const keysA = Object.keys(objA);
17
+ const keysB = Object.keys(objB);
18
+
19
+ if (keysA.length !== keysB.length) {
20
+ return false;
21
+ }
22
+
23
+ for (const key of keysA) {
24
+ if (objA[key] !== objB[key] || !Object.prototype.hasOwnProperty.call(objB, key)) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ return true;
30
+ }
@@ -0,0 +1,65 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { timeout } from './timeout.js';
4
+
5
+ describe('timeout', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ it('should resolve after specified milliseconds', async () => {
15
+ const promise = timeout(1000);
16
+
17
+ vi.advanceTimersByTime(1000);
18
+
19
+ await expect(promise).resolves.toBeUndefined();
20
+ });
21
+
22
+ it('should not resolve before specified time', async () => {
23
+ let resolved = false;
24
+ const promise = timeout(1000).then(() => {
25
+ resolved = true;
26
+ });
27
+
28
+ vi.advanceTimersByTime(500);
29
+
30
+ expect(resolved).toBe(false);
31
+
32
+ vi.advanceTimersByTime(500);
33
+ await promise;
34
+
35
+ expect(resolved).toBe(true);
36
+ });
37
+
38
+ it('should resolve immediately when milliseconds is 0', async () => {
39
+ const promise = timeout(0);
40
+
41
+ vi.advanceTimersByTime(0);
42
+
43
+ await expect(promise).resolves.toBeUndefined();
44
+ });
45
+
46
+ it('should handle multiple concurrent timeouts', async () => {
47
+ const results: number[] = [];
48
+
49
+ const promise1 = timeout(100).then(() => results.push(1));
50
+ const promise2 = timeout(200).then(() => results.push(2));
51
+ const promise3 = timeout(150).then(() => results.push(3));
52
+
53
+ vi.advanceTimersByTime(100);
54
+ await promise1;
55
+ expect(results).toEqual([1]);
56
+
57
+ vi.advanceTimersByTime(50);
58
+ await promise3;
59
+ expect(results).toEqual([1, 3]);
60
+
61
+ vi.advanceTimersByTime(50);
62
+ await promise2;
63
+ expect(results).toEqual([1, 3, 2]);
64
+ });
65
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Returns a promise that resolves after a specified number of milliseconds.
3
+ *
4
+ * @param ms The number of milliseconds to wait.
5
+ * @returns A promise that resolves after the specified time.
6
+ */
7
+ export function timeout(ms: number): Promise<void> {
8
+ return new Promise((resolve) => {
9
+ setTimeout(() => {
10
+ resolve();
11
+ }, ms);
12
+ });
13
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { uid } from './uid';
4
+
5
+ describe('uid', () => {
6
+ it('should return a string', () => {
7
+ const id = uid();
8
+ expect(typeof id).toBe('string');
9
+ });
10
+
11
+ it('should return a non-empty string', () => {
12
+ const id = uid();
13
+ expect(id.length).toBeGreaterThan(0);
14
+ });
15
+
16
+ it('should return different values on subsequent calls', () => {
17
+ const ids = new Set(Array.from({ length: 1000 }, () => uid()));
18
+ expect(ids.size).toBe(1000);
19
+ });
20
+
21
+ it('should only contain alphanumeric characters', () => {
22
+ const id = uid();
23
+ expect(/^[a-z0-9]+$/i.test(id)).toBe(true);
24
+ });
25
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Generates a unique identifier string
3
+ *
4
+ * @returns Random string that can be used as unique identifier
5
+ */
6
+ export function uid() {
7
+ return Math.random().toString(36).substring(2);
8
+ }
@@ -0,0 +1,87 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { timeout } from './timeout.js';
4
+ import { waitForDOMReady } from './wait-for-dom-ready.js';
5
+
6
+ describe('waitForDOMReady', () => {
7
+ let originalReadyState: DocumentReadyState;
8
+
9
+ beforeEach(() => {
10
+ originalReadyState = document.readyState;
11
+ });
12
+
13
+ afterEach(() => {
14
+ Object.defineProperty(document, 'readyState', {
15
+ configurable: true,
16
+ get() { return originalReadyState; },
17
+ });
18
+
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ it('should resolve immediately if readyState is "interactive"', async () => {
23
+ Object.defineProperty(document, 'readyState', {
24
+ configurable: true,
25
+ get() { return 'interactive'; },
26
+ });
27
+
28
+ const spy = vi.fn();
29
+ void waitForDOMReady().then(spy);
30
+
31
+ await timeout(0);
32
+ expect(spy).toHaveBeenCalled();
33
+ });
34
+
35
+ it('should resolve immediately if readyState is "complete"', async () => {
36
+ Object.defineProperty(document, 'readyState', {
37
+ configurable: true,
38
+ get() { return 'complete'; },
39
+ });
40
+
41
+ const spy = vi.fn();
42
+ void waitForDOMReady().then(spy);
43
+
44
+ await new Promise(resolve => setTimeout(resolve, 0));
45
+ expect(spy).toHaveBeenCalled();
46
+ });
47
+
48
+ it('should wait for DOMContentLoaded event if readyState is "loading"', async () => {
49
+ Object.defineProperty(document, 'readyState', {
50
+ configurable: true,
51
+ get() { return 'loading'; },
52
+ });
53
+
54
+ const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
55
+ const spy = vi.fn();
56
+
57
+ void waitForDOMReady().then(spy);
58
+
59
+ expect(addEventListenerSpy).toHaveBeenCalledWith('DOMContentLoaded', expect.any(Function), { once: true });
60
+ expect(spy).not.toHaveBeenCalled();
61
+
62
+ // Simulate the event
63
+ const handler = addEventListenerSpy.mock.calls[0]?.[1] as EventListener;
64
+ handler({} as Event);
65
+
66
+ await new Promise(resolve => setTimeout(resolve, 0));
67
+ expect(spy).toHaveBeenCalled();
68
+ });
69
+
70
+ it('should warn and resolve immediately for unexpected readyState', async () => {
71
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
72
+
73
+ Object.defineProperty(document, 'readyState', {
74
+ configurable: true,
75
+ get() { return 'unexpected'; },
76
+ });
77
+
78
+ const spy = vi.fn();
79
+ void waitForDOMReady().then(spy);
80
+
81
+ await timeout(0);
82
+ expect(consoleWarnSpy).toHaveBeenCalledWith('Unexpected document.readyState:', 'unexpected');
83
+ expect(spy).toHaveBeenCalled();
84
+
85
+ consoleWarnSpy.mockRestore();
86
+ });
87
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Returns a promise that resolves when the DOM is fully loaded and ready.
3
+ */
4
+ export function waitForDOMReady(): Promise<void> {
5
+ return new Promise((resolve) => {
6
+ switch (document.readyState) {
7
+ case 'loading':
8
+ document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
9
+ break;
10
+
11
+ case 'interactive':
12
+ case 'complete':
13
+ setTimeout(resolve, 0);
14
+ break;
15
+
16
+ default:
17
+ console.warn('Unexpected document.readyState:', document.readyState);
18
+ setTimeout(resolve, 0);
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,93 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { markElementAsInteractive, waitForInteractiveAttribute } from './wait-for-interactive-attribute';
4
+
5
+ describe('waitForInteractiveAttribute', () => {
6
+ let element: HTMLElement;
7
+
8
+ beforeEach(() => {
9
+ vi.useFakeTimers();
10
+ element = document.createElement('div');
11
+ document.body.appendChild(element);
12
+ });
13
+
14
+ afterEach(() => {
15
+ document.body.removeChild(element);
16
+ vi.restoreAllMocks();
17
+ vi.useRealTimers();
18
+ });
19
+
20
+ it('should resolve immediately if the element already has the data-cke-interactive attribute', async () => {
21
+ markElementAsInteractive(element);
22
+
23
+ const result = waitForInteractiveAttribute(element);
24
+
25
+ await expect(result.promise).resolves.toBeUndefined();
26
+ });
27
+
28
+ it('should wait for the data-cke-interactive attribute to be added', async () => {
29
+ const result = waitForInteractiveAttribute(element);
30
+ const successSpy = vi.fn();
31
+
32
+ // Attach a spy to the promise to track its resolution status
33
+ void result.promise.then(successSpy);
34
+
35
+ // 1. Ensure the promise is pending initially (flush microtasks)
36
+ await vi.advanceTimersByTimeAsync(0);
37
+ expect(successSpy).not.toHaveBeenCalled();
38
+
39
+ // 2. Add the attribute
40
+ markElementAsInteractive(element);
41
+
42
+ // 3. Ensure the promise resolves after the mutation
43
+ await expect(result.promise).resolves.toBeUndefined();
44
+ expect(successSpy).toHaveBeenCalled();
45
+ });
46
+
47
+ it('should NOT resolve if a different attribute is added', async () => {
48
+ const result = waitForInteractiveAttribute(element);
49
+ const successSpy = vi.fn();
50
+
51
+ void result.promise.then(successSpy);
52
+
53
+ // Add an irrelevant attribute
54
+ element.setAttribute('data-other', '');
55
+
56
+ // Advance time and flush tasks to give the observer a chance to react (incorrectly)
57
+ await vi.advanceTimersByTimeAsync(100);
58
+
59
+ // The promise should still be pending
60
+ expect(successSpy).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it('should disconnect the observer automatically when the attribute is added', async () => {
64
+ // Spy on the native MutationObserver disconnect method
65
+ const disconnectSpy = vi.spyOn(MutationObserver.prototype, 'disconnect');
66
+
67
+ const result = waitForInteractiveAttribute(element);
68
+ markElementAsInteractive(element);
69
+
70
+ await result.promise;
71
+
72
+ expect(disconnectSpy).toHaveBeenCalled();
73
+ });
74
+
75
+ it('should allow disconnecting the observer manually and stop listening', async () => {
76
+ const disconnectSpy = vi.spyOn(MutationObserver.prototype, 'disconnect');
77
+ const result = waitForInteractiveAttribute(element);
78
+ const successSpy = vi.fn();
79
+
80
+ void result.promise.then(successSpy);
81
+
82
+ // 1. Manually disconnect
83
+ result.disconnect();
84
+ expect(disconnectSpy).toHaveBeenCalled();
85
+
86
+ // 2. Add the attribute AFTER disconnecting
87
+ markElementAsInteractive(element);
88
+
89
+ // 3. Verify the promise remains pending (observer should be dead)
90
+ await vi.advanceTimersByTimeAsync(100);
91
+ expect(successSpy).not.toHaveBeenCalled();
92
+ });
93
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Waits for the 'data-cke-interactive' attribute to be added to the element.
3
+ */
4
+ export function waitForInteractiveAttribute(element: HTMLElement): WaitForInteractiveResult {
5
+ if (element.hasAttribute('data-cke-interactive')) {
6
+ return { promise: Promise.resolve(), disconnect: () => {} };
7
+ }
8
+
9
+ let observer: MutationObserver;
10
+
11
+ const promise = new Promise<void>((resolve) => {
12
+ observer = new MutationObserver((mutations) => {
13
+ for (const mutation of mutations) {
14
+ if (
15
+ mutation.type === 'attributes'
16
+ && mutation.attributeName === 'data-cke-interactive'
17
+ && element.hasAttribute('data-cke-interactive')
18
+ ) {
19
+ observer.disconnect();
20
+ resolve();
21
+ break;
22
+ }
23
+ }
24
+ });
25
+
26
+ observer.observe(element, { attributes: true });
27
+ });
28
+
29
+ return {
30
+ promise,
31
+ disconnect: () => observer?.disconnect(),
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Marks the given element as interactive by adding the 'data-cke-interactive' attribute.
37
+ *
38
+ * @param element - The HTML element to mark as interactive.
39
+ */
40
+ export function markElementAsInteractive(element: HTMLElement) {
41
+ element.setAttribute('data-cke-interactive', 'true');
42
+ }
43
+
44
+ /**
45
+ * Result of waiting for the interactive attribute.
46
+ */
47
+ export type WaitForInteractiveResult = {
48
+ promise: Promise<void>;
49
+ disconnect: () => void;
50
+ };