neo.mjs 10.5.2 → 10.5.4

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.
@@ -0,0 +1,5 @@
1
+ # Neo.mjs v10.5.3 Release Notes
2
+
3
+ ## Bug Fixes
4
+
5
+ - **grid.Container**: Corrected the calculation for the `aria-rowcount` attribute. The count was previously `store.count + 2`, and has been adjusted to `store.count + 1` to accurately reflect the number of data rows plus the single header row, improving accessibility for screen readers.
@@ -0,0 +1,4 @@
1
+ # Neo.mjs v10.5.4 Release Notes
2
+
3
+ * Added the new blog post to the Portal App (neo website)
4
+ * Friends link: https://tobiasuhlig.medium.com/benchmarking-frontends-in-2025-f6bbf43b7721?source=friends_link&sk=af0f2c6745a7ca4993bc0ae60ad0ebb4
package/ServiceWorker.mjs CHANGED
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='10.5.2'
23
+ * @member {String} version='10.5.4'
24
24
  */
25
- version: '10.5.2'
25
+ version: '10.5.4'
26
26
  }
27
27
 
28
28
  /**
@@ -16,7 +16,7 @@
16
16
  "@type": "Organization",
17
17
  "name": "Neo.mjs"
18
18
  },
19
- "datePublished": "2025-08-14",
19
+ "datePublished": "2025-08-18",
20
20
  "publisher": {
21
21
  "@type": "Organization",
22
22
  "name": "Neo.mjs"
@@ -1,4 +1,16 @@
1
1
  [{
2
+ "author" : "Tobias Uhlig",
3
+ "authorImage" : "author_TobiasUhlig.jpeg",
4
+ "date" : "Aug 18, 2025",
5
+ "id" : 69,
6
+ "image" : "BenchmarkingFrontends.jpg",
7
+ "name" : "Benchmarking Frontends in 2025",
8
+ "provider" : "Medium",
9
+ "publisher" : "",
10
+ "selectedInto": [],
11
+ "type" : "Blog Post",
12
+ "url" : "https://tobiasuhlig.medium.com/benchmarking-frontends-in-2025-f6bbf43b7721?source=friends_link&sk=af0f2c6745a7ca4993bc0ae60ad0ebb4"
13
+ }, {
2
14
  "author" : "Tobias Uhlig",
3
15
  "authorImage" : "author_TobiasUhlig.jpeg",
4
16
  "date" : "Aug 04, 2025",
@@ -108,7 +108,7 @@ class FooterContainer extends Container {
108
108
  }, {
109
109
  module: Component,
110
110
  cls : ['neo-version'],
111
- text : 'v10.5.2'
111
+ text : 'v10.5.4'
112
112
  }]
113
113
  }],
114
114
  /**
@@ -0,0 +1,173 @@
1
+ # Benchmarking Frontends in 2025
2
+
3
+ **Stop Measuring Page Loads. Start Measuring Resilience.**
4
+
5
+ ## The Two Worlds of Web Performance
6
+
7
+ For the better part of a decade, the web performance community has rightfully focused on one critical goal: making the initial page load faster. Driven by tools like Google's Lighthouse and the Core Web Vitals (CWV) initiative, we've become experts at optimizing for the "first-impression" web. This is the world of e-commerce, marketing sites, and news articles, where success is measured in milliseconds of Largest Contentful Paint (LCP) and a low Interaction to Next Paint (INP). For this half of the web, these tools are essential and have made the user experience immeasurably better.
8
+
9
+ But another half of the web has been quietly evolving, operating under a completely different set of pressures. This is the "lived-in" web: the complex, data-intensive applications where users spend their entire workday. Think of real-time trading dashboards, enterprise SaaS platforms, and data science tools. Here, the initial load is a distant memory. True performance is defined by what happens hours into a session: Can the UI handle a stream of 1,000 real-time updates per second without stuttering? Can a data grid ingest and render 100,000 new rows without freezing? Does the entire application grind to a halt during a heavy background calculation?
10
+
11
+ [Intro Video on YouTube](https://youtu.be/VAaHVG5anh0)
12
+
13
+ > *A quick note on the authorial voice: You'll see "we" used throughout this article. This isn't a royal "we" or a corporate "we." It's a literal "we," representing the collaborative effort between a human engineer (me, Tobi) and dozens of iterative sessions with Gemini 2.5 Pro, which acted as a pair programmer, critic, and co-author in building this project and its narrative. This benchmark is as much a product of that unique human-AI partnership as it is of the code itself.*
14
+
15
+ ## An Ecosystem Measuring Only Half the Story
16
+
17
+ The uncomfortable truth is that our benchmarking ecosystem is still primarily designed to measure the "first-impression" web.
18
+
19
+ Core Web Vitals are the gold standard, but their focus is, by definition, on the loading experience. LCP measures render speed, INP measures initial input delay, and CLS measures visual stability *as the page loads*. They are not designed to tell you if your application will suffer catastrophic jank ten minutes into a session under a heavy, concurrent load.
20
+
21
+ The `js-framework-benchmark` (often called "krausest") was a vital step in the right direction, moving the focus to interactivity like creating, swapping, and deleting rows. However, it was designed with two specific limitations that make it unsuitable for the "lived-in" applications we target.
22
+
23
+ First, **it explicitly forbids buffered rendering or virtualization.** The benchmark's goal is to measure how fast a framework can render *every single row* into the live DOM. This is a valid test for smaller datasets, but it's completely disconnected from how a real-world application must handle 100,000 rows or 20 million cells. It tests a scenario that is impossible at scale.
24
+
25
+ Second, **it tests operations in a sterile lab, in isolation.** It doesn't simulate the chaos of a real application where a user is scrolling *while* a background task is running *and* a WebSocket is pushing real-time updates. There is no concept of duress or concurrent stress.
26
+
27
+ This leaves developers of complex applications flying blind. We are building a generation of incredibly demanding, "lived-in" applications but are forced to measure them with tools designed for a simpler, "first-impression" world.
28
+
29
+ ## The Need for a New Harness
30
+
31
+ This is the gap we set out to fill. We needed to build a new kind of benchmark from the ground up—one that could simulate real-world concurrency and measure an application's resilience under sustained duress.
32
+
33
+ To do this, we had to start from scratch. We needed a harness that could automate complex, multi-step interactions across all modern browsers and give us the low-level control to measure with scientific precision. That's why we chose Playwright as our foundation. But as we quickly learned, simply choosing a tool wasn't enough. We had to fundamentally rethink *how* to use it.
34
+
35
+ ## Challenge 1: The Parallelism Trap
36
+
37
+ Our first instinct, and the default for most modern test runners, was to run our test suites in parallel to save time. Playwright is configured out-of-the-box to use one worker process per CPU core. For standard functional testing, this is a massive win. For performance benchmarking, it was a disaster.
38
+
39
+ The very purpose of a benchmark is to push a browser and framework to its limits, heavily stressing CPU cores. When we ran tests in parallel, we weren't giving each test a clean, idle core to run on. We were forcing multiple, already maxed-out browser instances to fight for the same overloaded CPU resources. The result was massive context switching and resource starvation, with performance numbers dropping by as much as 50% compared to running a single test. The data was useless.
40
+
41
+ **Lesson 1: For accurate performance benchmarking, serial execution is non-negotiable.** We immediately configured our test runner to use a single worker (`--workers=1`), ensuring that every test run gets the full, undivided attention of the machine. It takes longer, but the stability and reliability of the results are paramount.
42
+
43
+ ## Challenge 2: The Latency Chasm and the Atomic Measurement
44
+
45
+ With our tests running serially, we still saw unacceptable noise in the data. The problem was the "observer effect" caused by the constant back-and-forth communication between the Playwright test runner (living in a Node.js process) and the browser page. Each command and response adds milliseconds of unpredictable latency, completely separate from the framework's actual performance.
46
+
47
+ Our solution was to make each measurement **atomic**. The entire test—triggering an action, waiting for a specific condition to be met, and measuring the duration—had to be executed in a single, uninterrupted block of code *inside the browser's context*. We use `page.addInitScript()` to inject our measurement helpers into the page, then wrap each test in a single `page.evaluate()` call. This gives us a portal into the browser's own thread, allowing us to run our entire measurement logic atomically. Only the final, high-precision number is returned to the Node.js process.
48
+
49
+ **Lesson 2: Eliminate test runner latency by performing all measurement logic atomically inside the browser.** This is the only way to be certain you are measuring the framework, not the harness.
50
+
51
+ ## Challenge 3: The Polling Fallacy
52
+
53
+ Even with atomic, in-browser measurements, our results for very short-duration tasks were wildly inconsistent. We discovered the reason the hard way: Playwright's `waitFor` helpers, like most automation "wait" functions, use long-polling, checking the DOM only every 30ms or more to avoid pegging the CPU.
54
+
55
+ This is fine for functional testing, but for performance measurement, it's a fatal flaw. You cannot use a ruler with 30ms markings to accurately measure a 20ms event.
56
+
57
+ This realization forced us to throw out polling entirely and build our own high-precision waiting mechanism using the browser's native `MutationObserver`. Our `measurePerformanceInBrowser` function attaches an observer that checks our pass condition on *every single DOM mutation*. This allows us to stop the timer at the exact moment the UI reaches its desired state, giving us microsecond-level precision. It is the technical heart of our benchmark's credibility.
58
+
59
+ ```javascript
60
+ // The core of our high-precision, in-browser measurement utility
61
+ export const measurePerformanceInBrowser = (testName, action, condition) => {
62
+ return new Promise((resolve, reject) => {
63
+ const observer = new MutationObserver(() => {
64
+ // This condition check runs on every DOM change
65
+ if (condition()) {
66
+ const endTime = performance.now();
67
+ observer.disconnect();
68
+ clearTimeout(timeoutId);
69
+ resolve(endTime - startTime);
70
+ }
71
+ });
72
+
73
+ observer.observe(document.body, {attributes: true, childList: true, subtree: true});
74
+
75
+ const timeoutId = setTimeout(() => {
76
+ observer.disconnect();
77
+ reject(new Error(`Benchmark timed out for "${testName}".`));
78
+ }, 30000);
79
+
80
+ // Start the timer right before triggering the action
81
+ const startTime = performance.now();
82
+ action();
83
+
84
+ // Check condition immediately in case the action was synchronous
85
+ if (condition()) {
86
+ const endTime = performance.now();
87
+ observer.disconnect();
88
+ clearTimeout(timeoutId);
89
+ resolve(endTime - startTime);
90
+ }
91
+ });
92
+ };
93
+ ```
94
+
95
+ **Lesson 3: You can't trust polling-based "wait" functions for performance measurement.** For high-precision results, you must use a `MutationObserver` to react to DOM changes instantly.
96
+
97
+ ## The Story Behind the Numbers: A Response to Skepticism
98
+
99
+ This project didn't start in a vacuum. It began as a direct response to the feedback from our Neo.mjs v10 blog series. In those articles, we made some extraordinary claims: that the "memoization tax" in frameworks like React is an unnecessary burden, that our architecture, leveraging a virtual DOM-like approach, enables efficient multi-threading, and that the "tyranny of the main thread" is the single biggest bottleneck in modern web applications.
100
+
101
+ The community's response was rightly skeptical: "I don't believe it's faster." "Where are the meaningful benchmarks?" "I don't believe off-main-thread architecture makes a real difference."
102
+
103
+ This skepticism is understandable. In a world of hype cycles, extraordinary claims require extraordinary evidence. This project was built to be that evidence. The detailed benchmark reports, including standard deviations for each metric, are available on our GitHub repository, allowing for further statistical analysis of the variability and significance of the results.
104
+
105
+ ## What We Can Finally See Clearly: The Evidence
106
+
107
+ By building this high-precision harness, we can now move beyond architectural theory and into empirical proof. The results, aggregated from 5 independent test runs, are not just a leaderboard; they are the direct validation of our claims.
108
+
109
+ 1. **The Main-Thread Bottleneck is Real and Severe:** We claimed that forcing all application logic onto a single main thread was a recipe for UI jank. Our "Heavy Calculation" test proves it. When a heavy task runs on the main thread, frameworks like React and Angular see their UI frame rates plummet to 30 FPS or even 0 FPS, causing visible freezing. In contrast, the worker-based architecture of Neo.mjs maintains a perfect 60 FPS because the UI thread is never blocked. This benchmark quantifies the "tyranny of the main thread" in lost frames.
110
+
111
+ 2. **Extreme Loads Reveal Architectural Breaking Points:** We argued that the "memoization tax" (the performance overhead incurred by techniques like React.memo to prevent unnecessary re-renders) is a workaround, not a solution, for performance under pressure. Our "Scrolling Under Duress" test (1 million rows with a live data feed) shows what happens when those workarounds are overwhelmed. The React/TanStack Virtual implementation doesn't just get slow—it crashes the browser tab. The Angular app remains barely usable, with over a second of lag. The worker-based app, which eliminates the need for such manual optimizations at an architectural level, stays responsive. This is the ultimate cost of a bottlenecked main thread: not just jank, but catastrophic failure.
112
+
113
+ 3. **Best-in-Class Components Can't Escape the Architecture:** To prove our claims, our "Big Data Benchmark" pits a new Neo.mjs grid against the undisputed industry leader, AG Grid, running in React. We measured "UI Update Time"—the pure grid rendering performance—across several demanding scenarios. The results were not just a win; they were a confirmation of our architectural thesis.
114
+
115
+ ### True Performance Analysis (UI Update Times Only)
116
+
117
+ #### 1. Large Column Operations (50→200 columns, 100k rows)
118
+
119
+ | Browser | Neo.mjs UI Time | React AG Grid UI Time | Neo.mjs Advantage |
120
+ | :--- | :--- | :--- | :--- |
121
+ | **Chromium** | 380-404ms | 2,962-3,071ms | **7.5x faster** |
122
+ | **Firefox** | 382-402ms | 4,328-4,460ms | **11x faster** |
123
+ | **Webkit** | 380-483ms | 5,256-5,500ms | **10.9x faster** |
124
+
125
+ **Architectural Implication**: Neo's off-main-thread rendering engine handles complex column layouts with dramatically less DOM manipulation overhead, resulting in an order-of-magnitude performance gain.
126
+
127
+ #### 2. Large Row Operations (1,000→100,000 rows)
128
+
129
+ | Browser | Neo.mjs UI Time | React AG Grid UI Time | Neo.mjs Advantage |
130
+ | :--- | :--- | :--- | :--- |
131
+ | **Chromium** | 150-225ms | 899-954ms | **~5x faster** |
132
+ | **Firefox** | 104-154ms | 1,431-1,484ms | **~10x faster** |
133
+ | **Webkit** | 115-165ms | 1,180-1,231ms | **~8x faster** |
134
+
135
+ **Critical Finding**: Neo.mjs maintains sub-200ms UI updates even with a 100x increase in row count, an update speed that feels instant to the user. In contrast, the main-thread architecture of React + AG Grid degrades to 1+ second updates, which is a significant and noticeable lag.
136
+
137
+ The data speaks for itself. AG Grid is the undisputed industry leader for feature-rich data grids, which makes these results even more significant. They provide powerful evidence of an architectural ceiling: even the most mature and heavily optimized component will eventually be bottlenecked by the single-threaded paradigm it operates in. The performance limitations are not a question of implementation detail, but of fundamental architectural constraints.
138
+
139
+ > *These results raise important questions about the relationship between architecture and performance at scale. If there's community interest, we plan to publish a detailed analysis exploring the architectural factors behind these performance differences and their implications for enterprise application development.*
140
+
141
+ ## An MVP at a Crossroads
142
+
143
+ What you see today is the MVP of that effort—a robust foundation, built in a rapid, ten-day engineering sprint to ensure the results are precise, credible, and reproducible. But it is just the beginning of what's possible.
144
+
145
+ The potential for this project is immense:
146
+ - **CI/CD Integration:** Imagine this suite running automatically in the cloud across diverse hardware configurations, providing a living, breathing dataset of framework performance.
147
+ - **AI-Powered Reporting:** We could use LLMs to transform the raw data into insightful, narrative-driven reports, making the results accessible to a broader audience.
148
+ - **Expanded Scope:** We could add more frameworks, more complex widgets (schedulers, Gantt charts), and entirely new metrics that push the boundaries of what we measure.
149
+ - **Better Onboarding:** We could create detailed specifications for each test, making it far easier for framework experts to contribute high-quality, best-practice implementations.
150
+
151
+ This project is more than just a report; it's a high-precision instrument that we are opening up to the entire web development community. We especially invite framework authors and component library vendors to use this harness. See how your tools perform under the kind of concurrent stress that your most demanding users face every day. Use it to identify hidden bottlenecks and validate optimizations. The data this benchmark provides goes beyond simple metrics; it reveals the deep, real-world impact of architectural choices. Perhaps it will confirm your current approach, or perhaps it will inspire you to explore new ones.
152
+
153
+ > *Beyond framework authors, we see immense potential for enterprise decision-making. Imagine a consulting firm getting a client request: 'We need to handle 500,000 rows with real-time updates - which stack should we choose?' Today, that's answered with vendor marketing and gut instinct. Tomorrow, it could be answered with AI-generated reports that synthesize objective benchmark data into tailored architectural recommendations. The same precision we're bringing to measurement could revolutionize how teams make framework decisions.*
154
+
155
+ To better understand the idea of automated AI generated reports, here is an example of how Claude would do it:
156
+ [Neo vs React AG Grid - True Performance Analysis](https://claude.ai/public/artifacts/8b4bc869-e36b-4ba5-9b5b-5af99d45772b)
157
+
158
+ The path forward requires significant engagement, either through active community contributions or through corporate sponsorship. We believe this project has immense potential to benefit the broader web development community, and your active participation can accelerate its growth and expand its impact. We invite you to join us in shaping the future of frontend performance measurement.
159
+
160
+ So, the question we pose to the community is: is there an interest for more? If you believe in the need for a better, more honest way to benchmark the modern web, we invite you to get involved.
161
+
162
+ Beyond the automated test results, all benchmark applications are fully functional. We encourage you to run them locally in your browser of choice to manually verify and experience the performance differences firsthand. For an immediate demonstration, an almost identical version of the Neo.mjs Big Data grid is deployed here:
163
+
164
+ - **Live Demo:** [Neo.mjs Big Data Grid](https://neomjs.com/dist/production/examples/grid/bigData/index.html)
165
+
166
+ The entire project, including all applications, test suites, and results, is publicly available on GitHub.
167
+
168
+ - **Explore the code and results:** [https://github.com/neomjs/benchmarks](https://github.com/neomjs/benchmarks)
169
+ - **Read our detailed methodology:** [METHODOLOGY.md](https://github.com/neomjs/benchmarks/METHODOLOGY.md)
170
+ - **Run the benchmarks yourself:** [REPRODUCIBILITY.md](https://github.com/neomjs/benchmarks/REPRODUCIBILITY.md)
171
+
172
+ Best regards & happy coding,
173
+ Tobias
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "10.5.2",
3
+ "version": "10.5.4",
4
4
  "description": "Neo.mjs: The multi-threaded UI framework for building ultra-fast, desktop-like web applications with uncompromised responsiveness, inherent security, and a transpilation-free dev mode.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -83,7 +83,7 @@
83
83
  "acorn": "^8.15.0",
84
84
  "astring": "^1.9.0",
85
85
  "autoprefixer": "^10.4.21",
86
- "chalk": "^5.5.0",
86
+ "chalk": "^5.6.0",
87
87
  "clean-webpack-plugin": "^4.0.0",
88
88
  "commander": "^14.0.0",
89
89
  "cssnano": "^7.1.0",
@@ -92,8 +92,8 @@
92
92
  "fs-extra": "^11.3.1",
93
93
  "highlightjs-line-numbers.js": "^2.9.0",
94
94
  "html-minifier-terser": "^7.2.0",
95
- "inquirer": "^12.9.2",
96
- "marked": "^16.1.2",
95
+ "inquirer": "^12.9.3",
96
+ "marked": "^16.2.0",
97
97
  "monaco-editor": "0.50.0",
98
98
  "neo-jsdoc": "1.0.1",
99
99
  "neo-jsdoc-x": "1.0.5",
@@ -103,7 +103,7 @@
103
103
  "siesta-lite": "5.5.2",
104
104
  "terser": "^5.43.1",
105
105
  "url": "^0.11.4",
106
- "webpack": "^5.101.1",
106
+ "webpack": "^5.101.2",
107
107
  "webpack-cli": "^6.0.1",
108
108
  "webpack-dev-server": "^5.2.2",
109
109
  "webpack-hook-plugin": "^1.0.7",
@@ -299,12 +299,12 @@ const DefaultConfig = {
299
299
  useVdomWorker: true,
300
300
  /**
301
301
  * buildScripts/injectPackageVersion.mjs will update this value
302
- * @default '10.5.2'
302
+ * @default '10.5.4'
303
303
  * @memberOf! module:Neo
304
304
  * @name config.version
305
305
  * @type String
306
306
  */
