cotomy 0.1.72 → 0.2.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/LICENSE +20 -20
- package/README.md +101 -93
- package/dist/browser/cotomy.js +132 -44
- package/dist/browser/cotomy.js.map +1 -1
- package/dist/browser/cotomy.min.js +1 -1
- package/dist/browser/cotomy.min.js.map +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/api.js +39 -13
- package/dist/esm/api.js.map +1 -1
- package/dist/esm/form.js +47 -27
- package/dist/esm/form.js.map +1 -1
- package/dist/esm/view.js +45 -3
- package/dist/esm/view.js.map +1 -1
- package/dist/types/api.d.ts +5 -0
- package/dist/types/form.d.ts +9 -7
- package/dist/types/view.d.ts +5 -0
- package/package.json +61 -61
package/LICENSE
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
Copyright (c) 2025 Yasuhiro Arakawa
|
|
3
|
-
|
|
4
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
-
in the Software without restriction, including without limitation the rights
|
|
7
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
-
furnished to do so, subject to the following conditions:
|
|
10
|
-
|
|
11
|
-
The above copyright notice and this permission notice shall be included in all
|
|
12
|
-
copies or substantial portions of the Software.
|
|
13
|
-
|
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
-
SOFTWARE.
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2025 Yasuhiro Arakawa
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# Cotomy
|
|
2
|
-
|
|
3
|
-
> This library targets ES2020+.
|
|
4
|
-
> For older browsers (e.g. iOS 13 or IE), you will need a Polyfill such as `core-js`.
|
|
5
|
-
|
|
6
|
-
**Cotomy** is a lightweight framework for managing form behavior and page controllers in web applications.
|
|
7
|
-
It is suitable for both SPAs (Single Page Applications) and traditional web apps requiring dynamic form operations.
|
|
8
|
-
|
|
9
|
-
⚠️ **Warning**: This project is in early development. APIs may change without notice until version 1.0.0.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
To install Cotomy in your project, run the following command:
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npm i cotomy
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Usage
|
|
19
|
-
|
|
1
|
+
# Cotomy
|
|
2
|
+
|
|
3
|
+
> This library targets ES2020+.
|
|
4
|
+
> For older browsers (e.g. iOS 13 or IE), you will need a Polyfill such as `core-js`.
|
|
5
|
+
|
|
6
|
+
**Cotomy** is a lightweight framework for managing form behavior and page controllers in web applications.
|
|
7
|
+
It is suitable for both SPAs (Single Page Applications) and traditional web apps requiring dynamic form operations.
|
|
8
|
+
|
|
9
|
+
⚠️ **Warning**: This project is in early development. APIs may change without notice until version 1.0.0.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
To install Cotomy in your project, run the following command:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i cotomy
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
20
|
Cotomy will continue to expand with more detailed usage instructions and code examples added to the README in the future.
|
|
21
21
|
For the latest updates, please check the official documentation or repository regularly.
|
|
22
22
|
|
|
@@ -75,6 +75,7 @@ The View layer provides thin wrappers around DOM elements and window events.
|
|
|
75
75
|
- `append(child): this` / `prepend(child): this` / `appendAll(children): this`
|
|
76
76
|
- `insertBefore(sibling): this` / `insertAfter(sibling): this`
|
|
77
77
|
- `appendTo(target): this` / `prependTo(target): this`
|
|
78
|
+
- `clone(type?): CotomyElement` — Returns a deep-cloned element, optionally typed
|
|
78
79
|
- `clear(): this` — Removes all descendants and text
|
|
79
80
|
- `remove(): void`
|
|
80
81
|
- Geometry & visibility
|
|
@@ -170,8 +171,8 @@ The Form layer builds on `CotomyElement` for common form flows.
|
|
|
170
171
|
- `initialized: boolean` — Set after `initialize()`
|
|
171
172
|
- `submitAsync(): Promise<void>` — Abstract in base
|
|
172
173
|
- Routing & reload
|
|
173
|
-
- `method
|
|
174
|
-
- `actionUrl
|
|
174
|
+
- `method: string` — Getter that defaults to `get` in base; specialized in subclasses
|
|
175
|
+
- `actionUrl: string` — Getter that defaults to the `action` attribute or current path
|
|
175
176
|
- `reloadAsync(): Promise<void>` — Page reload using `CotomyWindow`
|
|
176
177
|
- `autoReload: boolean` — Backed by `data-cotomy-autoreload` (default true)
|
|
177
178
|
|
|
@@ -184,8 +185,8 @@ The Form layer builds on `CotomyElement` for common form flows.
|
|
|
184
185
|
|
|
185
186
|
- API integration
|
|
186
187
|
- `apiClient(): CotomyApi` — Override to inject a client; default creates a new one
|
|
187
|
-
- `actionUrl
|
|
188
|
-
- `method
|
|
188
|
+
- `actionUrl: string` — Uses `action` attribute
|
|
189
|
+
- `method: string` — Defaults to `post`
|
|
189
190
|
- `formData(): FormData` — Builds from form, converts `datetime-local` to ISO (UTC offset)
|
|
190
191
|
- `submitAsync()` — Calls `submitToApiAsync(formData)`
|
|
191
192
|
- `submitToApiAsync(formData): Promise<CotomyApiResponse>` — Uses `CotomyApi.submitAsync`
|
|
@@ -199,8 +200,8 @@ The Form layer builds on `CotomyElement` for common form flows.
|
|
|
199
200
|
- Surrogate key flow
|
|
200
201
|
- `data-cotomy-entity-key` — Holds the entity identifier if present
|
|
201
202
|
- `data-cotomy-identify` — Defaults to true; when true and `201 Created` is returned, the form extracts the key from `Location` and stores it in `data-cotomy-entity-key`
|
|
202
|
-
- `actionUrl
|
|
203
|
-
- `method
|
|
203
|
+
- `actionUrl` — Appends the key to the base `action` when present; otherwise normalizes trailing slash for collection URL
|
|
204
|
+
- `method` — `put` when key exists; otherwise `post` (unless `method` attribute is explicitly set)
|
|
204
205
|
|
|
205
206
|
### CotomyEntityFillApiForm
|
|
206
207
|
|
|
@@ -208,14 +209,21 @@ The Form layer builds on `CotomyElement` for common form flows.
|
|
|
208
209
|
- `initialize()` — Adds default fillers and triggers `loadAsync()` on `CotomyWindow.ready`
|
|
209
210
|
- `reloadAsync()` — Alias to `loadAsync()`
|
|
210
211
|
- `loadAsync(): Promise<CotomyApiResponse>` — Calls `CotomyApi.getAsync` when `canLoad()` is true
|
|
211
|
-
- `loadActionUrl(): string` — Defaults to `actionUrl
|
|
212
|
+
- `loadActionUrl(): string` — Defaults to `actionUrl`; override for custom endpoints
|
|
212
213
|
- `canLoad(): boolean` — Defaults to `hasEntityKey`
|
|
213
214
|
- Naming & binding
|
|
214
215
|
- `bindNameGenerator(): ICotomyBindNameGenerator` — Defaults to `CotomyBracketBindNameGenerator` (`user[name]`)
|
|
215
216
|
- `renderer(): CotomyViewRenderer` — Applies `[data-cotomy-bind]` to view elements
|
|
216
|
-
|
|
217
|
+
- `filler(type, (input, value))` — Register fillers; defaults provided for `datetime-local`, `checkbox`, `radio`
|
|
217
218
|
- Fills non-array, non-object fields by matching input/select/textarea `name`
|
|
218
219
|
|
|
220
|
+
#### Array binding
|
|
221
|
+
|
|
222
|
+
- Both `CotomyViewRenderer.applyAsync` and `CotomyEntityFillApiForm.fillAsync` resolve array elements by index via the active `ICotomyBindNameGenerator` (dot style → `items[0].name`, bracket style → `items[0][name]`).
|
|
223
|
+
- Cotomy does **not** create or clone templates for you. Prepare the necessary DOM (e.g., table rows, list items, individual inputs) ahead of time, then call `fillAsync`/`applyAsync` to populate the values.
|
|
224
|
+
- Primitive arrays (strings, numbers, booleans, etc.) are treated the same way—have matching `[data-cotomy-bind]`/`name` attributes ready for every index you want to show.
|
|
225
|
+
- If you need dynamic row counts, generate the markup yourself before invoking Cotomy; the framework purposely avoids mutating the structure so it does not get in your way.
|
|
226
|
+
|
|
219
227
|
Example:
|
|
220
228
|
|
|
221
229
|
```ts
|
|
@@ -228,69 +236,69 @@ form.submitFailed(e => console.warn("Submit failed", e.response.status));
|
|
|
228
236
|
```
|
|
229
237
|
|
|
230
238
|
### Entity API forms
|
|
231
|
-
|
|
232
|
-
`CotomyEntityApiForm` targets REST endpoints that identify records with a single surrogate key.
|
|
233
|
-
Attach `data-cotomy-entity-key="<id>"` to the form when editing an existing entity; omit the attribute (or leave it empty) to issue a `POST` to the base `action` URL.
|
|
234
|
-
On `201 Created`, the form reads the `Location` header and stores the generated key back into `data-cotomy-entity-key`, enabling subsequent `PUT` submissions.
|
|
235
|
-
Composite or natural keys are no longer supported—migrate any legacy markup that relied on `data-cotomy-keyindex` or multiple key inputs to the new surrogate-key flow.
|
|
236
|
-
When you must integrate with endpoints that still expect natural identifiers, subclass `CotomyEntityApiForm`/`CotomyEntityFillApiForm`, override `canLoad()` to supply your own load condition, and adjust `loadActionUrl()` (plus any submission hooks) to build the appropriate URL fragments.
|
|
237
|
-
|
|
238
|
-
The core of Cotomy is `CotomyElement`, which is constructed as a wrapper for `Element`.
|
|
239
|
-
By passing HTML and CSS strings to the constructor, it is possible to generate Element designs with a limited scope.
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
const ce = new CotomyElement({
|
|
243
|
-
html: /* html */`
|
|
244
|
-
<div>
|
|
245
|
-
<p>Text</p>
|
|
246
|
-
</div>
|
|
247
|
-
`,
|
|
248
|
-
css: /* css */`
|
|
249
|
-
[scope] {
|
|
250
|
-
display: block;
|
|
251
|
-
}
|
|
252
|
-
[scope] > p {
|
|
253
|
-
text-align: center;
|
|
254
|
-
}
|
|
255
|
-
`
|
|
256
|
-
});
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
- `"display HTML in character literals with color coding"` → `"syntax highlighting for embedded HTML"`
|
|
260
|
-
- `"generate Element designs with a limited scope"` → `"generate scoped DOM elements with associated styles"`
|
|
261
|
-
|
|
262
|
-
## Development
|
|
263
|
-
|
|
264
|
-
Cotomy ships with both ESM (`dist/esm`) and CommonJS (`dist/cjs`) builds, plus generated type definitions in `dist/types`.
|
|
265
|
-
For direct `<script>` usage, browser-ready bundles are available at `dist/browser/cotomy.js` and `dist/browser/cotomy.min.js` (also served via the npm `unpkg` entry).
|
|
266
|
-
Include the minified build like so:
|
|
267
|
-
|
|
268
|
-
```html
|
|
269
|
-
<script src="https://unpkg.com/cotomy/dist/browser/cotomy.min.js"></script>
|
|
270
|
-
<script>
|
|
271
|
-
const el = new Cotomy.CotomyElement("<div>Hello</div>");
|
|
272
|
-
document.body.appendChild(el.element);
|
|
273
|
-
</script>
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
Run the build to refresh every target bundle:
|
|
277
|
-
|
|
278
|
-
```bash
|
|
279
|
-
npm install
|
|
280
|
-
npm run build
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
The Vitest-based test suite can be executed via:
|
|
284
|
-
|
|
285
|
-
```bash
|
|
286
|
-
npx vitest run
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
## License
|
|
290
|
-
|
|
291
|
-
This project is licensed under the [MIT License](LICENSE).
|
|
292
|
-
|
|
293
|
-
## Contact
|
|
294
|
-
|
|
295
|
-
You can reach out to me at: [yshr1920@gmail.com](mailto:yshr1920@gmail.com)
|
|
296
|
-
GitHub repository: [https://github.com/yshr1920/cotomy](https://github.com/yshr1920/cotomy)
|
|
239
|
+
|
|
240
|
+
`CotomyEntityApiForm` targets REST endpoints that identify records with a single surrogate key.
|
|
241
|
+
Attach `data-cotomy-entity-key="<id>"` to the form when editing an existing entity; omit the attribute (or leave it empty) to issue a `POST` to the base `action` URL.
|
|
242
|
+
On `201 Created`, the form reads the `Location` header and stores the generated key back into `data-cotomy-entity-key`, enabling subsequent `PUT` submissions.
|
|
243
|
+
Composite or natural keys are no longer supported—migrate any legacy markup that relied on `data-cotomy-keyindex` or multiple key inputs to the new surrogate-key flow.
|
|
244
|
+
When you must integrate with endpoints that still expect natural identifiers, subclass `CotomyEntityApiForm`/`CotomyEntityFillApiForm`, override `canLoad()` to supply your own load condition, and adjust `loadActionUrl()` (plus any submission hooks) to build the appropriate URL fragments.
|
|
245
|
+
|
|
246
|
+
The core of Cotomy is `CotomyElement`, which is constructed as a wrapper for `Element`.
|
|
247
|
+
By passing HTML and CSS strings to the constructor, it is possible to generate Element designs with a limited scope.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const ce = new CotomyElement({
|
|
251
|
+
html: /* html */`
|
|
252
|
+
<div>
|
|
253
|
+
<p>Text</p>
|
|
254
|
+
</div>
|
|
255
|
+
`,
|
|
256
|
+
css: /* css */`
|
|
257
|
+
[scope] {
|
|
258
|
+
display: block;
|
|
259
|
+
}
|
|
260
|
+
[scope] > p {
|
|
261
|
+
text-align: center;
|
|
262
|
+
}
|
|
263
|
+
`
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- `"display HTML in character literals with color coding"` → `"syntax highlighting for embedded HTML"`
|
|
268
|
+
- `"generate Element designs with a limited scope"` → `"generate scoped DOM elements with associated styles"`
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
Cotomy ships with both ESM (`dist/esm`) and CommonJS (`dist/cjs`) builds, plus generated type definitions in `dist/types`.
|
|
273
|
+
For direct `<script>` usage, browser-ready bundles are available at `dist/browser/cotomy.js` and `dist/browser/cotomy.min.js` (also served via the npm `unpkg` entry).
|
|
274
|
+
Include the minified build like so:
|
|
275
|
+
|
|
276
|
+
```html
|
|
277
|
+
<script src="https://unpkg.com/cotomy/dist/browser/cotomy.min.js"></script>
|
|
278
|
+
<script>
|
|
279
|
+
const el = new Cotomy.CotomyElement("<div>Hello</div>");
|
|
280
|
+
document.body.appendChild(el.element);
|
|
281
|
+
</script>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Run the build to refresh every target bundle:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
npm install
|
|
288
|
+
npm run build
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The Vitest-based test suite can be executed via:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
npx vitest run
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
300
|
+
|
|
301
|
+
## Contact
|
|
302
|
+
|
|
303
|
+
You can reach out to me at: [yshr1920@gmail.com](mailto:yshr1920@gmail.com)
|
|
304
|
+
GitHub repository: [https://github.com/yshr1920/cotomy](https://github.com/yshr1920/cotomy)
|
package/dist/browser/cotomy.js
CHANGED
|
@@ -174,7 +174,7 @@ module.exports = cuid;
|
|
|
174
174
|
/***/ 826:
|
|
175
175
|
/***/ (function(module) {
|
|
176
176
|
|
|
177
|
-
!function(t,i){ true?module.exports=i():0}(this,(function(){"use strict";var t="minute",i=/[+-]\d\d(?::?\d\d)?/g,e=/([+-]|\d\d)/g;return function(s,f,n){var u=f.prototype;n.utc=function(t){var i={date:t,utc:!0,args:arguments};return new f(i)},u.utc=function(i){var e=n(this.toDate(),{locale:this.$L,utc:!0});return i?e.add(this.utcOffset(),t):e},u.local=function(){return n(this.toDate(),{locale:this.$L,utc:!1})};var
|
|
177
|
+
!function(t,i){ true?module.exports=i():0}(this,(function(){"use strict";var t="minute",i=/[+-]\d\d(?::?\d\d)?/g,e=/([+-]|\d\d)/g;return function(s,f,n){var u=f.prototype;n.utc=function(t){var i={date:t,utc:!0,args:arguments};return new f(i)},u.utc=function(i){var e=n(this.toDate(),{locale:this.$L,utc:!0});return i?e.add(this.utcOffset(),t):e},u.local=function(){return n(this.toDate(),{locale:this.$L,utc:!1})};var o=u.parse;u.parse=function(t){t.utc&&(this.$u=!0),this.$utils().u(t.$offset)||(this.$offset=t.$offset),o.call(this,t)};var r=u.init;u.init=function(){if(this.$u){var t=this.$d;this.$y=t.getUTCFullYear(),this.$M=t.getUTCMonth(),this.$D=t.getUTCDate(),this.$W=t.getUTCDay(),this.$H=t.getUTCHours(),this.$m=t.getUTCMinutes(),this.$s=t.getUTCSeconds(),this.$ms=t.getUTCMilliseconds()}else r.call(this)};var a=u.utcOffset;u.utcOffset=function(s,f){var n=this.$utils().u;if(n(s))return this.$u?0:n(this.$offset)?a.call(this):this.$offset;if("string"==typeof s&&(s=function(t){void 0===t&&(t="");var s=t.match(i);if(!s)return null;var f=(""+s[0]).match(e)||["-",0,0],n=f[0],u=60*+f[1]+ +f[2];return 0===u?0:"+"===n?u:-u}(s),null===s))return this;var u=Math.abs(s)<=16?60*s:s,o=this;if(f)return o.$offset=u,o.$u=0===s,o;if(0!==s){var r=this.$u?this.toDate().getTimezoneOffset():-1*this.utcOffset();(o=this.local().add(u+r,t)).$offset=u,o.$x.$localOffset=r}else o=this.utc();return o};var h=u.format;u.format=function(t){var i=t||(this.$u?"YYYY-MM-DDTHH:mm:ss[Z]":"");return h.call(this,i)},u.valueOf=function(){var t=this.$utils().u(this.$offset)?0:this.$offset+(this.$x.$localOffset||this.$d.getTimezoneOffset());return this.$d.valueOf()-6e4*t},u.isUTC=function(){return!!this.$u},u.toISOString=function(){return this.toDate().toISOString()},u.toString=function(){return this.toDate().toUTCString()};var l=u.toDate;u.toDate=function(t){return"s"===t&&this.$offset?n(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate():l.call(this)};var c=u.diff;u.diff=function(t,i,e){if(t&&this.$u===t.$u)return c.call(this,t,i,e);var s=this.local(),f=n(t).local();return c.call(s,f,i,e)}}}));
|
|
178
178
|
|
|
179
179
|
/***/ })
|
|
180
180
|
|
|
@@ -804,12 +804,12 @@ class HandlerRegistory {
|
|
|
804
804
|
}
|
|
805
805
|
}
|
|
806
806
|
class EventRegistry {
|
|
807
|
-
static get instance() {
|
|
808
|
-
return this._instance ?? (this._instance = new EventRegistry());
|
|
809
|
-
}
|
|
810
807
|
constructor() {
|
|
811
808
|
this._registry = new Map();
|
|
812
809
|
}
|
|
810
|
+
static get instance() {
|
|
811
|
+
return this._instance ?? (this._instance = new EventRegistry());
|
|
812
|
+
}
|
|
813
813
|
map(target) {
|
|
814
814
|
const scopeId = target.scopeId;
|
|
815
815
|
let registry = this._registry.get(scopeId);
|
|
@@ -994,6 +994,10 @@ class CotomyElement {
|
|
|
994
994
|
get element() {
|
|
995
995
|
return this._element;
|
|
996
996
|
}
|
|
997
|
+
clone(type) {
|
|
998
|
+
const ctor = (type ?? CotomyElement);
|
|
999
|
+
return new ctor(this.element.cloneNode(true));
|
|
1000
|
+
}
|
|
997
1001
|
get tagname() {
|
|
998
1002
|
return this.element.tagName.toLowerCase();
|
|
999
1003
|
}
|
|
@@ -1023,6 +1027,14 @@ class CotomyElement {
|
|
|
1023
1027
|
}
|
|
1024
1028
|
return true;
|
|
1025
1029
|
}
|
|
1030
|
+
match(selector) {
|
|
1031
|
+
try {
|
|
1032
|
+
return this.element.matches(selector);
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1026
1038
|
get empty() {
|
|
1027
1039
|
const nonEmptyTags = new Set([
|
|
1028
1040
|
"input", "select", "textarea", "img", "video", "audio", "br", "hr",
|
|
@@ -1339,6 +1351,36 @@ class CotomyElement {
|
|
|
1339
1351
|
return undefined;
|
|
1340
1352
|
}
|
|
1341
1353
|
}
|
|
1354
|
+
previousSibling(selector = "*", type) {
|
|
1355
|
+
const element = this.element.previousElementSibling;
|
|
1356
|
+
if (element !== null && element instanceof HTMLElement) {
|
|
1357
|
+
const ctor = (type ?? CotomyElement);
|
|
1358
|
+
const ce = new ctor(element);
|
|
1359
|
+
return ce.match(selector) ? ce : ce.previousSibling(selector, type);
|
|
1360
|
+
}
|
|
1361
|
+
else {
|
|
1362
|
+
return undefined;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
nextSibling(selector = "*", type) {
|
|
1366
|
+
const element = this.element.nextElementSibling;
|
|
1367
|
+
if (element !== null && element instanceof HTMLElement) {
|
|
1368
|
+
const ctor = (type ?? CotomyElement);
|
|
1369
|
+
const ce = new ctor(element);
|
|
1370
|
+
return ce.match(selector) ? ce : ce.nextSibling(selector, type);
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
return undefined;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
siblings(selector = "*", type) {
|
|
1377
|
+
const parent = this.element.parentElement;
|
|
1378
|
+
if (!parent)
|
|
1379
|
+
return [];
|
|
1380
|
+
const ctor = (type ?? CotomyElement);
|
|
1381
|
+
return Array.from(parent.children).filter((e) => e instanceof HTMLElement
|
|
1382
|
+
&& e !== this.element).map(e => new ctor(e)).filter(e => e.match(selector));
|
|
1383
|
+
}
|
|
1342
1384
|
find(selector, type) {
|
|
1343
1385
|
const elements = Array.from(this.element.querySelectorAll(selector));
|
|
1344
1386
|
return elements.map(e => new (type ?? CotomyElement)(e));
|
|
@@ -2078,11 +2120,17 @@ class CotomyBracketBindNameGenerator {
|
|
|
2078
2120
|
create(name, parent) {
|
|
2079
2121
|
return parent ? `${parent}[${name}]` : name;
|
|
2080
2122
|
}
|
|
2123
|
+
createIndex(parent, index) {
|
|
2124
|
+
return this.create(String(index), parent);
|
|
2125
|
+
}
|
|
2081
2126
|
}
|
|
2082
2127
|
class CotomyDotBindNameGenerator {
|
|
2083
2128
|
create(name, parent) {
|
|
2084
2129
|
return parent ? `${parent}.${name}` : name;
|
|
2085
2130
|
}
|
|
2131
|
+
createIndex(parent, index) {
|
|
2132
|
+
return parent ? `${parent}[${index}]` : `[${index}]`;
|
|
2133
|
+
}
|
|
2086
2134
|
}
|
|
2087
2135
|
class CotomyViewRenderer {
|
|
2088
2136
|
constructor(element, bindNameGenerator) {
|
|
@@ -2146,30 +2194,50 @@ class CotomyViewRenderer {
|
|
|
2146
2194
|
}
|
|
2147
2195
|
return this;
|
|
2148
2196
|
}
|
|
2197
|
+
bindPrimitiveValue(propertyName, value) {
|
|
2198
|
+
this.element.find(`[data-cotomy-bind="${propertyName}" i]`).forEach(element => {
|
|
2199
|
+
if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Bind)) {
|
|
2200
|
+
console.debug(`Binding data to element [data-cotomy-bind="${propertyName}"]:`, value);
|
|
2201
|
+
}
|
|
2202
|
+
const type = element.attribute("data-cotomy-bindtype")?.toLowerCase();
|
|
2203
|
+
if (type && this._renderers[type]) {
|
|
2204
|
+
this._renderers[type](element, value);
|
|
2205
|
+
}
|
|
2206
|
+
else {
|
|
2207
|
+
element.text = String(value ?? "");
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
async applyArrayAsync(values, propertyName) {
|
|
2212
|
+
for (let index = 0; index < values.length; index++) {
|
|
2213
|
+
const item = values[index];
|
|
2214
|
+
const itemName = this.bindNameGenerator.createIndex(propertyName, index);
|
|
2215
|
+
if (Array.isArray(item)) {
|
|
2216
|
+
await this.applyArrayAsync(item, itemName);
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
if (item && typeof item === "object") {
|
|
2220
|
+
await this.applyObjectAsync(item, itemName);
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
this.bindPrimitiveValue(itemName, item);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2149
2226
|
async applyObjectAsync(target, propertyName = undefined) {
|
|
2150
2227
|
if (!propertyName) {
|
|
2151
2228
|
this.element.find("[data-cotomy-bind]").forEach(e => e.clear());
|
|
2152
2229
|
}
|
|
2153
2230
|
for (const [key, value] of Object.entries(await target)) {
|
|
2154
2231
|
const pname = this.bindNameGenerator.create(key, propertyName);
|
|
2155
|
-
if (Array.isArray(value))
|
|
2232
|
+
if (Array.isArray(value)) {
|
|
2233
|
+
await this.applyArrayAsync(value, pname);
|
|
2156
2234
|
continue;
|
|
2235
|
+
}
|
|
2157
2236
|
if (value && typeof value === "object") {
|
|
2158
2237
|
await this.applyObjectAsync(value, pname);
|
|
2159
2238
|
continue;
|
|
2160
2239
|
}
|
|
2161
|
-
this.
|
|
2162
|
-
if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Bind)) {
|
|
2163
|
-
console.debug(`Binding data to element [data-cotomy-bind="${pname}"]:`, value);
|
|
2164
|
-
}
|
|
2165
|
-
const type = element.attribute("data-cotomy-bindtype")?.toLowerCase();
|
|
2166
|
-
if (type && this._renderers[type]) {
|
|
2167
|
-
this._renderers[type](element, value);
|
|
2168
|
-
}
|
|
2169
|
-
else {
|
|
2170
|
-
element.text = String(value ?? "");
|
|
2171
|
-
}
|
|
2172
|
-
});
|
|
2240
|
+
this.bindPrimitiveValue(pname, value);
|
|
2173
2241
|
}
|
|
2174
2242
|
}
|
|
2175
2243
|
async applyAsync(respose) {
|
|
@@ -2370,10 +2438,10 @@ class CotomyForm extends CotomyElement {
|
|
|
2370
2438
|
generateId(prefix = "__cotomy_form__") {
|
|
2371
2439
|
return super.generateId(prefix);
|
|
2372
2440
|
}
|
|
2373
|
-
method() {
|
|
2441
|
+
get method() {
|
|
2374
2442
|
return this.attribute("method") ?? "get";
|
|
2375
2443
|
}
|
|
2376
|
-
actionUrl() {
|
|
2444
|
+
get actionUrl() {
|
|
2377
2445
|
return this.attribute("action") ?? location.pathname + location.search;
|
|
2378
2446
|
}
|
|
2379
2447
|
async reloadAsync() {
|
|
@@ -2406,11 +2474,11 @@ class CotomyForm extends CotomyElement {
|
|
|
2406
2474
|
}
|
|
2407
2475
|
}
|
|
2408
2476
|
class CotomyQueryForm extends CotomyForm {
|
|
2409
|
-
method() {
|
|
2477
|
+
get method() {
|
|
2410
2478
|
return "get";
|
|
2411
2479
|
}
|
|
2412
2480
|
async submitAsync() {
|
|
2413
|
-
const url = this.actionUrl
|
|
2481
|
+
const url = this.actionUrl;
|
|
2414
2482
|
const queryParams = {};
|
|
2415
2483
|
const queryString = url.split("?")[1];
|
|
2416
2484
|
if (queryString) {
|
|
@@ -2452,7 +2520,7 @@ class CotomyApiForm extends CotomyForm {
|
|
|
2452
2520
|
apiClient() {
|
|
2453
2521
|
return new CotomyApi();
|
|
2454
2522
|
}
|
|
2455
|
-
actionUrl() {
|
|
2523
|
+
get actionUrl() {
|
|
2456
2524
|
return this.attribute("action");
|
|
2457
2525
|
}
|
|
2458
2526
|
apiFailed(handle) {
|
|
@@ -2479,7 +2547,7 @@ class CotomyApiForm extends CotomyForm {
|
|
|
2479
2547
|
console.error("Submit failed:", response);
|
|
2480
2548
|
}
|
|
2481
2549
|
}
|
|
2482
|
-
method() {
|
|
2550
|
+
get method() {
|
|
2483
2551
|
return this.attribute("method") ?? "post";
|
|
2484
2552
|
}
|
|
2485
2553
|
formData() {
|
|
@@ -2508,8 +2576,8 @@ class CotomyApiForm extends CotomyForm {
|
|
|
2508
2576
|
const api = this.apiClient();
|
|
2509
2577
|
try {
|
|
2510
2578
|
const response = await api.submitAsync({
|
|
2511
|
-
method: this.method
|
|
2512
|
-
action: this.actionUrl
|
|
2579
|
+
method: this.method,
|
|
2580
|
+
action: this.actionUrl,
|
|
2513
2581
|
body: formData,
|
|
2514
2582
|
});
|
|
2515
2583
|
return response;
|
|
@@ -2533,7 +2601,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
|
|
|
2533
2601
|
get hasEntityKey() {
|
|
2534
2602
|
return !!this.entityKey;
|
|
2535
2603
|
}
|
|
2536
|
-
actionUrl() {
|
|
2604
|
+
get actionUrl() {
|
|
2537
2605
|
const action = this.attribute("action");
|
|
2538
2606
|
const normalized = action.replace(/\/+$/, "");
|
|
2539
2607
|
if (!this.entityKey) {
|
|
@@ -2541,7 +2609,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
|
|
|
2541
2609
|
}
|
|
2542
2610
|
return `${normalized}/${encodeURIComponent(this.entityKey)}`;
|
|
2543
2611
|
}
|
|
2544
|
-
method() {
|
|
2612
|
+
get method() {
|
|
2545
2613
|
if (this.hasAttribute("method") && this.attribute("method") !== "") {
|
|
2546
2614
|
return this.attribute("method");
|
|
2547
2615
|
}
|
|
@@ -2583,7 +2651,7 @@ class CotomyEntityApiForm extends CotomyApiForm {
|
|
|
2583
2651
|
this.attribute("data-cotomy-entity-key", addedParts[0]);
|
|
2584
2652
|
}
|
|
2585
2653
|
else {
|
|
2586
|
-
const msg = `Location does not contain a single entity key segment.
|
|
2654
|
+
const msg = `Location does not contain a single entity key segment.
|
|
2587
2655
|
action="${baseAction}", location="${locPath}", added=["${addedParts.join('","')}"]`;
|
|
2588
2656
|
throw new Error(msg);
|
|
2589
2657
|
}
|
|
@@ -2641,7 +2709,7 @@ class CotomyEntityFillApiForm extends CotomyEntityApiForm {
|
|
|
2641
2709
|
await this.loadAsync();
|
|
2642
2710
|
}
|
|
2643
2711
|
loadActionUrl() {
|
|
2644
|
-
return this.actionUrl
|
|
2712
|
+
return this.actionUrl;
|
|
2645
2713
|
}
|
|
2646
2714
|
bindNameGenerator() {
|
|
2647
2715
|
return new CotomyBracketBindNameGenerator();
|
|
@@ -2672,32 +2740,52 @@ class CotomyEntityFillApiForm extends CotomyEntityApiForm {
|
|
|
2672
2740
|
throw error;
|
|
2673
2741
|
}
|
|
2674
2742
|
}
|
|
2743
|
+
applyValueToInputs(pname, value) {
|
|
2744
|
+
this.find(`input[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple]),
|
|
2745
|
+
textarea[name="${pname}" i]:not([data-cotomy-fill="false"]),
|
|
2746
|
+
select[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple])`).forEach(input => {
|
|
2747
|
+
if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Fill)) {
|
|
2748
|
+
console.debug(`Filling input[name="${pname}"] with value:`, value);
|
|
2749
|
+
}
|
|
2750
|
+
const type = input.attribute("type")?.toLowerCase();
|
|
2751
|
+
if (type && this._fillers[type]) {
|
|
2752
|
+
this._fillers[type](input, value);
|
|
2753
|
+
}
|
|
2754
|
+
else {
|
|
2755
|
+
input.value = String(value || "");
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
async fillArrayAsync(bindNameGenerator, values, propertyName) {
|
|
2760
|
+
for (let index = 0; index < values.length; index++) {
|
|
2761
|
+
const item = values[index];
|
|
2762
|
+
const itemName = bindNameGenerator.createIndex(propertyName, index);
|
|
2763
|
+
if (Array.isArray(item)) {
|
|
2764
|
+
await this.fillArrayAsync(bindNameGenerator, item, itemName);
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
if (item && typeof item === "object") {
|
|
2768
|
+
await this.fillObjectAsync(bindNameGenerator, item, itemName);
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
this.applyValueToInputs(itemName, item);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2675
2774
|
async fillObjectAsync(bindNameGenerator, target, propertyName = undefined) {
|
|
2676
2775
|
for (const [key, value] of Object.entries(target)) {
|
|
2677
2776
|
if (key.endsWith('[]')) {
|
|
2678
2777
|
continue;
|
|
2679
2778
|
}
|
|
2680
2779
|
const pname = bindNameGenerator.create(key, propertyName);
|
|
2681
|
-
if (Array.isArray(value))
|
|
2780
|
+
if (Array.isArray(value)) {
|
|
2781
|
+
await this.fillArrayAsync(bindNameGenerator, value, pname);
|
|
2682
2782
|
continue;
|
|
2783
|
+
}
|
|
2683
2784
|
if (value && typeof value === "object") {
|
|
2684
2785
|
await this.fillObjectAsync(bindNameGenerator, value, pname);
|
|
2685
2786
|
continue;
|
|
2686
2787
|
}
|
|
2687
|
-
this.
|
|
2688
|
-
textarea[name="${pname}" i]:not([data-cotomy-fill="false"]),
|
|
2689
|
-
select[name="${pname}" i]:not([data-cotomy-fill="false"]):not([multiple])`).forEach(input => {
|
|
2690
|
-
if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Fill)) {
|
|
2691
|
-
console.debug(`Filling input[name="${pname}"] with value:`, value);
|
|
2692
|
-
}
|
|
2693
|
-
const type = input.attribute("type")?.toLowerCase();
|
|
2694
|
-
if (type && this._fillers[type]) {
|
|
2695
|
-
this._fillers[type](input, value);
|
|
2696
|
-
}
|
|
2697
|
-
else {
|
|
2698
|
-
input.value = String(value || "");
|
|
2699
|
-
}
|
|
2700
|
-
});
|
|
2788
|
+
this.applyValueToInputs(pname, value);
|
|
2701
2789
|
}
|
|
2702
2790
|
}
|
|
2703
2791
|
async fillAsync(response) {
|