@rathnasgala/theme 0.0.16 → 0.0.17
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/package.json +1 -1
- package/payload/.gala/managed-files.json +13 -6
- package/payload/lib/performance-budget.js +10 -1
- package/payload/src/_includes/components/ui.njk +5 -6
- package/payload/src/_includes/layouts/base.njk +7 -6
- package/payload/src/assets/engagement-comments.js +264 -0
- package/payload/src/assets/engagement-transport.js +73 -0
- package/payload/src/assets/interactions.js +12 -196
- package/payload/src/assets/theme.css +3 -1
- package/payload/src/assets/vendor/hooks.js +2 -0
- package/payload/src/assets/vendor/htm.js +2 -0
- package/payload/src/assets/vendor/preact.js +2 -0
- package/payload/src/assets/vendor/signals-core.js +2 -0
- package/payload/src/assets/vendor/signals.js +2 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"themePackage": {
|
|
4
4
|
"name": "@rathnasgala/theme",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.17",
|
|
6
6
|
"availableDesignThemes": [
|
|
7
7
|
"editorial"
|
|
8
8
|
],
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"eleventy.config.js": "efe587447e3dc374a220c7fa037fdf2fffc02d09ba43a4d2b96d6c6aae0e3325",
|
|
15
15
|
"lib/build-manifest.js": "88949f0112d683ca809bb67ca969e15634792423559a67bc79a534cd807e5535",
|
|
16
16
|
"lib/engagement-snapshot.js": "298c11b1ab135a3a98a4e31b6320ab60dfd7fda6a9d9053df37d86282a5a6687",
|
|
17
|
-
"lib/performance-budget.js": "
|
|
17
|
+
"lib/performance-budget.js": "c279622dfde237cd6e3c64775c91d770a0e1f54f5f94257ccc95d9a44de2c4c9",
|
|
18
18
|
"lib/provider-fixtures/embeds.v1.json": "5f26909fb433135321ddf7fdd14e45aeb702805c88f5676941166c5735143ee2",
|
|
19
19
|
"lib/provider-fixtures/share-intents.v1.json": "66a8eedfcff711253ee45ceb94b53f7d5a6be3ddbbb3d1f0d4e31a114326cc0a",
|
|
20
20
|
"lib/publication-state.js": "1b59c29a18bbb1193bddea898dac3a4fb832c7bc4751263be086f3d64392dea0",
|
|
@@ -30,18 +30,25 @@
|
|
|
30
30
|
"src/_data/feedLinks.js": "06c93352ebd888acb431a1c9a8be6ecff757895468e9f2d336761038751c2d5e",
|
|
31
31
|
"src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
|
|
32
32
|
"src/_data/site.js": "d9c001f4cc12a34027dd92303530b11b5402d47613459bb997e22c4bcbea326d",
|
|
33
|
-
"src/_includes/components/ui.njk": "
|
|
34
|
-
"src/_includes/layouts/base.njk": "
|
|
33
|
+
"src/_includes/components/ui.njk": "23eb647d8658868449ec6bb809e965a85302124a97639f94d5d28af72df56e4c",
|
|
34
|
+
"src/_includes/layouts/base.njk": "f504b5a80020b90eb47d59db5e626a4554e2f2d4ef64b3b25c98869830581ebe",
|
|
35
35
|
"src/_includes/layouts/post.njk": "1accd2832064ad19ee7bf18daf5fa1e60569f32c9b7a547934a6361f747d7eb7",
|
|
36
36
|
"src/assets/embed-codepen.svg": "e9def697f38c466fb2b6652535c4c814e8cbbbefc6e052a69f335aeb27e8356b",
|
|
37
37
|
"src/assets/embed-gist.svg": "fcd9bc2bf29a5c118492b853c0a01390b7539c38ddb985da3c2a6e2f246a39f7",
|
|
38
38
|
"src/assets/embed-x.svg": "0ae74edc6790ec7e933067fe5392bce756e93fa7ad71caacf1c1ca99fa4ec9b6",
|
|
39
39
|
"src/assets/embed-youtube.svg": "10d192f2b14b67664fea88c8cf246321d20190def51d59fad5762e552219b067",
|
|
40
|
-
"src/assets/
|
|
40
|
+
"src/assets/engagement-comments.js": "b20d9268c9d908b4453a42188eaca25349f25534fb04a9293c9e688a982cd8f8",
|
|
41
|
+
"src/assets/engagement-transport.js": "4f9ca0cd669bf8abc25f2909104e3e4b231657ef97d36a17d06e53454d711786",
|
|
42
|
+
"src/assets/interactions.js": "dd4e343505e528d89596ca67db679b52c989e022f93399193e4377db01fc8a08",
|
|
41
43
|
"src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
|
|
42
44
|
"src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
|
|
43
45
|
"src/assets/theme-mode.js": "3797c84dbaa090b5b21427a31928b5dd97f13d04d99833f5e1e74c6b50e01dcc",
|
|
44
|
-
"src/assets/theme.css": "
|
|
46
|
+
"src/assets/theme.css": "94839833f93997415f570041a691da0c96c27992f897e116948077a8b25e8dfa",
|
|
47
|
+
"src/assets/vendor/hooks.js": "ae6ec58592683ca64fa4d65b20d40b103df62a673bcc4712515cf165950b07f3",
|
|
48
|
+
"src/assets/vendor/htm.js": "e4dac9bc40a6f2ec590413d787ae30bbcfdfcde4f28971ee65d9578271b7aad4",
|
|
49
|
+
"src/assets/vendor/preact.js": "712517dd2a42c3c22b5ab0264ad7e9a2b1e8be6cf97fa00dbedd98203617e616",
|
|
50
|
+
"src/assets/vendor/signals-core.js": "1e10560fec5118c9e35eb312c869fe51f4870424766ef2ec0f7cd069bd898c8b",
|
|
51
|
+
"src/assets/vendor/signals.js": "717a88151bacf4f8b79b528a96d71be20a133fb3345230c1146929903cecfd19",
|
|
45
52
|
"src/cname.11ty.js": "206fc224df91c569e18312158e7f69803061f2952513f501ada54bd06c4f8006",
|
|
46
53
|
"src/contact.njk": "f4d74d038781e15af0acef571901c91e7753a2e21b6bef617429585698839a34",
|
|
47
54
|
"src/feed.11ty.js": "009bd2384e08e02a9405683bab1653bab3aeaec16accb0fd01ba69b24c777da9",
|
|
@@ -24,7 +24,16 @@ function assertWithin(actual, maximum, label) {
|
|
|
24
24
|
export async function enforcePerformanceBudgets({ outputDirectory, budgets }) {
|
|
25
25
|
const output = path.resolve(outputDirectory);
|
|
26
26
|
const files = await filesBelow(output);
|
|
27
|
-
|
|
27
|
+
/*
|
|
28
|
+
* Everything under `assets`, nested included. This used to compare `path.dirname` against the
|
|
29
|
+
* assets directory exactly, so anything in a subdirectory was invisible to the budget — the
|
|
30
|
+
* moment the reader runtime moved into `assets/vendor` it would have stopped being counted, and
|
|
31
|
+
* a budget that quietly ignores two thirds of the JavaScript it is meant to guard is worse than
|
|
32
|
+
* having none.
|
|
33
|
+
*/
|
|
34
|
+
const assets = path.join(output, 'assets');
|
|
35
|
+
const managedAssets = files.filter(
|
|
36
|
+
(file) => file === assets || file.startsWith(assets + path.sep));
|
|
28
37
|
const javascriptBytes = await totalBytes(managedAssets.filter((file) => file.endsWith('.js')));
|
|
29
38
|
const cssBytes = await totalBytes(managedAssets.filter((file) => file.endsWith('.css')));
|
|
30
39
|
assertWithin(javascriptBytes, budgets.managedJavaScriptBytes, 'Managed JavaScript');
|
|
@@ -33,13 +33,12 @@
|
|
|
33
33
|
] %}<button type="button" data-reaction="{{ reaction.value }}" aria-pressed="false">{{ reaction.label }}</button>{% endfor %}
|
|
34
34
|
</div>
|
|
35
35
|
<button type="button" data-follow-article aria-pressed="false">Follow article</button>
|
|
36
|
-
<form data-comment-create>
|
|
37
|
-
<label>Comment<textarea name="body" maxlength="5000" required></textarea></label>
|
|
38
|
-
<p data-comment-reply-context hidden></p>
|
|
39
|
-
<button type="submit">Post comment</button>
|
|
40
|
-
<button type="button" data-cancel-reply hidden>Cancel reply</button>
|
|
41
|
-
</form>
|
|
42
36
|
</section>
|
|
37
|
+
{# The conversation is an island: it owns its own state, because a thread five deep with a page
|
|
38
|
+
cursor, an open reply form and an edit in progress is state, and hand-written DOM kept losing
|
|
39
|
+
track of it. Nothing here is needed by a search engine — comments have always arrived from the
|
|
40
|
+
API after load — so rendering them on the client costs nothing the article cares about. #}
|
|
41
|
+
<div data-gala-comments></div>
|
|
43
42
|
<output data-engagement-status aria-live="polite"></output>
|
|
44
43
|
</section>
|
|
45
44
|
{% endmacro %}
|
|
@@ -27,29 +27,30 @@
|
|
|
27
27
|
<script src="{{ '/assets/theme-mode.js' | url }}" defer></script>
|
|
28
28
|
<script src="{{ '/assets/preferences.js' | url }}" defer></script>
|
|
29
29
|
<script src="{{ '/assets/interactions.js' | url }}" defer></script>
|
|
30
|
+
<script type="module" src="{{ '/assets/engagement-comments.js' | url }}"></script>
|
|
30
31
|
<script src="{{ '/assets/search.js' | url }}" defer></script>
|
|
31
32
|
</head>
|
|
32
33
|
<body>
|
|
33
34
|
<header class="gala-site-header">
|
|
34
35
|
<a class="gala-icon-control" href="{{ '/' | url }}" aria-label="Home" title="Home">
|
|
35
|
-
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M3
|
|
36
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.2 12 3.5l9 6.7"/><path d="M5.4 8.9V19a1.4 1.4 0 0 0 1.4 1.4h3.1v-5.1h4.2v5.1h3.1A1.4 1.4 0 0 0 18.6 19V8.9"/></svg>
|
|
36
37
|
</a>
|
|
37
38
|
<nav class="gala-site-header__actions" aria-label="Site controls">
|
|
38
39
|
{% if site.contact.enabled %}<a class="gala-icon-control" href="{{ '/contact/' | url }}" aria-label="Contact author" title="Contact">
|
|
39
|
-
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="
|
|
40
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5.4" width="18" height="13.2" rx="2.2"/><path d="m3.9 7.3 7.2 5.1a1.6 1.6 0 0 0 1.8 0l7.2-5.1"/></svg>
|
|
40
41
|
</a>{% endif %}
|
|
41
42
|
<button class="gala-icon-control" type="button" data-theme-mode-toggle aria-label="Color mode: system. Activate for light." title="Appearance">
|
|
42
|
-
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="
|
|
43
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20.1 13.6A8.2 8.2 0 1 1 10.4 3.9a6.4 6.4 0 0 0 9.7 9.7Z"/></svg>
|
|
43
44
|
<span class="gala-visually-hidden" data-theme-mode-label>Theme: system</span>
|
|
44
45
|
</button>
|
|
45
46
|
<button class="gala-icon-control" type="button" data-open-dialog="gala-settings-dialog" aria-label="Settings" title="Settings">
|
|
46
|
-
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="
|
|
47
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2.9"/><path d="M19.3 14.6a1.5 1.5 0 0 0 .3 1.7l.1.1a1.8 1.8 0 1 1-2.6 2.6l-.1-.1a1.5 1.5 0 0 0-2.5 1.1v.3a1.8 1.8 0 0 1-3.6 0v-.2a1.5 1.5 0 0 0-2.6-1.1l-.1.1a1.8 1.8 0 1 1-2.6-2.6l.1-.1a1.5 1.5 0 0 0-1.1-2.5h-.3a1.8 1.8 0 0 1 0-3.6h.2a1.5 1.5 0 0 0 1.1-2.6l-.1-.1a1.8 1.8 0 1 1 2.6-2.6l.1.1a1.5 1.5 0 0 0 1.7.3h.1a1.5 1.5 0 0 0 .9-1.4v-.3a1.8 1.8 0 0 1 3.6 0v.2a1.5 1.5 0 0 0 2.5 1.1l.1-.1a1.8 1.8 0 1 1 2.6 2.6l-.1.1a1.5 1.5 0 0 0-.3 1.7v.1a1.5 1.5 0 0 0 1.4.9h.3a1.8 1.8 0 0 1 0 3.6h-.2a1.5 1.5 0 0 0-1.4.9Z"/></svg>
|
|
47
48
|
</button>
|
|
48
49
|
<button class="gala-icon-control" type="button" data-open-dialog="gala-search-dialog" aria-label="Search" title="Search">
|
|
49
|
-
<svg aria-hidden="true" viewBox="0 0 24 24"
|
|
50
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20.3 20.3-4.4-4.4"/></svg>
|
|
50
51
|
</button>
|
|
51
52
|
<button class="gala-icon-control" type="button" data-open-dialog="gala-account-dialog" data-user-control aria-label="Sign in or view account" title="Account">
|
|
52
|
-
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="
|
|
53
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 20.4v-1.7a3.9 3.9 0 0 0-3.9-3.9H8.9A3.9 3.9 0 0 0 5 18.7v1.7"/><circle cx="12" cy="7.6" r="3.9"/></svg>
|
|
53
54
|
</button>
|
|
54
55
|
</nav>
|
|
55
56
|
</header>
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation under an article.
|
|
3
|
+
*
|
|
4
|
+
* Built as an island rather than by hand-writing DOM, because the state here is real: a thread
|
|
5
|
+
* five deep, a page of roots with more behind a cursor, a reply form belonging to one comment, an
|
|
6
|
+
* edit in progress, and a reader who may sign in halfway through. The imperative version rebuilt
|
|
7
|
+
* the whole region from a fresh fetch after every write, which is how a posted comment could take
|
|
8
|
+
* a browser cache's word for it and not appear at all — and a comment that is not rendered has no
|
|
9
|
+
* Reply button, so the thread died with it.
|
|
10
|
+
*
|
|
11
|
+
* The article itself is never touched. Eleventy renders the post and everything search engines
|
|
12
|
+
* read; this mounts only into the comments container, which was always filled from the API and so
|
|
13
|
+
* has nothing to lose by being rendered here.
|
|
14
|
+
*/
|
|
15
|
+
import { h, render } from './vendor/preact.js';
|
|
16
|
+
import { useCallback, useEffect, useMemo, useState } from './vendor/hooks.js';
|
|
17
|
+
import htm from './vendor/htm.js';
|
|
18
|
+
// Importing the signals integration is what lets a component re-render when it reads `.value`.
|
|
19
|
+
import './vendor/signals.js';
|
|
20
|
+
import {
|
|
21
|
+
engagementErrorMessage, requestSignIn, sendEngagementWrite, sessionUser,
|
|
22
|
+
} from './engagement-transport.js';
|
|
23
|
+
|
|
24
|
+
const html = htm.bind(h);
|
|
25
|
+
|
|
26
|
+
/** The server's own limit. A reply deeper than this is refused, so it is not offered. */
|
|
27
|
+
const MAXIMUM_DEPTH = 5;
|
|
28
|
+
|
|
29
|
+
const isoTime = (value) => {
|
|
30
|
+
const at = Date.parse(value);
|
|
31
|
+
return Number.isFinite(at) ? new Date(at).toLocaleDateString() : '';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Roots in order, each followed by its own descendants, from one flat API page. */
|
|
35
|
+
function toThreads(items) {
|
|
36
|
+
const byParent = new Map();
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
const key = item.parentCommentId ?? '';
|
|
39
|
+
if (!byParent.has(key)) byParent.set(key, []);
|
|
40
|
+
byParent.get(key).push(item);
|
|
41
|
+
}
|
|
42
|
+
const build = (parentId, depth) => (byParent.get(parentId) ?? []).map((comment) => ({
|
|
43
|
+
...comment,
|
|
44
|
+
depth,
|
|
45
|
+
replies: build(comment.commentId, depth + 1),
|
|
46
|
+
}));
|
|
47
|
+
return build('', 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Comment({ comment, reader, onReply, onEdit, onDelete, busy }) {
|
|
51
|
+
const [replying, setReplying] = useState(false);
|
|
52
|
+
const [editing, setEditing] = useState(false);
|
|
53
|
+
const [draft, setDraft] = useState('');
|
|
54
|
+
const mine = !comment.deleted && reader && comment.author?.userId === reader.id;
|
|
55
|
+
const canReply = !comment.deleted && comment.depth < MAXIMUM_DEPTH;
|
|
56
|
+
|
|
57
|
+
const submitReply = async (event) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
if (!draft.trim()) return;
|
|
60
|
+
const posted = await onReply(comment.commentId, draft.trim());
|
|
61
|
+
if (posted) { setDraft(''); setReplying(false); }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const submitEdit = async (event) => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
if (!draft.trim()) return;
|
|
67
|
+
const saved = await onEdit(comment.commentId, draft.trim());
|
|
68
|
+
if (saved) setEditing(false);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return html`
|
|
72
|
+
<li class="gala-comment" data-comment-id=${comment.commentId} data-depth=${comment.depth}>
|
|
73
|
+
<p class="gala-comment__meta">
|
|
74
|
+
<strong>${comment.author?.displayName ?? '[deleted]'}</strong>
|
|
75
|
+
${comment.createdAt && html`<time datetime=${comment.createdAt}>${isoTime(comment.createdAt)}</time>`}
|
|
76
|
+
${comment.editedAt && html`<span class="gala-comment__edited">edited</span>`}
|
|
77
|
+
${comment.pending && html`<span class="gala-comment__pending">sending…</span>`}
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
${editing
|
|
81
|
+
? html`<form class="gala-comment__form" onSubmit=${submitEdit}>
|
|
82
|
+
<label class="gala-visually-hidden" for=${`edit-${comment.commentId}`}>Edit your comment</label>
|
|
83
|
+
<textarea id=${`edit-${comment.commentId}`} rows="3" value=${draft}
|
|
84
|
+
onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
85
|
+
<div class="gala-comment__controls">
|
|
86
|
+
<button type="submit" disabled=${busy}>Save</button>
|
|
87
|
+
<button type="button" onClick=${() => setEditing(false)}>Cancel</button>
|
|
88
|
+
</div>
|
|
89
|
+
</form>`
|
|
90
|
+
: html`<p class="gala-comment__body">${comment.deleted ? '[deleted]' : comment.body}</p>`}
|
|
91
|
+
|
|
92
|
+
${!comment.deleted && html`
|
|
93
|
+
<div class="gala-comment-actions">
|
|
94
|
+
${canReply && html`<button type="button" data-reply-comment=${comment.commentId}
|
|
95
|
+
onClick=${() => { setReplying((open) => !open); setDraft(''); }}>Reply</button>`}
|
|
96
|
+
${mine && html`<button type="button" data-edit-comment=${comment.commentId}
|
|
97
|
+
onClick=${() => { setEditing(true); setDraft(comment.body ?? ''); }}>Edit</button>`}
|
|
98
|
+
${mine && html`<button type="button" data-delete-comment=${comment.commentId}
|
|
99
|
+
onClick=${() => onDelete(comment.commentId)} disabled=${busy}>Delete</button>`}
|
|
100
|
+
</div>`}
|
|
101
|
+
|
|
102
|
+
${replying && html`
|
|
103
|
+
<form class="gala-comment__form" onSubmit=${submitReply}>
|
|
104
|
+
<label class="gala-visually-hidden" for=${`reply-${comment.commentId}`}>
|
|
105
|
+
Reply to ${comment.author?.displayName ?? 'this comment'}
|
|
106
|
+
</label>
|
|
107
|
+
<textarea id=${`reply-${comment.commentId}`} rows="3" value=${draft} autofocus
|
|
108
|
+
placeholder="Write a reply" onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
109
|
+
<div class="gala-comment__controls">
|
|
110
|
+
<button type="submit" disabled=${busy || !draft.trim()}>Post reply</button>
|
|
111
|
+
<button type="button" onClick=${() => setReplying(false)}>Cancel</button>
|
|
112
|
+
</div>
|
|
113
|
+
</form>`}
|
|
114
|
+
|
|
115
|
+
${comment.replies.length > 0 && html`
|
|
116
|
+
<ol class="gala-comment-replies">
|
|
117
|
+
${comment.replies.map((reply) => html`
|
|
118
|
+
<${Comment} key=${reply.commentId} comment=${reply} reader=${reader} busy=${busy}
|
|
119
|
+
onReply=${onReply} onEdit=${onEdit} onDelete=${onDelete} />`)}
|
|
120
|
+
</ol>`}
|
|
121
|
+
</li>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Comments({ endpoint }) {
|
|
125
|
+
// Reading `.value` during render subscribes this component to the signal, so a sign-in
|
|
126
|
+
// anywhere on the page re-renders the conversation without anything having to tell it.
|
|
127
|
+
const reader = sessionUser.value;
|
|
128
|
+
const [items, setItems] = useState([]);
|
|
129
|
+
const [cursor, setCursor] = useState(null);
|
|
130
|
+
const [total, setTotal] = useState(0);
|
|
131
|
+
const [status, setStatus] = useState('');
|
|
132
|
+
const [busy, setBusy] = useState(false);
|
|
133
|
+
const [draft, setDraft] = useState('');
|
|
134
|
+
const [loading, setLoading] = useState(true);
|
|
135
|
+
|
|
136
|
+
/* `fresh` skips the browser cache. The endpoint is `max-age=60, public`, which is right for a
|
|
137
|
+
cold visit and wrong straight after a write: the browser would answer from its own cache with
|
|
138
|
+
the state from before the reader wrote anything. */
|
|
139
|
+
const load = useCallback(async (nextCursor = '', { append = false, fresh = false } = {}) => {
|
|
140
|
+
const url = new URL(endpoint);
|
|
141
|
+
if (nextCursor) url.searchParams.set('commentsCursor', nextCursor);
|
|
142
|
+
const response = await fetch(url, {
|
|
143
|
+
headers: { Accept: 'application/json' },
|
|
144
|
+
credentials: 'omit',
|
|
145
|
+
cache: fresh ? 'no-store' : 'default',
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) throw new Error(`Comments returned HTTP ${response.status}`);
|
|
148
|
+
const payload = await response.json();
|
|
149
|
+
const page = payload?.data?.comments;
|
|
150
|
+
if (!page) throw new TypeError('Comment page is invalid');
|
|
151
|
+
setItems((current) => (append ? [...current, ...page.items] : page.items));
|
|
152
|
+
setCursor(page.nextCursor ?? null);
|
|
153
|
+
if (Number.isSafeInteger(page.totalCount)) setTotal(page.totalCount);
|
|
154
|
+
return page;
|
|
155
|
+
}, [endpoint]);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
let live = true;
|
|
159
|
+
load().catch(() => { if (live) setStatus('Comments are temporarily unavailable.'); })
|
|
160
|
+
.finally(() => { if (live) setLoading(false); });
|
|
161
|
+
return () => { live = false; };
|
|
162
|
+
}, [load]);
|
|
163
|
+
|
|
164
|
+
// Signing in changes what the reader may do, not what the article says, so only re-read when
|
|
165
|
+
// the reader actually changes.
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!reader) return undefined;
|
|
168
|
+
load('', { fresh: true }).catch(() => {});
|
|
169
|
+
return undefined;
|
|
170
|
+
}, [reader?.id]);
|
|
171
|
+
|
|
172
|
+
const write = useCallback(async (operation, payload, optimistic) => {
|
|
173
|
+
if (!reader) { requestSignIn({ kind: 'comment' }); return false; }
|
|
174
|
+
setBusy(true);
|
|
175
|
+
setStatus('');
|
|
176
|
+
if (optimistic) setItems((current) => [optimistic, ...current]);
|
|
177
|
+
try {
|
|
178
|
+
await sendEngagementWrite(operation, payload);
|
|
179
|
+
/* Re-read past the cache so what is on screen is what the server actually holds — the
|
|
180
|
+
optimistic row above is a promise to the reader, not a source of truth. */
|
|
181
|
+
await load('', { fresh: true });
|
|
182
|
+
return true;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (optimistic) {
|
|
185
|
+
setItems((current) => current.filter((item) => item.commentId !== optimistic.commentId));
|
|
186
|
+
}
|
|
187
|
+
setStatus(engagementErrorMessage(error.message));
|
|
188
|
+
return false;
|
|
189
|
+
} finally {
|
|
190
|
+
setBusy(false);
|
|
191
|
+
}
|
|
192
|
+
}, [reader?.id, load]);
|
|
193
|
+
|
|
194
|
+
const articleId = useMemo(() => {
|
|
195
|
+
const match = /\/v1\/articles\/([^/]+)\/engagement/.exec(endpoint);
|
|
196
|
+
return match ? match[1] : '';
|
|
197
|
+
}, [endpoint]);
|
|
198
|
+
|
|
199
|
+
const post = async (event) => {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
const body = draft.trim();
|
|
202
|
+
if (!body) return;
|
|
203
|
+
const posted = await write('comment.create', { articleId, body }, {
|
|
204
|
+
commentId: `pending-${crypto.randomUUID()}`,
|
|
205
|
+
parentCommentId: null,
|
|
206
|
+
body,
|
|
207
|
+
depth: 0,
|
|
208
|
+
createdAt: new Date().toISOString(),
|
|
209
|
+
author: { userId: reader?.id, displayName: reader?.displayName },
|
|
210
|
+
pending: true,
|
|
211
|
+
});
|
|
212
|
+
if (posted) setDraft('');
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const reply = (parentCommentId, body) =>
|
|
216
|
+
write('comment.create', { articleId, parentCommentId, body });
|
|
217
|
+
const edit = (commentId, body) => write('comment.edit', { articleId, commentId, body });
|
|
218
|
+
const remove = (commentId) => write('comment.delete', { articleId, commentId });
|
|
219
|
+
|
|
220
|
+
const threads = useMemo(() => toThreads(items), [items]);
|
|
221
|
+
|
|
222
|
+
return html`
|
|
223
|
+
<section class="gala-comments-island" aria-label="Comments">
|
|
224
|
+
<h2 class="gala-comments__heading">${total === 1 ? '1 comment' : `${total} comments`}</h2>
|
|
225
|
+
|
|
226
|
+
${reader
|
|
227
|
+
? html`<form class="gala-comment__form" onSubmit=${post}>
|
|
228
|
+
<label class="gala-visually-hidden" for="gala-new-comment">Add a comment</label>
|
|
229
|
+
<textarea id="gala-new-comment" rows="3" value=${draft} placeholder="Add a comment"
|
|
230
|
+
onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
231
|
+
<button type="submit" disabled=${busy || !draft.trim()}>Post comment</button>
|
|
232
|
+
</form>`
|
|
233
|
+
: html`<p class="gala-comments__prompt">
|
|
234
|
+
<button type="button" onClick=${() => requestSignIn({ kind: 'comment' })}>
|
|
235
|
+
Sign in to join the conversation
|
|
236
|
+
</button>
|
|
237
|
+
</p>`}
|
|
238
|
+
|
|
239
|
+
${status && html`<p class="gala-comments__status" role="status">${status}</p>`}
|
|
240
|
+
|
|
241
|
+
${loading
|
|
242
|
+
? html`<p class="gala-comments__status" role="status">Loading comments…</p>`
|
|
243
|
+
: threads.length === 0
|
|
244
|
+
? html`<p class="gala-comments__empty">No comments yet.</p>`
|
|
245
|
+
: html`<ol class="gala-comments">
|
|
246
|
+
${threads.map((comment) => html`
|
|
247
|
+
<${Comment} key=${comment.commentId} comment=${comment} reader=${reader}
|
|
248
|
+
busy=${busy} onReply=${reply} onEdit=${edit} onDelete=${remove} />`)}
|
|
249
|
+
</ol>`}
|
|
250
|
+
|
|
251
|
+
${cursor && html`
|
|
252
|
+
<button type="button" class="gala-comments__more" disabled=${busy}
|
|
253
|
+
onClick=${() => { setBusy(true); load(cursor, { append: true })
|
|
254
|
+
.catch(() => setStatus('Could not load more comments.'))
|
|
255
|
+
.finally(() => setBusy(false)); }}>
|
|
256
|
+
Show more comments
|
|
257
|
+
</button>`}
|
|
258
|
+
</section>`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const mount of document.querySelectorAll('[data-gala-comments]')) {
|
|
262
|
+
const endpoint = mount.closest('[data-engagement-url]')?.dataset.engagementUrl;
|
|
263
|
+
if (endpoint) render(html`<${Comments} endpoint=${endpoint} />`, mount);
|
|
264
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reader's identity, and the only way to write on their behalf.
|
|
3
|
+
*
|
|
4
|
+
* Both belong to the account frame, which is served by the API and runs on its origin. The
|
|
5
|
+
* reader's token lives there and nowhere else — this page never sees it, and never should: the
|
|
6
|
+
* page is the writer's own site, and a reader's credentials are not the writer's to hold. So a
|
|
7
|
+
* write is a request posted into the frame, and the frame answers with a result.
|
|
8
|
+
*
|
|
9
|
+
* `sessionUser` is a signal rather than a variable because more than one island depends on it and
|
|
10
|
+
* they must not each keep their own copy going stale in its own way.
|
|
11
|
+
*/
|
|
12
|
+
import { signal } from './vendor/signals.js';
|
|
13
|
+
|
|
14
|
+
export const sessionUser = signal(null);
|
|
15
|
+
|
|
16
|
+
const sessionFrame = document.querySelector('[data-gala-session-frame]');
|
|
17
|
+
const frameOrigin = sessionFrame ? new URL(sessionFrame.src).origin : null;
|
|
18
|
+
const pending = new Map();
|
|
19
|
+
|
|
20
|
+
/** Readable text for the codes the API actually returns. */
|
|
21
|
+
export function engagementErrorMessage(code) {
|
|
22
|
+
if (code === 'AUTHENTICATION_REQUIRED' || code === 'INVALID_BEARER_TOKEN'
|
|
23
|
+
|| code === 'REAUTHENTICATION_REQUIRED') return 'Sign in again to continue.';
|
|
24
|
+
if (code === 'ENGAGEMENT_RATE_LIMITED') return 'You are posting too quickly; try again shortly.';
|
|
25
|
+
if (code === 'CONTACT_RATE_LIMITED') return 'You are sending too quickly; try again later.';
|
|
26
|
+
if (code === 'RESOURCE_NOT_FOUND') return 'That item is no longer available.';
|
|
27
|
+
if (code === 'INVALID_ENGAGEMENT_WRITE' || code === 'INVALID_CONTACT_SUBMISSION'
|
|
28
|
+
|| code === 'INVALID_REQUEST') return 'Check your entry and try again.';
|
|
29
|
+
if (code === 'ACCESS_DENIED') return 'That action is not available for this account.';
|
|
30
|
+
if (code === 'IDEMPOTENCY_CONFLICT' || code === 'ENGAGEMENT_STATE_CONFLICT') {
|
|
31
|
+
return 'The item changed; reload and try again.';
|
|
32
|
+
}
|
|
33
|
+
return 'The action could not be completed. Try again.';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sendEngagementWrite(operation, payload) {
|
|
37
|
+
if (!sessionFrame || !sessionUser.value) {
|
|
38
|
+
return Promise.reject(new Error('AUTHENTICATION_REQUIRED'));
|
|
39
|
+
}
|
|
40
|
+
const requestId = crypto.randomUUID();
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
pending.set(requestId, { resolve, reject });
|
|
43
|
+
sessionFrame.contentWindow.postMessage(
|
|
44
|
+
{ type: 'gala-engagement-write', requestId, operation, payload }, frameOrigin);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Asks the page to start a sign-in and to put the reader back where they were afterwards.
|
|
50
|
+
*
|
|
51
|
+
* Dispatched as an event rather than called directly: opening the window and replaying the
|
|
52
|
+
* interrupted intent belongs to the page, and an island should not have to know how that works.
|
|
53
|
+
*/
|
|
54
|
+
export function requestSignIn(intent) {
|
|
55
|
+
document.dispatchEvent(new CustomEvent('gala-request-sign-in', { detail: intent }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (sessionFrame) {
|
|
59
|
+
window.addEventListener('message', (event) => {
|
|
60
|
+
if (event.origin !== frameOrigin || event.source !== sessionFrame.contentWindow) return;
|
|
61
|
+
if (event.data?.type === 'gala-engagement-result') {
|
|
62
|
+
const waiting = pending.get(event.data.requestId);
|
|
63
|
+
if (!waiting) return;
|
|
64
|
+
pending.delete(event.data.requestId);
|
|
65
|
+
if (event.data.ok === true) waiting.resolve(event.data.result);
|
|
66
|
+
else waiting.reject(new Error(event.data.error?.code || 'ENGAGEMENT_WRITE_FAILED'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (event.data?.type !== 'gala-session') return;
|
|
70
|
+
const user = event.data.user;
|
|
71
|
+
sessionUser.value = user && typeof user.id === 'string' ? user : null;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -70,61 +70,6 @@ document.addEventListener('click', async (event) => {
|
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const reply = event.target.closest('[data-reply-comment]');
|
|
74
|
-
if (reply) {
|
|
75
|
-
const region = reply.closest('[data-engagement-url]');
|
|
76
|
-
const form = region?.querySelector('[data-comment-create]');
|
|
77
|
-
if (!form) return;
|
|
78
|
-
form.dataset.parentCommentId = reply.dataset.replyComment;
|
|
79
|
-
const context = form.querySelector('[data-comment-reply-context]');
|
|
80
|
-
if (context) {
|
|
81
|
-
context.hidden = false;
|
|
82
|
-
context.textContent = `Replying to ${reply.dataset.replyAuthor || 'comment'}.`;
|
|
83
|
-
}
|
|
84
|
-
const cancel = form.querySelector('[data-cancel-reply]');
|
|
85
|
-
if (cancel) cancel.hidden = false;
|
|
86
|
-
form.querySelector('textarea')?.focus();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const cancelReply = event.target.closest('[data-cancel-reply]');
|
|
91
|
-
if (cancelReply) {
|
|
92
|
-
clearReply(cancelReply.closest('[data-comment-create]'));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const edit = event.target.closest('[data-edit-comment]');
|
|
97
|
-
if (edit) {
|
|
98
|
-
showCommentEditor(edit);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const cancelEdit = event.target.closest('[data-cancel-comment-edit]');
|
|
103
|
-
if (cancelEdit) {
|
|
104
|
-
cancelEdit.closest('[data-comment-editor]')?.remove();
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const remove = event.target.closest('[data-delete-comment]');
|
|
109
|
-
if (remove) {
|
|
110
|
-
const region = remove.closest('[data-engagement-url]');
|
|
111
|
-
if (!region) return;
|
|
112
|
-
await mutateEngagement(region, 'comment.delete', {
|
|
113
|
-
articleId: region.dataset.articleId,
|
|
114
|
-
commentId: remove.dataset.deleteComment
|
|
115
|
-
});
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const moreComments = event.target.closest('[data-comments-cursor]');
|
|
120
|
-
if (moreComments) {
|
|
121
|
-
const region = moreComments.closest('[data-engagement-url]');
|
|
122
|
-
if (!region) return;
|
|
123
|
-
moreComments.disabled = true;
|
|
124
|
-
await refreshEngagement(region, moreComments.dataset.commentsCursor, true);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
73
|
const share = event.target.closest('[data-copy-url]');
|
|
129
74
|
if (share) {
|
|
130
75
|
const value = share.dataset.copyUrl;
|
|
@@ -187,57 +132,6 @@ function engagementCounts(data) {
|
|
|
187
132
|
];
|
|
188
133
|
}
|
|
189
134
|
|
|
190
|
-
function commentList(items, currentUser) {
|
|
191
|
-
const roots = document.createElement('ol');
|
|
192
|
-
roots.className = 'gala-comments';
|
|
193
|
-
const nodes = new Map();
|
|
194
|
-
for (const comment of items) {
|
|
195
|
-
const item = document.createElement('li');
|
|
196
|
-
item.dataset.commentId = comment.commentId;
|
|
197
|
-
const author = comment.author?.displayName ?? '[deleted]';
|
|
198
|
-
item.append(textElement('strong', author));
|
|
199
|
-
const body = textElement('p', comment.deleted ? '[deleted]' : comment.body);
|
|
200
|
-
body.dataset.commentBody = '';
|
|
201
|
-
item.append(body);
|
|
202
|
-
if (!comment.deleted && currentUser) {
|
|
203
|
-
const actions = document.createElement('div');
|
|
204
|
-
actions.className = 'gala-comment-actions';
|
|
205
|
-
const reply = textElement('button', 'Reply');
|
|
206
|
-
reply.type = 'button';
|
|
207
|
-
reply.dataset.replyComment = comment.commentId;
|
|
208
|
-
reply.dataset.replyAuthor = author;
|
|
209
|
-
actions.append(reply);
|
|
210
|
-
if (comment.author?.userId === currentUser.id) {
|
|
211
|
-
const edit = textElement('button', 'Edit');
|
|
212
|
-
edit.type = 'button';
|
|
213
|
-
edit.dataset.editComment = comment.commentId;
|
|
214
|
-
const remove = textElement('button', 'Delete');
|
|
215
|
-
remove.type = 'button';
|
|
216
|
-
remove.dataset.deleteComment = comment.commentId;
|
|
217
|
-
actions.append(edit, remove);
|
|
218
|
-
}
|
|
219
|
-
item.append(actions);
|
|
220
|
-
}
|
|
221
|
-
nodes.set(comment.commentId, item);
|
|
222
|
-
}
|
|
223
|
-
for (const comment of items) {
|
|
224
|
-
const item = nodes.get(comment.commentId);
|
|
225
|
-
const parent = comment.parentCommentId ? nodes.get(comment.parentCommentId) : null;
|
|
226
|
-
if (!parent) {
|
|
227
|
-
roots.append(item);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
let replies = parent.querySelector(':scope > ol');
|
|
231
|
-
if (!replies) {
|
|
232
|
-
replies = document.createElement('ol');
|
|
233
|
-
replies.className = 'gala-comment-replies';
|
|
234
|
-
parent.append(replies);
|
|
235
|
-
}
|
|
236
|
-
replies.append(item);
|
|
237
|
-
}
|
|
238
|
-
return roots;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
135
|
function renderEngagement(region, payload, appendComments = false) {
|
|
242
136
|
const data = payload?.data;
|
|
243
137
|
if (!data || typeof data !== 'object') throw new TypeError('Engagement data is invalid');
|
|
@@ -267,60 +161,19 @@ function renderEngagement(region, payload, appendComments = false) {
|
|
|
267
161
|
live.append(summary);
|
|
268
162
|
}
|
|
269
163
|
|
|
270
|
-
if (Array.isArray(data.comments?.items) && data.comments.items.length > 0) {
|
|
271
|
-
live.append(commentList(data.comments.items, sessionUser));
|
|
272
|
-
}
|
|
273
|
-
live.querySelector('[data-comments-cursor]')?.remove();
|
|
274
|
-
if (data.comments?.nextCursor) {
|
|
275
|
-
const more = textElement('button', 'Load more comments');
|
|
276
|
-
more.type = 'button';
|
|
277
|
-
more.dataset.commentsCursor = data.comments.nextCursor;
|
|
278
|
-
live.append(more);
|
|
279
|
-
}
|
|
280
164
|
region.querySelector('[data-engagement-snapshot]')?.remove();
|
|
281
165
|
region.querySelector('.gala-engagement__placeholder')?.remove();
|
|
282
166
|
}
|
|
283
167
|
|
|
284
|
-
function
|
|
285
|
-
if (!form) return;
|
|
286
|
-
delete form.dataset.parentCommentId;
|
|
287
|
-
const context = form.querySelector('[data-comment-reply-context]');
|
|
288
|
-
if (context) {
|
|
289
|
-
context.hidden = true;
|
|
290
|
-
context.textContent = '';
|
|
291
|
-
}
|
|
292
|
-
const cancel = form.querySelector('[data-cancel-reply]');
|
|
293
|
-
if (cancel) cancel.hidden = true;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function showCommentEditor(control) {
|
|
297
|
-
const item = control.closest('[data-comment-id]');
|
|
298
|
-
if (!item || item.querySelector('[data-comment-editor]')) return;
|
|
299
|
-
const form = document.createElement('form');
|
|
300
|
-
form.dataset.commentEditor = control.dataset.editComment;
|
|
301
|
-
const textarea = document.createElement('textarea');
|
|
302
|
-
textarea.name = 'body';
|
|
303
|
-
textarea.maxLength = 5000;
|
|
304
|
-
textarea.required = true;
|
|
305
|
-
textarea.value = item.querySelector('[data-comment-body]')?.textContent ?? '';
|
|
306
|
-
const save = textElement('button', 'Save');
|
|
307
|
-
save.type = 'submit';
|
|
308
|
-
const cancel = textElement('button', 'Cancel');
|
|
309
|
-
cancel.type = 'button';
|
|
310
|
-
cancel.dataset.cancelCommentEdit = '';
|
|
311
|
-
form.append(textarea, save, cancel);
|
|
312
|
-
item.append(form);
|
|
313
|
-
textarea.focus();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function refreshEngagement(region, commentsCursor = '', appendComments = false) {
|
|
168
|
+
async function refreshEngagement(region, commentsCursor = '', appendComments = false, fresh = false) {
|
|
317
169
|
const status = region.querySelector('[data-engagement-status]');
|
|
318
170
|
try {
|
|
319
171
|
const requestUrl = new URL(region.dataset.engagementUrl);
|
|
320
172
|
if (commentsCursor) requestUrl.searchParams.set('commentsCursor', commentsCursor);
|
|
321
173
|
const response = await fetch(requestUrl, {
|
|
322
174
|
headers: { Accept: 'application/json' },
|
|
323
|
-
credentials: 'omit'
|
|
175
|
+
credentials: 'omit',
|
|
176
|
+
cache: fresh ? 'no-store' : 'default'
|
|
324
177
|
});
|
|
325
178
|
if (!response.ok) throw new Error(`Engagement returned HTTP ${response.status}`);
|
|
326
179
|
const payload = await response.json();
|
|
@@ -358,52 +211,14 @@ function recordView(region) {
|
|
|
358
211
|
}).catch(() => {});
|
|
359
212
|
}
|
|
360
213
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
214
|
+
/*
|
|
215
|
+
* The comments island asks for a sign-in when the reader reaches for something that needs one.
|
|
216
|
+
* Opening the window and replaying the interrupted intent stays here, because that is a property
|
|
217
|
+
* of the page, not of any one island.
|
|
218
|
+
*/
|
|
219
|
+
document.addEventListener('gala-request-sign-in', (event) => {
|
|
364
220
|
if (sessionUser || pendingIntent) return;
|
|
365
|
-
|
|
366
|
-
if (!field) return;
|
|
367
|
-
const region = field.closest('[data-engagement-url]');
|
|
368
|
-
const form = field.closest('[data-comment-create], [data-comment-editor]');
|
|
369
|
-
if (!region || !form) return;
|
|
370
|
-
requestSession({ kind: 'comment', region,
|
|
371
|
-
selector: `${form.matches('[data-comment-create]') ? '[data-comment-create]' : '[data-comment-editor]'} textarea`,
|
|
372
|
-
field });
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
document.addEventListener('submit', async (event) => {
|
|
376
|
-
const create = event.target.closest('[data-comment-create]');
|
|
377
|
-
const editor = event.target.closest('[data-comment-editor]');
|
|
378
|
-
if (!create && !editor) return;
|
|
379
|
-
event.preventDefault();
|
|
380
|
-
const form = create ?? editor;
|
|
381
|
-
const region = form.closest('[data-engagement-url]');
|
|
382
|
-
const body = form.elements.body?.value;
|
|
383
|
-
if (!region || typeof body !== 'string' || body.trim() === '') return;
|
|
384
|
-
if (!sessionUser) {
|
|
385
|
-
return requestSession({ kind: 'comment', region,
|
|
386
|
-
selector: `${create ? '[data-comment-create]' : '[data-comment-editor]'} textarea`,
|
|
387
|
-
field: form.querySelector('textarea') });
|
|
388
|
-
}
|
|
389
|
-
if (create) {
|
|
390
|
-
const saved = await mutateEngagement(region, 'comment.create', {
|
|
391
|
-
articleId: region.dataset.articleId,
|
|
392
|
-
parentCommentId: create.dataset.parentCommentId || null,
|
|
393
|
-
body
|
|
394
|
-
});
|
|
395
|
-
if (!saved) return;
|
|
396
|
-
create.reset();
|
|
397
|
-
clearReply(create);
|
|
398
|
-
} else {
|
|
399
|
-
const saved = await mutateEngagement(region, 'comment.edit', {
|
|
400
|
-
articleId: region.dataset.articleId,
|
|
401
|
-
commentId: editor.dataset.commentEditor,
|
|
402
|
-
body
|
|
403
|
-
});
|
|
404
|
-
if (!saved) return;
|
|
405
|
-
editor.remove();
|
|
406
|
-
}
|
|
221
|
+
requestSession({ kind: event.detail?.kind ?? 'comment' });
|
|
407
222
|
});
|
|
408
223
|
|
|
409
224
|
let sessionUser = null;
|
|
@@ -556,7 +371,8 @@ async function mutateEngagement(region, operation, payload) {
|
|
|
556
371
|
if (status) status.textContent = 'Saving…';
|
|
557
372
|
await sendEngagementWrite(operation, payload);
|
|
558
373
|
if (status) status.textContent = 'Saved.';
|
|
559
|
-
|
|
374
|
+
// The reader's own write must be visible to them at once, so this one bypasses the cache.
|
|
375
|
+
await refreshEngagement(region, '', false, true);
|
|
560
376
|
return true;
|
|
561
377
|
} catch (error) {
|
|
562
378
|
if (status) status.textContent = engagementErrorMessage(error.message);
|
|
@@ -87,7 +87,9 @@ body {
|
|
|
87
87
|
}
|
|
88
88
|
.gala-icon-control:hover { background: var(--gala-color-surface); }
|
|
89
89
|
.gala-icon-control:focus-visible { outline: 0.1875rem solid currentcolor; outline-offset: 0.1875rem; }
|
|
90
|
-
|
|
90
|
+
/* Stroked, not filled: the icons are drawn on one 24px grid at a single weight, so they read as
|
|
91
|
+
a set rather than as six unrelated silhouettes. */
|
|
92
|
+
.gala-icon-control svg { inline-size: 100%; block-size: 100%; fill: none; stroke: currentcolor; }
|
|
91
93
|
|
|
92
94
|
.gala-dialog {
|
|
93
95
|
inline-size: min(42rem, calc(100% - 2rem));
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{options as n}from"./preact.js";var t,r,u,i,o=0,f=[],c=n,e=c.__b,a=c.__r,v=c.diffed,l=c.__c,m=c.unmount,p=c.__;function s(n,t){c.__h&&c.__h(r,n,o||t),o=0;var u=r.__H||(r.__H={__:[],__h:[]});return n>=u.__.length&&u.__.push({}),u.__[n]}function d(n){return o=1,y(D,n)}function y(n,u,i){var o=s(t++,2);if(o.t=n,!o.__c&&(o.__=[i?i(u):D(void 0,u),function(n){var t=o.__N?o.__N[0]:o.__[0],r=o.t(t,n);t!==r&&(o.__N=[r,o.__[1]],o.__c.setState({}))}],o.__c=r,!r.__f)){var f=function(n,t,r){if(!o.__c.__H)return!0;var u=!1,i=o.__c.props!==n;if(o.__c.__H.__.some(function(n){if(n.__N){u=!0;var t=n.__[0];n.__=n.__N,n.__N=void 0,t!==n.__[0]&&(i=!0)}}),c){var f=c.call(this,n,t,r);return u?f||i:f}return!u||i};r.__f=!0;var c=r.shouldComponentUpdate,e=r.componentWillUpdate;r.componentWillUpdate=function(n,t,r){if(this.__e){var u=c;c=void 0,f(n,t,r),c=u}e&&e.call(this,n,t,r)},r.shouldComponentUpdate=f}return o.__N||o.__}function h(n,u){var i=s(t++,3);!c.__s&&C(i.__H,u)&&(i.__=n,i.u=u,r.__H.__h.push(i))}function _(n,u){var i=s(t++,4);!c.__s&&C(i.__H,u)&&(i.__=n,i.u=u,r.__h.push(i))}function A(n){return o=5,T(function(){return{current:n}},[])}function F(n,t,r){o=6,_(function(){if("function"==typeof n){var r=n(t());return function(){n(null),r&&"function"==typeof r&&r()}}if(n)return n.current=t(),function(){return n.current=null}},null==r?r:r.concat(n))}function T(n,r){var u=s(t++,7);return C(u.__H,r)&&(u.__=n(),u.__H=r,u.__h=n),u.__}function q(n,t){return o=8,T(function(){return n},t)}function x(n){var u=r.context[n.__c],i=s(t++,9);return i.c=n,u?(null==i.__&&(i.__=!0,u.sub(r)),u.props.value):n.__}function P(n,t){c.useDebugValue&&c.useDebugValue(t?t(n):n)}function b(n){var u=s(t++,10),i=d();return u.__=n,r.componentDidCatch||(r.componentDidCatch=function(n,t){u.__&&u.__(n,t),i[1](n)}),[i[0],function(){i[1](void 0)}]}function g(){var n=s(t++,11);if(!n.__){for(var u=r.__v;null!==u&&!u.__m&&null!==u.__;)u=u.__;var i=u.__m||(u.__m=[0,0]);n.__="P"+i[0]+"-"+i[1]++}return n.__}function j(){for(var n;n=f.shift();){var t=n.__H;if(n.__P&&t)try{t.__h.some(z),t.__h.some(B),t.__h=[]}catch(r){t.__h=[],c.__e(r,n.__v)}}}c.__b=function(n){r=null,e&&e(n)},c.__=function(n,t){n&&t.__k&&t.__k.__m&&(n.__m=t.__k.__m),p&&p(n,t)},c.__r=function(n){a&&a(n),t=0;var i=(r=n.__c).__H;i&&(u===r?(i.__h=[],r.__h=[],i.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(i.__h.some(z),i.__h.some(B),i.__h=[],t=0)),u=r},c.diffed=function(n){v&&v(n);var t=n.__c;t&&t.__H&&(t.__H.__h.length&&(1!==f.push(t)&&i===c.requestAnimationFrame||((i=c.requestAnimationFrame)||w)(j)),t.__H.__.some(function(n){n.u&&(n.__H=n.u,n.u=void 0)})),u=r=null},c.__c=function(n,t){t.some(function(n){try{n.__h.some(z),n.__h=n.__h.filter(function(n){return!n.__||B(n)})}catch(r){t.some(function(n){n.__h&&(n.__h=[])}),t=[],c.__e(r,n.__v)}}),l&&l(n,t)},c.unmount=function(n){m&&m(n);var t,r=n.__c;r&&r.__H&&(r.__H.__.some(function(n){try{z(n)}catch(n){t=n}}),r.__H=void 0,t&&c.__e(t,r.__v))};var k="function"==typeof requestAnimationFrame;function w(n){var t,r=function(){clearTimeout(u),k&&cancelAnimationFrame(t),setTimeout(n)},u=setTimeout(r,35);k&&(t=requestAnimationFrame(r))}function z(n){var t=r,u=n.__c;"function"==typeof u&&(n.__c=void 0,u()),r=t}function B(n){var t=r;n.__c=n.__(),r=t}function C(n,t){return!n||n.length!==t.length||t.some(function(t,r){return t!==n[r]})}function D(n,t){return"function"==typeof t?t(n):t}export{q as useCallback,x as useContext,P as useDebugValue,h as useEffect,b as useErrorBoundary,g as useId,F as useImperativeHandle,_ as useLayoutEffect,T as useMemo,y as useReducer,A as useRef,d as useState};
|
|
2
|
+
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h<s.length;h++){var p=s[h++],a=s[h]?(s[0]|=p?1:2,r[s[h++]]):s[++h];3===p?e[0]=a:4===p?e[1]=Object.assign(e[1]||{},a):5===p?(e[1]=e[1]||{})[s[++h]]=a:6===p?e[1][s[++h]]+=a+"":p?(u=t.apply(a,n(t,a,r,["",null])),e.push(u),a[0]?s[0]|=2:(s[h-2]=0,s[h]=u)):e.push(a)}return e},t=new Map;export default function(s){var r=t.get(this);return r||(r=new Map,t.set(this,r)),(r=n(this,r.get(s)||(r.set(s,r=function(n){for(var t,s,r=1,e="",u="",h=[0],p=function(n){1===r&&(n||(e=e.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,e):3===r&&(n||e)?(h.push(3,n,e),r=2):2===r&&"..."===e&&n?h.push(4,n,0):2===r&&e&&!n?h.push(5,0,!0,e):r>=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e=""},a=0;a<n.length;a++){a&&(1===r&&p(),p(a));for(var l=0;l<n[a].length;l++)t=n[a][l],1===r?"<"===t?(p(),h=[h],r=3):e+=t:4===r?"--"===e&&">"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0])}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]}
|
|
2
|
+
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var n,l,u,t,i,r,o,e,f,c,a,s,h,p,v,y,d={},w=[],_=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,g=Array.isArray;function m(n,l){for(var u in l)n[u]=l[u];return n}function b(n){n&&n.parentNode&&n.parentNode.removeChild(n)}function k(l,u,t){var i,r,o,e={};for(o in u)"key"==o?i=u[o]:"ref"==o?r=u[o]:e[o]=u[o];if(arguments.length>2&&(e.children=arguments.length>3?n.call(arguments,2):t),"function"==typeof l&&null!=l.defaultProps)for(o in l.defaultProps)void 0===e[o]&&(e[o]=l.defaultProps[o]);return x(l,e,i,r,null)}function x(n,t,i,r,o){var e={type:n,props:t,key:i,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:null==o?++u:o,__i:-1,__u:0};return null==o&&null!=l.vnode&&l.vnode(e),e}function M(){return{current:null}}function S(n){return n.children}function C(n,l){this.props=n,this.context=l}function $(n,l){if(null==l)return n.__?$(n.__,n.__i+1):null;for(var u;l<n.__k.length;l++)if(null!=(u=n.__k[l])&&null!=u.__e)return u.__e;return"function"==typeof n.type?$(n):null}function I(n){if(n.__P&&n.__d){var u=n.__v,t=u.__e,i=[],r=[],o=m({},u);o.__v=u.__v+1,l.vnode&&l.vnode(o),q(n.__P,o,u,n.__n,n.__P.namespaceURI,32&u.__u?[t]:null,i,null==t?$(u):t,!!(32&u.__u),r),o.__v=u.__v,o.__.__k[o.__i]=o,D(i,o,r),u.__e=u.__=null,o.__e!=t&&P(o)}}function P(n){if(null!=(n=n.__)&&null!=n.__c)return n.__e=n.__c.base=null,n.__k.some(function(l){if(null!=l&&null!=l.__e)return n.__e=n.__c.base=l.__e}),P(n)}function A(n){(!n.__d&&(n.__d=!0)&&i.push(n)&&!H.__r++||r!=l.debounceRendering)&&((r=l.debounceRendering)||o)(H)}function H(){try{for(var n,l=1;i.length;)i.length>l&&i.sort(e),n=i.shift(),l=i.length,I(n)}finally{i.length=H.__r=0}}function L(n,l,u,t,i,r,o,e,f,c,a){var s,h,p,v,y,_,g=t&&t.__k||w,m=l.length;for(f=T(u,l,g,f,m),s=0;s<m;s++)null!=(p=u.__k[s])&&(h=-1!=p.__i&&g[p.__i]||d,p.__i=s,_=q(n,p,h,i,r,o,e,f,c,a),v=p.__e,p.ref&&h.ref!=p.ref&&(h.ref&&J(h.ref,null,p),a.push(p.ref,p.__c||v,p)),null==y&&null!=v&&(y=v),4&p.__u?(f=j(p,f,n),h.__e&&(h.__e=null)):"function"==typeof p.type&&void 0!==_?f=_:v&&(f=v.nextSibling),p.__u&=-7);return u.__e=y,f}function T(n,l,u,t,i){var r,o,e,f,c,a=u.length,s=a,h=0;for(n.__k=new Array(i),r=0;r<i;r++)null!=(o=l[r])&&"boolean"!=typeof o&&"function"!=typeof o?("string"==typeof o||"number"==typeof o||"bigint"==typeof o||o.constructor==String?o=n.__k[r]=x(null,o,null,null,null):g(o)?o=n.__k[r]=x(S,{children:o},null,null,null):void 0===o.constructor&&o.__b>0?o=n.__k[r]=x(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):n.__k[r]=o,f=r+h,o.__=n,o.__b=n.__b+1,e=null,-1!=(c=o.__i=O(o,u,f,s))&&(s--,(e=u[c])&&(e.__u|=2)),null==e||null==e.__v?(-1==c&&(i>a?h--:i<a&&h++),"function"!=typeof o.type&&(o.__u|=4)):c!=f&&(c==f-1?h--:c==f+1?h++:(c>f?h--:h++,o.__u|=4))):n.__k[r]=null;if(s)for(r=0;r<a;r++)null!=(e=u[r])&&0==(2&e.__u)&&(e.__e==t&&(t=$(e)),K(e,e));return t}function j(n,l,u){var t,i;if("function"==typeof n.type){for(t=n.__k,i=0;t&&i<t.length;i++)t[i]&&(t[i].__=n,l=j(t[i],l,u));return l}n.__e!=l&&(l&&n.type&&!l.parentNode&&(l=$(n)),l=u.insertBefore(n.__e,l||null));do{l=l&&l.nextSibling}while(null!=l&&8==l.nodeType);return l}function F(n,l){return l=l||[],null==n||"boolean"==typeof n||(g(n)?n.some(function(n){F(n,l)}):l.push(n)),l}function O(n,l,u,t){var i,r,o,e=n.key,f=n.type,c=l[u],a=null!=c&&0==(2&c.__u);if(null===c&&null==e||a&&e==c.key&&f==c.type)return u;if(t>(a?1:0))for(i=u-1,r=u+1;i>=0||r<l.length;)if(null!=(c=l[o=i>=0?i--:r++])&&0==(2&c.__u)&&e==c.key&&f==c.type)return o;return-1}function z(n,l,u){"-"==l[0]?n.setProperty(l,null==u?"":u):n[l]=null==u?"":"number"!=typeof u||_.test(l)?u:u+"px"}function N(n,l,u,t,i){var r,o;n:if("style"==l)if("string"==typeof u)n.style.cssText=u;else{if("string"==typeof t&&(n.style.cssText=t=""),t)for(l in t)u&&l in u||z(n.style,l,"");if(u)for(l in u)t&&u[l]==t[l]||z(n.style,l,u[l])}else if("o"==l[0]&&"n"==l[1])r=l!=(l=l.replace(s,"$1")),o=l.toLowerCase(),l=o in n||"onFocusOut"==l||"onFocusIn"==l?o.slice(2):l.slice(2),n.l||(n.l={}),n.l[l+r]=u,u?t?u[a]=t[a]:(u[a]=h,n.addEventListener(l,r?v:p,r)):n.removeEventListener(l,r?v:p,r);else{if("http://www.w3.org/2000/svg"==i)l=l.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!=l&&"height"!=l&&"href"!=l&&"list"!=l&&"form"!=l&&"tabIndex"!=l&&"download"!=l&&"rowSpan"!=l&&"colSpan"!=l&&"role"!=l&&"popover"!=l&&l in n)try{n[l]=null==u?"":u;break n}catch(n){}"function"==typeof u||(null==u||!1===u&&"-"!=l[4]?n.removeAttribute(l):n.setAttribute(l,"popover"==l&&1==u?"":u))}}function V(n){return function(u){if(this.l){var t=this.l[u.type+n];if(null==u[c])u[c]=h++;else if(u[c]<t[a])return;return t(l.event?l.event(u):u)}}}function q(n,u,t,i,r,o,e,f,c,a){var s,h,p,v,y,d,_,k,x,M,I,P,A,H,T,j,F=u.type;if(void 0!==u.constructor)return null;128&t.__u&&(c=!!(32&t.__u),o=[f=u.__e=t.__e]),(s=l.__b)&&s(u);n:if("function"==typeof F){h=e.length;try{if(x=u.props,M=F.prototype&&F.prototype.render,I=(s=F.contextType)&&i[s.__c],P=s?I?I.props.value:s.__:i,t.__c?k=(p=u.__c=t.__c).__=p.__E:(M?u.__c=p=new F(x,P):(u.__c=p=new C(x,P),p.constructor=F,p.render=Q),I&&I.sub(p),p.state||(p.state={}),p.__n=i,v=p.__d=!0,p.__h=[],p._sb=[]),M&&null==p.__s&&(p.__s=p.state),M&&null!=F.getDerivedStateFromProps&&(p.__s==p.state&&(p.__s=m({},p.__s)),m(p.__s,F.getDerivedStateFromProps(x,p.__s))),y=p.props,d=p.state,p.__v=u,v)M&&null==F.getDerivedStateFromProps&&null!=p.componentWillMount&&p.componentWillMount(),M&&null!=p.componentDidMount&&p.__h.push(p.componentDidMount);else{if(M&&null==F.getDerivedStateFromProps&&x!==y&&null!=p.componentWillReceiveProps&&p.componentWillReceiveProps(x,P),u.__v==t.__v||!p.__e&&null!=p.shouldComponentUpdate&&!1===p.shouldComponentUpdate(x,p.__s,P)){u.__v!=t.__v&&(p.props=x,p.state=p.__s,p.__d=!1),u.__e=t.__e,u.__k=t.__k,u.__k.some(function(n){n&&(n.__=u)}),w.push.apply(p.__h,p._sb),p._sb=[],p.__h.length&&e.push(p),f=$(t);break n}null!=p.componentWillUpdate&&p.componentWillUpdate(x,p.__s,P),M&&null!=p.componentDidUpdate&&p.__h.push(function(){p.componentDidUpdate(y,d,_)})}if(p.context=P,p.props=x,p.__P=n,p.__e=!1,A=l.__r,H=0,M)p.state=p.__s,p.__d=!1,A&&A(u),s=p.render(p.props,p.state,p.context),w.push.apply(p.__h,p._sb),p._sb=[];else do{p.__d=!1,A&&A(u),s=p.render(p.props,p.state,p.context),p.state=p.__s}while(p.__d&&++H<25);p.state=p.__s,null!=p.getChildContext&&(i=m(m({},i),p.getChildContext())),M&&!v&&null!=p.getSnapshotBeforeUpdate&&(_=p.getSnapshotBeforeUpdate(y,d)),T=null!=s&&s.type===S&&null==s.key?E(s.props.children):s,f=L(n,g(T)?T:[T],u,t,i,r,o,e,f,c,a),p.base=u.__e,u.__u&=-161,p.__h.length&&e.push(p),k&&(p.__E=p.__=null)}catch(n){if(e.length=h,u.__v=null,c||null!=o){if(n.then){for(u.__u|=c?160:128;f&&8==f.nodeType&&f.nextSibling;)f=f.nextSibling;null!=o&&(o[o.indexOf(f)]=null),u.__e=f}else if(null!=o)for(j=o.length;j--;)b(o[j])}else u.__e=t.__e;null==u.__k&&(u.__k=t.__k||[]),n.then||B(u),l.__e(n,u,t)}}else null==o&&u.__v==t.__v?(u.__k=t.__k,u.__e=t.__e):f=u.__e=G(t.__e,u,t,i,r,o,e,c,a);return(s=l.diffed)&&s(u),128&u.__u?void 0:f}function B(n){n&&(n.__c&&(n.__c.__e=!0),n.__k&&n.__k.some(B))}function D(n,u,t){for(var i=0;i<t.length;i++)J(t[i],t[++i],t[++i]);l.__c&&l.__c(u,n),n.some(function(u){try{n=u.__h,u.__h=[],n.some(function(n){n.call(u)})}catch(n){l.__e(n,u.__v)}})}function E(n){return"object"!=typeof n||null==n||n.__b>0?n:g(n)?n.map(E):void 0!==n.constructor?null:m({},n)}function G(u,t,i,r,o,e,f,c,a){var s,h,p,v,y,w,_,m=i.props||d,k=t.props,x=t.type;if("svg"==x?o="http://www.w3.org/2000/svg":"math"==x?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),null!=e)for(s=0;s<e.length;s++)if((y=e[s])&&"setAttribute"in y==!!x&&(x?y.localName==x:3==y.nodeType)){u=y,e[s]=null;break}if(null==u){if(null==x)return document.createTextNode(k);u=document.createElementNS(o,x,k.is&&k),c&&(l.__m&&l.__m(t,e),c=!1),e=null}if(null==x)m===k||c&&u.data==k||(u.data=k);else{if(e="textarea"==x&&null!=k.defaultValue?null:e&&n.call(u.childNodes),!c&&null!=e)for(m={},s=0;s<u.attributes.length;s++)m[(y=u.attributes[s]).name]=y.value;for(s in m)y=m[s],"dangerouslySetInnerHTML"==s?p=y:"children"==s||s in k||"value"==s&&"defaultValue"in k||"checked"==s&&"defaultChecked"in k||N(u,s,null,y,o);for(s in k)y=k[s],"children"==s?v=y:"dangerouslySetInnerHTML"==s?h=y:"value"==s?w=y:"checked"==s?_=y:c&&"function"!=typeof y||m[s]===y||N(u,s,y,m[s],o);if(h)c||p&&(h.__html==p.__html||h.__html==u.innerHTML)||(u.innerHTML=h.__html),t.__k=[];else if(p&&(u.innerHTML=""),L("template"==t.type?u.content:u,g(v)?v:[v],t,i,r,"foreignObject"==x?"http://www.w3.org/1999/xhtml":o,e,f,e?e[0]:i.__k&&$(i,0),c,a),null!=e)for(s=e.length;s--;)b(e[s]);c&&"textarea"!=x||(s="value","progress"==x&&null==w?u.removeAttribute("value"):null!=w&&(w!==u[s]||"progress"==x&&!w||"option"==x&&w!=m[s])&&N(u,s,w,m[s],o),s="checked",null!=_&&_!=u[s]&&N(u,s,_,m[s],o))}return u}function J(n,u,t){try{if("function"==typeof n){var i="function"==typeof n.__u;i&&n.__u(),i&&null==u||(n.__u=n(u))}else n.current=u}catch(n){l.__e(n,t)}}function K(n,u,t){var i,r;if(l.unmount&&l.unmount(n),(i=n.ref)&&(i.current&&i.current!=n.__e||J(i,null,u)),null!=(i=n.__c)){if(i.componentWillUnmount)try{i.componentWillUnmount()}catch(n){l.__e(n,u)}i.base=i.__P=i.__n=null}if(i=n.__k)for(r=0;r<i.length;r++)i[r]&&K(i[r],u,t||"function"!=typeof n.type);t||b(n.__e),n.__c=n.__=n.__e=void 0}function Q(n,l,u){return this.constructor(n,u)}function R(u,t,i){var r,o,e,f;t==document&&(t=document.documentElement),l.__&&l.__(u,t),o=(r="function"==typeof i)?null:i&&i.__k||t.__k,e=[],f=[],q(t,u=(!r&&i||t).__k=k(S,null,[u]),o||d,d,t.namespaceURI,!r&&i?[i]:o?null:t.firstChild?n.call(t.childNodes):null,e,!r&&i?i:o?o.__e:t.firstChild,r,f),D(e,u,f),u.props.children=null}function U(n,l){R(n,l,U)}function W(l,u,t){var i,r,o,e,f=m({},l.props);for(o in l.type&&l.type.defaultProps&&(e=l.type.defaultProps),u)"key"==o?i=u[o]:"ref"==o?r=u[o]:f[o]=void 0===u[o]&&null!=e?e[o]:u[o];return arguments.length>2&&(f.children=arguments.length>3?n.call(arguments,2):t),x(l.type,f,i||l.key,r||l.ref,null)}function X(n){function l(n){var u,t;return this.getChildContext||(u=new Set,(t={})[l.__c]=this,this.getChildContext=function(){return t},this.componentWillUnmount=function(){u=null},this.shouldComponentUpdate=function(n){this.props.value!=n.value&&u.forEach(function(n){n.__e=!0,A(n)})},this.sub=function(n){u.add(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){u&&u.delete(n),l&&l.call(n)}}),n.children}return l.__c="__cC"+y++,l.__=n,l.Provider=l.__l=(l.Consumer=function(n,l){return n.children(l)}).contextType=l,l}n=w.slice,l={__e:function(n,l,u,t){for(var i,r,o;l=l.__;)if((i=l.__c)&&!i.__)try{if((r=i.constructor)&&null!=r.getDerivedStateFromError&&(i.setState(r.getDerivedStateFromError(n)),o=i.__d),null!=i.componentDidCatch&&(i.componentDidCatch(n,t||{}),o=i.__d),o)return i.__E=i}catch(l){n=l}throw n}},u=0,t=function(n){return null!=n&&void 0===n.constructor},C.prototype.setState=function(n,l){var u;u=null!=this.__s&&this.__s!=this.state?this.__s:this.__s=m({},this.state),"function"==typeof n&&(n=n(m({},u),this.props)),n&&m(u,n),null!=n&&this.__v&&(l&&this._sb.push(l),A(this))},C.prototype.forceUpdate=function(n){this.__v&&(this.__e=!0,n&&this.__h.push(n),A(this))},C.prototype.render=S,i=[],o="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,e=function(n,l){return n.__v.__b-l.__v.__b},H.__r=0,f=Math.random().toString(8),c="__d"+f,a="__a"+f,s=/(PointerCapture)$|Capture$/i,h=0,p=V(!1),v=V(!0),y=0;export{C as Component,S as Fragment,W as cloneElement,X as createContext,k as createElement,M as createRef,k as h,U as hydrate,t as isValidElement,l as options,R as render,F as toChildArray};
|
|
2
|
+
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const i=Symbol.for("preact-signals");function t(){if(e>1){e--;return}let i,t=!1;!function(){let i=f;f=void 0;while(void 0!==i){const t=i.S;if(t.v===i.v)for(let n=t.t;void 0!==n;n=n.x)if(n.i===i.i)n.i=t.i;i=i.o}}();while(void 0!==h){let n=h;h=void 0;c++;while(void 0!==n){const o=n.u;n.u=void 0;n.f&=-3;if(!(8&n.f)&&w(n))try{n.c()}catch(n){if(!t){i=n;t=!0}}n=o}}c=0;e--;if(t)throw i}function n(i){if(e>0)return i();d=++u;e++;try{return i()}finally{t()}}let o,s,h;function r(i){const t=o,n=s;o=void 0;s=void 0;try{return i()}finally{o=t;s=n}}let f,e=0,c=0,u=0,d=0,v=0;function l(i){if(void 0===o)return;let t=i.n;if(void 0===t||t.t!==o){t={i:0,S:i,p:o.s,n:void 0,t:o,e:void 0,x:void 0,r:t};if(void 0!==o.s)o.s.n=t;o.s=t;i.n=t;if(32&o.f)i.S(t);return t}else if(-1===t.i){t.i=0;if(void 0!==t.n){t.n.p=t.p;if(void 0!==t.p)t.p.n=t.n;t.p=o.s;t.n=void 0;o.s.n=t;o.s=t}return t}}function a(i,t){this.v=i;this.i=0;this.n=void 0;this.t=void 0;this.l=0;this.W=null==t?void 0:t.watched;this.Z=null==t?void 0:t.unwatched;this.name=null==t?void 0:t.name}a.prototype.brand=i;a.prototype.h=function(){return!0};a.prototype.S=function(i){const t=this.t;if(t!==i&&void 0===i.e){i.x=t;this.t=i;if(void 0!==t)t.e=i;else r(()=>{var i;null==(i=this.W)||i.call(this)})}};a.prototype.U=function(i){if(void 0!==this.t){const t=i.e,n=i.x;if(void 0!==t){t.x=n;i.e=void 0}if(void 0!==n){n.e=t;i.x=void 0}if(i===this.t){this.t=n;if(void 0===n)r(()=>{var i;null==(i=this.Z)||i.call(this)})}}};a.prototype.subscribe=function(i){return j(()=>{const t=this.value;r(()=>i(t))},{name:"sub"})};a.prototype.valueOf=function(){return this.value};a.prototype.toString=function(){return this.value+""};a.prototype.toJSON=function(){return this.value};a.prototype.peek=function(){return r(()=>this.value)};Object.defineProperty(a.prototype,"value",{get(){const i=l(this);if(void 0!==i)i.i=this.i;return this.v},set(i){if(i!==this.v){if(c>100)throw new Error("Cycle detected");!function(i){if(0!==e&&0===c)if(i.l!==d){i.l=d;f={S:i,v:i.v,i:i.i,o:f}}}(this);this.v=i;this.i++;v++;e++;try{for(let i=this.t;void 0!==i;i=i.x)i.t.N()}finally{t()}}}});function y(i,t){return new a(i,t)}function w(i){for(let t=i.s;void 0!==t;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function _(i){for(let t=i.s;void 0!==t;t=t.n){const n=t.S.n;if(void 0!==n)t.r=n;t.S.n=t;t.i=-1;if(void 0===t.n){i.s=t;break}}}function b(i){let t,n=i.s;while(void 0!==n){const i=n.p;if(-1===n.i){n.S.U(n);if(void 0!==i)i.n=n.n;if(void 0!==n.n)n.n.p=i}else t=n;n.S.n=n.r;if(void 0!==n.r)n.r=void 0;n=i}i.s=t}function p(i,t){a.call(this,void 0,t);this.x=i;this.s=void 0;this.g=v-1;this.f=4}p.prototype=new a;p.prototype.h=function(){this.f&=-3;if(1&this.f)return!1;if(32==(36&this.f))return!0;this.f&=-5;if(this.g===v)return!0;this.g=v;this.f|=1;if(this.i>0&&!w(this)){this.f&=-2;return!0}const i=o;try{_(this);o=this;const i=this.x();if(16&this.f||this.v!==i||0===this.i){this.v=i;this.f&=-17;this.i++}}catch(i){this.v=i;this.f|=16;this.i++}o=i;b(this);this.f&=-2;return!0};p.prototype.S=function(i){if(void 0===this.t){this.f|=36;for(let i=this.s;void 0!==i;i=i.n)i.S.S(i)}a.prototype.S.call(this,i)};p.prototype.U=function(i){if(void 0!==this.t){a.prototype.U.call(this,i);if(void 0===this.t){this.f&=-33;for(let i=this.s;void 0!==i;i=i.n)i.S.U(i)}}};p.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(let i=this.t;void 0!==i;i=i.x)i.t.N()}};Object.defineProperty(p.prototype,"value",{get(){if(1&this.f)throw new Error("Cycle detected");const i=l(this);this.h();if(void 0!==i)i.i=this.i;if(16&this.f)throw this.v;return this.v}});function g(i,t){return new p(i,t)}function S(i){const n=i.m;i.m=void 0;if("function"==typeof n){e++;const s=o;o=void 0;try{n()}catch(t){i.f&=-2;i.f|=8;m(i);throw t}finally{o=s;t()}}}function m(i){for(let t=i.s;void 0!==t;t=t.n)t.S.U(t);i.x=void 0;i.s=void 0;S(i)}function x(i){if(o!==this)throw new Error("Out-of-order effect");b(this);o=i;this.f&=-2;if(8&this.f)m(this);t()}function E(i,t){this.x=i;this.m=void 0;this.s=void 0;this.u=void 0;this.f=32;this.name=null==t?void 0:t.name;if(s)s.push(this)}E.prototype.c=function(){const i=this.S();try{if(8&this.f)return;if(void 0===this.x)return;const t=this.x();if("function"==typeof t)this.m=t}finally{i()}};E.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1;this.f&=-9;S(this);_(this);e++;const i=o;o=this;return x.bind(this,i)};E.prototype.N=function(){if(!(2&this.f)){this.f|=2;this.u=h;h=this}};E.prototype.d=function(){this.f|=8;if(!(1&this.f))m(this)};E.prototype.dispose=function(){this.d()};function j(i,t){const n=new E(i,t);try{n.c()}catch(i){n.d();throw i}const o=n.d.bind(n);o[Symbol.dispose]=o;return o}function C(i){return function(...t){return n(()=>r(()=>i.apply(this,t)))}}const O=i=>{for(const t in i){const n=i[t];if("function"==typeof n)i[t]=C(n);else if("object"==typeof n&&null!==n&&!("brand"in n))O(n)}};function k(i){return function(...t){let n,o;const h=function(){let i=s;s=[];return function(){let t=s;if(s&&i)i=i.concat(s);s=i;return t}}();try{o=i(...t)}catch(i){s=void 0;throw i}finally{n=h()}O(o);o[Symbol.dispose]=C(function(){if(n)for(let i=0;i<n.length;i++)n[i].dispose();n=void 0});return o}}export{p as Computed,E as Effect,a as Signal,C as action,n as batch,g as computed,k as createModel,j as effect,y as signal,r as untracked};
|
|
2
|
+
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Component as t,options as i,isValidElement as n,Fragment as e}from"./preact.js";import{useMemo as o,useRef as f,useEffect as r}from"./hooks.js";import{effect as u,Signal as c,computed as s,signal as l,batch as a}from"./signals-core.js";export{Signal,action,batch,computed,createModel,effect,signal,untracked}from"./signals-core.js";const h="undefined"!=typeof window&&!!window.__PREACT_SIGNALS_DEVTOOLS__;let d,p,m,v=[],_=[];u(function(){d=this.N})();function g(t,n){i[t]=n.bind(null,i[t]||(()=>{}))}function b(t){if(m){const t=m;m=void 0;t()}m=t&&t.S()}function y({data:t}){const i=useSignal(t);i.name="ReactiveDom";i.value=t;const[e,f]=o(()=>{let t=this,e=this.__v;while(e=e.__)if(e.__c){e.__c.__$f|=4;break}const o=s(()=>{let t=i.value.value;return 0===t?0:!0===t?"":t||""}),f=s(()=>!Array.isArray(o.value)&&!n(o.value)),r=u(function(){this.N=F;if(f.value){const i=o.value;if(t.__v&&t.__v.__e&&3===t.__v.__e.nodeType)t.__v.__e.data=i}}),c=this.__$u.d;this.__$u.d=function(){r();c.call(this)};return[f,o]},[]);return e.value?f.peek():f.value}y.displayName="ReactiveTextNode";Object.defineProperties(c.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:y},props:{configurable:!0,get(){const t=this;return{data:{get value(){return t.value}}}}},__b:{configurable:!0,value:1}});g("__b",(t,i)=>{if("string"==typeof i.type){let t,n=i.props;for(let e in n){if("children"===e)continue;let o=n[e];if(o instanceof c){if(!t)i.__np=t={};t[e]=o;n[e]=o.peek()}}}t(i)});g("__r",(t,i)=>{t(i);if(i.type!==e){b();let t,n=i.__c;if(n){n.__$f&=-2;t=n.__$u;if(void 0===t)n.__$u=t=function(t,i){let n;u(function(){n=this},{name:i});n.c=t;return n}(function(t){return function(){var i;if(h)null==(i=this.y)||i.call(this);t.__$f|=1;t.setState({})}}(n),"function"==typeof i.type?i.type.displayName||i.type.name:"")}p=n;b(t)}});g("__e",(t,i,n,e)=>{b();p=void 0;t(i,n,e)});g("diffed",(t,i)=>{b();p=void 0;let n;if("string"==typeof i.type&&(n=i.__e)){let t=i.__np,e=i.props,o=n.U;if(o)for(let i in o){let n=o[i];if(!(void 0===n||t&&i in t)){n.d();o[i]=void 0}}if(t){if(!o){o={};n.U=o}for(let i in t){let f=o[i],r=t[i];if(void 0===f){f=w(n,i,r,e);o[i]=f}else f.o(r,e)}}}t(i)});function w(t,i,n,e){const o=i in t&&void 0===t.ownerSVGElement,f=l(n);return{o:(t,i)=>{f.value=t;e=i},d:u(function(){this.N=F;const n=f.value.value;if(e[i]!==n){e[i]=n;if(o)t[i]=n;else if(null!=n&&(!1!==n||"-"===i[4]))t.setAttribute(i,n);else t.removeAttribute(i)}})}}g("unmount",(t,i)=>{if("string"==typeof i.type){let t=i.__e;if(t){const i=t.U;if(i){t.U=void 0;for(let t in i){let n=i[t];if(n)n.d()}}}let n=i.__np;if(n){let t=i.props;for(let i in n)t[i]=n[i]}i.__np=void 0}else{let t=i.__c;if(t){const i=t.__$u;if(i){t.__$u=void 0;i.d()}}}t(i)});g("__h",(t,i,n,e)=>{if(e<3)i.__$f|=2;t(i,n,e)});t.prototype.shouldComponentUpdate=function(t,i){if(this.__R)return!0;const n=this.__$u,e=n&&void 0!==n.s;for(let t in i)return!0;if(this.__f||"boolean"==typeof this.u&&!0===this.u){const t=2&this.__$f;if(!(e||t||4&this.__$f))return!0;if(1&this.__$f)return!0}else{if(!(e||4&this.__$f))return!0;if(3&this.__$f)return!0}for(let i in t)if("__source"!==i&&t[i]!==this.props[i])return!0;for(let i in this.props)if(!(i in t))return!0;return!1};function useSignal(t,i){return o(()=>l(t,i),[])}function useComputed(t,i){const n=f(t);n.current=t;p.__$f|=4;return o(()=>s(()=>n.current(),i),[])}const k="undefined"==typeof requestAnimationFrame?setTimeout:function(t){const i=()=>{clearTimeout(n);cancelAnimationFrame(e);t()},n=setTimeout(i,35),e=requestAnimationFrame(i)},q=t=>{queueMicrotask(()=>{queueMicrotask(t)})};function A(){a(()=>{let t;while(t=v.shift())d.call(t)})}function T(){if(1===v.push(this))(i.requestAnimationFrame||k)(A)}function x(){a(()=>{let t;while(t=_.shift())d.call(t)})}function F(){if(1===_.push(this))(i.requestAnimationFrame||q)(x)}function useSignalEffect(t,i){const n=f(t);n.current=t;r(()=>u(function(){this.N=T;return n.current()},i),[])}function M(t){const i=o(()=>t(),[]);r(()=>i[Symbol.dispose],[i]);return i}export{useComputed,M as useModel,useSignal,useSignalEffect};
|
|
2
|
+
|