@shortfuse/materialdesignweb 0.9.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -37
- package/api/README.md +5 -0
- package/api/css.css-data.json +23 -0
- package/api/custom-elements.json +116343 -0
- package/api/html.html-data.json +2994 -0
- package/bin/mdw-css.js +8 -8
- package/components/BottomSheet.js +2 -2
- package/components/Box.js +2 -2
- package/components/NavItem.js +4 -1
- package/components/Page.js +3 -0
- package/components/Pane.js +0 -2
- package/components/SideSheet.js +2 -2
- package/components/Surface.js +2 -2
- package/core/Composition.js +57 -16
- package/core/CompositionAdapter.js +55 -12
- package/core/CustomElement.js +177 -34
- package/core/customTypes.js +66 -0
- package/core/jsonMergePatch.js +203 -20
- package/core/observe.js +256 -10
- package/dist/CustomElement.min.js +1 -1
- package/dist/CustomElement.min.js.map +3 -3
- package/dist/index.min.js +171 -158
- package/dist/index.min.js.map +4 -4
- package/dist/meta.json +1 -1
- package/loaders/theme.js +4 -1
- package/mixins/{FlexableMixin.js → FlexboxMixin.js} +5 -3
- package/package.json +32 -10
- package/services/theme.js +115 -97
- package/types/components/NavBarItem.d.ts +1 -0
- package/types/components/NavDrawerItem.d.ts +1 -0
- package/types/components/NavItem.d.ts +1 -0
- package/types/components/NavItem.d.ts.map +1 -1
- package/types/components/NavRailItem.d.ts +1 -0
- package/types/components/Page.d.ts.map +1 -1
- package/types/core/Composition.d.ts +5 -1
- package/types/core/Composition.d.ts.map +1 -1
- package/types/core/CompositionAdapter.d.ts +0 -25
- package/types/core/CompositionAdapter.d.ts.map +1 -1
- package/types/core/CustomElement.d.ts +122 -19
- package/types/core/CustomElement.d.ts.map +1 -1
- package/types/core/customTypes.d.ts +8 -0
- package/types/core/customTypes.d.ts.map +1 -1
- package/types/core/jsonMergePatch.d.ts +58 -4
- package/types/core/jsonMergePatch.d.ts.map +1 -1
- package/types/core/observe.d.ts +21 -2
- package/types/core/observe.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/mixins/FlexboxMixin.d.ts +14 -0
- package/types/mixins/FlexboxMixin.d.ts.map +1 -0
- package/types/services/theme.d.ts +6 -0
- package/types/services/theme.d.ts.map +1 -1
- package/types/mixins/FlexableMixin.d.ts +0 -14
- package/types/mixins/FlexableMixin.d.ts.map +0 -1
package/core/CustomElement.js
CHANGED
|
@@ -113,16 +113,6 @@ import { addInlineFunction, html } from './template.js';
|
|
|
113
113
|
* @typedef {(T | Array<[keyof T & string, T[keyof T]]>)} ObjectOrObjectEntries
|
|
114
114
|
*/
|
|
115
115
|
|
|
116
|
-
/**
|
|
117
|
-
* @template {abstract new (...args: any) => unknown} T
|
|
118
|
-
* @param {InstanceType<T>} instance
|
|
119
|
-
*/
|
|
120
|
-
function superOf(instance) {
|
|
121
|
-
const staticContext = instance.constructor;
|
|
122
|
-
const superOfStatic = Object.getPrototypeOf(staticContext);
|
|
123
|
-
return superOfStatic.prototype;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
116
|
/**
|
|
127
117
|
* Clone attribute
|
|
128
118
|
* @param {string} name
|
|
@@ -191,13 +181,8 @@ export default class CustomElement extends HTMLElement {
|
|
|
191
181
|
static supportsElementInternalsRole = CustomElement.supportsElementInternals
|
|
192
182
|
&& 'role' in ElementInternals.prototype;
|
|
193
183
|
|
|
194
|
-
/** @type {boolean} */
|
|
195
|
-
static templatable = null;
|
|
196
|
-
|
|
197
184
|
static defined = false;
|
|
198
185
|
|
|
199
|
-
static autoRegistration = true;
|
|
200
|
-
|
|
201
186
|
/** @type {Map<string, typeof CustomElement>} */
|
|
202
187
|
static registrations = new Map();
|
|
203
188
|
|
|
@@ -326,6 +311,42 @@ export default class CustomElement extends HTMLElement {
|
|
|
326
311
|
this[collection].push(callback);
|
|
327
312
|
}
|
|
328
313
|
|
|
314
|
+
/**
|
|
315
|
+
* @this T
|
|
316
|
+
* @template {typeof CustomElement} T
|
|
317
|
+
* @template {keyof T} K
|
|
318
|
+
* @param {K} collection
|
|
319
|
+
* @param {Function} [callback]
|
|
320
|
+
*/
|
|
321
|
+
static _removeCallback(collection, callback) {
|
|
322
|
+
if (!this.hasOwnProperty(collection)) {
|
|
323
|
+
// @ts-expect-error not typed
|
|
324
|
+
this[collection] = [...this[collection]];
|
|
325
|
+
}
|
|
326
|
+
// @ts-expect-error any
|
|
327
|
+
this[collection] = this[collection].filter((fn) => fn !== callback);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @param {Map<string, Function[]>} map
|
|
332
|
+
* @param {string} name
|
|
333
|
+
* @param {Function} [callback]
|
|
334
|
+
* @return {void}
|
|
335
|
+
*/
|
|
336
|
+
static _removeFromCallbackMap(map, name, callback) {
|
|
337
|
+
if (!map.has(name)) return;
|
|
338
|
+
if (!callback) {
|
|
339
|
+
map.delete(name);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const next = map.get(name).filter((fn) => fn !== callback);
|
|
343
|
+
if (next.length === 0) {
|
|
344
|
+
map.delete(name);
|
|
345
|
+
} else {
|
|
346
|
+
map.set(name, next);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
329
350
|
/**
|
|
330
351
|
* Append parts to composition
|
|
331
352
|
* @type {{
|
|
@@ -627,7 +648,7 @@ export default class CustomElement extends HTMLElement {
|
|
|
627
648
|
}
|
|
628
649
|
// TODO: Inspect possible closure bloat
|
|
629
650
|
config.changedCallback = function wrappedChangedCallback(oldValue, newValue, changes) {
|
|
630
|
-
this.
|
|
651
|
+
this.#onObserverPropertyChanged.call(this, name, oldValue, newValue, changes);
|
|
631
652
|
};
|
|
632
653
|
|
|
633
654
|
this.propList.set(name, config);
|
|
@@ -644,6 +665,32 @@ export default class CustomElement extends HTMLElement {
|
|
|
644
665
|
return this;
|
|
645
666
|
}
|
|
646
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Creates observable property on instances (via prototype)
|
|
670
|
+
* @type {{
|
|
671
|
+
* <
|
|
672
|
+
* CLASS extends typeof CustomElement,
|
|
673
|
+
* ARGS extends ConstructorParameters<CLASS>,
|
|
674
|
+
* INSTANCE extends InstanceType<CLASS>,
|
|
675
|
+
* KEY extends string,
|
|
676
|
+
* OPTIONS extends ObserverPropertyType
|
|
677
|
+
* | ObserverOptions<ObserverPropertyType, unknown, INSTANCE>
|
|
678
|
+
* | ((this:INSTANCE, data:Partial<INSTANCE>, fn?: () => any) => any)
|
|
679
|
+
* > (this: CLASS, name: KEY, options: OPTIONS)
|
|
680
|
+
* : OPTIONS extends (...args2:any[]) => infer R ? R
|
|
681
|
+
* : OPTIONS extends ObserverPropertyType ? import('./observe').ParsedObserverPropertyType<OPTIONS>
|
|
682
|
+
* : OPTIONS extends {type: 'object'} & ObserverOptions<any, infer R> ? (unknown extends R ? object : R)
|
|
683
|
+
* : OPTIONS extends {type: ObserverPropertyType} ? import('./observe').ParsedObserverPropertyType<OPTIONS['type']>
|
|
684
|
+
* : OPTIONS extends ObserverOptions<any, infer R> ? (unknown extends R ? string : R)
|
|
685
|
+
* : never
|
|
686
|
+
* }}
|
|
687
|
+
*/
|
|
688
|
+
static setPrototype(name, typeOrOptions) {
|
|
689
|
+
// @ts-expect-error Can't cast T
|
|
690
|
+
this.prop(name, typeOrOptions);
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
|
|
647
694
|
/**
|
|
648
695
|
* Define properties on instances via Object.defineProperties().
|
|
649
696
|
* Automatically sets property non-enumerable if name begins with `_`.
|
|
@@ -924,6 +971,109 @@ export default class CustomElement extends HTMLElement {
|
|
|
924
971
|
return this;
|
|
925
972
|
}
|
|
926
973
|
|
|
974
|
+
/**
|
|
975
|
+
* @type {{
|
|
976
|
+
* <
|
|
977
|
+
* T1 extends typeof CustomElement,
|
|
978
|
+
* T2 extends InstanceType<T1>,
|
|
979
|
+
* T3 extends CompositionCallback<T2, T2>,
|
|
980
|
+
* T4 extends keyof T3,
|
|
981
|
+
* >
|
|
982
|
+
* (this: T1, name: T3|T4, callbacks?: T3[T4] & ThisType<T2>): T1
|
|
983
|
+
* }}
|
|
984
|
+
*/
|
|
985
|
+
static off(nameOrCallbacks, callback) {
|
|
986
|
+
const callbacks = typeof nameOrCallbacks === 'string'
|
|
987
|
+
? { [nameOrCallbacks]: callback }
|
|
988
|
+
: nameOrCallbacks;
|
|
989
|
+
for (const [name, fn] of Object.entries(callbacks)) {
|
|
990
|
+
/** @type {keyof (typeof CustomElement)} */
|
|
991
|
+
let arrayPropName;
|
|
992
|
+
switch (name) {
|
|
993
|
+
case 'composed': arrayPropName = '_onComposeCallbacks'; break;
|
|
994
|
+
case 'constructed': arrayPropName = '_onConstructedCallbacks'; break;
|
|
995
|
+
case 'connected': arrayPropName = '_onConnectedCallbacks'; break;
|
|
996
|
+
case 'disconnected': arrayPropName = '_onDisconnectedCallbacks'; break;
|
|
997
|
+
case 'props':
|
|
998
|
+
this.offPropChanged(fn);
|
|
999
|
+
continue;
|
|
1000
|
+
case 'attrs':
|
|
1001
|
+
this.offAttributeChanged(fn);
|
|
1002
|
+
continue;
|
|
1003
|
+
default:
|
|
1004
|
+
if (name.endsWith('Changed')) {
|
|
1005
|
+
const prop = name.slice(0, name.length - 'Changed'.length);
|
|
1006
|
+
// @ts-expect-error Computed key
|
|
1007
|
+
this.offPropChanged({ [prop]: fn });
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
throw new Error('Invalid callback name');
|
|
1011
|
+
}
|
|
1012
|
+
this._removeCallback(arrayPropName, fn);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return this;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* @type {{
|
|
1020
|
+
* <
|
|
1021
|
+
* T1 extends typeof CustomElement,
|
|
1022
|
+
* T2 extends InstanceType<T1>
|
|
1023
|
+
* >
|
|
1024
|
+
* (
|
|
1025
|
+
* this: T1,
|
|
1026
|
+
* options: ObjectOrObjectEntries<{
|
|
1027
|
+
* [P in keyof T2]? : (
|
|
1028
|
+
* this: T2,
|
|
1029
|
+
* oldValue: T2[P],
|
|
1030
|
+
* newValue: T2[P],
|
|
1031
|
+
* changes:any,
|
|
1032
|
+
* element: T2
|
|
1033
|
+
* ) => void
|
|
1034
|
+
* }>,
|
|
1035
|
+
* ): T1;
|
|
1036
|
+
* }}
|
|
1037
|
+
*/
|
|
1038
|
+
static offPropChanged(options) {
|
|
1039
|
+
const entries = Array.isArray(options) ? options : Object.entries(options);
|
|
1040
|
+
const { propChangedCallbacks } = this;
|
|
1041
|
+
for (const [prop, callback] of entries) {
|
|
1042
|
+
this._removeFromCallbackMap(propChangedCallbacks, prop, callback);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return this;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* @type {{
|
|
1050
|
+
* <
|
|
1051
|
+
* T1 extends typeof CustomElement,
|
|
1052
|
+
* T2 extends InstanceType<T1>
|
|
1053
|
+
* >
|
|
1054
|
+
* (
|
|
1055
|
+
* this: T1,
|
|
1056
|
+
* options: {
|
|
1057
|
+
* [x:string]: (
|
|
1058
|
+
* this: T2,
|
|
1059
|
+
* oldValue: string,
|
|
1060
|
+
* newValue: string,
|
|
1061
|
+
* element: T2
|
|
1062
|
+
* ) => void
|
|
1063
|
+
* },
|
|
1064
|
+
* ): T1;
|
|
1065
|
+
* }}
|
|
1066
|
+
*/
|
|
1067
|
+
static offAttributeChanged(options) {
|
|
1068
|
+
const entries = Array.isArray(options) ? options : Object.entries(options);
|
|
1069
|
+
const { attributeChangedCallbacks } = this;
|
|
1070
|
+
for (const [name, callback] of entries) {
|
|
1071
|
+
this._removeFromCallbackMap(attributeChangedCallbacks, name, callback);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return this;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
927
1077
|
/**
|
|
928
1078
|
* @type {{
|
|
929
1079
|
* <
|
|
@@ -1009,10 +1159,10 @@ export default class CustomElement extends HTMLElement {
|
|
|
1009
1159
|
#pendingPatchRenders = [];
|
|
1010
1160
|
|
|
1011
1161
|
/** @type {Map<string,{stringValue:string, parsedValue:any}>} */
|
|
1012
|
-
|
|
1162
|
+
#propAttributeCache;
|
|
1013
1163
|
|
|
1014
1164
|
/** @type {CallbackArguments} */
|
|
1015
|
-
|
|
1165
|
+
#callbackArguments = null;
|
|
1016
1166
|
|
|
1017
1167
|
/** @param {any[]} args */
|
|
1018
1168
|
constructor(...args) {
|
|
@@ -1141,7 +1291,7 @@ export default class CustomElement extends HTMLElement {
|
|
|
1141
1291
|
* @param {any} newValue
|
|
1142
1292
|
* @param {any} changes
|
|
1143
1293
|
*/
|
|
1144
|
-
|
|
1294
|
+
#onObserverPropertyChanged(name, oldValue, newValue, changes) {
|
|
1145
1295
|
const { propList } = this.static;
|
|
1146
1296
|
if (propList.has(name)) {
|
|
1147
1297
|
const { reflect, attr } = propList.get(name);
|
|
@@ -1177,10 +1327,12 @@ export default class CustomElement extends HTMLElement {
|
|
|
1177
1327
|
this.propChangedCallback(name, oldValue, newValue, changes);
|
|
1178
1328
|
}
|
|
1179
1329
|
|
|
1330
|
+
get static() { return /** @type {typeof CustomElement} */ (/** @type {unknown} */ (this.constructor)); }
|
|
1331
|
+
|
|
1180
1332
|
/** @param {any} patch */
|
|
1181
1333
|
patch(patch) {
|
|
1182
1334
|
this.#patching = true;
|
|
1183
|
-
applyMergePatch(this, patch);
|
|
1335
|
+
applyMergePatch(this, patch, 'object');
|
|
1184
1336
|
for (const [name, changes, state] of this.#pendingPatchRenders) {
|
|
1185
1337
|
if (name in patch) continue;
|
|
1186
1338
|
this.render.byProp(name, changes, state);
|
|
@@ -1204,9 +1356,6 @@ export default class CustomElement extends HTMLElement {
|
|
|
1204
1356
|
* @return {Element}
|
|
1205
1357
|
*/
|
|
1206
1358
|
get: (target, tag) => {
|
|
1207
|
-
if (!this.#composition) {
|
|
1208
|
-
console.warn(this.static.name, 'Attempted to access references before composing!');
|
|
1209
|
-
}
|
|
1210
1359
|
const composition = this.composition;
|
|
1211
1360
|
let element;
|
|
1212
1361
|
if (!composition.interpolated) {
|
|
@@ -1241,20 +1390,16 @@ export default class CustomElement extends HTMLElement {
|
|
|
1241
1390
|
|
|
1242
1391
|
get attributeCache() {
|
|
1243
1392
|
// eslint-disable-next-line no-return-assign
|
|
1244
|
-
return (this
|
|
1393
|
+
return (this.#propAttributeCache ??= new Map());
|
|
1245
1394
|
}
|
|
1246
1395
|
|
|
1247
|
-
get static() { return /** @type {typeof CustomElement} */ (/** @type {unknown} */ (this.constructor)); }
|
|
1248
|
-
|
|
1249
|
-
get unique() { return false; }
|
|
1250
|
-
|
|
1251
1396
|
/**
|
|
1252
1397
|
* @template {CustomElement} T
|
|
1253
1398
|
* @this {T}
|
|
1254
1399
|
*/
|
|
1255
1400
|
get callbackArguments() {
|
|
1256
1401
|
// eslint-disable-next-line no-return-assign
|
|
1257
|
-
return this
|
|
1402
|
+
return this.#callbackArguments ??= {
|
|
1258
1403
|
composition: this.#composition,
|
|
1259
1404
|
refs: this.refs,
|
|
1260
1405
|
html: html.bind(this),
|
|
@@ -1268,7 +1413,7 @@ export default class CustomElement extends HTMLElement {
|
|
|
1268
1413
|
get composition() {
|
|
1269
1414
|
if (this.#composition) return this.#composition;
|
|
1270
1415
|
|
|
1271
|
-
if (
|
|
1416
|
+
if (this.static.hasOwnProperty('_composition')) {
|
|
1272
1417
|
this.#composition = this.static._composition;
|
|
1273
1418
|
return this.static._composition;
|
|
1274
1419
|
}
|
|
@@ -1281,10 +1426,8 @@ export default class CustomElement extends HTMLElement {
|
|
|
1281
1426
|
callback.call(this, this.callbackArguments);
|
|
1282
1427
|
}
|
|
1283
1428
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
this.static._composition = this.#composition;
|
|
1287
|
-
}
|
|
1429
|
+
// Cache compilation into static property
|
|
1430
|
+
this.static._composition = this.#composition;
|
|
1288
1431
|
|
|
1289
1432
|
return this.#composition;
|
|
1290
1433
|
}
|
package/core/customTypes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @typedef {import('./CustomElement').default} CustomElement */
|
|
2
2
|
|
|
3
3
|
import { attrNameFromPropName } from './dom.js';
|
|
4
|
+
import { subscribeProxy } from './observe.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @see https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-attributes
|
|
@@ -53,6 +54,71 @@ export const EVENT_HANDLER_TYPE = {
|
|
|
53
54
|
},
|
|
54
55
|
};
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @template {unknown} T
|
|
59
|
+
* @typedef {import('./observe.js').ObserverOptions<'object', T, CustomElement>} STORE_PROXY_TYPE
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/** @typedef {WeakMap<CustomElement, Map<string, Function>>} */
|
|
63
|
+
const StoreProxySubscriptions = new WeakMap();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Proxy store binding. Expects a proxy with a `.subscribe(fn)` method that
|
|
67
|
+
* returns an unsubscribe function.
|
|
68
|
+
* @template {unknown} T
|
|
69
|
+
* @type {import('./observe.js').ObserverOptions<'object', T, CustomElement>}
|
|
70
|
+
*/
|
|
71
|
+
export const STORE_PROXY_TYPE = {
|
|
72
|
+
type: 'object',
|
|
73
|
+
reflect: false,
|
|
74
|
+
parser(v) { return v; },
|
|
75
|
+
diff: null, // Skip computing entire change
|
|
76
|
+
propChangedCallback(name, oldValue, newValue) {
|
|
77
|
+
if (oldValue === newValue) return;
|
|
78
|
+
/** @return {void} */
|
|
79
|
+
function connectedCallback() {
|
|
80
|
+
let subscriptions = StoreProxySubscriptions.get(this);
|
|
81
|
+
if (!subscriptions) {
|
|
82
|
+
subscriptions = new Map();
|
|
83
|
+
StoreProxySubscriptions.set(this, subscriptions);
|
|
84
|
+
}
|
|
85
|
+
const unsubscribeFn = subscribeProxy(this[name], (patch) => {
|
|
86
|
+
this.render.byProp(name, patch, this);
|
|
87
|
+
});
|
|
88
|
+
subscriptions.set(name, unsubscribeFn);
|
|
89
|
+
// Render immediately
|
|
90
|
+
this.render.byProp(name, this[name], this);
|
|
91
|
+
}
|
|
92
|
+
/** @return {void} */
|
|
93
|
+
function disconnectedCallback() {
|
|
94
|
+
const subscriptions = StoreProxySubscriptions.get(this);
|
|
95
|
+
if (!subscriptions?.has(name)) return;
|
|
96
|
+
const unsubscribeFn = subscriptions.get(name);
|
|
97
|
+
unsubscribeFn();
|
|
98
|
+
subscriptions.delete(name);
|
|
99
|
+
}
|
|
100
|
+
if (oldValue) {
|
|
101
|
+
// Perform unsubscribe
|
|
102
|
+
disconnectedCallback.call(this);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (newValue) {
|
|
106
|
+
this.static.on({
|
|
107
|
+
connected: connectedCallback,
|
|
108
|
+
disconnected: disconnectedCallback,
|
|
109
|
+
});
|
|
110
|
+
if (this.isConnected) {
|
|
111
|
+
connectedCallback.call(this);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
this.static.off({
|
|
115
|
+
connected: connectedCallback,
|
|
116
|
+
disconnected: disconnectedCallback,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
56
122
|
// const weakRefValues = new WeakMap();
|
|
57
123
|
|
|
58
124
|
// /**
|
package/core/jsonMergePatch.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
/** @
|
|
1
|
+
/** @see https://www.rfc-editor.org/rfc/rfc7396 */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
+
* Default behavior: arrays are treated like objects (index/length merge).
|
|
4
5
|
* @template T1
|
|
5
6
|
* @template T2
|
|
6
7
|
* @param {T1} target
|
|
7
8
|
* @param {T2} patch
|
|
8
9
|
* @return {T1|T2|(T1 & T2)}
|
|
9
10
|
*/
|
|
10
|
-
export function
|
|
11
|
+
export function applyMergePatchObject(target, patch) {
|
|
11
12
|
// @ts-ignore Runtime check
|
|
12
13
|
if (target === patch) return target;
|
|
14
|
+
if (Array.isArray(patch)) return patch;
|
|
13
15
|
if (target == null || patch == null || typeof patch !== 'object') return patch;
|
|
14
16
|
if (typeof target !== 'object') {
|
|
15
17
|
// @ts-ignore Forced cast to object
|
|
@@ -19,30 +21,119 @@ export function applyMergePatch(target, patch) {
|
|
|
19
21
|
if (value == null) {
|
|
20
22
|
// @ts-ignore Runtime check
|
|
21
23
|
if (key in target) {
|
|
22
|
-
// @ts-ignore
|
|
24
|
+
// @ts-ignore target is object
|
|
23
25
|
delete target[key];
|
|
24
26
|
}
|
|
25
27
|
} else {
|
|
26
|
-
// @ts-ignore
|
|
27
|
-
target[key] =
|
|
28
|
+
// @ts-ignore target is object
|
|
29
|
+
target[key] = applyMergePatchObject(target[key], value);
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
return target;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* RFC 7396 semantics: only JSON objects are merged. Arrays are replaced.
|
|
37
|
+
* @template T1
|
|
38
|
+
* @template T2
|
|
39
|
+
* @param {T1} target
|
|
40
|
+
* @param {T2} patch
|
|
41
|
+
* @return {T1|T2|(T1 & T2)}
|
|
42
|
+
*/
|
|
43
|
+
function applyMergePatchReference(target, patch) {
|
|
44
|
+
// @ts-ignore Runtime check
|
|
45
|
+
if (target === patch) return target;
|
|
46
|
+
const patchIsObject = patch != null && typeof patch === 'object' && !Array.isArray(patch);
|
|
47
|
+
const targetIsObject = target != null && typeof target === 'object' && !Array.isArray(target);
|
|
48
|
+
if (!patchIsObject || !targetIsObject) return patch;
|
|
49
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
50
|
+
if (value == null) {
|
|
51
|
+
// @ts-ignore Runtime check
|
|
52
|
+
if (key in target) {
|
|
53
|
+
// @ts-ignore target is object
|
|
54
|
+
delete target[key];
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// @ts-ignore target is object
|
|
58
|
+
target[key] = applyMergePatchReference(target[key], value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return target;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @template T1
|
|
66
|
+
* @template T2
|
|
67
|
+
* @param {T1} target
|
|
68
|
+
* @param {T2} patch
|
|
69
|
+
* @param {'clone'|'object'|'reference'} [arrayStrategy='reference']
|
|
70
|
+
* @return {T1|T2|(T1 & T2)}
|
|
71
|
+
*/
|
|
72
|
+
export function applyMergePatch(target, patch, arrayStrategy = 'reference') {
|
|
73
|
+
switch (arrayStrategy) {
|
|
74
|
+
case 'clone':
|
|
75
|
+
case 'object':
|
|
76
|
+
return applyMergePatchObject(target, patch);
|
|
77
|
+
default:
|
|
78
|
+
return applyMergePatchReference(target, patch);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a JSON Merge patch based
|
|
84
|
+
* Allows different strategies for arrays
|
|
85
|
+
* - `reference`: Returns array as-is (replacement).
|
|
86
|
+
* @param {object|number|string|boolean} previous
|
|
87
|
+
* @param {object|number|string|boolean} current
|
|
88
|
+
* @return {any} Patch
|
|
89
|
+
*/
|
|
90
|
+
export function buildMergePatchReference(previous, current) {
|
|
91
|
+
if (previous === current) return null;
|
|
92
|
+
if (current == null || typeof current !== 'object') return current;
|
|
93
|
+
if (previous == null || typeof previous !== 'object') {
|
|
94
|
+
return structuredClone(current);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const patch = {};
|
|
98
|
+
if (Array.isArray(current)) {
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const previousKeys = new Set(Object.keys(previous));
|
|
103
|
+
for (const [key, value] of Object.entries(current)) {
|
|
104
|
+
previousKeys.delete(key);
|
|
105
|
+
if (value === null) {
|
|
106
|
+
// @ts-ignore patch is Object
|
|
107
|
+
patch[key] = null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (value == null) {
|
|
111
|
+
continue; // Skip undefined
|
|
112
|
+
}
|
|
113
|
+
// @ts-ignore previous is Object
|
|
114
|
+
const changes = buildMergePatchReference(previous[key], value);
|
|
115
|
+
if (changes !== null) {
|
|
116
|
+
// @ts-ignore patch is Object
|
|
117
|
+
patch[key] = changes;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const key of previousKeys) {
|
|
121
|
+
// @ts-ignore patch is Object
|
|
122
|
+
patch[key] = null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return patch;
|
|
126
|
+
}
|
|
127
|
+
|
|
33
128
|
/**
|
|
34
129
|
* Creates a JSON Merge patch based
|
|
35
130
|
* Allows different strategies for arrays
|
|
36
|
-
* - `reference`: Per spec, returns array as is
|
|
37
131
|
* - `clone`: Clones all entries with no inspection.
|
|
38
|
-
* - `object`: Convert to flattened, array-like objects. Requires
|
|
39
|
-
* consumer of patch to be aware of the schema beforehand.
|
|
40
132
|
* @param {object|number|string|boolean} previous
|
|
41
133
|
* @param {object|number|string|boolean} current
|
|
42
|
-
* @param {'clone'|'object'|'reference'} [arrayStrategy='reference']
|
|
43
134
|
* @return {any} Patch
|
|
44
135
|
*/
|
|
45
|
-
export function
|
|
136
|
+
export function buildMergePatchClone(previous, current) {
|
|
46
137
|
if (previous === current) return null;
|
|
47
138
|
if (current == null || typeof current !== 'object') return current;
|
|
48
139
|
if (previous == null || typeof previous !== 'object') {
|
|
@@ -51,13 +142,53 @@ export function buildMergePatch(previous, current, arrayStrategy = 'reference')
|
|
|
51
142
|
|
|
52
143
|
const patch = {};
|
|
53
144
|
if (Array.isArray(current)) {
|
|
54
|
-
|
|
55
|
-
|
|
145
|
+
return structuredClone(current);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const previousKeys = new Set(Object.keys(previous));
|
|
149
|
+
for (const [key, value] of Object.entries(current)) {
|
|
150
|
+
previousKeys.delete(key);
|
|
151
|
+
if (value === null) {
|
|
152
|
+
// @ts-ignore patch is Object
|
|
153
|
+
patch[key] = null;
|
|
154
|
+
continue;
|
|
56
155
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
156
|
+
if (value == null) {
|
|
157
|
+
continue; // Skip undefined
|
|
158
|
+
}
|
|
159
|
+
// @ts-ignore previous is Object
|
|
160
|
+
const changes = buildMergePatchClone(previous[key], value);
|
|
161
|
+
if (changes !== null) {
|
|
162
|
+
// @ts-ignore patch is Object
|
|
163
|
+
patch[key] = changes;
|
|
60
164
|
}
|
|
165
|
+
}
|
|
166
|
+
for (const key of previousKeys) {
|
|
167
|
+
// @ts-ignore patch is Object
|
|
168
|
+
patch[key] = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return patch;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Creates a JSON Merge patch based
|
|
176
|
+
* Allows different strategies for arrays
|
|
177
|
+
* - `object`: Convert to flattened, array-like objects. Requires
|
|
178
|
+
* consumer of patch to be aware of the schema beforehand.
|
|
179
|
+
* @param {object|number|string|boolean} previous
|
|
180
|
+
* @param {object|number|string|boolean} current
|
|
181
|
+
* @return {any} Patch
|
|
182
|
+
*/
|
|
183
|
+
export function buildMergePatchObject(previous, current) {
|
|
184
|
+
if (previous === current) return null;
|
|
185
|
+
if (current == null || typeof current !== 'object') return current;
|
|
186
|
+
if (previous == null || typeof previous !== 'object') {
|
|
187
|
+
return structuredClone(current);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const patch = {};
|
|
191
|
+
if (Array.isArray(current)) {
|
|
61
192
|
for (const [index, value] of current.entries()) {
|
|
62
193
|
if (value === null) {
|
|
63
194
|
// @ts-ignore patch is ArrayLike
|
|
@@ -68,7 +199,7 @@ export function buildMergePatch(previous, current, arrayStrategy = 'reference')
|
|
|
68
199
|
continue; // Skip undefined
|
|
69
200
|
}
|
|
70
201
|
// @ts-ignore previous is ArrayLike
|
|
71
|
-
const changes =
|
|
202
|
+
const changes = buildMergePatchObject(previous[index], value);
|
|
72
203
|
if (changes !== null) {
|
|
73
204
|
// @ts-ignore patch is ArrayLike
|
|
74
205
|
patch[index] = changes;
|
|
@@ -96,7 +227,7 @@ export function buildMergePatch(previous, current, arrayStrategy = 'reference')
|
|
|
96
227
|
continue; // Skip undefined
|
|
97
228
|
}
|
|
98
229
|
// @ts-ignore previous is Object
|
|
99
|
-
const changes =
|
|
230
|
+
const changes = buildMergePatchObject(previous[key], value);
|
|
100
231
|
if (changes !== null) {
|
|
101
232
|
// @ts-ignore patch is Object
|
|
102
233
|
patch[key] = changes;
|
|
@@ -111,13 +242,36 @@ export function buildMergePatch(previous, current, arrayStrategy = 'reference')
|
|
|
111
242
|
}
|
|
112
243
|
|
|
113
244
|
/**
|
|
114
|
-
*
|
|
245
|
+
* Creates a JSON Merge patch based
|
|
246
|
+
* Allows different strategies for arrays
|
|
247
|
+
* - `reference`: Per spec, returns array as is
|
|
248
|
+
* - `clone`: Clones all entries with no inspection.
|
|
249
|
+
* - `object`: Convert to flattened, array-like objects. Requires
|
|
250
|
+
* consumer of patch to be aware of the schema beforehand.
|
|
251
|
+
* @param {object|number|string|boolean} previous
|
|
252
|
+
* @param {object|number|string|boolean} current
|
|
253
|
+
* @param {'clone'|'object'|'reference'} [arrayStrategy='reference']
|
|
254
|
+
* @return {any} Patch
|
|
255
|
+
*/
|
|
256
|
+
export function buildMergePatch(previous, current, arrayStrategy = 'reference') {
|
|
257
|
+
switch (arrayStrategy) {
|
|
258
|
+
case 'clone':
|
|
259
|
+
return buildMergePatchClone(previous, current);
|
|
260
|
+
case 'object':
|
|
261
|
+
return buildMergePatchObject(previous, current);
|
|
262
|
+
default:
|
|
263
|
+
return buildMergePatchReference(previous, current);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Default behavior: arrays are treated like objects (index/length merge).
|
|
115
269
|
* @template T
|
|
116
270
|
* @param {T} target
|
|
117
271
|
* @param {Partial<T>} patch
|
|
118
272
|
* @return {boolean}
|
|
119
273
|
*/
|
|
120
|
-
export function
|
|
274
|
+
export function hasMergePatchReference(target, patch) {
|
|
121
275
|
if (target === patch) return false;
|
|
122
276
|
if (patch == null || typeof patch !== 'object') return true;
|
|
123
277
|
if (target != null && typeof target !== 'object') {
|
|
@@ -130,9 +284,38 @@ export function hasMergePatch(target, patch) {
|
|
|
130
284
|
return true;
|
|
131
285
|
}
|
|
132
286
|
// @ts-ignore T is object
|
|
133
|
-
} else if (
|
|
287
|
+
} else if (hasMergePatchReference(target[key], value)) {
|
|
134
288
|
return true;
|
|
135
289
|
}
|
|
136
290
|
}
|
|
137
291
|
return false;
|
|
138
292
|
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Object-merge semantics for arrays (alias of reference strategy).
|
|
296
|
+
* @template T
|
|
297
|
+
* @param {T} target
|
|
298
|
+
* @param {Partial<T>} patch
|
|
299
|
+
* @return {boolean}
|
|
300
|
+
*/
|
|
301
|
+
export function hasMergePatchObject(target, patch) {
|
|
302
|
+
if (Array.isArray(patch)) return target !== patch;
|
|
303
|
+
return hasMergePatchReference(target, patch);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @template T
|
|
308
|
+
* @param {T} target
|
|
309
|
+
* @param {Partial<T>} patch
|
|
310
|
+
* @param {'clone'|'object'|'reference'} [arrayStrategy='reference']
|
|
311
|
+
* @return {boolean}
|
|
312
|
+
*/
|
|
313
|
+
export function hasMergePatch(target, patch, arrayStrategy = 'reference') {
|
|
314
|
+
switch (arrayStrategy) {
|
|
315
|
+
case 'clone':
|
|
316
|
+
case 'object':
|
|
317
|
+
return hasMergePatchObject(target, patch);
|
|
318
|
+
default:
|
|
319
|
+
return hasMergePatchReference(target, patch);
|
|
320
|
+
}
|
|
321
|
+
}
|