307
- version: '10.5.2'
307
+ version: '10.5.4'
308
308
  };
309
309
 
310
310
  Object.assign(DefaultConfig, {
@@ -0,0 +1,375 @@
1
+ import Component from '../Base.mjs';
2
+ import ClassSystemUtil from '../../util/ClassSystem.mjs';
3
+ import Store from '../../data/Store.mjs';
4
+
5
+ /**
6
+ * In development: Do not use inside your apps until the implementation is finished.
7
+ * @class Neo.component.wrapper.OpenStreetMaps
8
+ * @extends Neo.component.Base
9
+ * @experimental
10
+ */
11
+ class OpenStreetMaps extends Component {
12
+ static config = {
13
+ /**
14
+ * @member {String} className='Neo.component.wrapper.OpenStreetMaps'
15
+ * @protected
16
+ */
17
+ className: 'Neo.component.wrapper.OpenStreetMaps',
18
+ /**
19
+ * @member {String} ntype='openstreetmaps'
20
+ * @protected
21
+ */
22
+ ntype: 'openstreetmaps',
23
+ /**
24
+ * Specify lat & lng for the current focus position
25
+ * @member {Object} center_={lat: -34.397, lng: 150.644}
26
+ * @reactive
27
+ */
28
+ center_: {lat: -34.397, lng: 150.644},
29
+ /**
30
+ * Prefer to use markerStoreConfig instead.
31
+ * @member {Neo.data.Store|Object} markerStore_
32
+ * @protected
33
+ * @reactive
34
+ */
35
+ markerStore_: {
36
+ model: {
37
+ fields: [{
38
+ name: 'anchorPoint',
39
+ type: 'Object'
40
+ }, {
41
+ name: 'icon',
42
+ type: 'Object'
43
+ }, {
44
+ name: 'id'
45
+ }, {
46
+ name: 'label',
47
+ type: 'String'
48
+ }, {
49
+ name: 'position',
50
+ type: 'Object'
51
+ }, {
52
+ name: 'title',
53
+ type: 'String'
54
+ }]
55
+ }
56
+ },
57
+ /**
58
+ * @member {Number} zoom_=8
59
+ * @reactive
60
+ */
61
+ zoom_: 8
62
+ }
63
+
64
+ /**
65
+ * false hides the default fullscreen control
66
+ * @member {Boolean} fullscreenControl=true
67
+ */
68
+ fullscreenControl = true
69
+ /**
70
+ * Internal flag. Gets set to true once Neo.main.addon.OpenStreetMaps.create() is finished.
71
+ * @member {Boolean} mapCreated=false
72
+ */
73
+ mapCreated = false
74
+ /**
75
+ * Pass any options to the map instance which are not explicitly defined here
76
+ * @member {Object} mapOptions={}
77
+ */
78
+ mapOptions = {}
79
+ /**
80
+ * @member {Object} markerStoreConfig=null
81
+ */
82
+ markerStoreConfig = null
83
+ /**
84
+ * null => the maximum zoom from the current map type is used instead
85
+ * @member {Number|null} maxZoom=null
86
+ */
87
+ maxZoom = null
88
+ /**
89
+ null => the minimum zoom from the current map type is used instead
90
+ * @member {Number|null} minZoom=null
91
+ */
92
+ minZoom = null
93
+ /**
94
+ * false hides the default zoom control
95
+ * @member {Boolean} zoomControl=true
96
+ */
97
+ zoomControl = true
98
+
99
+ /**
100
+ * @param {Object} config
101
+ */
102
+ construct(config) {
103
+ super.construct(config);
104
+
105
+ let me = this;
106
+
107
+ me.addDomListeners({
108
+ openStreetMapZoomChange: me.onMapZoomChange,
109
+ openStreetMapMarkerClick : me.parseMarkerClick,
110
+ local : false,
111
+ scope : me
112
+ })
113
+ }
114
+
115
+ /**
116
+ * @param {Object} data
117
+ * @param {Object} [data.anchorPoint] x & y
118
+ * @param {String} [data.icon]
119
+ * @param {String} data.id
120
+ * @param {String} [data.label]
121
+ * @param {String} data.mapId
122
+ * @param {Object} data.position
123
+ * @param {String} [data.title]
124
+ */
125
+ addMarker(data) {
126
+ let {appName, windowId} = this;
127
+
128
+ Neo.main.addon.OpenStreetMaps.addMarker({
129
+ appName,
130
+ windowId,
131
+ ...data
132
+ })
133
+ }
134
+
135
+ /**
136
+ * Triggered after the center config got changed
137
+ * @param {Object} value
138
+ * @param {Object} oldValue
139
+ * @protected
140
+ */
141
+ afterSetCenter(value, oldValue) {
142
+ let {appName, id, windowId} = this;
143
+
144
+ if (this.mapCreated) {
145
+ Neo.main.addon.OpenStreetMaps.setCenter({
146
+ appName,
147
+ id,
148
+ value,
149
+ windowId
150
+ })
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Triggered after the markerStore config got changed
156
+ * @param {Object} value
157
+ * @param {Object} oldValue
158
+ * @protected
159
+ */
160
+ afterSetMarkerStore(value, oldValue) {
161
+ let me = this;
162
+
163
+ value.on({
164
+ load : me.onMarkerStoreLoad,
165
+ scope: me
166
+ });
167
+
168
+ if (value.items.length > 0) {
169
+ me.onMarkerStoreLoad()
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Triggered after the mounted config got changed
175
+ * @param {Boolean} value
176
+ * @param {Boolean} oldValue
177
+ * @protected
178
+ */
179
+ afterSetMounted(value, oldValue) {
180
+ let me = this;
181
+
182
+ if (value === false && oldValue !== undefined) {
183
+ me.removeMap()
184
+ }
185
+
186
+ super.afterSetMounted(value, oldValue);
187
+
188
+ if (value) {
189
+ let opts = {
190
+ appName : me.appName,
191
+ center : me.center,
192
+ fullscreenControl: me.fullscreenControl,
193
+ id : me.id,
194
+ mapOptions : me.mapOptions,
195
+ maxZoom : me.maxZoom,
196
+ minZoom : me.minZoom,
197
+ zoom : me.zoom,
198
+ zoomControl : me.zoomControl
199
+ };
200
+
201
+ me.timeout(50).then(() => {
202
+ Neo.main.addon.OpenStreetMaps.create(opts).then(() => {
203
+ me.mapCreated = true;
204
+ me.onComponentMounted()
205
+ })
206
+ })
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Triggered after the zoom config got changed
212
+ * @param {Number} value
213
+ * @param {Number} oldValue
214
+ * @protected
215
+ */
216
+ afterSetZoom(value, oldValue) {
217
+ let me = this,
218
+ {appName, id, windowId} = me;
219
+
220
+ if (me.mapCreated) {
221
+ Neo.main.addon.OpenStreetMaps.setZoom({
222
+ appName,
223
+ id,
224
+ value,
225
+ windowId
226
+ });
227
+
228
+ me.fire('zoomChange', {id, value})
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Triggered before the markerStore config gets changed.
234
+ * @param {Object} value
235
+ * @param {Object} oldValue
236
+ * @protected
237
+ */
238
+ beforeSetMarkerStore(value, oldValue) {
239
+ oldValue?.destroy();
240
+
241
+ return ClassSystemUtil.beforeSetInstance(value, Store, this.markerStoreConfig)
242
+ }
243
+
244
+ /**
245
+ * @param {Boolean} updateParentVdom=false
246
+ * @param {Boolean} silent=false
247
+ */
248
+ destroy(updateParentVdom=false, silent=false) {
249
+ this.removeMap();
250
+ super.destroy(updateParentVdom, silent)
251
+ }
252
+
253
+ /**
254
+ * @param {String} id
255
+ */
256
+ hideMarker(id) {
257
+ let {appName, windowId} = this;
258
+
259
+ Neo.main.addon.OpenStreetMaps.hideMarker({
260
+ appName,
261
+ id,
262
+ mapId: this.id,
263
+ windowId
264
+ })
265
+ }
266
+
267
+ /**
268
+ * Hook to use once the map instance got rendered
269
+ */
270
+ onComponentMounted() {
271
+ }
272
+
273
+ /**
274
+ * @param {Object} data
275
+ */
276
+ onMapZoomChange(data) {
277
+ this.zoom = data.value
278
+ }
279
+
280
+ /**
281
+ *
282
+ */
283
+ onMarkerStoreLoad() {
284
+ let {appName, id, windowId} = this;
285
+
286
+ Neo.main.addon.OpenStreetMaps.destroyMarkers({
287
+ appName,
288
+ id,
289
+ windowId
290
+ });
291
+
292
+ this.markerStore.items.forEach(item => {
293
+ Neo.main.addon.OpenStreetMaps.addMarker({
294
+ appName,
295
+ mapId: id,
296
+ windowId,
297
+ ...item
298
+ })
299
+ })
300
+ }
301
+
302
+ /**
303
+ * @param {Object} position
304
+ * @param {Number} position.lat
305
+ * @param {Number} position.lng
306
+ */
307
+ panTo(position) {
308
+ let {appName, id, windowId} = this;
309
+
310
+ Neo.main.addon.OpenStreetMaps.panTo({
311
+ appName,
312
+ mapId: id,
313
+ position,
314
+ windowId
315
+ })
316
+ }
317
+
318
+ /**
319
+ * Internal function. Use onMarkerClick() or the markerClick event instead
320
+ * @param {Object} data
321
+ * @protected
322
+ */
323
+ parseMarkerClick(data) {
324
+ let me = this;
325
+
326
+ data.record = me.markerStore.get(data.id);
327
+
328
+ me.onMarkerClick?.(data);
329
+
330
+ me.fire('markerClick', {id: me.id, data})
331
+ }
332
+
333
+ /**
334
+ *
335
+ */
336
+ removeMap() {
337
+ let {appName, id, windowId} = this;
338
+
339
+ Neo.main.addon.OpenStreetMaps.removeMap({
340
+ appName,
341
+ mapId: id,
342
+ windowId
343
+ })
344
+ }
345
+
346
+ /**
347
+ * @param {String} id
348
+ */
349
+ removeMarker(id) {
350
+ let {appName, windowId} = this;
351
+
352
+ Neo.main.addon.OpenStreetMaps.removeMarker({
353
+ appName,
354
+ id,
355
+ mapId: this.id,
356
+ windowId
357
+ })
358
+ }
359
+
360
+ /**
361
+ * @param {String} id
362
+ */
363
+ showMarker(id) {
364
+ let {appName, windowId} = this;
365
+
366
+ Neo.main.addon.OpenStreetMaps.showMarker({
367
+ appName,
368
+ id,
369
+ mapId: this.id,
370
+ windowId
371
+ })
372
+ }
373
+ }
374
+
375
+ export default Neo.setupClass(OpenStreetMaps);
package/src/core/Base.mjs CHANGED
@@ -6,7 +6,6 @@ import {isDescriptor} from './ConfigSy
6
6
  import IdGenerator from './IdGenerator.mjs';
7
7
  import EffectManager from './EffectManager.mjs';
8
8
 
9
-
10
9
  const configSymbol = Symbol.for('configSymbol'),
11
10
  forceAssignConfigs = Symbol('forceAssignConfigs'),
12
11
  isInstance = Symbol('isInstance');
@@ -234,7 +233,7 @@ class Base {
234
233
  me.id = config.id || IdGenerator.getId(this.getIdKey());
235
234
  delete config.id;
236
235
 
237
- // assign class field values prior to configs
236
+ // Assign class field values prior to configs
238
237
  config = me.setFields(config);
239
238
 
240
239
  me.initConfig(config);
@@ -282,7 +281,7 @@ class Base {
282
281
 
283
282
  if (value) {
284
283
  if (hasManager) {
285
- Neo.manager.Instance.register(me);
284
+ Neo.manager.Instance.register(me)
286
285
  } else {
287
286
  Neo.idMap ??= {};
288
287
  Neo.idMap[value] = me
@@ -357,7 +356,7 @@ class Base {
357
356
  cls = this.prototype;
358
357
 
359
358
  if (cls[item]) {
360
- // add to overwrittenMethods
359
+ // Add to overwrittenMethods
361
360
  cls.constructor.overwrittenMethods[item] = cls[item]
362
361
  }
363
362
  }
@@ -716,7 +715,7 @@ class Base {
716
715
  }
717
716
 
718
717
  /**
719
- * Helper method to replace string based values containing "@config:" with the matching config value
718
+ * Helper method to replace string-based values containing "@config:" with the matching config value
720
719
  * of this instance.
721
720
  * @param {Object|Object[]} items
722
721
  */
@@ -1,3 +1 @@
1
-
2
- // e.g., in a new file src/core/ConfigSymbols.mjs
3
1
  export const isDescriptor = Symbol.for('Neo.Config.isDescriptor');
@@ -696,7 +696,7 @@ class GridContainer extends BaseContainer {
696
696
  let me = this,
697
697
  finalCount = count ? count : me.store.count;
698
698
 
699
- me.getVdomRoot()['aria-rowcount'] = finalCount + 2;
699
+ me.getVdomRoot()['aria-rowcount'] = finalCount + 1;
700
700
  !silent && me.update()
701
701
  }
702
702
  }
@@ -0,0 +1,199 @@
1
+ import Addon from './Base.mjs';
2
+ import DomAccess from '../DomAccess.mjs';
3
+ import DomEvents from '../DomEvents.mjs';
4
+ import Observable from '../../core/Observable.mjs';
5
+
6
+ /**
7
+ * In development: Do not use inside your apps until the implementation is finished.
8
+ * @class Neo.main.addon.OpenLayers
9
+ * @extends Neo.main.addon.Base
10
+ * @mixes Neo.core.Observable
11
+ * @experimental
12
+ */
13
+ class OpenLayers extends Addon {
14
+ /**
15
+ * True automatically applies the core.Observable mixin
16
+ * @member {Boolean} observable=true
17
+ * @static
18
+ */
19
+ static observable = true
20
+
21
+ static config = {
22
+ /**
23
+ * @member {String} className='Neo.main.addon.OpenLayers'
24
+ * @protected
25
+ */
26
+ className: 'Neo.main.addon.OpenLayers',
27
+ /**
28
+ * @member {Object} remote
29
+ * @protected
30
+ */
31
+ remote: {
32
+ app: [
33
+ 'addMarker',
34
+ 'create',
35
+ 'destroyMarkers',
36
+ 'geocode',
37
+ 'hideMarker',
38
+ 'panTo',
39
+ 'removeMap',
40
+ 'removeMarker',
41
+ 'setCenter',
42
+ 'setZoom',
43
+ 'showMarker'
44
+ ]
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @member {Object} maps={}
50
+ */
51
+ maps = {}
52
+ /**
53
+ * @member {Object} markers={}
54
+ */
55
+ markers = {}
56
+ /**
57
+ * @member {Object} vectorLayers={}
58
+ */
59
+ vectorLayers = {}
60
+ /**
61
+ * @member {Object} vectorSources={}
62
+ */
63
+ vectorSources = {}
64
+
65
+ /**
66
+ * @param {Object} data
67
+ * @param {Object} [data.anchorPoint] x & y
68
+ * @param {String} [data.icon]
69
+ * @param {String} data.id
70
+ * @param {String} [data.label]
71
+ * @param {String} data.mapId
72
+ * @param {Object} data.position
73
+ * @param {String} [data.title]
74
+ */
75
+ addMarker(data) {
76
+ // TODO: Implement OpenLayers marker creation
77
+ }
78
+
79
+ /**
80
+ * @param {Object} data
81
+ * @param {Object} data.center
82
+ * @param {Boolean} data.fullscreenControl
83
+ * @param {String} data.id
84
+ * @param {Object} data.mapOptions
85
+ * @param {Number} data.maxZoom
86
+ * @param {Number} data.minZoom
87
+ * @param {Number} data.zoom
88
+ * @param {Boolean} data.zoomControl
89
+ */
90
+ create(data) {
91
+ // TODO: Implement OpenLayers map creation
92
+ }
93
+
94
+ /**
95
+ * @param {Object} data
96
+ * @param {String} data.mapId
97
+ */
98
+ destroyMarkers(data) {
99
+ // TODO: Implement marker destruction
100
+ }
101
+
102
+ /**
103
+ * @param {Object} data
104
+ * @param {String} data.address
105
+ * @param {Object} data.location
106
+ * @param {String} data.placeId
107
+ * @returns {Object}
108
+ */
109
+ async geocode(data) {
110
+ // TODO: Implement geocoding (likely using Nominatim or other service)
111
+ }
112
+
113
+ /**
114
+ * @param {Object} data
115
+ * @param {String} data.id
116
+ * @param {String} data.mapId
117
+ */
118
+ hideMarker(data) {
119
+ // TODO: Implement marker hiding
120
+ }
121
+
122
+ /**
123
+ * @protected
124
+ */
125
+ async loadFiles() {
126
+ // TODO: Load OpenLayers CSS and JS files
127
+ }
128
+
129
+ /**
130
+ * @param {ol.Map} map
131
+ * @param {String} mapId
132
+ */
133
+ onMapZoomChange(map, mapId) {
134
+ // TODO: Handle zoom change events
135
+ }
136
+
137
+ /**
138
+ * @param {ol.Feature} feature
139
+ * @param {Object} event
140
+ */
141
+ onMarkerClick(feature, event) {
142
+ // TODO: Handle marker click events
143
+ }
144
+
145
+ /**
146
+ * @param {Object} data
147
+ * @param {String} data.mapId
148
+ * @param {Object} data.position
149
+ */
150
+ panTo(data) {
151
+ // TODO: Implement pan to position
152
+ }
153
+
154
+ /**
155
+ * @param {Object} data
156
+ * @param {String} data.mapId
157
+ */
158
+ removeMap(data) {
159
+ // TODO: Implement map removal
160
+ }
161
+
162
+ /**
163
+ * @param {Object} data
164
+ * @param {String} data.id
165
+ * @param {String} data.mapId
166
+ */
167
+ removeMarker(data) {
168
+ // TODO: Implement marker removal
169
+ }
170
+
171
+ /**
172
+ * @param {Object} data
173
+ * @param {String} data.id
174
+ * @param {Object} data.value
175
+ */
176
+ setCenter(data) {
177
+ // TODO: Implement set center
178
+ }
179
+
180
+ /**
181
+ * @param {Object} data
182
+ * @param {String} data.id
183
+ * @param {Number} data.value
184
+ */
185
+ setZoom(data) {
186
+ // TODO: Implement set zoom
187
+ }
188
+
189
+ /**
190
+ * @param {Object} data
191
+ * @param {String} data.id
192
+ * @param {String} data.mapId
193
+ */
194
+ showMarker(data) {
195
+ // TODO: Implement marker showing
196
+ }
197
+ }
198
+
199
+ export default Neo.setupClass(OpenLayers);