@schukai/monster 4.97.0 → 4.98.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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/source/components/datatable/columnbar.mjs +5 -1
- package/source/components/form/message-state-button.mjs +6 -8
- package/source/components/state/log.mjs +300 -15
- package/source/components/state/style/thread.pcss +172 -0
- package/source/components/state/stylesheet/thread.mjs +38 -0
- package/source/components/state/thread/entry.mjs +242 -0
- package/source/components/state/thread.mjs +1034 -0
- package/source/dom/updater.mjs +8 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
|
|
3
|
+
* Node module: @schukai/monster
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
|
|
6
|
+
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
7
|
+
*
|
|
8
|
+
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
|
|
9
|
+
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
|
|
10
|
+
* For more information about purchasing a commercial license, please contact Volker Schukai.
|
|
11
|
+
*
|
|
12
|
+
* SPDX-License-Identifier: AGPL-3.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { instanceSymbol } from "../../constants.mjs";
|
|
16
|
+
import {
|
|
17
|
+
assembleMethodSymbol,
|
|
18
|
+
CustomElement,
|
|
19
|
+
registerCustomElement,
|
|
20
|
+
} from "../../dom/customelement.mjs";
|
|
21
|
+
import { ThreadStyleSheet } from "./stylesheet/thread.mjs";
|
|
22
|
+
import { Entry } from "./thread/entry.mjs";
|
|
23
|
+
import { validateInstance, validateString } from "../../types/validate.mjs";
|
|
24
|
+
import "./state.mjs";
|
|
25
|
+
import { isArray } from "../../types/is.mjs";
|
|
26
|
+
import { fireCustomEvent } from "../../dom/events.mjs";
|
|
27
|
+
import { ProxyObserver } from "../../types/proxyobserver.mjs";
|
|
28
|
+
import { Updater } from "../../dom/updater.mjs";
|
|
29
|
+
import { Pathfinder } from "../../data/pathfinder.mjs";
|
|
30
|
+
import { getLocaleOfDocument } from "../../dom/locale.mjs";
|
|
31
|
+
|
|
32
|
+
export { Thread };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @private
|
|
36
|
+
* @type {symbol}
|
|
37
|
+
*/
|
|
38
|
+
const collapsedStateSymbol = Symbol("collapsedState");
|
|
39
|
+
const entriesSymbol = Symbol("entries");
|
|
40
|
+
const entryMapSymbol = Symbol("entryMap");
|
|
41
|
+
const entryObserverMapSymbol = Symbol("entryObserverMap");
|
|
42
|
+
const entryUpdaterMapSymbol = Symbol("entryUpdaterMap");
|
|
43
|
+
const entryElementMapSymbol = Symbol("entryElementMap");
|
|
44
|
+
const entryTemplateSymbol = Symbol("entryTemplate");
|
|
45
|
+
const entriesListSymbol = Symbol("entriesList");
|
|
46
|
+
const emptyStateSymbol = Symbol("emptyStateElement");
|
|
47
|
+
const idCounterSymbol = Symbol("idCounter");
|
|
48
|
+
const timeAgoIntervalSymbol = Symbol("timeAgoInterval");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A discussion thread with hierarchical entries.
|
|
52
|
+
*
|
|
53
|
+
* @fragments /fragments/components/state/thread
|
|
54
|
+
*
|
|
55
|
+
* @example /examples/components/state/thread-simple Thread
|
|
56
|
+
*
|
|
57
|
+
* @issue https://localhost.alvine.dev:8444/development/issues/open/374.html
|
|
58
|
+
*
|
|
59
|
+
* @since 3.77.0
|
|
60
|
+
* @copyright Volker Schukai
|
|
61
|
+
* @summary The thread control visualizes nested discussion entries.
|
|
62
|
+
**/
|
|
63
|
+
class Thread extends CustomElement {
|
|
64
|
+
/**
|
|
65
|
+
* @return {void}
|
|
66
|
+
*/
|
|
67
|
+
[assembleMethodSymbol]() {
|
|
68
|
+
super[assembleMethodSymbol]();
|
|
69
|
+
|
|
70
|
+
initControlReferences.call(this);
|
|
71
|
+
this[entriesSymbol] = [];
|
|
72
|
+
this[collapsedStateSymbol] = new Map();
|
|
73
|
+
this[entryMapSymbol] = new Map();
|
|
74
|
+
this[entryObserverMapSymbol] = new Map();
|
|
75
|
+
this[entryUpdaterMapSymbol] = new Map();
|
|
76
|
+
this[entryElementMapSymbol] = new Map();
|
|
77
|
+
this[idCounterSymbol] = 0;
|
|
78
|
+
initTimeAgoTicker.call(this);
|
|
79
|
+
initEventHandler.call(this);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* This method is called by the `instanceof` operator.
|
|
84
|
+
* @return {symbol}
|
|
85
|
+
*/
|
|
86
|
+
static get [instanceSymbol]() {
|
|
87
|
+
return Symbol.for("@schukai/monster/components/state/thread@@instance");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* To set the options via the HTML tag, the attribute `data-monster-options` must be used.
|
|
92
|
+
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
|
|
93
|
+
*
|
|
94
|
+
* The individual configuration values can be found in the table.
|
|
95
|
+
*
|
|
96
|
+
* @property {Object} templates Template definitions
|
|
97
|
+
* @property {string} templates.main Main template
|
|
98
|
+
* @property {Object} labels Labels
|
|
99
|
+
* @property {string} labels.nothingToReport Label for empty state
|
|
100
|
+
* @property {number} updateFrequency Update frequency in milliseconds for the timestamp
|
|
101
|
+
*/
|
|
102
|
+
get defaults() {
|
|
103
|
+
return Object.assign({}, super.defaults, {
|
|
104
|
+
templates: {
|
|
105
|
+
main: getTemplate(),
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
labels: {
|
|
109
|
+
nothingToReport: "There is nothing to report yet.",
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
features: {
|
|
113
|
+
timeAgoMaxHours: 12,
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
updateFrequency: 10000,
|
|
117
|
+
|
|
118
|
+
entries: [],
|
|
119
|
+
|
|
120
|
+
length: 0,
|
|
121
|
+
|
|
122
|
+
timestamp: 0,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} path
|
|
128
|
+
* @param {*} defaultValue
|
|
129
|
+
* @return {*}
|
|
130
|
+
*/
|
|
131
|
+
getOption(path, defaultValue = undefined) {
|
|
132
|
+
if (path === "entries" || path?.startsWith("entries.")) {
|
|
133
|
+
try {
|
|
134
|
+
return new Pathfinder({
|
|
135
|
+
entries: this[entriesSymbol],
|
|
136
|
+
}).getVia(path);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
return defaultValue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return super.getOption(path, defaultValue);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} path
|
|
147
|
+
* @param {*} value
|
|
148
|
+
* @return {Thread}
|
|
149
|
+
*/
|
|
150
|
+
setOption(path, value) {
|
|
151
|
+
if (path === "entries") {
|
|
152
|
+
const prepared = prepareEntries(value);
|
|
153
|
+
this[entriesSymbol] = prepared;
|
|
154
|
+
this[idCounterSymbol] = 0;
|
|
155
|
+
this[collapsedStateSymbol] = new Map();
|
|
156
|
+
renderEntries.call(this, prepared);
|
|
157
|
+
super.setOption("length", countEntries(prepared));
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
super.setOption(path, value);
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {object|string} options
|
|
167
|
+
* @return {Thread}
|
|
168
|
+
*/
|
|
169
|
+
setOptions(options) {
|
|
170
|
+
if (options && typeof options === "object" && options.entries) {
|
|
171
|
+
const { entries, ...rest } = options;
|
|
172
|
+
if (Object.keys(rest).length > 0) {
|
|
173
|
+
super.setOptions(rest);
|
|
174
|
+
}
|
|
175
|
+
this.setOption("entries", entries);
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
super.setOptions(options);
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Clear the thread.
|
|
185
|
+
*
|
|
186
|
+
* @return {Thread}
|
|
187
|
+
*/
|
|
188
|
+
clear() {
|
|
189
|
+
this.setOption("entries", []);
|
|
190
|
+
this.setOption("length", 0);
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Add an entry to the thread.
|
|
196
|
+
*
|
|
197
|
+
* @param {Entry|Object} entry
|
|
198
|
+
* @param {string|null} parentId
|
|
199
|
+
* @return {Thread}
|
|
200
|
+
*/
|
|
201
|
+
addEntry(entry, parentId = null) {
|
|
202
|
+
entry = normalizeEntry(entry);
|
|
203
|
+
|
|
204
|
+
let entries = this.getOption("entries");
|
|
205
|
+
if (!isArray(entries)) {
|
|
206
|
+
entries = [];
|
|
207
|
+
this[entriesSymbol] = entries;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (parentId) {
|
|
211
|
+
const parent =
|
|
212
|
+
this[entryMapSymbol]?.get(parentId) || findEntryById(entries, parentId);
|
|
213
|
+
if (!parent) {
|
|
214
|
+
throw new Error(`parent entry not found: ${parentId}`);
|
|
215
|
+
}
|
|
216
|
+
applyEntryDefaults(entry, { isTopLevel: false });
|
|
217
|
+
|
|
218
|
+
if (parent.collapsed === true) {
|
|
219
|
+
parent.hiddenChildren = isArray(parent.hiddenChildren)
|
|
220
|
+
? parent.hiddenChildren
|
|
221
|
+
: [];
|
|
222
|
+
parent.hiddenChildren.push(entry);
|
|
223
|
+
} else {
|
|
224
|
+
parent.children = isArray(parent.children) ? parent.children : [];
|
|
225
|
+
parent.children.push(entry);
|
|
226
|
+
|
|
227
|
+
const parentElement = this[entryElementMapSymbol]?.get(parentId);
|
|
228
|
+
const childrenList = parentElement?.querySelector(
|
|
229
|
+
"[data-monster-role=children]",
|
|
230
|
+
);
|
|
231
|
+
if (childrenList) {
|
|
232
|
+
renderEntry(this, entry, childrenList);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
parent.replyCount = countEntries([
|
|
237
|
+
...parent.children,
|
|
238
|
+
...parent.hiddenChildren,
|
|
239
|
+
]);
|
|
240
|
+
syncEntryField(this, parentId, "replyCount", parent.replyCount);
|
|
241
|
+
} else {
|
|
242
|
+
entries.push(entry);
|
|
243
|
+
applyEntryDefaults(entry, {
|
|
244
|
+
isTopLevel: true,
|
|
245
|
+
newestEntry: null,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (this[entriesListSymbol]) {
|
|
249
|
+
renderEntry(this, entry, this[entriesListSymbol]);
|
|
250
|
+
}
|
|
251
|
+
if (this[emptyStateSymbol]) {
|
|
252
|
+
this[emptyStateSymbol].style.display = "none";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
indexEntries(this, [entry]);
|
|
257
|
+
super.setOption("length", countEntries(entries));
|
|
258
|
+
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Add a message entry to the thread.
|
|
264
|
+
*
|
|
265
|
+
* @param {string} message
|
|
266
|
+
* @param {Date} date
|
|
267
|
+
* @param {string|null} parentId
|
|
268
|
+
* @return {Thread}
|
|
269
|
+
* @throws {TypeError} message is not a string
|
|
270
|
+
*/
|
|
271
|
+
addMessage(message, date, parentId = null) {
|
|
272
|
+
if (!date) {
|
|
273
|
+
date = new Date();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
validateString(message);
|
|
277
|
+
|
|
278
|
+
this.addEntry(
|
|
279
|
+
new Entry({
|
|
280
|
+
message: message,
|
|
281
|
+
date: date,
|
|
282
|
+
}),
|
|
283
|
+
parentId,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
*
|
|
291
|
+
* @return {string}
|
|
292
|
+
*/
|
|
293
|
+
static getTag() {
|
|
294
|
+
return "monster-thread";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @return {CSSStyleSheet[]}
|
|
299
|
+
*/
|
|
300
|
+
static getCSSStyleSheet() {
|
|
301
|
+
return [ThreadStyleSheet];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Toggle the collapsed state of an entry.
|
|
306
|
+
*
|
|
307
|
+
* @param {string} entryId
|
|
308
|
+
* @return {Thread}
|
|
309
|
+
*/
|
|
310
|
+
toggleEntry(entryId) {
|
|
311
|
+
const current = this[collapsedStateSymbol]?.get(entryId) === true;
|
|
312
|
+
const next = !current;
|
|
313
|
+
this[collapsedStateSymbol]?.set(entryId, next);
|
|
314
|
+
setEntryCollapsed(this, entryId, next);
|
|
315
|
+
setCollapsedInDom(this, entryId, next);
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get collapsed state for all entries.
|
|
321
|
+
*
|
|
322
|
+
* @return {Object<string, boolean>}
|
|
323
|
+
*/
|
|
324
|
+
getCollapsedState() {
|
|
325
|
+
const state = {};
|
|
326
|
+
if (!(this[collapsedStateSymbol] instanceof Map)) {
|
|
327
|
+
return state;
|
|
328
|
+
}
|
|
329
|
+
for (const [key, value] of this[collapsedStateSymbol].entries()) {
|
|
330
|
+
state[key] = value;
|
|
331
|
+
}
|
|
332
|
+
return state;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Set collapsed state for entries by id.
|
|
337
|
+
*
|
|
338
|
+
* @param {Object<string, boolean>} stateMap
|
|
339
|
+
* @return {Thread}
|
|
340
|
+
*/
|
|
341
|
+
setCollapsedState(stateMap) {
|
|
342
|
+
if (!stateMap || typeof stateMap !== "object") {
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!(this[collapsedStateSymbol] instanceof Map)) {
|
|
347
|
+
this[collapsedStateSymbol] = new Map();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const [entryId, value] of Object.entries(stateMap)) {
|
|
351
|
+
const collapsed = Boolean(value);
|
|
352
|
+
this[collapsedStateSymbol].set(entryId, collapsed);
|
|
353
|
+
setEntryCollapsed(this, entryId, collapsed);
|
|
354
|
+
setCollapsedInDom(this, entryId, collapsed);
|
|
355
|
+
}
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get ids of entries that are currently open.
|
|
361
|
+
*
|
|
362
|
+
* @return {string[]}
|
|
363
|
+
*/
|
|
364
|
+
getOpenEntries() {
|
|
365
|
+
return collectCollapsedIds(this[collapsedStateSymbol], false);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get ids of entries that are currently collapsed.
|
|
370
|
+
*
|
|
371
|
+
* @return {string[]}
|
|
372
|
+
*/
|
|
373
|
+
getClosedEntries() {
|
|
374
|
+
return collectCollapsedIds(this[collapsedStateSymbol], true);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
function initEventHandler() {
|
|
382
|
+
const root = this.shadowRoot || this;
|
|
383
|
+
root.addEventListener("click", (event) => {
|
|
384
|
+
const button = event.target.closest("[data-action=toggle]");
|
|
385
|
+
if (!button) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const entryId = button.getAttribute("data-entry-id");
|
|
390
|
+
if (!entryId) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.toggleEntry(entryId);
|
|
395
|
+
const entry = this[entryMapSymbol]?.get(entryId) || null;
|
|
396
|
+
const collapsed = this[collapsedStateSymbol]?.get(entryId) === true;
|
|
397
|
+
fireCustomEvent(
|
|
398
|
+
this,
|
|
399
|
+
collapsed ? "monster-thread-collapse" : "monster-thread-expand",
|
|
400
|
+
{
|
|
401
|
+
entryId,
|
|
402
|
+
entry,
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
root.addEventListener("click", (event) => {
|
|
408
|
+
const button = event.target.closest("button[data-action]");
|
|
409
|
+
if (!button) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const action = button.getAttribute("data-action");
|
|
414
|
+
if (!action || action === "toggle") {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const entryId = button.getAttribute("data-entry-id");
|
|
419
|
+
const entry = this[entryMapSymbol]?.get(entryId) || null;
|
|
420
|
+
fireCustomEvent(this, "monster-thread-action", {
|
|
421
|
+
action,
|
|
422
|
+
entryId,
|
|
423
|
+
entry,
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* @private
|
|
430
|
+
* @param {Entry|Object} entry
|
|
431
|
+
* @return {Entry}
|
|
432
|
+
*/
|
|
433
|
+
function normalizeEntry(entry) {
|
|
434
|
+
if (entry instanceof Entry) {
|
|
435
|
+
return entry;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (entry && typeof entry === "object") {
|
|
439
|
+
return new Entry(entry);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
validateInstance(entry, Entry);
|
|
443
|
+
return entry;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* @private
|
|
448
|
+
* @param {Entry[]|*} entries
|
|
449
|
+
* @return {Entry[]}
|
|
450
|
+
*/
|
|
451
|
+
function prepareEntries(entries) {
|
|
452
|
+
const list = isArray(entries) ? entries.map(normalizeEntry) : [];
|
|
453
|
+
const newestEntry = findNewestEntry(list);
|
|
454
|
+
|
|
455
|
+
for (const entry of list) {
|
|
456
|
+
applyEntryDefaults(entry, {
|
|
457
|
+
isTopLevel: true,
|
|
458
|
+
newestEntry,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return list;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @private
|
|
467
|
+
* @param {Entry} entry
|
|
468
|
+
* @param {{isTopLevel:boolean, newestEntry?:Entry}} context
|
|
469
|
+
* @return {void}
|
|
470
|
+
*/
|
|
471
|
+
function applyEntryDefaults(entry, { isTopLevel, newestEntry } = {}) {
|
|
472
|
+
entry.children = isArray(entry.children)
|
|
473
|
+
? entry.children.map(normalizeEntry)
|
|
474
|
+
: [];
|
|
475
|
+
entry.hiddenChildren = isArray(entry.hiddenChildren)
|
|
476
|
+
? entry.hiddenChildren.map(normalizeEntry)
|
|
477
|
+
: [];
|
|
478
|
+
|
|
479
|
+
if (
|
|
480
|
+
(entry.collapsed === false ||
|
|
481
|
+
entry.collapsed === null ||
|
|
482
|
+
entry.collapsed === undefined) &&
|
|
483
|
+
entry.children.length === 0 &&
|
|
484
|
+
entry.hiddenChildren.length > 0
|
|
485
|
+
) {
|
|
486
|
+
entry.children = entry.hiddenChildren;
|
|
487
|
+
entry.hiddenChildren = [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (let index = 0; index < entry.children.length; index += 1) {
|
|
491
|
+
const child = entry.children[index];
|
|
492
|
+
applyEntryDefaults(child, {
|
|
493
|
+
isTopLevel: false,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
entry.replyCount = countEntries([...entry.children, ...entry.hiddenChildren]);
|
|
498
|
+
|
|
499
|
+
if (entry.collapsed === null || entry.collapsed === undefined) {
|
|
500
|
+
if (isTopLevel) {
|
|
501
|
+
entry.collapsed = newestEntry ? entry !== newestEntry : false;
|
|
502
|
+
} else {
|
|
503
|
+
entry.collapsed = false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (entry.collapsed === true && entry.children.length > 0) {
|
|
508
|
+
entry.hiddenChildren = entry.children;
|
|
509
|
+
entry.children = [];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* @private
|
|
515
|
+
* @param {Entry[]} entries
|
|
516
|
+
* @param {string} id
|
|
517
|
+
* @return {Entry|null}
|
|
518
|
+
*/
|
|
519
|
+
function findEntryById(entries, id) {
|
|
520
|
+
for (const entry of entries) {
|
|
521
|
+
if (entry?.id === id) {
|
|
522
|
+
return entry;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const children = isArray(entry?.children) ? entry.children : [];
|
|
526
|
+
const match = findEntryById(children, id);
|
|
527
|
+
if (match) {
|
|
528
|
+
return match;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const hidden = isArray(entry?.hiddenChildren) ? entry.hiddenChildren : [];
|
|
532
|
+
const hiddenMatch = findEntryById(hidden, id);
|
|
533
|
+
if (hiddenMatch) {
|
|
534
|
+
return hiddenMatch;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* @private
|
|
543
|
+
* @param {Entry[]} entries
|
|
544
|
+
* @return {number}
|
|
545
|
+
*/
|
|
546
|
+
function countEntries(entries) {
|
|
547
|
+
let count = 0;
|
|
548
|
+
for (const entry of entries) {
|
|
549
|
+
count += 1;
|
|
550
|
+
if (isArray(entry?.children) && entry.children.length > 0) {
|
|
551
|
+
count += countEntries(entry.children);
|
|
552
|
+
}
|
|
553
|
+
if (isArray(entry?.hiddenChildren) && entry.hiddenChildren.length > 0) {
|
|
554
|
+
count += countEntries(entry.hiddenChildren);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return count;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @private
|
|
562
|
+
* @param {Entry[]} entries
|
|
563
|
+
* @return {Entry|null}
|
|
564
|
+
*/
|
|
565
|
+
function findNewestEntry(entries) {
|
|
566
|
+
if (!isArray(entries) || entries.length === 0) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let newest = entries[entries.length - 1];
|
|
571
|
+
let newestTime = getEntryTimestamp(newest);
|
|
572
|
+
|
|
573
|
+
for (const entry of entries) {
|
|
574
|
+
const time = getEntryTimestamp(entry);
|
|
575
|
+
if (time >= newestTime) {
|
|
576
|
+
newest = entry;
|
|
577
|
+
newestTime = time;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return newest;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @private
|
|
586
|
+
* @param {Entry} entry
|
|
587
|
+
* @return {number}
|
|
588
|
+
*/
|
|
589
|
+
function getEntryTimestamp(entry) {
|
|
590
|
+
if (!entry?.date) {
|
|
591
|
+
return Number.NEGATIVE_INFINITY;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const time = new Date(entry.date).getTime();
|
|
595
|
+
return Number.isNaN(time) ? Number.NEGATIVE_INFINITY : time;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* @private
|
|
600
|
+
* @param {Thread} thread
|
|
601
|
+
* @param {string} entryId
|
|
602
|
+
* @param {boolean} collapsed
|
|
603
|
+
* @return {void}
|
|
604
|
+
*/
|
|
605
|
+
function setCollapsedInDom(thread, entryId, collapsed) {
|
|
606
|
+
const root = thread.shadowRoot || thread;
|
|
607
|
+
const item = thread[entryElementMapSymbol]?.get(entryId);
|
|
608
|
+
if (!item) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
item.setAttribute("data-collapsed", collapsed ? "true" : "false");
|
|
613
|
+
const children = item.querySelector("[data-monster-role=children]");
|
|
614
|
+
if (children) {
|
|
615
|
+
children.setAttribute("data-collapsed", collapsed ? "true" : "false");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* @private
|
|
621
|
+
* @param {Thread} thread
|
|
622
|
+
* @param {string} entryId
|
|
623
|
+
* @param {boolean} collapsed
|
|
624
|
+
* @return {void}
|
|
625
|
+
*/
|
|
626
|
+
function setEntryCollapsed(thread, entryId, collapsed) {
|
|
627
|
+
const entry = thread[entryMapSymbol]?.get(entryId);
|
|
628
|
+
if (!entry) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
entry.collapsed = collapsed;
|
|
633
|
+
syncEntryField(thread, entryId, "collapsed", collapsed);
|
|
634
|
+
|
|
635
|
+
if (collapsed) {
|
|
636
|
+
if (entry.children.length > 0) {
|
|
637
|
+
entry.hiddenChildren = entry.children;
|
|
638
|
+
entry.children = [];
|
|
639
|
+
}
|
|
640
|
+
} else if (entry.hiddenChildren.length > 0) {
|
|
641
|
+
entry.children = entry.hiddenChildren;
|
|
642
|
+
entry.hiddenChildren = [];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const childrenContainer = thread[entryElementMapSymbol]
|
|
646
|
+
?.get(entryId)
|
|
647
|
+
?.querySelector("[data-monster-role=children]");
|
|
648
|
+
if (!childrenContainer) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (collapsed) {
|
|
653
|
+
clearContainer(childrenContainer);
|
|
654
|
+
removeEntrySubtree(thread, entry.hiddenChildren);
|
|
655
|
+
childrenContainer.style.display = "none";
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
for (const child of entry.children) {
|
|
660
|
+
renderEntry(thread, child, childrenContainer);
|
|
661
|
+
}
|
|
662
|
+
childrenContainer.style.display = "";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* @private
|
|
667
|
+
* @param {Map<string, boolean>} stateMap
|
|
668
|
+
* @param {boolean} collapsed
|
|
669
|
+
* @return {string[]}
|
|
670
|
+
*/
|
|
671
|
+
function collectCollapsedIds(stateMap, collapsed) {
|
|
672
|
+
if (!(stateMap instanceof Map)) {
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const list = [];
|
|
677
|
+
for (const [key, value] of stateMap.entries()) {
|
|
678
|
+
if (Boolean(value) === collapsed) {
|
|
679
|
+
list.push(key);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return list;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* @private
|
|
687
|
+
* @return {void}
|
|
688
|
+
*/
|
|
689
|
+
function initControlReferences() {
|
|
690
|
+
if (!this.shadowRoot) {
|
|
691
|
+
throw new Error("no shadow-root is defined");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
this[entriesListSymbol] = this.shadowRoot.querySelector(
|
|
695
|
+
"[data-monster-role=entries-list]",
|
|
696
|
+
);
|
|
697
|
+
this[emptyStateSymbol] = this.shadowRoot.querySelector(
|
|
698
|
+
"[data-monster-role=empty-state]",
|
|
699
|
+
);
|
|
700
|
+
this[entryTemplateSymbol] = this.shadowRoot.querySelector("template#entry");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* @private
|
|
705
|
+
* @return {void}
|
|
706
|
+
*/
|
|
707
|
+
function initTimeAgoTicker() {
|
|
708
|
+
if (this[timeAgoIntervalSymbol]) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const refresh = () => {
|
|
713
|
+
updateTimeAgo(this);
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
refresh();
|
|
717
|
+
this[timeAgoIntervalSymbol] = setInterval(
|
|
718
|
+
refresh,
|
|
719
|
+
this.getOption("updateFrequency"),
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* @private
|
|
725
|
+
* @param {Entry[]} entries
|
|
726
|
+
* @return {void}
|
|
727
|
+
*/
|
|
728
|
+
function renderEntries(entries) {
|
|
729
|
+
if (!this[entriesListSymbol]) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
clearContainer(this[entriesListSymbol]);
|
|
734
|
+
this[entryMapSymbol] = new Map();
|
|
735
|
+
this[entryObserverMapSymbol] = new Map();
|
|
736
|
+
this[entryUpdaterMapSymbol] = new Map();
|
|
737
|
+
this[entryElementMapSymbol] = new Map();
|
|
738
|
+
|
|
739
|
+
indexEntries(this, entries);
|
|
740
|
+
|
|
741
|
+
const fragment = document.createDocumentFragment();
|
|
742
|
+
for (const entry of entries) {
|
|
743
|
+
renderEntry(this, entry, fragment);
|
|
744
|
+
}
|
|
745
|
+
this[entriesListSymbol].appendChild(fragment);
|
|
746
|
+
updateTimeAgo(this);
|
|
747
|
+
|
|
748
|
+
if (this[emptyStateSymbol]) {
|
|
749
|
+
this[emptyStateSymbol].style.display =
|
|
750
|
+
entries.length > 0 ? "none" : "block";
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* @private
|
|
756
|
+
* @param {Thread} thread
|
|
757
|
+
* @param {Entry} entry
|
|
758
|
+
* @param {HTMLElement} parentList
|
|
759
|
+
* @return {void}
|
|
760
|
+
*/
|
|
761
|
+
function renderEntry(thread, entry, parentList) {
|
|
762
|
+
if (!entry.id) {
|
|
763
|
+
thread[idCounterSymbol] += 1;
|
|
764
|
+
entry.id = `entry-${thread[idCounterSymbol]}`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const template = thread[entryTemplateSymbol];
|
|
768
|
+
if (!template) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const fragment = template.content.cloneNode(true);
|
|
773
|
+
const item = fragment.querySelector("[data-monster-role=entry]");
|
|
774
|
+
if (!item) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
item.setAttribute("data-entry-id", entry.id);
|
|
779
|
+
item.setAttribute("data-collapsed", entry.collapsed ? "true" : "false");
|
|
780
|
+
|
|
781
|
+
parentList.appendChild(item);
|
|
782
|
+
|
|
783
|
+
const observer = new ProxyObserver({ entry });
|
|
784
|
+
const updater = new Updater(item, observer);
|
|
785
|
+
updater.run().catch(() => {});
|
|
786
|
+
|
|
787
|
+
thread[entryObserverMapSymbol].set(entry.id, observer);
|
|
788
|
+
thread[entryUpdaterMapSymbol].set(entry.id, updater);
|
|
789
|
+
thread[entryElementMapSymbol].set(entry.id, item);
|
|
790
|
+
|
|
791
|
+
const childrenContainer = item.querySelector("[data-monster-role=children]");
|
|
792
|
+
if (!childrenContainer) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const timeAgo = item.querySelector("[data-monster-role=time-ago]");
|
|
797
|
+
if (timeAgo) {
|
|
798
|
+
timeAgo.dataset.entryId = entry.id;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const toggleButton = item.querySelector("[data-action=toggle]");
|
|
802
|
+
if (toggleButton) {
|
|
803
|
+
toggleButton.setAttribute("data-entry-id", entry.id);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const children = entry.collapsed ? [] : entry.children;
|
|
807
|
+
if (children.length === 0 && entry.hiddenChildren.length === 0) {
|
|
808
|
+
childrenContainer.style.display = "none";
|
|
809
|
+
} else {
|
|
810
|
+
childrenContainer.style.display = "";
|
|
811
|
+
}
|
|
812
|
+
for (const child of children) {
|
|
813
|
+
renderEntry(thread, child, childrenContainer);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* @private
|
|
819
|
+
* @param {Thread} thread
|
|
820
|
+
* @return {void}
|
|
821
|
+
*/
|
|
822
|
+
function updateTimeAgo(thread) {
|
|
823
|
+
const locale = getLocaleOfDocument().toString();
|
|
824
|
+
const maxHours = Number(thread.getOption("features.timeAgoMaxHours", 12));
|
|
825
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
|
|
826
|
+
for (const [entryId, element] of thread[entryElementMapSymbol].entries()) {
|
|
827
|
+
const entry = thread[entryMapSymbol]?.get(entryId);
|
|
828
|
+
if (!entry?.date) {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const timeElement = element.querySelector("[data-monster-role=time-ago]");
|
|
832
|
+
if (!timeElement) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
timeElement.textContent = formatRelativeTime(
|
|
837
|
+
new Date(entry.date),
|
|
838
|
+
locale,
|
|
839
|
+
maxHours,
|
|
840
|
+
rtf,
|
|
841
|
+
);
|
|
842
|
+
} catch (e) {}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* @private
|
|
848
|
+
* @param {Date} date
|
|
849
|
+
* @param {string} locale
|
|
850
|
+
* @param {number} maxHours
|
|
851
|
+
* @param {Intl.RelativeTimeFormat} rtf
|
|
852
|
+
* @return {string}
|
|
853
|
+
*/
|
|
854
|
+
function formatRelativeTime(date, locale, maxHours, rtf) {
|
|
855
|
+
let diffSeconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
856
|
+
if (!Number.isFinite(diffSeconds) || diffSeconds < 0) {
|
|
857
|
+
diffSeconds = 0;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (diffSeconds < 5) {
|
|
861
|
+
return "just now";
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (diffSeconds < 60) {
|
|
865
|
+
return rtf.format(-diffSeconds, "second");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
869
|
+
if (diffMinutes < 60) {
|
|
870
|
+
return rtf.format(-diffMinutes, "minute");
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const diffHours = Math.floor(diffMinutes / 60);
|
|
874
|
+
if (diffHours < maxHours) {
|
|
875
|
+
return rtf.format(-diffHours, "hour");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return date.toLocaleDateString(locale, {
|
|
879
|
+
year: "numeric",
|
|
880
|
+
month: "2-digit",
|
|
881
|
+
day: "2-digit",
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* @private
|
|
887
|
+
* @param {Thread} thread
|
|
888
|
+
* @param {Entry[]} entries
|
|
889
|
+
* @return {void}
|
|
890
|
+
*/
|
|
891
|
+
function indexEntries(thread, entries) {
|
|
892
|
+
for (const entry of entries) {
|
|
893
|
+
if (!entry.id) {
|
|
894
|
+
thread[idCounterSymbol] += 1;
|
|
895
|
+
entry.id = `entry-${thread[idCounterSymbol]}`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
thread[entryMapSymbol].set(entry.id, entry);
|
|
899
|
+
thread[collapsedStateSymbol].set(entry.id, Boolean(entry.collapsed));
|
|
900
|
+
|
|
901
|
+
const children = [
|
|
902
|
+
...(isArray(entry.children) ? entry.children : []),
|
|
903
|
+
...(isArray(entry.hiddenChildren) ? entry.hiddenChildren : []),
|
|
904
|
+
];
|
|
905
|
+
|
|
906
|
+
if (children.length > 0) {
|
|
907
|
+
indexEntries(thread, children);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* @private
|
|
914
|
+
* @param {Thread} thread
|
|
915
|
+
* @param {string} entryId
|
|
916
|
+
* @param {string} key
|
|
917
|
+
* @param {*} value
|
|
918
|
+
* @return {void}
|
|
919
|
+
*/
|
|
920
|
+
function syncEntryField(thread, entryId, key, value) {
|
|
921
|
+
const observer = thread[entryObserverMapSymbol]?.get(entryId);
|
|
922
|
+
if (!observer) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const subject = observer.getSubject();
|
|
927
|
+
if (!subject?.entry) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
subject.entry[key] = value;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* @private
|
|
936
|
+
* @param {Thread} thread
|
|
937
|
+
* @param {Entry[]} entries
|
|
938
|
+
* @return {void}
|
|
939
|
+
*/
|
|
940
|
+
function removeEntrySubtree(thread, entries) {
|
|
941
|
+
for (const entry of entries) {
|
|
942
|
+
if (entry?.id) {
|
|
943
|
+
thread[entryObserverMapSymbol]?.delete(entry.id);
|
|
944
|
+
thread[entryUpdaterMapSymbol]?.delete(entry.id);
|
|
945
|
+
thread[entryElementMapSymbol]?.delete(entry.id);
|
|
946
|
+
}
|
|
947
|
+
if (isArray(entry?.children) && entry.children.length > 0) {
|
|
948
|
+
removeEntrySubtree(thread, entry.children);
|
|
949
|
+
}
|
|
950
|
+
if (isArray(entry?.hiddenChildren) && entry.hiddenChildren.length > 0) {
|
|
951
|
+
removeEntrySubtree(thread, entry.hiddenChildren);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* @private
|
|
958
|
+
* @param {HTMLElement} container
|
|
959
|
+
* @return {void}
|
|
960
|
+
*/
|
|
961
|
+
function clearContainer(container) {
|
|
962
|
+
while (container.firstChild) {
|
|
963
|
+
container.removeChild(container.firstChild);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* @private
|
|
969
|
+
* @return {string}
|
|
970
|
+
*/
|
|
971
|
+
function getTemplate() {
|
|
972
|
+
// language=HTML
|
|
973
|
+
return `
|
|
974
|
+
<template id="entry">
|
|
975
|
+
<li data-monster-role="entry">
|
|
976
|
+
<div data-monster-role="entry-card">
|
|
977
|
+
<div data-monster-role="meta">
|
|
978
|
+
<span data-monster-replace="path:entry.user"
|
|
979
|
+
data-monster-attributes="class path:entry.user | ?:user:hidden"></span>
|
|
980
|
+
<span data-monster-replace="path:entry.title"
|
|
981
|
+
data-monster-attributes="class path:entry.title | ?:title:hidden"></span>
|
|
982
|
+
<span data-monster-role="time-ago"
|
|
983
|
+
data-monster-replace="path:entry.date | time-ago"
|
|
984
|
+
data-monster-attributes="title path:entry.date | datetime"></span>
|
|
985
|
+
</div>
|
|
986
|
+
<div data-monster-role="message"
|
|
987
|
+
data-monster-replace="path:entry.message"
|
|
988
|
+
data-monster-attributes="class path:entry.message | ?:message:hidden"></div>
|
|
989
|
+
<div data-monster-role="thread-controls">
|
|
990
|
+
<button type="button"
|
|
991
|
+
class="monster-button-outline-secondary"
|
|
992
|
+
data-action="toggle"
|
|
993
|
+
data-monster-attributes="data-entry-id path:entry.id, data-reply-count path:entry.replyCount">
|
|
994
|
+
Replies
|
|
995
|
+
<span data-monster-role="badge"
|
|
996
|
+
data-monster-replace="path:entry.replyCount"
|
|
997
|
+
data-monster-attributes="data-reply-count path:entry.replyCount"></span>
|
|
998
|
+
</button>
|
|
999
|
+
<div data-monster-role="actions"
|
|
1000
|
+
data-monster-replace="path:entry.actions"
|
|
1001
|
+
data-monster-attributes="class path:entry.actions | ?:actions:hidden"></div>
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
<ul data-monster-role="children"></ul>
|
|
1005
|
+
</li>
|
|
1006
|
+
</template>
|
|
1007
|
+
|
|
1008
|
+
<div part="control" data-monster-role="control">
|
|
1009
|
+
<div data-monster-role="empty-state">
|
|
1010
|
+
<monster-state>
|
|
1011
|
+
<div part="visual">
|
|
1012
|
+
<svg width="4rem" height="4rem" viewBox="0 -12 512.00032 512"
|
|
1013
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
1014
|
+
<path d="m455.074219 172.613281 53.996093-53.996093c2.226563-2.222657 3.273438-5.367188 2.828126-8.480469-.441407-3.113281-2.328126-5.839844-5.085938-7.355469l-64.914062-35.644531c-4.839844-2.65625-10.917969-.886719-13.578126 3.953125-2.65625 4.84375-.890624 10.921875 3.953126 13.578125l53.234374 29.230469-46.339843 46.335937-166.667969-91.519531 46.335938-46.335938 46.839843 25.722656c4.839844 2.65625 10.921875.890626 13.578125-3.953124 2.660156-4.839844.890625-10.921876-3.953125-13.578126l-53.417969-29.335937c-3.898437-2.140625-8.742187-1.449219-11.882812 1.695313l-54 54-54-54c-3.144531-3.144532-7.988281-3.832032-11.882812-1.695313l-184.929688 101.546875c-2.757812 1.515625-4.644531 4.238281-5.085938 7.355469-.445312 3.113281.601563 6.257812 2.828126 8.480469l53.996093 53.996093-53.996093 53.992188c-2.226563 2.226562-3.273438 5.367187-2.828126 8.484375.441407 3.113281 2.328126 5.839844 5.085938 7.351562l55.882812 30.6875v102.570313c0 3.652343 1.988282 7.011719 5.1875 8.769531l184.929688 101.542969c1.5.824219 3.15625 1.234375 4.8125 1.234375s3.3125-.410156 4.8125-1.234375l184.929688-101.542969c3.199218-1.757812 5.1875-5.117188 5.1875-8.769531v-102.570313l55.882812-30.683594c2.757812-1.515624 4.644531-4.242187 5.085938-7.355468.445312-3.113282-.601563-6.257813-2.828126-8.480469zm-199.074219 90.132813-164.152344-90.136719 164.152344-90.140625 164.152344 90.140625zm-62.832031-240.367188 46.332031 46.335938-166.667969 91.519531-46.335937-46.335937zm-120.328125 162.609375 166.667968 91.519531-46.339843 46.339844-166.671875-91.519531zm358.089844 184.796875-164.929688 90.5625v-102.222656c0-5.523438-4.476562-10-10-10s-10 4.476562-10 10v102.222656l-164.929688-90.5625v-85.671875l109.046876 59.878907c1.511718.828124 3.167968 1.234374 4.808593 1.234374 2.589844 0 5.152344-1.007812 7.074219-2.929687l54-54 54 54c1.921875 1.925781 4.484375 2.929687 7.074219 2.929687 1.640625 0 3.296875-.40625 4.808593-1.234374l109.046876-59.878907zm-112.09375-46.9375-46.339844-46.34375 166.667968-91.515625 46.34375 46.335938zm0 0"/>
|
|
1015
|
+
<path d="m404.800781 68.175781c2.628907 0 5.199219-1.070312 7.070313-2.933593 1.859375-1.859376 2.929687-4.4375 2.929687-7.066407 0-2.632812-1.070312-5.210937-2.929687-7.070312-1.859375-1.863281-4.441406-2.929688-7.070313-2.929688-2.640625 0-5.210937 1.066407-7.070312 2.929688-1.871094 1.859375-2.929688 4.4375-2.929688 7.070312 0 2.628907 1.058594 5.207031 2.929688 7.066407 1.859375 1.863281 4.441406 2.933593 7.070312 2.933593zm0 0"/>
|
|
1016
|
+
<path d="m256 314.925781c-2.628906 0-5.210938 1.066407-7.070312 2.929688-1.859376 1.867187-2.929688 4.4375-2.929688 7.070312 0 2.636719 1.070312 5.207031 2.929688 7.078125 1.859374 1.859375 4.441406 2.921875 7.070312 2.921875s5.210938-1.0625 7.070312-2.921875c1.859376-1.871094 2.929688-4.441406 2.929688-7.078125 0-2.632812-1.070312-5.203125-2.929688-7.070312-1.859374-1.863281-4.441406-2.929688-7.070312-2.929688zm0 0"/>
|
|
1017
|
+
</svg>
|
|
1018
|
+
</div>
|
|
1019
|
+
<div part="content" data-monster-replace="path:labels.nothingToReport">
|
|
1020
|
+
There is nothing to report yet.
|
|
1021
|
+
</div>
|
|
1022
|
+
</monster-state>
|
|
1023
|
+
</div>
|
|
1024
|
+
<div data-monster-role="entries">
|
|
1025
|
+
<ul data-monster-role="entries-list"></ul>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div part="editor" data-monster-role="editor">
|
|
1028
|
+
<slot name="editor"></slot>
|
|
1029
|
+
</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
`;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
registerCustomElement(Thread);
|