@webwriter/widget 0.0.0-stage → 1.0.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 +21 -0
- package/index.js +308 -0
- package/index.ts +349 -0
- package/options.ts +131 -0
- package/package.json +26 -4
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frederic Salmen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
var javaScriptMimeTypes = /* @__PURE__ */ new Set([
|
|
3
|
+
"application/ecmascript",
|
|
4
|
+
"application/javascript",
|
|
5
|
+
"application/x-ecmascript",
|
|
6
|
+
"application/x-javascript",
|
|
7
|
+
"text/ecmascript",
|
|
8
|
+
"text/javascript",
|
|
9
|
+
"text/javascript1.0",
|
|
10
|
+
"text/javascript1.1",
|
|
11
|
+
"text/javascript1.2",
|
|
12
|
+
"text/javascript1.3",
|
|
13
|
+
"text/javascript1.4",
|
|
14
|
+
"text/javascript1.5",
|
|
15
|
+
"text/jscript",
|
|
16
|
+
"text/livescript",
|
|
17
|
+
"text/x-ecmascript",
|
|
18
|
+
"text/x-javascript"
|
|
19
|
+
]);
|
|
20
|
+
function isDataBlockType(type) {
|
|
21
|
+
const essence = type.split(";")[0].trim().toLowerCase();
|
|
22
|
+
return /^[^\s/]+\/[^\s/]+$/.test(essence) && !javaScriptMimeTypes.has(essence);
|
|
23
|
+
}
|
|
24
|
+
var contextFollowers = /* @__PURE__ */ new WeakMap();
|
|
25
|
+
var dataEvents = ["datapending", "datachange", "dataerror", "datarelease"];
|
|
26
|
+
var widgetRuntimeAttributes = ["contenteditable", "lang", "shared", "id", "standalone"];
|
|
27
|
+
function widgetLanguage(host) {
|
|
28
|
+
return host.closest("[lang]")?.getAttribute("lang") ?? "";
|
|
29
|
+
}
|
|
30
|
+
var WidgetRuntime = class {
|
|
31
|
+
constructor(host, callbacks = {}) {
|
|
32
|
+
this.host = host;
|
|
33
|
+
this.callbacks = callbacks;
|
|
34
|
+
}
|
|
35
|
+
#observedDocument;
|
|
36
|
+
#language;
|
|
37
|
+
#editable;
|
|
38
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
39
|
+
#snapshot;
|
|
40
|
+
get lang() {
|
|
41
|
+
return this.callbacks.language?.() ?? widgetLanguage(this.host);
|
|
42
|
+
}
|
|
43
|
+
get editable() {
|
|
44
|
+
return this.callbacks.editable?.() ?? this.host.isContentEditable;
|
|
45
|
+
}
|
|
46
|
+
get standalone() {
|
|
47
|
+
return this.host.getAttribute("standalone");
|
|
48
|
+
}
|
|
49
|
+
set standalone(value) {
|
|
50
|
+
if (value === null) this.host.removeAttribute("standalone");
|
|
51
|
+
else this.host.setAttribute("standalone", value);
|
|
52
|
+
}
|
|
53
|
+
/** The same snapshot object is returned until an observed value changes. */
|
|
54
|
+
getSnapshot = () => {
|
|
55
|
+
const dataBlock = this.dataBlock;
|
|
56
|
+
const previous = this.#snapshot;
|
|
57
|
+
const attributes = Object.fromEntries((this.callbacks.attributes?.() ?? []).map((name) => [name, this.host.getAttribute(name)]));
|
|
58
|
+
const sameAttributes = previous && Object.keys(attributes).length === Object.keys(previous.attributes).length && Object.entries(attributes).every(([name, value]) => previous.attributes[name] === value);
|
|
59
|
+
const next = {
|
|
60
|
+
editable: this.editable,
|
|
61
|
+
lang: this.lang,
|
|
62
|
+
standalone: this.standalone,
|
|
63
|
+
attributes: sameAttributes ? previous.attributes : Object.freeze(attributes),
|
|
64
|
+
data: dataBlock?.textContent ?? null,
|
|
65
|
+
dataType: this.#typeOf(dataBlock),
|
|
66
|
+
dataBlock,
|
|
67
|
+
dataSlot: dataBlock?.getAttribute("slot") ?? null,
|
|
68
|
+
dataMode: this.#mode,
|
|
69
|
+
dataReady: this.#isDataReady(dataBlock),
|
|
70
|
+
dataError: this.#error
|
|
71
|
+
};
|
|
72
|
+
if (previous && Object.entries(next).every(([key, value]) => previous[key] === value)) return previous;
|
|
73
|
+
return this.#snapshot = Object.freeze(next);
|
|
74
|
+
};
|
|
75
|
+
/** Subscribe to host updates and return an unsubscribe function. */
|
|
76
|
+
subscribe = (listener) => {
|
|
77
|
+
this.#listeners.add(listener);
|
|
78
|
+
return () => {
|
|
79
|
+
this.#listeners.delete(listener);
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
#publish(name, previous) {
|
|
83
|
+
this.callbacks.update?.(name, previous);
|
|
84
|
+
this.#listeners.forEach((listener) => listener());
|
|
85
|
+
}
|
|
86
|
+
attributeChanged(name, oldValue, value) {
|
|
87
|
+
if (oldValue === value) return;
|
|
88
|
+
if (name === "lang" || name === "contenteditable") {
|
|
89
|
+
this.contextChanged();
|
|
90
|
+
if (this.callbacks.attributes?.().includes(name)) this.#publish(name, oldValue);
|
|
91
|
+
} else if (name === "shared" || name === "id") {
|
|
92
|
+
this.#readyBlock = null;
|
|
93
|
+
if (this.host.isConnected) this.#syncData();
|
|
94
|
+
else if (this.callbacks.attributes?.().includes(name)) this.#publish(name, oldValue);
|
|
95
|
+
} else this.#publish(name, oldValue);
|
|
96
|
+
}
|
|
97
|
+
contextChanged() {
|
|
98
|
+
const language = this.lang, editable = this.editable;
|
|
99
|
+
if (language !== this.#language) {
|
|
100
|
+
const before = this.#language;
|
|
101
|
+
this.#language = language;
|
|
102
|
+
this.#publish("lang", before);
|
|
103
|
+
}
|
|
104
|
+
if (editable !== this.#editable) {
|
|
105
|
+
const before = this.#editable;
|
|
106
|
+
this.#editable = editable;
|
|
107
|
+
this.#publish("editable", before);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
get dataBlock() {
|
|
111
|
+
const blocks = Array.from(this.host.children).filter((child) => child.namespaceURI === "http://www.w3.org/1999/xhtml" && child.localName === "script" && isDataBlockType(child.type) && !child.hasAttribute("src"));
|
|
112
|
+
return blocks.find((block) => this.#scoped(block)) ?? blocks[0] ?? null;
|
|
113
|
+
}
|
|
114
|
+
#scoped(block) {
|
|
115
|
+
return !!block && block.getAttribute("slot") === "data" && ["application/json", "application/xml"].includes(block.type);
|
|
116
|
+
}
|
|
117
|
+
#scope(block) {
|
|
118
|
+
return JSON.stringify([
|
|
119
|
+
this.host.ownerDocument.documentElement.id,
|
|
120
|
+
this.host.id,
|
|
121
|
+
this.host.getAttribute("shared"),
|
|
122
|
+
block?.type,
|
|
123
|
+
block?.getAttribute("slot"),
|
|
124
|
+
...Array.from(this.host.childNodes).filter((node) => node.nodeType === 7 && node.nodeName === "ww-grouping").map((node) => node.nodeValue)
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
get data() {
|
|
128
|
+
return this.dataBlock?.textContent ?? null;
|
|
129
|
+
}
|
|
130
|
+
set data(value) {
|
|
131
|
+
this.setData(this.dataType ?? "text/plain", value ?? "");
|
|
132
|
+
}
|
|
133
|
+
get dataType() {
|
|
134
|
+
return this.#typeOf(this.dataBlock);
|
|
135
|
+
}
|
|
136
|
+
#typeOf(block) {
|
|
137
|
+
return block?.type ?? this.host.constructor.dataType;
|
|
138
|
+
}
|
|
139
|
+
#block;
|
|
140
|
+
#bindingScope;
|
|
141
|
+
#readyBlock = null;
|
|
142
|
+
#readyScope;
|
|
143
|
+
#managed = false;
|
|
144
|
+
#lastText;
|
|
145
|
+
#mode = "local";
|
|
146
|
+
#syncing = false;
|
|
147
|
+
#contentObserver;
|
|
148
|
+
#error = null;
|
|
149
|
+
/** False while the current participant scope is loading or unavailable. */
|
|
150
|
+
get dataReady() {
|
|
151
|
+
return this.#isDataReady(this.dataBlock);
|
|
152
|
+
}
|
|
153
|
+
#isDataReady(block) {
|
|
154
|
+
if (!this.host.isConnected) return false;
|
|
155
|
+
if (!this.#scoped(block)) return true;
|
|
156
|
+
const scope = this.#scope(block);
|
|
157
|
+
return block === this.#block && this.#bindingScope === scope && (!this.#managed || block === this.#readyBlock && this.#readyScope === scope);
|
|
158
|
+
}
|
|
159
|
+
#context(block = this.dataBlock) {
|
|
160
|
+
return { block, mode: this.#mode };
|
|
161
|
+
}
|
|
162
|
+
#syncData() {
|
|
163
|
+
if (!this.host.isConnected || this.#syncing) return;
|
|
164
|
+
this.#syncing = true;
|
|
165
|
+
try {
|
|
166
|
+
const block = this.dataBlock, scope = this.#scope(block);
|
|
167
|
+
if (block !== this.#block || scope !== this.#bindingScope) {
|
|
168
|
+
this.#block = block;
|
|
169
|
+
this.#bindingScope = scope;
|
|
170
|
+
this.#readyBlock = null;
|
|
171
|
+
this.#error = null;
|
|
172
|
+
this.#managed = false;
|
|
173
|
+
this.#mode = "local";
|
|
174
|
+
if (this.#scoped(block)) {
|
|
175
|
+
const Request = this.host.ownerDocument.defaultView?.CustomEvent ?? CustomEvent;
|
|
176
|
+
const request = new Request("datarequest", { bubbles: true, cancelable: true });
|
|
177
|
+
if (!block.dispatchEvent(request)) this.#managed = true;
|
|
178
|
+
}
|
|
179
|
+
if (!this.#managed) this.#acceptData("local");
|
|
180
|
+
} else if (!this.#managed && this.data !== this.#lastText) this.#acceptData("local");
|
|
181
|
+
this.#publish("data");
|
|
182
|
+
} finally {
|
|
183
|
+
this.#syncing = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
#acceptData(mode) {
|
|
187
|
+
this.#mode = mode;
|
|
188
|
+
this.#readyBlock = this.dataBlock;
|
|
189
|
+
this.#readyScope = this.#scope(this.#readyBlock);
|
|
190
|
+
this.#lastText = this.data;
|
|
191
|
+
this.#error = null;
|
|
192
|
+
this.callbacks.dataChanged?.(this.data, this.#context());
|
|
193
|
+
this.#publish("data");
|
|
194
|
+
}
|
|
195
|
+
#dataEvent = (event) => {
|
|
196
|
+
const block = this.dataBlock;
|
|
197
|
+
if (!this.host.isConnected || !block || event.target !== block || !this.#scoped(block)) return;
|
|
198
|
+
if (event.type === "datarelease") {
|
|
199
|
+
this.#block = void 0;
|
|
200
|
+
this.#syncData();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const detail = event.detail;
|
|
204
|
+
this.#managed = true;
|
|
205
|
+
this.#block = block;
|
|
206
|
+
this.#bindingScope = this.#scope(block);
|
|
207
|
+
if (detail?.mode) this.#mode = detail.mode;
|
|
208
|
+
else if (this.#mode === "local") this.#mode = !this.host.hasAttribute("shared") ? "individual" : this.host.getAttribute("shared") === "group" ? "group" : "all";
|
|
209
|
+
if (event.type === "datachange") this.#acceptData(this.#mode);
|
|
210
|
+
else {
|
|
211
|
+
if (event.type === "datapending") {
|
|
212
|
+
this.#readyBlock = null;
|
|
213
|
+
this.#error = null;
|
|
214
|
+
this.callbacks.dataPending?.(this.#context(block));
|
|
215
|
+
} else {
|
|
216
|
+
this.#error = new Error(detail?.message ?? "Widget data is unavailable");
|
|
217
|
+
this.callbacks.dataError?.(this.#error, this.#context(block));
|
|
218
|
+
}
|
|
219
|
+
this.#publish("data");
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
/** Updates a data block in place, preserving its identity and attributes.
|
|
223
|
+
* A managed block cannot be written until its current scope is ready. */
|
|
224
|
+
setData(type, value, attrs = {}) {
|
|
225
|
+
if (!isDataBlockType(type)) throw new TypeError(`'${type}' is not a data block type`);
|
|
226
|
+
if (Object.keys(attrs).some((name) => name.toLowerCase() === "src")) throw new TypeError("Data blocks cannot have src");
|
|
227
|
+
this.#syncData();
|
|
228
|
+
const old = this.dataBlock;
|
|
229
|
+
if (old && this.#managed && !this.dataReady) throw new DOMException("Wait for the current data scope to become ready", "InvalidStateError");
|
|
230
|
+
const text = type === "application/json" ? value.replace(/</g, "\\u003c") : value;
|
|
231
|
+
if (/<\/script[\s/>]/i.test(text)) throw new TypeError("Data blocks cannot contain a closing script tag");
|
|
232
|
+
const block = old ?? this.host.ownerDocument.createElement("script");
|
|
233
|
+
for (const [name, value2] of Object.entries(attrs)) block.setAttribute(name, value2);
|
|
234
|
+
block.type = type;
|
|
235
|
+
block.textContent = text;
|
|
236
|
+
if (!old) this.host.append(block);
|
|
237
|
+
this.#publish("data");
|
|
238
|
+
}
|
|
239
|
+
connect() {
|
|
240
|
+
const { host } = this;
|
|
241
|
+
if (this.#observedDocument) return;
|
|
242
|
+
const doc = this.#observedDocument = host.ownerDocument;
|
|
243
|
+
const Observer = doc.defaultView?.MutationObserver ?? MutationObserver;
|
|
244
|
+
let followers = contextFollowers.get(doc);
|
|
245
|
+
if (!followers) {
|
|
246
|
+
const runtimes = /* @__PURE__ */ new Set();
|
|
247
|
+
const observer = new Observer(() => runtimes.forEach((runtime) => runtime.contextChanged()));
|
|
248
|
+
observer.observe(doc.documentElement, { attributes: true, attributeFilter: ["lang", "contenteditable"], subtree: true });
|
|
249
|
+
followers = { runtimes, observer };
|
|
250
|
+
contextFollowers.set(doc, followers);
|
|
251
|
+
}
|
|
252
|
+
followers.runtimes.add(this);
|
|
253
|
+
this.#language = void 0;
|
|
254
|
+
this.contextChanged();
|
|
255
|
+
const events = doc.defaultView?.EventTarget.prototype ?? EventTarget.prototype;
|
|
256
|
+
for (const type of dataEvents) events.addEventListener.call(host, type, this.#dataEvent);
|
|
257
|
+
this.#contentObserver = new Observer(() => this.#syncData());
|
|
258
|
+
this.#contentObserver.observe(host, { childList: true, characterData: true, subtree: true, attributes: true, attributeFilter: ["type", "src", "slot"] });
|
|
259
|
+
this.#block = void 0;
|
|
260
|
+
this.#syncData();
|
|
261
|
+
}
|
|
262
|
+
disconnect() {
|
|
263
|
+
const doc = this.#observedDocument;
|
|
264
|
+
if (!doc) return;
|
|
265
|
+
this.#observedDocument = void 0;
|
|
266
|
+
this.#contentObserver?.disconnect();
|
|
267
|
+
this.#contentObserver = void 0;
|
|
268
|
+
const events = doc.defaultView?.EventTarget.prototype ?? EventTarget.prototype;
|
|
269
|
+
for (const type of dataEvents) events.removeEventListener.call(this.host, type, this.#dataEvent);
|
|
270
|
+
this.#readyBlock = null;
|
|
271
|
+
const followers = contextFollowers.get(doc);
|
|
272
|
+
followers?.runtimes.delete(this);
|
|
273
|
+
if (followers && !followers.runtimes.size) {
|
|
274
|
+
followers.observer.disconnect();
|
|
275
|
+
contextFollowers.delete(doc);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
function withWidgetRuntime(Base) {
|
|
280
|
+
return class WidgetElement extends Base {
|
|
281
|
+
static widgetAttributes = [];
|
|
282
|
+
static get observedAttributes() {
|
|
283
|
+
return [.../* @__PURE__ */ new Set([...super.observedAttributes ?? [], ...widgetRuntimeAttributes, ...this.widgetAttributes])];
|
|
284
|
+
}
|
|
285
|
+
widgetRuntime = new WidgetRuntime(this, {
|
|
286
|
+
attributes: () => this.constructor.widgetAttributes
|
|
287
|
+
});
|
|
288
|
+
connectedCallback() {
|
|
289
|
+
this.widgetRuntime.connect();
|
|
290
|
+
super.connectedCallback?.();
|
|
291
|
+
}
|
|
292
|
+
disconnectedCallback() {
|
|
293
|
+
this.widgetRuntime.disconnect();
|
|
294
|
+
super.disconnectedCallback?.();
|
|
295
|
+
}
|
|
296
|
+
attributeChangedCallback(name, oldValue, value) {
|
|
297
|
+
if (Base.observedAttributes?.includes(name)) super.attributeChangedCallback?.(name, oldValue, value);
|
|
298
|
+
this.widgetRuntime.attributeChanged(name, oldValue, value);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
WidgetRuntime,
|
|
304
|
+
isDataBlockType,
|
|
305
|
+
widgetLanguage,
|
|
306
|
+
widgetRuntimeAttributes,
|
|
307
|
+
withWidgetRuntime
|
|
308
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import type {LocalizedText} from "./options"
|
|
2
|
+
|
|
3
|
+
export type * from "./options"
|
|
4
|
+
|
|
5
|
+
/** MIME type essences browsers execute as classic scripts. */
|
|
6
|
+
const javaScriptMimeTypes = new Set([
|
|
7
|
+
"application/ecmascript", "application/javascript", "application/x-ecmascript", "application/x-javascript",
|
|
8
|
+
"text/ecmascript", "text/javascript", "text/javascript1.0", "text/javascript1.1", "text/javascript1.2",
|
|
9
|
+
"text/javascript1.3", "text/javascript1.4", "text/javascript1.5", "text/jscript", "text/livescript",
|
|
10
|
+
"text/x-ecmascript", "text/x-javascript",
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
/** Whether a script `type` is an inert data block type, such as `application/json`. WebWriter keeps such `<script>` children of a widget as its data containers. */
|
|
14
|
+
export function isDataBlockType(type: string) {
|
|
15
|
+
const essence = type.split(";")[0].trim().toLowerCase()
|
|
16
|
+
return /^[^\s/]+\/[^\s/]+$/.test(essence) && !javaScriptMimeTypes.has(essence)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type WidgetDataMode = "individual" | "all" | "group" | "local"
|
|
20
|
+
export type WidgetDataContext = {block: HTMLScriptElement | null, mode: WidgetDataMode}
|
|
21
|
+
|
|
22
|
+
export type WidgetSnapshot = Readonly<{
|
|
23
|
+
editable: boolean
|
|
24
|
+
lang: string
|
|
25
|
+
standalone: string | null
|
|
26
|
+
attributes: Readonly<Record<string, string | null>>
|
|
27
|
+
data: string | null
|
|
28
|
+
dataType: string | undefined
|
|
29
|
+
dataBlock: HTMLScriptElement | null
|
|
30
|
+
dataSlot: string | null
|
|
31
|
+
dataMode: WidgetDataMode
|
|
32
|
+
dataReady: boolean
|
|
33
|
+
dataError: Error | null
|
|
34
|
+
}>
|
|
35
|
+
|
|
36
|
+
type WidgetRuntimeCallbacks = {
|
|
37
|
+
attributes?: () => readonly string[]
|
|
38
|
+
language?: () => string
|
|
39
|
+
editable?: () => boolean
|
|
40
|
+
update?: (name?: string, previous?: unknown) => void
|
|
41
|
+
dataChanged?: (value: string | null, context: WidgetDataContext) => void
|
|
42
|
+
dataPending?: (context: WidgetDataContext) => void
|
|
43
|
+
dataError?: (error: Error, context: WidgetDataContext) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ContextFollowers = {runtimes: Set<WidgetRuntime>, observer: MutationObserver}
|
|
47
|
+
const contextFollowers = new WeakMap<Document, ContextFollowers>()
|
|
48
|
+
const dataEvents = ["datapending", "datachange", "dataerror", "datarelease"] as const
|
|
49
|
+
export const widgetRuntimeAttributes: readonly string[] = ["contenteditable", "lang", "shared", "id", "standalone"]
|
|
50
|
+
|
|
51
|
+
export function widgetLanguage(host: HTMLElement) {
|
|
52
|
+
return host.closest("[lang]")?.getAttribute("lang") ?? ""
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Observes an authored custom element. Snapshots are read-only views of its DOM. */
|
|
56
|
+
export class WidgetRuntime {
|
|
57
|
+
constructor(readonly host: HTMLElement, private readonly callbacks: WidgetRuntimeCallbacks = {}) {}
|
|
58
|
+
|
|
59
|
+
#observedDocument?: Document
|
|
60
|
+
#language?: string
|
|
61
|
+
#editable?: boolean
|
|
62
|
+
#listeners = new Set<() => void>()
|
|
63
|
+
#snapshot?: WidgetSnapshot
|
|
64
|
+
|
|
65
|
+
get lang() { return this.callbacks.language?.() ?? widgetLanguage(this.host) }
|
|
66
|
+
get editable() { return this.callbacks.editable?.() ?? this.host.isContentEditable }
|
|
67
|
+
|
|
68
|
+
get standalone() { return this.host.getAttribute("standalone") }
|
|
69
|
+
set standalone(value: string | null) {
|
|
70
|
+
if(value === null) this.host.removeAttribute("standalone")
|
|
71
|
+
else this.host.setAttribute("standalone", value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The same snapshot object is returned until an observed value changes. */
|
|
75
|
+
readonly getSnapshot = (): WidgetSnapshot => {
|
|
76
|
+
const dataBlock = this.dataBlock
|
|
77
|
+
const previous = this.#snapshot
|
|
78
|
+
const attributes = Object.fromEntries((this.callbacks.attributes?.() ?? []).map(name => [name, this.host.getAttribute(name)]))
|
|
79
|
+
const sameAttributes = previous && Object.keys(attributes).length === Object.keys(previous.attributes).length
|
|
80
|
+
&& Object.entries(attributes).every(([name, value]) => previous.attributes[name] === value)
|
|
81
|
+
const next = {
|
|
82
|
+
editable: this.editable,
|
|
83
|
+
lang: this.lang,
|
|
84
|
+
standalone: this.standalone,
|
|
85
|
+
attributes: sameAttributes ? previous.attributes : Object.freeze(attributes),
|
|
86
|
+
data: dataBlock?.textContent ?? null,
|
|
87
|
+
dataType: this.#typeOf(dataBlock),
|
|
88
|
+
dataBlock,
|
|
89
|
+
dataSlot: dataBlock?.getAttribute("slot") ?? null,
|
|
90
|
+
dataMode: this.#mode,
|
|
91
|
+
dataReady: this.#isDataReady(dataBlock),
|
|
92
|
+
dataError: this.#error,
|
|
93
|
+
}
|
|
94
|
+
if(previous && Object.entries(next).every(([key, value]) => previous[key as keyof WidgetSnapshot] === value)) return previous
|
|
95
|
+
return this.#snapshot = Object.freeze(next)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Subscribe to host updates and return an unsubscribe function. */
|
|
99
|
+
readonly subscribe = (listener: () => void) => {
|
|
100
|
+
this.#listeners.add(listener)
|
|
101
|
+
return () => { this.#listeners.delete(listener) }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#publish(name?: string, previous?: unknown) {
|
|
105
|
+
this.callbacks.update?.(name, previous)
|
|
106
|
+
this.#listeners.forEach(listener => listener())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
attributeChanged(name: string, oldValue: string | null, value: string | null) {
|
|
110
|
+
if(oldValue === value) return
|
|
111
|
+
if(name === "lang" || name === "contenteditable") {
|
|
112
|
+
this.contextChanged()
|
|
113
|
+
if(this.callbacks.attributes?.().includes(name)) this.#publish(name, oldValue)
|
|
114
|
+
}
|
|
115
|
+
else if(name === "shared" || name === "id") {
|
|
116
|
+
this.#readyBlock = null
|
|
117
|
+
if(this.host.isConnected) this.#syncData()
|
|
118
|
+
else if(this.callbacks.attributes?.().includes(name)) this.#publish(name, oldValue)
|
|
119
|
+
}
|
|
120
|
+
else this.#publish(name, oldValue)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
contextChanged() {
|
|
124
|
+
const language = this.lang, editable = this.editable
|
|
125
|
+
if(language !== this.#language) {
|
|
126
|
+
const before = this.#language
|
|
127
|
+
this.#language = language
|
|
128
|
+
this.#publish("lang", before)
|
|
129
|
+
}
|
|
130
|
+
if(editable !== this.#editable) {
|
|
131
|
+
const before = this.#editable
|
|
132
|
+
this.#editable = editable
|
|
133
|
+
this.#publish("editable", before)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get dataBlock(): HTMLScriptElement | null {
|
|
138
|
+
const blocks = Array.from(this.host.children).filter((child): child is HTMLScriptElement =>
|
|
139
|
+
child.namespaceURI === "http://www.w3.org/1999/xhtml" && child.localName === "script"
|
|
140
|
+
&& isDataBlockType((child as HTMLScriptElement).type) && !child.hasAttribute("src"))
|
|
141
|
+
return blocks.find(block => this.#scoped(block)) ?? blocks[0] ?? null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#scoped(block: HTMLScriptElement | null) {
|
|
145
|
+
return !!block && block.getAttribute("slot") === "data"
|
|
146
|
+
&& ["application/json", "application/xml"].includes(block.type)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#scope(block: HTMLScriptElement | null) {
|
|
150
|
+
return JSON.stringify([this.host.ownerDocument.documentElement.id, this.host.id, this.host.getAttribute("shared"),
|
|
151
|
+
block?.type, block?.getAttribute("slot"),
|
|
152
|
+
...Array.from(this.host.childNodes).filter(node => node.nodeType === 7 && node.nodeName === "ww-grouping").map(node => node.nodeValue)])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get data() { return this.dataBlock?.textContent ?? null }
|
|
156
|
+
set data(value: string | null) { this.setData(this.dataType ?? "text/plain", value ?? "") }
|
|
157
|
+
get dataType() { return this.#typeOf(this.dataBlock) }
|
|
158
|
+
|
|
159
|
+
#typeOf(block: HTMLScriptElement | null) {
|
|
160
|
+
return block?.type ?? (this.host.constructor as {dataType?: string}).dataType
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#block: HTMLScriptElement | null | undefined
|
|
164
|
+
#bindingScope?: string
|
|
165
|
+
#readyBlock: HTMLScriptElement | null = null
|
|
166
|
+
#readyScope?: string
|
|
167
|
+
#managed = false
|
|
168
|
+
#lastText: string | null | undefined
|
|
169
|
+
#mode: WidgetDataMode = "local"
|
|
170
|
+
#syncing = false
|
|
171
|
+
#contentObserver?: MutationObserver
|
|
172
|
+
#error: Error | null = null
|
|
173
|
+
|
|
174
|
+
/** False while the current participant scope is loading or unavailable. */
|
|
175
|
+
get dataReady() { return this.#isDataReady(this.dataBlock) }
|
|
176
|
+
|
|
177
|
+
#isDataReady(block: HTMLScriptElement | null) {
|
|
178
|
+
if(!this.host.isConnected) return false
|
|
179
|
+
if(!this.#scoped(block)) return true
|
|
180
|
+
const scope = this.#scope(block)
|
|
181
|
+
return block === this.#block && this.#bindingScope === scope
|
|
182
|
+
&& (!this.#managed || block === this.#readyBlock && this.#readyScope === scope)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#context(block = this.dataBlock): WidgetDataContext { return {block, mode: this.#mode} }
|
|
186
|
+
|
|
187
|
+
#syncData() {
|
|
188
|
+
if(!this.host.isConnected || this.#syncing) return
|
|
189
|
+
this.#syncing = true
|
|
190
|
+
try {
|
|
191
|
+
const block = this.dataBlock, scope = this.#scope(block)
|
|
192
|
+
if(block !== this.#block || scope !== this.#bindingScope) {
|
|
193
|
+
this.#block = block
|
|
194
|
+
this.#bindingScope = scope
|
|
195
|
+
this.#readyBlock = null
|
|
196
|
+
this.#error = null
|
|
197
|
+
this.#managed = false
|
|
198
|
+
this.#mode = "local"
|
|
199
|
+
if(this.#scoped(block)) {
|
|
200
|
+
const Request = this.host.ownerDocument.defaultView?.CustomEvent ?? CustomEvent
|
|
201
|
+
// Cancellation acknowledges ownership; an unhandled request means a
|
|
202
|
+
// standalone document using ordinary local authored data.
|
|
203
|
+
const request = new Request("datarequest", {bubbles: true, cancelable: true})
|
|
204
|
+
if(!block!.dispatchEvent(request)) this.#managed = true
|
|
205
|
+
}
|
|
206
|
+
if(!this.#managed) this.#acceptData("local")
|
|
207
|
+
}
|
|
208
|
+
else if(!this.#managed && this.data !== this.#lastText) this.#acceptData("local")
|
|
209
|
+
this.#publish("data")
|
|
210
|
+
}
|
|
211
|
+
finally { this.#syncing = false }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#acceptData(mode: WidgetDataMode) {
|
|
215
|
+
this.#mode = mode
|
|
216
|
+
this.#readyBlock = this.dataBlock
|
|
217
|
+
this.#readyScope = this.#scope(this.#readyBlock)
|
|
218
|
+
this.#lastText = this.data
|
|
219
|
+
this.#error = null
|
|
220
|
+
this.callbacks.dataChanged?.(this.data, this.#context())
|
|
221
|
+
this.#publish("data")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#dataEvent = (event: Event) => {
|
|
225
|
+
const block = this.dataBlock
|
|
226
|
+
if(!this.host.isConnected || !block || event.target !== block || !this.#scoped(block)) return
|
|
227
|
+
if(event.type === "datarelease") {
|
|
228
|
+
this.#block = undefined
|
|
229
|
+
this.#syncData()
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
const detail = (event as CustomEvent<{mode?: WidgetDataMode, message?: string}>).detail
|
|
233
|
+
this.#managed = true
|
|
234
|
+
this.#block = block
|
|
235
|
+
this.#bindingScope = this.#scope(block)
|
|
236
|
+
if(detail?.mode) this.#mode = detail.mode
|
|
237
|
+
else if(this.#mode === "local") this.#mode = !this.host.hasAttribute("shared") ? "individual" : this.host.getAttribute("shared") === "group" ? "group" : "all"
|
|
238
|
+
if(event.type === "datachange") this.#acceptData(this.#mode)
|
|
239
|
+
else {
|
|
240
|
+
if(event.type === "datapending") {
|
|
241
|
+
this.#readyBlock = null
|
|
242
|
+
this.#error = null
|
|
243
|
+
this.callbacks.dataPending?.(this.#context(block))
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
this.#error = new Error(detail?.message ?? "Widget data is unavailable")
|
|
247
|
+
this.callbacks.dataError?.(this.#error, this.#context(block))
|
|
248
|
+
}
|
|
249
|
+
this.#publish("data")
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Updates a data block in place, preserving its identity and attributes.
|
|
254
|
+
* A managed block cannot be written until its current scope is ready. */
|
|
255
|
+
setData(type: string, value: string, attrs: LocalizedText = {}) {
|
|
256
|
+
if(!isDataBlockType(type)) throw new TypeError(`'${type}' is not a data block type`)
|
|
257
|
+
if(Object.keys(attrs).some(name => name.toLowerCase() === "src")) throw new TypeError("Data blocks cannot have src")
|
|
258
|
+
this.#syncData()
|
|
259
|
+
const old = this.dataBlock
|
|
260
|
+
if(old && this.#managed && !this.dataReady) throw new DOMException("Wait for the current data scope to become ready", "InvalidStateError")
|
|
261
|
+
const text = type === "application/json" ? value.replace(/</g, "\\u003c") : value
|
|
262
|
+
if(/<\/script[\s/>]/i.test(text)) throw new TypeError("Data blocks cannot contain a closing script tag")
|
|
263
|
+
const block = old ?? this.host.ownerDocument.createElement("script")
|
|
264
|
+
for(const [name, value] of Object.entries(attrs)) block.setAttribute(name, value)
|
|
265
|
+
block.type = type
|
|
266
|
+
block.textContent = text
|
|
267
|
+
if(!old) this.host.append(block)
|
|
268
|
+
this.#publish("data")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
connect() {
|
|
272
|
+
const {host} = this
|
|
273
|
+
if(this.#observedDocument) return
|
|
274
|
+
const doc = this.#observedDocument = host.ownerDocument
|
|
275
|
+
const Observer = doc.defaultView?.MutationObserver ?? MutationObserver
|
|
276
|
+
let followers = contextFollowers.get(doc)
|
|
277
|
+
if(!followers) {
|
|
278
|
+
const runtimes = new Set<WidgetRuntime>()
|
|
279
|
+
const observer = new Observer(() => runtimes.forEach(runtime => runtime.contextChanged()))
|
|
280
|
+
observer.observe(doc.documentElement, {attributes: true, attributeFilter: ["lang", "contenteditable"], subtree: true})
|
|
281
|
+
followers = {runtimes, observer}
|
|
282
|
+
contextFollowers.set(doc, followers)
|
|
283
|
+
}
|
|
284
|
+
followers.runtimes.add(this)
|
|
285
|
+
this.#language = undefined
|
|
286
|
+
this.contextChanged()
|
|
287
|
+
// Listen to native DOM events on hosts that also expose framework events.
|
|
288
|
+
const events = doc.defaultView?.EventTarget.prototype ?? EventTarget.prototype
|
|
289
|
+
for(const type of dataEvents) events.addEventListener.call(host, type, this.#dataEvent)
|
|
290
|
+
this.#contentObserver = new Observer(() => this.#syncData())
|
|
291
|
+
this.#contentObserver.observe(host, {childList: true, characterData: true, subtree: true, attributes: true, attributeFilter: ["type", "src", "slot"]})
|
|
292
|
+
this.#block = undefined
|
|
293
|
+
this.#syncData()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
disconnect() {
|
|
297
|
+
const doc = this.#observedDocument
|
|
298
|
+
if(!doc) return
|
|
299
|
+
this.#observedDocument = undefined
|
|
300
|
+
this.#contentObserver?.disconnect()
|
|
301
|
+
this.#contentObserver = undefined
|
|
302
|
+
const events = doc.defaultView?.EventTarget.prototype ?? EventTarget.prototype
|
|
303
|
+
for(const type of dataEvents) events.removeEventListener.call(this.host, type, this.#dataEvent)
|
|
304
|
+
this.#readyBlock = null
|
|
305
|
+
const followers = contextFollowers.get(doc)
|
|
306
|
+
followers?.runtimes.delete(this)
|
|
307
|
+
if(followers && !followers.runtimes.size) {
|
|
308
|
+
followers.observer.disconnect()
|
|
309
|
+
contextFollowers.delete(doc)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
type Lifecycle = {
|
|
315
|
+
connectedCallback?(): void
|
|
316
|
+
disconnectedCallback?(): void
|
|
317
|
+
attributeChangedCallback?(name: string, oldValue: string | null, value: string | null): void
|
|
318
|
+
}
|
|
319
|
+
type WidgetElementConstructor = (new (...args: any[]) => HTMLElement & Lifecycle) & {observedAttributes?: readonly string[]}
|
|
320
|
+
export type WidgetHost = HTMLElement & {readonly widgetRuntime: WidgetRuntime}
|
|
321
|
+
|
|
322
|
+
/** Add the runtime to a custom-element class before registering it. */
|
|
323
|
+
export function withWidgetRuntime<T extends WidgetElementConstructor>(Base: T) {
|
|
324
|
+
return class WidgetElement extends Base {
|
|
325
|
+
static readonly widgetAttributes: readonly string[] = []
|
|
326
|
+
static get observedAttributes(): string[] {
|
|
327
|
+
return [...new Set([...(super.observedAttributes ?? []), ...widgetRuntimeAttributes, ...this.widgetAttributes])]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
readonly widgetRuntime = new WidgetRuntime(this, {
|
|
331
|
+
attributes: () => (this.constructor as typeof WidgetElement).widgetAttributes,
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
connectedCallback() {
|
|
335
|
+
this.widgetRuntime.connect()
|
|
336
|
+
super.connectedCallback?.()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
disconnectedCallback() {
|
|
340
|
+
this.widgetRuntime.disconnect()
|
|
341
|
+
super.disconnectedCallback?.()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
attributeChangedCallback(name: string, oldValue: string | null, value: string | null) {
|
|
345
|
+
if(Base.observedAttributes?.includes(name)) super.attributeChangedCallback?.(name, oldValue, value)
|
|
346
|
+
this.widgetRuntime.attributeChanged(name, oldValue, value)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
package/options.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export type LocalizedText = string | Record<string, string>
|
|
2
|
+
|
|
3
|
+
export interface OptionAvailability {
|
|
4
|
+
hidden?: boolean
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
disabledReason?: LocalizedText
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OptionGroupDeclaration {
|
|
10
|
+
label?: LocalizedText
|
|
11
|
+
description?: LocalizedText
|
|
12
|
+
collapsible?: boolean
|
|
13
|
+
collapsed?: boolean
|
|
14
|
+
advanced?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BaseOptionDeclaration<TypeHint=any> extends OptionAvailability {
|
|
18
|
+
attribute?: boolean | string
|
|
19
|
+
group?: string
|
|
20
|
+
readOnly?: boolean
|
|
21
|
+
unit?: string
|
|
22
|
+
type?: TypeHint
|
|
23
|
+
label?: LocalizedText
|
|
24
|
+
placeholder?: LocalizedText
|
|
25
|
+
description?: LocalizedText
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BooleanOptionDeclaration extends BaseOptionDeclaration<BooleanConstructor | "boolean"> {
|
|
29
|
+
control?: "checkbox" | "switch"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A boolean widget capability shown under Edit in element distribution permissions. */
|
|
33
|
+
export interface PermissionDeclaration extends Pick<BooleanOptionDeclaration, "label" | "description" | "hidden" | "disabled" | "disabledReason" | "readOnly"> {
|
|
34
|
+
attribute?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface InputOptionDeclaration<TypeHint=any> extends BaseOptionDeclaration<TypeHint> {
|
|
38
|
+
pattern?: string
|
|
39
|
+
minlength?: number
|
|
40
|
+
maxlength?: number
|
|
41
|
+
autocapitalize?: "none" | "characters" | "words" | "sentences"
|
|
42
|
+
spellcheck?: "true" | "false"
|
|
43
|
+
autocomplete?: HTMLInputElement["autocomplete"]
|
|
44
|
+
inputmode?: "none" | "text" | "decimal" | "numeric" | "tel" | "search" | "email" | "url"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface StringOptionDeclaration extends InputOptionDeclaration<StringConstructor | "string"> {
|
|
48
|
+
multiline?: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface NumberOptionDeclaration extends InputOptionDeclaration<NumberConstructor | "number"> {
|
|
52
|
+
control?: "input" | "range" | "duration"
|
|
53
|
+
steps?: number[]
|
|
54
|
+
min?: number
|
|
55
|
+
max?: number
|
|
56
|
+
step?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DateOptionDeclaration extends InputOptionDeclaration<"date"> {
|
|
60
|
+
min?: number
|
|
61
|
+
max?: number
|
|
62
|
+
step?: number
|
|
63
|
+
}
|
|
64
|
+
export interface DatetimeLocalOptionDeclaration extends InputOptionDeclaration<"datetime-local"> {
|
|
65
|
+
min?: number
|
|
66
|
+
max?: number
|
|
67
|
+
step?: number
|
|
68
|
+
}
|
|
69
|
+
export interface EmailOptionDeclaration extends InputOptionDeclaration<"email"> {}
|
|
70
|
+
export interface PasswordOptionDeclaration extends InputOptionDeclaration<"password"> {}
|
|
71
|
+
export interface TelOptionDeclaration extends InputOptionDeclaration<"tel"> {}
|
|
72
|
+
export interface TimeOptionDeclaration extends InputOptionDeclaration<"time"> {}
|
|
73
|
+
export interface UrlOptionDeclaration extends InputOptionDeclaration<"url"> {}
|
|
74
|
+
|
|
75
|
+
export interface ObjectOptionDeclaration extends BaseOptionDeclaration<ObjectConstructor | "object" | ArrayConstructor | "array"> {}
|
|
76
|
+
|
|
77
|
+
export interface ColorOptionDeclaration extends BaseOptionDeclaration<"color"> {
|
|
78
|
+
swatches?: string[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type SelectOptionChoice = {
|
|
82
|
+
value: string
|
|
83
|
+
label?: LocalizedText
|
|
84
|
+
description?: LocalizedText
|
|
85
|
+
children?: never
|
|
86
|
+
} | {
|
|
87
|
+
label?: LocalizedText
|
|
88
|
+
children: SelectOptionChoice[]
|
|
89
|
+
value?: never
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ReferenceOptionDeclaration extends BaseOptionDeclaration<"reference"> {
|
|
93
|
+
selector?: string
|
|
94
|
+
excludeSelf?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SelectOptionDeclaration extends BaseOptionDeclaration<"select"> {
|
|
98
|
+
control?: "select" | "radio" | "segmented" | "tree"
|
|
99
|
+
multiple?: boolean
|
|
100
|
+
options?: SelectOptionChoice[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type OptionDeclaration =
|
|
104
|
+
| BooleanOptionDeclaration
|
|
105
|
+
| StringOptionDeclaration
|
|
106
|
+
| NumberOptionDeclaration
|
|
107
|
+
| DateOptionDeclaration
|
|
108
|
+
| DatetimeLocalOptionDeclaration
|
|
109
|
+
| EmailOptionDeclaration
|
|
110
|
+
| PasswordOptionDeclaration
|
|
111
|
+
| TelOptionDeclaration
|
|
112
|
+
| TimeOptionDeclaration
|
|
113
|
+
| UrlOptionDeclaration
|
|
114
|
+
| ObjectOptionDeclaration
|
|
115
|
+
| ColorOptionDeclaration
|
|
116
|
+
| SelectOptionDeclaration
|
|
117
|
+
| ReferenceOptionDeclaration
|
|
118
|
+
|
|
119
|
+
export interface ActionDeclaration extends OptionAvailability {
|
|
120
|
+
/** Native chooser input; the action method receives transient File[]. */
|
|
121
|
+
input?: {type: "file", accept?: string, multiple?: boolean}
|
|
122
|
+
group?: string
|
|
123
|
+
/** Joins adjacent visible actions with the same ID within an option group. */
|
|
124
|
+
buttonGroup?: string
|
|
125
|
+
icon?: string
|
|
126
|
+
intent?: "default" | "danger"
|
|
127
|
+
confirm?: LocalizedText
|
|
128
|
+
label?: LocalizedText
|
|
129
|
+
placeholder?: LocalizedText
|
|
130
|
+
description?: LocalizedText
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webwriter/widget",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DOM-backed runtime for WebWriter widgets",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./index.ts",
|
|
13
|
+
"default": "./index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"index.js",
|
|
18
|
+
"index.ts",
|
|
19
|
+
"options.ts"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "esbuild ./index.ts --bundle --target=es2022 --format=esm --outfile=index.js",
|
|
23
|
+
"prepack": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"esbuild": "^0.25.4"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/README.md
DELETED