@geogirafe/lib-geoportal 1.0.2259547422 → 1.0.2259617981

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.
@@ -15,7 +15,7 @@ class ExternalLayersComponent extends GirafeHTMLElement {
15
15
  </style><style>
16
16
  #panel{background:var(--bkg-color);height:100%;color:var(--text-color);flex-direction:column;padding:0 1rem;display:flex}#content{flex-direction:column;flex-grow:1;margin:0;display:flex}header{background-color:var(--bkg-color);margin:0;position:sticky;top:-5px}h4{margin-bottom:.5rem}ul{margin-top:0;margin-left:0;padding-left:2rem;line-height:1.3rem;list-style-type:disc}.clearable-input{background-color:var(--bkg-color);border:1px solid #cfd6dd;border-radius:4px;flex-direction:row;margin-bottom:.1rem;display:flex}.scan-button{float:right}.layers{border-top:1px solid #ccc;flex-direction:column;flex-grow:1;margin-top:1rem;padding-top:.5rem;display:flex;overflow:hidden}ul.results{border:none;margin-bottom:.5rem;padding:0;list-style:none;overflow-y:auto}.result{height:1.6rem;display:flex}.result>button>span{text-align:left;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}#panel>div{padding:1rem}#layers li{margin-top:.3rem;margin-bottom:.3rem}#layers li button{text-align:left;overflow-wrap:anywhere;text-wrap-style:balance}#file-selector{display:grid}#file,#file-label{grid-area:1/1}#file{opacity:0;z-index:2;cursor:pointer}#file::file-selector-button{cursor:pointer}#loading-icon{flex-grow:1;align-self:center;width:3rem}.title{margin-top:0}.filter-buttons{flex-direction:row;margin:2px;display:flex}.filter-buttons span{flex-grow:1;align-self:center}.clearable-input-field{width:90%;color:var(--text-color);background:0 0;border:0;outline:none;flex:auto;padding:0;font-size:1rem}.filter-icon,.delete-icon{width:16px;color:var(--text-color);padding:0 .5rem}.delete-button{cursor:pointer;background-color:#0000;border:none;padding:0}
17
17
  </style>
18
- <div id="panel"><header class="gg-tabs"><button class="${this.selectedTab === 'wms_wmts' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('wms_wmts')}" i18n="wms_wmts"></button> <button class="${this.selectedTab === 'file' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('file')}" i18n="local file"></button></header><section class="${(this.selectedTab === 'wms_wmts') ? 'group' : 'hidden'}"><h4 class="title" i18n="Suggestions" ?hidden="${this.hasPredefinedWmsWmtsSources}">Suggestions</h4><ul class="suggestions" ?hidden="${this.hasPredefinedWmsWmtsSources}">${this.predefinedWmsWmtsSources.map(source => uHtmlFor(source, source.label) `<li><a href="javascript:void(0)" onclick="${() => this.scanSource(source.url, source.type)}">${source.label}</a></li>`)}</ul><h4 class="title" i18n="Custom service URL">Custom service URL</h4><div class="clearable-input"><input id="url" class="gg-input clearable-input-field" oninput="${() => this.typeUrl()}"><br><button id="clear-url" class="delete-button hidden" onclick="${() => this.clearUrl()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><button i18n="Scan Source" class="gg-button scan-button" onclick="${() => this.scanSource()}">Scan Source</button></section><section class="${(this.selectedTab === 'file') ? 'group' : 'hidden'}"><h4 class="title" i18n="Choose local file">Choose local file</h4><div id="file-selector"><label for="file" id="file-label"><button class="gg-button" i18n="Select File"></button> <span>${this.fileDescription}</span></label> <input type="file" accept=".gpx,.kml,.geojson,.json" id="file" onchange="${() => this.render()}"><br></div><button id="load" i18n="Load File" class="gg-button" onclick="${() => this.loadFile()}"></button></section><section class="${(this.selectedTab === 'wms_wmts' && this.externalLayers.length > 0) || this.loading ? 'layers' : 'hidden'}"><h4 class="title" i18n="Layers (from Capabilities)">Layers (from Capabilities)</h4><div class="clearable-input"><img class="filter-icon" alt="filter-button-icon" src="icons/filter.svg"> <input id="layer-search-field" class="gg-input clearable-input-field" placeholder="${this.context.i18nManager.getTranslation('Filter external layers...')}" i18n="Filter external layers..." autocomplete="off" autocorrect="off" oninput="${(e) => this.filterLayers(e.target.value)}"> <button class="${this.isFiltered ? 'delete-button' : 'hidden'}" onclick="${() => this.clearFilter()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><div class="filter-buttons"><span>${Object.values(this.externalLayers).filter(layer => layer.isSelected).length.toString()} ${this.context.i18nManager.getTranslation('layers selected')} </span><button class="gg-button" tip="Select all filtered layers" onclick="${() => this.selectVisible()}">+</button> <button class="gg-button" tip="Unselect all filtered layers" onclick="${() => this.deselectVisible()}">-</button> <button class="gg-button" tip="Select none" onclick="${() => this.deselectAll()}">--</button></div><img id="loading-icon" alt="loading-icon" src="icons/loading.svg" class="${this.loading ? 'gg-spin' : 'hidden'}"><ul class="results">${this.filteredLayers.map(layer => uHtmlFor(layer, layer.treeItemId) `<li class="result"><button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" tip="Activate / Deactivate" onclick="${() => this.selectLayer(layer)}"><img class="${layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-full.svg"> <img class="${!layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-no.svg"></button> <button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" onclick="${() => this.selectLayer(layer)}"><span i18n="${layer.name}" class="gg-tree-label">${layer.name}</span></button></li>`)}</ul><button id="add" tip="Add selected" i18n="Add selected" class="gg-button" onclick="${() => this.addSelectedLayers()}">Add selected</button></section></div>`;
18
+ <div id="panel"><header class="gg-tabs"><button class="${this.selectedTab === 'wms_wmts' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('wms_wmts')}" i18n="wms_wmts"></button> <button class="${this.selectedTab === 'file' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('file')}" i18n="local file"></button></header><section class="${(this.selectedTab === 'wms_wmts') ? 'group' : 'hidden'}"><h4 class="title" i18n="Suggestions" ?hidden="${!this.hasPredefinedWmsWmtsSources}">Suggestions</h4><ul class="suggestions" ?hidden="${!this.hasPredefinedWmsWmtsSources}">${this.predefinedWmsWmtsSources.map(source => uHtmlFor(source, source.label) `<li><a href="javascript:void(0)" onclick="${() => this.scanSource(source.url, source.type)}">${source.label}</a></li>`)}</ul><h4 class="title" i18n="Custom service URL">Custom service URL</h4><div class="clearable-input"><input id="url" class="gg-input clearable-input-field" oninput="${() => this.typeUrl()}"><br><button id="clear-url" class="delete-button hidden" onclick="${() => this.clearUrl()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><button i18n="Scan Source" class="gg-button scan-button" onclick="${() => this.scanSource()}">Scan Source</button></section><section class="${(this.selectedTab === 'file') ? 'group' : 'hidden'}"><h4 class="title" i18n="Choose local file">Choose local file</h4><div id="file-selector"><label for="file" id="file-label"><button class="gg-button" i18n="Select File"></button> <span>${this.fileDescription}</span></label> <input type="file" accept=".gpx,.kml,.geojson,.json" id="file" onchange="${() => this.render()}"><br></div><button id="load" i18n="Load File" class="gg-button" onclick="${() => this.loadFile()}"></button></section><section class="${(this.selectedTab === 'wms_wmts' && this.externalLayers.length > 0) || this.loading ? 'layers' : 'hidden'}"><h4 class="title" i18n="Layers (from Capabilities)">Layers (from Capabilities)</h4><div class="clearable-input"><img class="filter-icon" alt="filter-button-icon" src="icons/filter.svg"> <input id="layer-search-field" class="gg-input clearable-input-field" placeholder="${this.context.i18nManager.getTranslation('Filter external layers...')}" i18n="Filter external layers..." autocomplete="off" autocorrect="off" oninput="${(e) => this.filterLayers(e.target.value)}"> <button class="${this.isFiltered ? 'delete-button' : 'hidden'}" onclick="${() => this.clearFilter()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><div class="filter-buttons"><span>${Object.values(this.externalLayers).filter(layer => layer.isSelected).length.toString()} ${this.context.i18nManager.getTranslation('layers selected')} </span><button class="gg-button" tip="Select all filtered layers" onclick="${() => this.selectVisible()}">+</button> <button class="gg-button" tip="Unselect all filtered layers" onclick="${() => this.deselectVisible()}">-</button> <button class="gg-button" tip="Select none" onclick="${() => this.deselectAll()}">--</button></div><img id="loading-icon" alt="loading-icon" src="icons/loading.svg" class="${this.loading ? 'gg-spin' : 'hidden'}"><ul class="results">${this.filteredLayers.map(layer => uHtmlFor(layer, layer.treeItemId) `<li class="result"><button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" tip="Activate / Deactivate" onclick="${() => this.selectLayer(layer)}"><img class="${layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-full.svg"> <img class="${!layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-no.svg"></button> <button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" onclick="${() => this.selectLayer(layer)}"><span i18n="${layer.name}" class="gg-tree-label">${layer.name}</span></button></li>`)}</ul><button id="add" tip="Add selected" i18n="Add selected" class="gg-button" onclick="${() => this.addSelectedLayers()}">Add selected</button></section></div>`;
19
19
  };
20
20
  isPanelVisible = false;
21
21
  panelTitle = 'ext-layer-panel';
@@ -26,7 +26,7 @@ class ExternalLayersComponent extends GirafeHTMLElement {
26
26
  return this.predefinedSources.filter((source) => source.type === 'WMS' || source.type === 'WMTS');
27
27
  }
28
28
  get hasPredefinedWmsWmtsSources() {
29
- return this.predefinedWmsWmtsSources.length == 0;
29
+ return this.predefinedWmsWmtsSources.length > 0;
30
30
  }
31
31
  selectedTab = 'wms_wmts';
32
32
  wmtsManager;
@@ -39,10 +39,8 @@ header{border:none;flex-direction:row-reverse;margin-top:.5rem;margin-bottom:.5r
39
39
  this.refreshRender(layer);
40
40
  this.refreshRender(layer.parent);
41
41
  });
42
- this.subscribe('treeview.renderEnabled', (_oldValue, enabled) => {
43
- if (enabled) {
44
- this.refreshRender();
45
- }
42
+ this.subscribe('treeview.renderEnabled', () => {
43
+ this.refreshRender();
46
44
  });
47
45
  }
48
46
  connectedCallback() {
@@ -1,5 +1,12 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
1
7
  import LayerTimeFormatter from '../../tools/time/layertimeformatter.js';
2
8
  import Layer from './layer.js';
9
+ import { BrainIgnore } from '../../tools/state/brain/decorators.js';
3
10
  class LayerWms extends Layer {
4
11
  /**
5
12
  * This class is a used in the state of the application, which will be accessed behind a javascript proxy.
@@ -189,4 +196,7 @@ class LayerWms extends Layer {
189
196
  return opts;
190
197
  }
191
198
  }
199
+ __decorate([
200
+ BrainIgnore
201
+ ], LayerWms.prototype, "ogcServer", void 0);
192
202
  export default LayerWms;
@@ -1,3 +1,10 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BrainIgnore } from '../../tools/state/brain/decorators.js';
1
8
  import Layer from './layer.js';
2
9
  class LayerWmts extends Layer {
3
10
  /**
@@ -115,4 +122,7 @@ class LayerWmts extends Layer {
115
122
  };
116
123
  }
117
124
  }
125
+ __decorate([
126
+ BrainIgnore
127
+ ], LayerWmts.prototype, "ogcServer", void 0);
118
128
  export default LayerWmts;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "GeoGirafe PSC",
6
6
  "url": "https://doc.geomapfish.dev"
7
7
  },
8
- "version": "1.0.2259547422",
8
+ "version": "1.0.2259617981",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -1 +1 @@
1
- {"version":"1.0.2259547422", "build":"2259547422", "date":"13/01/2026"}
1
+ {"version":"1.0.2259617981", "build":"2259617981", "date":"13/01/2026"}
package/tools/main.d.ts CHANGED
@@ -62,11 +62,11 @@ export type { default as ISnappingConfig } from './snap/isnapconfig.js';
62
62
  export type { TProxy } from './state/brain/brain.js';
63
63
  export { default as Brain } from './state/brain/brain.js';
64
64
  export type { IBrainSerializable } from './state/brain/decorators.js';
65
- export { ignoreCloneSymbol, serializeSymbol, BrainIgnoreClone, BrainSerialize, isBrainSerializable } from './state/brain/decorators.js';
65
+ export { ignoreSymbol, ignoreCloneSymbol, serializeSymbol, BrainIgnoreClone, BrainIgnore, BrainSerialize, isBrainSerializable } from './state/brain/decorators.js';
66
66
  export { default as areEqual } from './state/brain/equality.js';
67
67
  export type { Constructor, IBrainSerializer } from './state/brain/serialize.js';
68
68
  export { default as BrainSerializer } from './state/brain/serialize.js';
69
- export { deepFreeze, deepCloneCustomizer, deepClone, isPrimitive, isFunction, isVirtualProperty, isConstructor, isIgnoredProperty, isFlagedIgnoreClone, isTypeSupported, isIterator } from './state/brain/tools.js';
69
+ export { deepFreeze, deepCloneCustomizer, deepClone, isPrimitive, isFunction, isVirtualProperty, isConstructor, isIgnoredProperty, isFlagedIgnoreClone, isFlagedIgnore, isTypeSupported, isIterator } from './state/brain/tools.js';
70
70
  export { default as ComponentManager } from './state/componentManager.js';
71
71
  export type { CameraConfig } from './state/globe.js';
72
72
  export { default as GlobeState } from './state/globe.js';
package/tools/main.js CHANGED
@@ -49,10 +49,10 @@ export { default as SessionManager } from './share/sessionmanager.js';
49
49
  export { default as ShareManager } from './share/sharemanager.js';
50
50
  export { default as StateSerializer } from './share/stateserializer.js';
51
51
  export { default as Brain } from './state/brain/brain.js';
52
- export { ignoreCloneSymbol, serializeSymbol, BrainIgnoreClone, BrainSerialize, isBrainSerializable } from './state/brain/decorators.js';
52
+ export { ignoreSymbol, ignoreCloneSymbol, serializeSymbol, BrainIgnoreClone, BrainIgnore, BrainSerialize, isBrainSerializable } from './state/brain/decorators.js';
53
53
  export { default as areEqual } from './state/brain/equality.js';
54
54
  export { default as BrainSerializer } from './state/brain/serialize.js';
55
- export { deepFreeze, deepCloneCustomizer, deepClone, isPrimitive, isFunction, isVirtualProperty, isConstructor, isIgnoredProperty, isFlagedIgnoreClone, isTypeSupported, isIterator } from './state/brain/tools.js';
55
+ export { deepFreeze, deepCloneCustomizer, deepClone, isPrimitive, isFunction, isVirtualProperty, isConstructor, isIgnoredProperty, isFlagedIgnoreClone, isFlagedIgnore, isTypeSupported, isIterator } from './state/brain/tools.js';
56
56
  export { default as ComponentManager } from './state/componentManager.js';
57
57
  export { default as GlobeState } from './state/globe.js';
58
58
  export { default as GraphicalInterface } from './state/graphicalInterface.js';
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { describe, it, expect, beforeEach } from 'vitest';
8
8
  import Brain from './brain';
9
- import { BrainIgnoreClone } from './decorators';
9
+ import { BrainIgnore, BrainIgnoreClone } from './decorators';
10
10
  let state;
11
11
  let brain;
12
12
  describe('State: base principles', () => {
@@ -377,7 +377,7 @@ describe('State: base principles', () => {
377
377
  });
378
378
  expect(counter).toBe(2);
379
379
  });
380
- it('Base principles: Do not clone objects with attribute BrainIgnoreClone', () => {
380
+ it('Base principles: Do not clone objects with decorator @BrainIgnoreClone', () => {
381
381
  class Pizza {
382
382
  key;
383
383
  owner;
@@ -406,7 +406,7 @@ describe('State: base principles', () => {
406
406
  brain.getState().objectValue = pizza2;
407
407
  expect(counter).toBe(2);
408
408
  });
409
- it('Base principles: Do not clone objects with attribute BrainIgnoreClone, but listen to changes', () => {
409
+ it('Base principles: Do not clone objects with decorator @BrainIgnoreClone, but listen to changes', () => {
410
410
  class Pizza {
411
411
  key;
412
412
  owner;
@@ -434,6 +434,35 @@ describe('State: base principles', () => {
434
434
  brain.getState().objectValue.owner.place = 'Bernwiller';
435
435
  expect(counter).toBe(2);
436
436
  });
437
+ it('Base principles: Do not create proxy for attributes with decorator @BrainIgnore', () => {
438
+ class Pizza {
439
+ key;
440
+ owner;
441
+ constructor(key, owner) {
442
+ this.key = key;
443
+ this.owner = owner;
444
+ }
445
+ }
446
+ __decorate([
447
+ BrainIgnore
448
+ ], Pizza.prototype, "owner", void 0);
449
+ const pizzeria = {
450
+ name: 'Noninna',
451
+ place: 'Aspach'
452
+ };
453
+ const pizza1 = new Pizza('pizza1', pizzeria);
454
+ state.objectValue = pizza1;
455
+ let counter = 1;
456
+ brain = new Brain(state, () => {
457
+ counter++;
458
+ });
459
+ const pizzaProxy = brain.getState().objectValue;
460
+ expect(pizzaProxy.__brainIsProxy).toBeTruthy();
461
+ const owner = pizzaProxy.owner;
462
+ expect(owner.__brainIsProxy).toBeFalsy();
463
+ owner.place = 'Bernwiller';
464
+ expect(counter).toBe(1);
465
+ });
437
466
  it('Base principles: Handles multiple parents referencing the same object', () => {
438
467
  const shared = { id: 42 };
439
468
  state.objectValue = { child: shared };
@@ -20,20 +20,20 @@ export default class Brain {
20
20
  this.externalCallback = callback;
21
21
  }
22
22
  callback(path, oldValue, newValue, parents) {
23
- if (!this.delayed) {
24
- // Immediate Callback
25
- this.externalCallback(path, oldValue, newValue, parents);
26
- }
27
- else {
23
+ if (this.delayed) {
28
24
  // Remember all callback infos
29
25
  const infos = this.delayedCallbacks.get(path);
30
- if (!infos) {
31
- this.delayedCallbacks.set(path, { oldValue: oldValue, newValue: newValue, parents: parents });
32
- }
33
- else {
26
+ if (infos) {
34
27
  // Just set the new value
35
28
  infos.newValue = newValue;
36
29
  }
30
+ else {
31
+ this.delayedCallbacks.set(path, { oldValue: oldValue, newValue: newValue, parents: parents });
32
+ }
33
+ }
34
+ else {
35
+ // Immediate Callback
36
+ this.externalCallback(path, oldValue, newValue, parents);
37
37
  }
38
38
  }
39
39
  /**
@@ -86,17 +86,21 @@ export default class Brain {
86
86
  return this.mergeMinimalPathsCache.get(cacheKey);
87
87
  }
88
88
  const minimalPaths = new Set();
89
+ const parentsPaths = [];
90
+ for (const p of parents) {
91
+ parentsPaths.push(...p.__brainFullPaths);
92
+ }
89
93
  for (const candidate of [...existingPaths, ...candidatePaths]) {
90
94
  // Already present
91
95
  if (minimalPaths.has(candidate)) {
92
96
  continue;
93
97
  }
94
98
  // If the candidate hat a minimal path as prefix
95
- if ([...minimalPaths].some((path) => candidate.startsWith(`${path}.`))) {
99
+ if (Array.from(minimalPaths).some((path) => candidate.startsWith(`${path}.`))) {
96
100
  continue;
97
101
  }
98
102
  // If not prefixed by any parent
99
- if (!this.isRightParent(candidate, parents)) {
103
+ if (!this.isRightParent(candidate, parentsPaths)) {
100
104
  continue;
101
105
  }
102
106
  minimalPaths.add(candidate);
@@ -105,30 +109,23 @@ export default class Brain {
105
109
  this.mergeMinimalPathsCache.set(cacheKey, returnValue);
106
110
  return returnValue;
107
111
  }
108
- isRightParent(candidate, parents) {
109
- const parentsPaths = [];
110
- for (const p of parents) {
111
- parentsPaths.push(...p.__brainFullPaths);
112
+ isRightParent(candidate, parentsPaths) {
113
+ if (parentsPaths.length === 0 || parentsPaths[0].length === 0) {
114
+ // On the root
115
+ return true;
112
116
  }
113
- if (parentsPaths?.length > 0 && parentsPaths[0].length > 0) {
114
- // Not on the root
115
- let circularReference = false;
116
- let rightParent = false;
117
- for (const parentPath of parentsPaths) {
118
- if (parentPath.includes(`${candidate}.`)) {
119
- circularReference = true;
120
- break;
121
- }
122
- else if (candidate.startsWith(`${parentPath}.`)) {
123
- rightParent = true;
124
- break;
125
- }
117
+ for (const parentPath of parentsPaths) {
118
+ if (parentPath.includes(`${candidate}.`)) {
119
+ // Circular reference
120
+ return true;
126
121
  }
127
- if (!circularReference && !rightParent) {
128
- return false;
122
+ if (candidate.startsWith(`${parentPath}.`)) {
123
+ // Parent found
124
+ return true;
129
125
  }
130
126
  }
131
- return true;
127
+ // Not a valid parent
128
+ return false;
132
129
  }
133
130
  getOrCreateProxyForValue(proxy, prop, value, childPaths) {
134
131
  if (isPrimitive(value)) {
@@ -150,8 +147,8 @@ export default class Brain {
150
147
  const fullPaths = valueProxy.__brainFullPaths;
151
148
  if (merged.length !== fullPaths.length || merged.some((p, i) => p !== fullPaths[i])) {
152
149
  fullPaths.splice(0, fullPaths.length, ...merged);
150
+ this.updateChildPathsRecursively(valueProxy, fullPaths);
153
151
  }
154
- this.updateChildPathsRecursively(valueProxy, fullPaths);
155
152
  }
156
153
  else {
157
154
  // Create a new proxy
@@ -197,7 +194,7 @@ export default class Brain {
197
194
  }
198
195
  recalculateChildrenForArray(proxy, target, oldValue) {
199
196
  if (!Array.isArray(target)) {
200
- throw new Error('This method is only for arrays');
197
+ throw new TypeError('This method is only for arrays');
201
198
  }
202
199
  // Clean previous childs
203
200
  proxy.__brainChildren.clear();
@@ -266,7 +263,7 @@ export default class Brain {
266
263
  }
267
264
  handleGetIgnored(target, prop) {
268
265
  const value = target[prop];
269
- if (typeof prop === 'symbol' || prop.startsWith('_')) {
266
+ if (isIgnoredProperty(target, prop)) {
270
267
  return value;
271
268
  }
272
269
  throw new Error(`Unknown ignored property: ${prop}`);
@@ -334,7 +331,7 @@ export default class Brain {
334
331
  };
335
332
  }
336
333
  getHandler(proxy, target, prop) {
337
- if (isIgnoredProperty(prop)) {
334
+ if (isIgnoredProperty(target, prop)) {
338
335
  return this.handleGetIgnored(target, prop);
339
336
  }
340
337
  if (isVirtualProperty(prop)) {
@@ -388,7 +385,7 @@ export default class Brain {
388
385
  let oldValue = target[prop];
389
386
  const childPaths = proxy.__brainFullPaths.map((path) => this.getFullPath(path, prop));
390
387
  this.cleanProxyForValue(proxy, prop, oldValue, childPaths);
391
- if (isIgnoredProperty(prop)) {
388
+ if (isIgnoredProperty(target, prop)) {
392
389
  // Ignored => No callback and no clone of the old version
393
390
  // Just set the new value
394
391
  target[prop] = newValue;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * A symbol used to mark properties that should be ignored during cloning operations.
3
+ */
4
+ export declare const ignoreSymbol: unique symbol;
1
5
  /**
2
6
  * A symbol used to mark properties that should be ignored during cloning operations.
3
7
  */
@@ -13,6 +17,13 @@ export declare const serializeSymbol: unique symbol;
13
17
  * @param propertyKey - The key of the property to be ignored during cloning.
14
18
  */
15
19
  export declare function BrainIgnoreClone(target: any, propertyKey: string | symbol): void;
20
+ /**
21
+ * A decorator function that marks a property of a class to be ignored. No proxy will be created for this object.
22
+ *
23
+ * @param target - The target object (class prototype) where the property resides.
24
+ * @param propertyKey - The key of the property to be ignored.
25
+ */
26
+ export declare function BrainIgnore(target: any, propertyKey: string | symbol): void;
16
27
  /**
17
28
  * A decorator function that marks a property of a class to be serializable.
18
29
  *
@@ -1,3 +1,7 @@
1
+ /**
2
+ * A symbol used to mark properties that should be ignored during cloning operations.
3
+ */
4
+ export const ignoreSymbol = Symbol('brainIgnore');
1
5
  /**
2
6
  * A symbol used to mark properties that should be ignored during cloning operations.
3
7
  */
@@ -16,6 +20,16 @@ export function BrainIgnoreClone(target, propertyKey) {
16
20
  target[ignoreCloneSymbol] ??= [];
17
21
  target[ignoreCloneSymbol].push(propertyKey);
18
22
  }
23
+ /**
24
+ * A decorator function that marks a property of a class to be ignored. No proxy will be created for this object.
25
+ *
26
+ * @param target - The target object (class prototype) where the property resides.
27
+ * @param propertyKey - The key of the property to be ignored.
28
+ */
29
+ export function BrainIgnore(target, propertyKey) {
30
+ target[ignoreSymbol] ??= [];
31
+ target[ignoreSymbol].push(propertyKey);
32
+ }
19
33
  /**
20
34
  * A decorator function that marks a property of a class to be serializable.
21
35
  *
@@ -62,7 +62,7 @@ export declare function isConstructor(prop: string): boolean;
62
62
  * @param prop - The property name or symbol to check.
63
63
  * @returns `true` if the property should be ignored, otherwise `false`.
64
64
  */
65
- export declare function isIgnoredProperty(prop: string | symbol): boolean;
65
+ export declare function isIgnoredProperty(target: any, prop: string | symbol): boolean;
66
66
  /**
67
67
  * Checks if a property on a target object is flagged to be ignored during cloning.
68
68
  *
@@ -71,6 +71,14 @@ export declare function isIgnoredProperty(prop: string | symbol): boolean;
71
71
  * @returns `true` if the property is flagged to be ignored, otherwise `false`.
72
72
  */
73
73
  export declare function isFlagedIgnoreClone(target: any, prop?: string | number): any;
74
+ /**
75
+ * Checks if a property on a target object is flagged to be totally ignored by brain.
76
+ *
77
+ * @param target - The object containing the property.
78
+ * @param prop - The property name or index to check.
79
+ * @returns `true` if the property is flagged to be ignored, otherwise `false`.
80
+ */
81
+ export declare function isFlagedIgnore(target: any, prop?: string | number): any;
74
82
  /**
75
83
  * Determines if a value is of a supported type.
76
84
  * Unsupported types include `WeakMap`, `WeakSet`, `Map`, and `Set`.
@@ -1,5 +1,5 @@
1
1
  import { cloneDeepWith } from 'lodash-es';
2
- import { ignoreCloneSymbol } from './decorators.js';
2
+ import { ignoreCloneSymbol, ignoreSymbol } from './decorators.js';
3
3
  /**
4
4
  * Recursively freezes an object and all its properties to make it immutable.
5
5
  * This function also handles circular references by using a `WeakSet` to track visited objects.
@@ -17,15 +17,15 @@ export function deepFreeze(obj, visitedObjects = new WeakSet()) {
17
17
  }
18
18
  visitedObjects.add(obj);
19
19
  Object.freeze(obj);
20
- Object.getOwnPropertyNames(obj).forEach((prop) => {
21
- if (!isIgnoredProperty(prop) && !isFlagedIgnoreClone(obj, prop)) {
20
+ for (const prop of Object.getOwnPropertyNames(obj)) {
21
+ if (!isIgnoredProperty(obj, prop) && !isFlagedIgnoreClone(obj, prop)) {
22
22
  // Do not freeze ignored objects
23
23
  const value = obj[prop];
24
24
  if (typeof value === 'object' && value !== null) {
25
25
  deepFreeze(value, visitedObjects);
26
26
  }
27
27
  }
28
- });
28
+ }
29
29
  }
30
30
  /**
31
31
  * Customizer function for `cloneDeepWith` to handle specific cloning rules.
@@ -39,14 +39,10 @@ export function deepFreeze(obj, visitedObjects = new WeakSet()) {
39
39
  * @returns The value to use for the cloned property, or `undefined` to use the default cloning behavior.
40
40
  */
41
41
  export function deepCloneCustomizer(value, prop, target) {
42
- if (prop?.toString().startsWith('_')) {
42
+ if (isFlagedIgnore(value, prop) || isFlagedIgnoreClone(target, prop) || prop?.toString().startsWith('_')) {
43
43
  // Do not clone : just copy the reference
44
44
  return value;
45
45
  }
46
- if (isFlagedIgnoreClone(target, prop)) {
47
- // Do not clone marked properties
48
- return value;
49
- }
50
46
  // Else : do nothing special, the default cloneDeep will be used.
51
47
  }
52
48
  /**
@@ -108,8 +104,14 @@ export function isConstructor(prop) {
108
104
  * @param prop - The property name or symbol to check.
109
105
  * @returns `true` if the property should be ignored, otherwise `false`.
110
106
  */
111
- export function isIgnoredProperty(prop) {
112
- return typeof prop === 'symbol' || (prop.startsWith('_') && !isVirtualProperty(prop));
107
+ export function isIgnoredProperty(target, prop) {
108
+ if (typeof prop === 'symbol') {
109
+ return true;
110
+ }
111
+ if (prop.startsWith('_') && !isVirtualProperty(prop)) {
112
+ return true;
113
+ }
114
+ return isFlagedIgnore(target, prop);
113
115
  }
114
116
  /**
115
117
  * Checks if a property on a target object is flagged to be ignored during cloning.
@@ -121,6 +123,16 @@ export function isIgnoredProperty(prop) {
121
123
  export function isFlagedIgnoreClone(target, prop) {
122
124
  return target?.[ignoreCloneSymbol]?.includes(prop);
123
125
  }
126
+ /**
127
+ * Checks if a property on a target object is flagged to be totally ignored by brain.
128
+ *
129
+ * @param target - The object containing the property.
130
+ * @param prop - The property name or index to check.
131
+ * @returns `true` if the property is flagged to be ignored, otherwise `false`.
132
+ */
133
+ export function isFlagedIgnore(target, prop) {
134
+ return target?.[ignoreSymbol]?.includes(prop);
135
+ }
124
136
  /**
125
137
  * Determines if a value is of a supported type.
126
138
  * Unsupported types include `WeakMap`, `WeakSet`, `Map`, and `Set`.
@@ -52,7 +52,7 @@ describe('StateManager.subscribe', () => {
52
52
  controlValue = 2;
53
53
  };
54
54
  try {
55
- manager.state.ogcServers = {};
55
+ manager.state.basemaps = {};
56
56
  manager.subscribe('ogcServers', callback);
57
57
  expect(controlValue).toEqual(1);
58
58
  }