@dou-ui/dashboard 0.1.0-alpha.3 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/document.js +10 -1
- package/dist/core/document.js.map +1 -1
- package/dist/core/history.js +23 -0
- package/dist/core/history.js.map +1 -1
- package/dist/core/transaction.js +8 -2
- package/dist/core/transaction.js.map +1 -1
- package/dist/dashboard/comment-button.js +6 -0
- package/dist/dashboard/comment-button.js.map +1 -0
- package/dist/dashboard/comment-button.vue_vue_type_script_setup_true_lang.js +59 -0
- package/dist/dashboard/comment-button.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/dashboard/comment-card.js +6 -0
- package/dist/dashboard/comment-card.js.map +1 -0
- package/dist/dashboard/comment-card.vue_vue_type_script_setup_true_lang.js +128 -0
- package/dist/dashboard/comment-card.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/dashboard/comments.js +144 -0
- package/dist/dashboard/comments.js.map +1 -0
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/dashboard.vue_vue_type_script_setup_true_lang.js +107 -18
- package/dist/dashboard/dashboard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/dashboard/frame-context.js.map +1 -1
- package/dist/dashboard/widget-frame.js.map +1 -1
- package/dist/dashboard/widget-frame.vue_vue_type_script_setup_true_lang.js +10 -0
- package/dist/dashboard/widget-frame.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/locale.js +6 -0
- package/dist/locale.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/style.js +2 -3
- package/dist/types/core/document.d.ts +3 -2
- package/dist/types/core/history.d.ts +7 -1
- package/dist/types/core/transaction.d.ts +10 -1
- package/dist/types/core/types.d.ts +5 -0
- package/dist/types/dashboard/comment-button.vue.d.ts +15 -0
- package/dist/types/dashboard/comment-card.vue.d.ts +13 -0
- package/dist/types/dashboard/comments.d.ts +44 -0
- package/dist/types/dashboard/dashboard.vue.d.ts +10 -0
- package/dist/types/dashboard/frame-context.d.ts +2 -0
- package/dist/types/dashboard/types.d.ts +30 -0
- package/dist/types/index.d.ts +20 -1
- package/dist/types/locale.d.ts +4 -0
- package/package.json +8 -8
package/dist/core/document.js
CHANGED
|
@@ -32,7 +32,8 @@ function shapeProblems(value, lists) {
|
|
|
32
32
|
/**
|
|
33
33
|
* The problems of a dashboard document, before it is saved or shown: an
|
|
34
34
|
* unknown version, widgets without an id or sharing one, types nobody
|
|
35
|
-
* registered,
|
|
35
|
+
* registered, places that are not whole cells, and `comments` that are
|
|
36
|
+
* not a list of thread ids.
|
|
36
37
|
*/
|
|
37
38
|
function checkDocument(document, options = {}) {
|
|
38
39
|
const shape = shapeProblems(document, ["widgets"]);
|
|
@@ -76,6 +77,14 @@ function checkDocument(document, options = {}) {
|
|
|
76
77
|
text: name,
|
|
77
78
|
message: `The place of "${widget.id}" on "${name}" is not whole cells`
|
|
78
79
|
});
|
|
80
|
+
const notes = widget.comments;
|
|
81
|
+
if (notes !== void 0 && !(Array.isArray(notes) && notes.every((id) => typeof id === "string"))) problems.push({
|
|
82
|
+
code: "bad-comments",
|
|
83
|
+
widgetId: widget.id,
|
|
84
|
+
path: `${path}.comments`,
|
|
85
|
+
text: widget.id,
|
|
86
|
+
message: `The comments of "${widget.id}" are not a list of thread ids`
|
|
87
|
+
});
|
|
79
88
|
});
|
|
80
89
|
return problems;
|
|
81
90
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document.js","names":[],"sources":["../../src/core/document.ts"],"sourcesContent":["import type { DashboardWidgetType } from '../widget/define'\nimport type { DashboardDocument, DashboardPlace, DashboardWidget } from './types'\n\n/** A dashboard with no widgets, the `v-model` when none is bound. */\nexport const emptyDocument = (): DashboardDocument => ({ version: 1, widgets: [] })\n\nexport interface DocumentProblem {\n code
|
|
1
|
+
{"version":3,"file":"document.js","names":[],"sources":["../../src/core/document.ts"],"sourcesContent":["import type { DashboardWidgetType } from '../widget/define'\nimport type { DashboardDocument, DashboardPlace, DashboardWidget } from './types'\n\n/** A dashboard with no widgets, the `v-model` when none is bound. */\nexport const emptyDocument = (): DashboardDocument => ({ version: 1, widgets: [] })\n\nexport interface DocumentProblem {\n code:\n | 'version'\n | 'shape'\n | 'duplicate-id'\n | 'missing-id'\n | 'unknown-type'\n | 'bad-place'\n | 'bad-comments'\n /** The widget at fault; none for the document's own problems. */\n widgetId?: string\n /** Where the problem is, e.g. `widgets[2]` or `widgets[2].place.lg`; `version` for the version. */\n path: string\n /** The id, type or breakpoint at fault, for the editor's message. */\n text: string\n /** An English description, for logs. */\n message: string\n}\n\nexport interface CheckDocumentOptions {\n /** The registered widget types; without them, types are not checked. */\n widgets?: readonly DashboardWidgetType<any>[]\n}\n\nconst isCount = (value: unknown, min: number) =>\n typeof value === 'number' && Number.isInteger(value) && value >= min\n\n/** Whether `value` is a place on a grid: whole numbers, the size at least 1. */\nexport function isDashboardPlace(value: unknown): value is DashboardPlace {\n if (!value || typeof value !== 'object') return false\n const place = value as Record<string, unknown>\n return isCount(place.x, 0) && isCount(place.y, 0) && isCount(place.w, 1) && isCount(place.h, 1)\n}\n\n/**\n * What is wrong with the shape itself: the document is not an object, or a\n * list in it is not an array. Nothing else can be checked until it is fixed.\n */\nfunction shapeProblems(value: unknown, lists: string[]): DocumentProblem[] {\n if (!value || typeof value !== 'object' || Array.isArray(value))\n return [{ code: 'shape', path: '', text: '', message: 'The document is not an object' }]\n return lists\n .filter((key) => !Array.isArray((value as Record<string, unknown>)[key]))\n .map((key) => ({ code: 'shape', path: key, text: '', message: `\"${key}\" is not a list` }))\n}\n\n/**\n * The problems of a dashboard document, before it is saved or shown: an\n * unknown version, widgets without an id or sharing one, types nobody\n * registered, places that are not whole cells, and `comments` that are\n * not a list of thread ids.\n */\nexport function checkDocument(\n document: DashboardDocument,\n options: CheckDocumentOptions = {},\n): DocumentProblem[] {\n const shape = shapeProblems(document, ['widgets'])\n if (shape.length) return shape\n const problems: DocumentProblem[] = []\n if (document.version !== 1)\n problems.push({\n code: 'version',\n path: 'version',\n text: String(document.version),\n message: `Version ${String(document.version)} is not known`,\n })\n const types = options.widgets && new Set(options.widgets.map((type) => type.type))\n const ids = new Set<string>()\n document.widgets.forEach((widget, i) => {\n const path = `widgets[${i}]`\n if (!widget.id) {\n problems.push({ code: 'missing-id', path, text: '', message: `${path} has no id` })\n } else if (ids.has(widget.id)) {\n problems.push({\n code: 'duplicate-id',\n widgetId: widget.id,\n path,\n text: widget.id,\n message: `The id \"${widget.id}\" is used more than once`,\n })\n }\n ids.add(widget.id)\n if (types && !types.has(widget.type))\n problems.push({\n code: 'unknown-type',\n widgetId: widget.id,\n path,\n text: widget.type,\n message: `No widget type \"${widget.type}\" is registered`,\n })\n for (const [name, place] of Object.entries(widget.place ?? {}))\n if (place !== undefined && !isDashboardPlace(place))\n problems.push({\n code: 'bad-place',\n widgetId: widget.id,\n path: `${path}.place.${name}`,\n text: name,\n message: `The place of \"${widget.id}\" on \"${name}\" is not whole cells`,\n })\n const notes: unknown = widget.comments\n if (\n notes !== undefined &&\n !(Array.isArray(notes) && notes.every((id) => typeof id === 'string'))\n )\n problems.push({\n code: 'bad-comments',\n widgetId: widget.id,\n path: `${path}.comments`,\n text: widget.id,\n message: `The comments of \"${widget.id}\" are not a list of thread ids`,\n })\n })\n return problems\n}\n\n/** The saved data as an object of version 1, or an error saying why it can't be. */\nfunction sourceOf(input: unknown): Record<string, unknown> {\n if (!input || typeof input !== 'object' || Array.isArray(input))\n throw new Error(\n `[@dou-ui/dashboard] a dashboard document must be an object, not ${Array.isArray(input) ? 'an array' : (JSON.stringify(input) ?? String(input))}`,\n )\n const source = input as Record<string, unknown>\n const version = source.version ?? 1\n if (version !== 1)\n throw new Error(\n `[@dou-ui/dashboard] unknown document version ${JSON.stringify(version) ?? String(version)} (expected 1); data saved by a newer @dou-ui/dashboard needs that version to open`,\n )\n return source\n}\n\n/**\n * Brings a saved document up to the current version, and fills what an\n * older or hand-written one may lack (`version`, `place`). Widgets that are\n * not objects and places that are not whole cells are dropped; anything\n * else is kept as it is. Throws for data that is not an object\n * (an array, `null`, a string) and for a version it does not know.\n */\nexport function migrateDocument(input: unknown): DashboardDocument {\n const source = sourceOf(input)\n const list = Array.isArray(source.widgets) ? source.widgets : []\n const widgets: DashboardWidget[] = []\n for (const value of list) {\n if (!value || typeof value !== 'object') continue\n const widget = value as Record<string, unknown>\n const place: DashboardWidget['place'] = {}\n if (widget.place && typeof widget.place === 'object')\n for (const [name, entry] of Object.entries(widget.place))\n if (isDashboardPlace(entry))\n place[name] = { x: entry.x, y: entry.y, w: entry.w, h: entry.h }\n widgets.push({\n ...widget,\n id: String(widget.id ?? ''),\n type: String(widget.type ?? ''),\n place,\n } as DashboardWidget)\n }\n return { ...source, version: 1, widgets } as DashboardDocument\n}\n"],"mappings":"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"}
|
package/dist/core/history.js
CHANGED
|
@@ -56,11 +56,34 @@ var DashboardHistory = class {
|
|
|
56
56
|
origin: "redo"
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Gives a change kept out of undo to the widget as undo and redo bring it
|
|
61
|
+
* back, too: comments started on a widget stay on it when its adding is
|
|
62
|
+
* undone and redone.
|
|
63
|
+
*/
|
|
64
|
+
carry(id, patch) {
|
|
65
|
+
const fix = (steps) => steps.map((step) => step.type === "add" && step.widget.id === id ? {
|
|
66
|
+
...step,
|
|
67
|
+
widget: withPatch(step.widget, patch)
|
|
68
|
+
} : step);
|
|
69
|
+
for (const entry of [...this.done, ...this.undone]) {
|
|
70
|
+
entry.steps = fix(entry.steps);
|
|
71
|
+
entry.inverse = fix(entry.inverse);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
59
74
|
clear() {
|
|
60
75
|
this.done = [];
|
|
61
76
|
this.undone = [];
|
|
62
77
|
}
|
|
63
78
|
};
|
|
79
|
+
function withPatch(widget, patch) {
|
|
80
|
+
const next = {
|
|
81
|
+
...widget,
|
|
82
|
+
...patch
|
|
83
|
+
};
|
|
84
|
+
for (const [key, value] of Object.entries(patch)) if (value === void 0) delete next[key];
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
64
87
|
export { DashboardHistory };
|
|
65
88
|
|
|
66
89
|
//# sourceMappingURL=history.js.map
|
package/dist/core/history.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history.js","names":[],"sources":["../../src/core/history.ts"],"sourcesContent":["import { applySteps } from './transaction'\nimport type { DashboardStep, DashboardTransaction } from './transaction'\nimport type { DashboardDocument } from './types'\n\ninterface Entry {\n steps: DashboardStep[]\n inverse: DashboardStep[]\n group?: string\n time: number\n}\n\n/** Transactions of one group merge when they come within this many ms. */\nconst GROUP_WINDOW = 1000\n\n/** Undo and redo of a dashboard: the steps of each transaction and the steps that undo them. */\nexport class DashboardHistory {\n private done: Entry[] = []\n private undone: Entry[] = []\n\n constructor(private depth = 200) {}\n\n get canUndo() {\n return this.done.length > 0\n }\n\n get canRedo() {\n return this.undone.length > 0\n }\n\n record(tr: DashboardTransaction, now = Date.now()) {\n if (!tr.changed || tr.meta.history === false) return\n this.undone = []\n const last = this.done[this.done.length - 1]\n const group = tr.meta.group\n if (last && group && last.group === group && now - last.time < GROUP_WINDOW) {\n last.steps = [...last.steps, ...tr.steps]\n last.inverse = [...tr.inverse, ...last.inverse]\n last.time = now\n return\n }\n this.done.push({ steps: tr.steps.slice(), inverse: tr.inverse.slice(), group, time: now })\n if (this.done.length > this.depth) this.done.shift()\n }\n\n undo(document: DashboardDocument) {\n const entry = this.done.pop()\n if (!entry) return null\n this.undone.push(entry)\n return applySteps(document, entry.inverse, { history: false, origin: 'undo' })\n }\n\n redo(document: DashboardDocument) {\n const entry = this.undone.pop()\n if (!entry) return null\n this.done.push({ ...entry, group: undefined })\n return applySteps(document, entry.steps, { history: false, origin: 'redo' })\n }\n\n clear() {\n this.done = []\n this.undone = []\n }\n}\n"],"mappings":";;;AAYA,IAAM,eAAe;;AAGrB,IAAa,mBAAb,MAA8B;CAIR;CAHpB,OAAwB,CAAC;CACzB,SAA0B,CAAC;CAE3B,YAAY,QAAgB,KAAK;EAAb,KAAA,QAAA;CAAc;CAElC,IAAI,UAAU;EACZ,OAAO,KAAK,KAAK,SAAS;CAC5B;CAEA,IAAI,UAAU;EACZ,OAAO,KAAK,OAAO,SAAS;CAC9B;CAEA,OAAO,IAA0B,MAAM,KAAK,IAAI,GAAG;EACjD,IAAI,CAAC,GAAG,WAAW,GAAG,KAAK,YAAY,OAAO;EAC9C,KAAK,SAAS,CAAC;EACf,MAAM,OAAO,KAAK,KAAK,KAAK,KAAK,SAAS;EAC1C,MAAM,QAAQ,GAAG,KAAK;EACtB,IAAI,QAAQ,SAAS,KAAK,UAAU,SAAS,MAAM,KAAK,OAAO,cAAc;GAC3E,KAAK,QAAQ,CAAC,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK;GACxC,KAAK,UAAU,CAAC,GAAG,GAAG,SAAS,GAAG,KAAK,OAAO;GAC9C,KAAK,OAAO;GACZ;EACF;EACA,KAAK,KAAK,KAAK;GAAE,OAAO,GAAG,MAAM,MAAM;GAAG,SAAS,GAAG,QAAQ,MAAM;GAAG;GAAO,MAAM;EAAI,CAAC;EACzF,IAAI,KAAK,KAAK,SAAS,KAAK,OAAO,KAAK,KAAK,MAAM;CACrD;CAEA,KAAK,UAA6B;EAChC,MAAM,QAAQ,KAAK,KAAK,IAAI;EAC5B,IAAI,CAAC,OAAO,OAAO;EACnB,KAAK,OAAO,KAAK,KAAK;EACtB,OAAO,WAAW,UAAU,MAAM,SAAS;GAAE,SAAS;GAAO,QAAQ;EAAO,CAAC;CAC/E;CAEA,KAAK,UAA6B;EAChC,MAAM,QAAQ,KAAK,OAAO,IAAI;EAC9B,IAAI,CAAC,OAAO,OAAO;EACnB,KAAK,KAAK,KAAK;GAAE,GAAG;GAAO,OAAO,KAAA;EAAU,CAAC;EAC7C,OAAO,WAAW,UAAU,MAAM,OAAO;GAAE,SAAS;GAAO,QAAQ;EAAO,CAAC;CAC7E;CAEA,QAAQ;EACN,KAAK,OAAO,CAAC;EACb,KAAK,SAAS,CAAC;CACjB;AACF"}
|
|
1
|
+
{"version":3,"file":"history.js","names":[],"sources":["../../src/core/history.ts"],"sourcesContent":["import { applySteps } from './transaction'\nimport type { DashboardStep, DashboardTransaction } from './transaction'\nimport type { DashboardDocument, DashboardWidget } from './types'\n\ninterface Entry {\n steps: DashboardStep[]\n inverse: DashboardStep[]\n group?: string\n time: number\n}\n\n/** Transactions of one group merge when they come within this many ms. */\nconst GROUP_WINDOW = 1000\n\n/** Undo and redo of a dashboard: the steps of each transaction and the steps that undo them. */\nexport class DashboardHistory {\n private done: Entry[] = []\n private undone: Entry[] = []\n\n constructor(private depth = 200) {}\n\n get canUndo() {\n return this.done.length > 0\n }\n\n get canRedo() {\n return this.undone.length > 0\n }\n\n record(tr: DashboardTransaction, now = Date.now()) {\n if (!tr.changed || tr.meta.history === false) return\n this.undone = []\n const last = this.done[this.done.length - 1]\n const group = tr.meta.group\n if (last && group && last.group === group && now - last.time < GROUP_WINDOW) {\n last.steps = [...last.steps, ...tr.steps]\n last.inverse = [...tr.inverse, ...last.inverse]\n last.time = now\n return\n }\n this.done.push({ steps: tr.steps.slice(), inverse: tr.inverse.slice(), group, time: now })\n if (this.done.length > this.depth) this.done.shift()\n }\n\n undo(document: DashboardDocument) {\n const entry = this.done.pop()\n if (!entry) return null\n this.undone.push(entry)\n return applySteps(document, entry.inverse, { history: false, origin: 'undo' })\n }\n\n redo(document: DashboardDocument) {\n const entry = this.undone.pop()\n if (!entry) return null\n this.done.push({ ...entry, group: undefined })\n return applySteps(document, entry.steps, { history: false, origin: 'redo' })\n }\n\n /**\n * Gives a change kept out of undo to the widget as undo and redo bring it\n * back, too: comments started on a widget stay on it when its adding is\n * undone and redone.\n */\n carry(id: string, patch: Partial<DashboardWidget>) {\n const fix = (steps: DashboardStep[]) =>\n steps.map((step) =>\n step.type === 'add' && step.widget.id === id\n ? { ...step, widget: withPatch(step.widget, patch) }\n : step,\n )\n for (const entry of [...this.done, ...this.undone]) {\n entry.steps = fix(entry.steps)\n entry.inverse = fix(entry.inverse)\n }\n }\n\n clear() {\n this.done = []\n this.undone = []\n }\n}\n\nfunction withPatch(widget: DashboardWidget, patch: Partial<DashboardWidget>): DashboardWidget {\n const next = { ...widget, ...patch }\n for (const [key, value] of Object.entries(patch))\n if (value === undefined) delete (next as Record<string, unknown>)[key]\n return next\n}\n"],"mappings":";;;AAYA,IAAM,eAAe;;AAGrB,IAAa,mBAAb,MAA8B;CAIR;CAHpB,OAAwB,CAAC;CACzB,SAA0B,CAAC;CAE3B,YAAY,QAAgB,KAAK;EAAb,KAAA,QAAA;CAAc;CAElC,IAAI,UAAU;EACZ,OAAO,KAAK,KAAK,SAAS;CAC5B;CAEA,IAAI,UAAU;EACZ,OAAO,KAAK,OAAO,SAAS;CAC9B;CAEA,OAAO,IAA0B,MAAM,KAAK,IAAI,GAAG;EACjD,IAAI,CAAC,GAAG,WAAW,GAAG,KAAK,YAAY,OAAO;EAC9C,KAAK,SAAS,CAAC;EACf,MAAM,OAAO,KAAK,KAAK,KAAK,KAAK,SAAS;EAC1C,MAAM,QAAQ,GAAG,KAAK;EACtB,IAAI,QAAQ,SAAS,KAAK,UAAU,SAAS,MAAM,KAAK,OAAO,cAAc;GAC3E,KAAK,QAAQ,CAAC,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK;GACxC,KAAK,UAAU,CAAC,GAAG,GAAG,SAAS,GAAG,KAAK,OAAO;GAC9C,KAAK,OAAO;GACZ;EACF;EACA,KAAK,KAAK,KAAK;GAAE,OAAO,GAAG,MAAM,MAAM;GAAG,SAAS,GAAG,QAAQ,MAAM;GAAG;GAAO,MAAM;EAAI,CAAC;EACzF,IAAI,KAAK,KAAK,SAAS,KAAK,OAAO,KAAK,KAAK,MAAM;CACrD;CAEA,KAAK,UAA6B;EAChC,MAAM,QAAQ,KAAK,KAAK,IAAI;EAC5B,IAAI,CAAC,OAAO,OAAO;EACnB,KAAK,OAAO,KAAK,KAAK;EACtB,OAAO,WAAW,UAAU,MAAM,SAAS;GAAE,SAAS;GAAO,QAAQ;EAAO,CAAC;CAC/E;CAEA,KAAK,UAA6B;EAChC,MAAM,QAAQ,KAAK,OAAO,IAAI;EAC9B,IAAI,CAAC,OAAO,OAAO;EACnB,KAAK,KAAK,KAAK;GAAE,GAAG;GAAO,OAAO,KAAA;EAAU,CAAC;EAC7C,OAAO,WAAW,UAAU,MAAM,OAAO;GAAE,SAAS;GAAO,QAAQ;EAAO,CAAC;CAC7E;;;;;;CAOA,MAAM,IAAY,OAAiC;EACjD,MAAM,OAAO,UACX,MAAM,KAAK,SACT,KAAK,SAAS,SAAS,KAAK,OAAO,OAAO,KACtC;GAAE,GAAG;GAAM,QAAQ,UAAU,KAAK,QAAQ,KAAK;EAAE,IACjD,IACN;EACF,KAAK,MAAM,SAAS,CAAC,GAAG,KAAK,MAAM,GAAG,KAAK,MAAM,GAAG;GAClD,MAAM,QAAQ,IAAI,MAAM,KAAK;GAC7B,MAAM,UAAU,IAAI,MAAM,OAAO;EACnC;CACF;CAEA,QAAQ;EACN,KAAK,OAAO,CAAC;EACb,KAAK,SAAS,CAAC;CACjB;AACF;AAEA,SAAS,UAAU,QAAyB,OAAkD;CAC5F,MAAM,OAAO;EAAE,GAAG;EAAQ,GAAG;CAAM;CACnC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,UAAU,KAAA,GAAW,OAAQ,KAAiC;CACpE,OAAO;AACT"}
|
package/dist/core/transaction.js
CHANGED
|
@@ -192,8 +192,14 @@ function longestRun(values) {
|
|
|
192
192
|
* that are gone are removed, new ones added, changed keys updated, and the
|
|
193
193
|
* fewest widgets taken out and put back to match the order. Unchanged
|
|
194
194
|
* widgets keep their objects.
|
|
195
|
+
*
|
|
196
|
+
* A widget that stays keeps its `comments`, the discussions on it, as they
|
|
197
|
+
* are: restoring a default layout doesn't take them off. Pass
|
|
198
|
+
* `{ keepComments: false }` for the exact steps, e.g. to apply a shared
|
|
199
|
+
* document.
|
|
195
200
|
*/
|
|
196
|
-
function replaceWidgets(tr, next) {
|
|
201
|
+
function replaceWidgets(tr, next, options = {}) {
|
|
202
|
+
const keep = options.keepComments !== false;
|
|
197
203
|
const wanted = new Map(next.widgets.map((widget, index) => [widget.id, index]));
|
|
198
204
|
for (const widget of tr.doc.widgets) {
|
|
199
205
|
const index = wanted.get(widget.id);
|
|
@@ -208,7 +214,7 @@ function replaceWidgets(tr, next) {
|
|
|
208
214
|
const patch = {};
|
|
209
215
|
const old = there;
|
|
210
216
|
const item = widget;
|
|
211
|
-
for (const key of /* @__PURE__ */ new Set([...Object.keys(old), ...Object.keys(item)])) if (!PROTECTED.has(key)) patch[key] = item[key];
|
|
217
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(old), ...Object.keys(item)])) if (!PROTECTED.has(key) && !(keep && key === "comments")) patch[key] = item[key];
|
|
212
218
|
tr.update(widget.id, patch);
|
|
213
219
|
});
|
|
214
220
|
return tr;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction.js","names":[],"sources":["../../src/core/transaction.ts"],"sourcesContent":["import type { DashboardDocument, DashboardWidget } from './types'\n\n/**\n * Keys of a widget to set; `undefined` removes the key. `id` and `type`\n * cannot be set: remove the widget and add another instead.\n */\nexport type DashboardWidgetPatch = { [key: string]: unknown } & {\n id?: never\n type?: never\n}\n\n/** One change to a dashboard. `index` is where in `widgets` an added widget goes. */\nexport type DashboardStep =\n | { type: 'add'; widget: DashboardWidget; index: number }\n | { type: 'remove'; id: string }\n | { type: 'update'; id: string; patch: DashboardWidgetPatch }\n\nexport interface DashboardTransactionMeta {\n /** `false` keeps the change out of undo, e.g. a change from another person. */\n history?: boolean\n /** Changes of the same group within a second become one undo step, e.g. typing in a setting. */\n group?: string\n /** Where the change came from: `layout`, `menu`, `keyboard`, `settings`, `api`… */\n origin?: string\n [key: string]: unknown\n}\n\nconst PROTECTED = new Set(['id', 'type'])\n\n/** Deep equality of JSON values; key order doesn't matter. */\nexport function sameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false\n if (Array.isArray(a) !== Array.isArray(b)) return false\n const keys = Object.keys(a)\n if (keys.length !== Object.keys(b).length) return false\n return keys.every((key) =>\n sameValue((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n )\n}\n\n/** Applies one step: the new document and the step that undoes it, or `null` when it changes nothing. */\nfunction applyStep(\n document: DashboardDocument,\n step: DashboardStep,\n): [DashboardDocument, DashboardStep, DashboardStep] | null {\n const widgets = document.widgets\n const indexOf = (id: string) => widgets.findIndex((widget) => widget.id === id)\n if (step.type === 'add') {\n if (indexOf(step.widget.id) >= 0) return null\n const index = Math.max(0, Math.min(widgets.length, step.index))\n const next = [...widgets.slice(0, index), step.widget, ...widgets.slice(index)]\n return [\n { ...document, widgets: next },\n { ...step, index },\n { type: 'remove', id: step.widget.id },\n ]\n }\n const index = indexOf(step.id)\n if (index < 0) return null\n const widget = widgets[index]!\n if (step.type === 'remove') {\n const next = [...widgets.slice(0, index), ...widgets.slice(index + 1)]\n return [{ ...document, widgets: next }, step, { type: 'add', widget, index }]\n }\n const old = widget as unknown as Record<string, unknown>\n const changed: Record<string, unknown> = { ...old }\n const patch: Record<string, unknown> = {}\n const undo: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(step.patch)) {\n if (PROTECTED.has(key))\n throw new Error(`[@dou-ui/dashboard] a widget's \"${key}\" cannot be changed`)\n if (sameValue(old[key], value)) continue\n patch[key] = value\n undo[key] = old[key]\n if (value === undefined) delete changed[key]\n else changed[key] = value\n }\n if (!Object.keys(patch).length) return null\n const next = widgets.slice()\n next[index] = changed as unknown as DashboardWidget\n return [\n { ...document, widgets: next },\n { type: 'update', id: step.id, patch: patch as DashboardWidgetPatch },\n { type: 'update', id: step.id, patch: undo as DashboardWidgetPatch },\n ]\n}\n\n/**\n * Changes to a dashboard, made step by step and then handed to the\n * dashboard's `dispatch` as one change (one undo step). A step that changes\n * nothing (a widget that isn't there, a patch to the same values, an id\n * already in use) is left out.\n *\n * ```ts\n * const tr = dashboard.value.transaction()\n * tr.update('orders', { title: '本周订单' }).remove('memo')\n * dashboard.value.dispatch(tr)\n * ```\n */\nexport class DashboardTransaction {\n /** The document after the steps so far. */\n doc: DashboardDocument\n readonly steps: DashboardStep[] = []\n /** The steps that undo the ones so far, last one first. */\n readonly inverse: DashboardStep[] = []\n readonly meta: DashboardTransactionMeta = {}\n\n constructor(\n /** The document the transaction starts from. */\n readonly before: DashboardDocument,\n ) {\n this.doc = before\n }\n\n /** Whether any step was made. */\n get changed() {\n return this.steps.length > 0\n }\n\n /** Apply a step as it is. */\n step(step: DashboardStep) {\n const result = applyStep(this.doc, step)\n if (!result) return this\n this.doc = result[0]\n this.steps.push(result[1])\n this.inverse.unshift(result[2])\n return this\n }\n\n /** Put a widget into the document at `index` (the end by default). */\n add(widget: DashboardWidget, index = Infinity) {\n return this.step({ type: 'add', widget, index })\n }\n\n /** Take a widget out. */\n remove(id: string) {\n return this.step({ type: 'remove', id })\n }\n\n /** Set keys of a widget, e.g. `{ title }`, `{ props }` or `{ place }`; `undefined` removes a key. */\n update(id: string, patch: DashboardWidgetPatch) {\n return this.step({ type: 'update', id, patch })\n }\n\n setMeta(key: string, value: unknown) {\n this.meta[key] = value\n return this\n }\n}\n\n/**\n * Applies steps to a document in one go, giving the transaction: its `doc`\n * (unchanged widgets are kept as they are) and the `inverse` steps that\n * undo it. For the server, tests, or saved steps.\n */\nexport function applySteps(\n document: DashboardDocument,\n steps: readonly DashboardStep[],\n meta: DashboardTransactionMeta = {},\n): DashboardTransaction {\n const tr = new DashboardTransaction(document)\n Object.assign(tr.meta, meta)\n for (const step of steps) tr.step(step)\n return tr\n}\n\n/** A deep copy of a JSON value (also of a reactive one). */\nexport const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T\n\n/** The indexes of the longest strictly increasing run of `values`. */\nfunction longestRun(values: readonly number[]): Set<number> {\n const tails: number[] = []\n const previous: number[] = []\n values.forEach((value, index) => {\n let low = 0\n let high = tails.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (values[tails[mid]!]! < value) low = mid + 1\n else high = mid\n }\n previous[index] = low > 0 ? tails[low - 1]! : -1\n tails[low] = index\n })\n const kept = new Set<number>()\n let index = tails.length ? tails[tails.length - 1]! : -1\n while (index >= 0) {\n kept.add(index)\n index = previous[index]!\n }\n return kept\n}\n\n/**\n * Adds the steps that turn the transaction's document into `next`: widgets\n * that are gone are removed, new ones added, changed keys updated, and the\n * fewest widgets taken out and put back to match the order. Unchanged\n * widgets keep their objects.\n */\nexport function replaceWidgets(tr: DashboardTransaction, next: DashboardDocument) {\n const wanted = new Map(next.widgets.map((widget, index) => [widget.id, index]))\n for (const widget of tr.doc.widgets) {\n const index = wanted.get(widget.id)\n // a widget whose type changed is a new widget\n if (index === undefined || next.widgets[index]!.type !== widget.type) tr.remove(widget.id)\n }\n const current = tr.doc.widgets\n const kept = longestRun(current.map((widget) => wanted.get(widget.id)!))\n for (const [index, widget] of current.entries()) if (!kept.has(index)) tr.remove(widget.id)\n next.widgets.forEach((widget, index) => {\n const there = tr.doc.widgets[index]\n if (there?.id !== widget.id) return void tr.add(widget, index)\n const patch: Record<string, unknown> = {}\n const old = there as unknown as Record<string, unknown>\n const item = widget as unknown as Record<string, unknown>\n for (const key of new Set([...Object.keys(old), ...Object.keys(item)]))\n if (!PROTECTED.has(key)) patch[key] = item[key]\n tr.update(widget.id, patch as DashboardWidgetPatch)\n })\n return tr\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transaction.js","names":[],"sources":["../../src/core/transaction.ts"],"sourcesContent":["import type { DashboardDocument, DashboardWidget } from './types'\n\n/**\n * Keys of a widget to set; `undefined` removes the key. `id` and `type`\n * cannot be set: remove the widget and add another instead.\n */\nexport type DashboardWidgetPatch = { [key: string]: unknown } & {\n id?: never\n type?: never\n}\n\n/** One change to a dashboard. `index` is where in `widgets` an added widget goes. */\nexport type DashboardStep =\n | { type: 'add'; widget: DashboardWidget; index: number }\n | { type: 'remove'; id: string }\n | { type: 'update'; id: string; patch: DashboardWidgetPatch }\n\nexport interface DashboardTransactionMeta {\n /** `false` keeps the change out of undo, e.g. a change from another person. */\n history?: boolean\n /** Changes of the same group within a second become one undo step, e.g. typing in a setting. */\n group?: string\n /** Where the change came from: `layout`, `menu`, `keyboard`, `settings`, `api`… */\n origin?: string\n [key: string]: unknown\n}\n\nconst PROTECTED = new Set(['id', 'type'])\n\nexport interface ReplaceWidgetsOptions {\n /** A widget that stays keeps its `comments`; `true` by default. */\n keepComments?: boolean\n}\n\n/** Deep equality of JSON values; key order doesn't matter. */\nexport function sameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false\n if (Array.isArray(a) !== Array.isArray(b)) return false\n const keys = Object.keys(a)\n if (keys.length !== Object.keys(b).length) return false\n return keys.every((key) =>\n sameValue((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n )\n}\n\n/** Applies one step: the new document and the step that undoes it, or `null` when it changes nothing. */\nfunction applyStep(\n document: DashboardDocument,\n step: DashboardStep,\n): [DashboardDocument, DashboardStep, DashboardStep] | null {\n const widgets = document.widgets\n const indexOf = (id: string) => widgets.findIndex((widget) => widget.id === id)\n if (step.type === 'add') {\n if (indexOf(step.widget.id) >= 0) return null\n const index = Math.max(0, Math.min(widgets.length, step.index))\n const next = [...widgets.slice(0, index), step.widget, ...widgets.slice(index)]\n return [\n { ...document, widgets: next },\n { ...step, index },\n { type: 'remove', id: step.widget.id },\n ]\n }\n const index = indexOf(step.id)\n if (index < 0) return null\n const widget = widgets[index]!\n if (step.type === 'remove') {\n const next = [...widgets.slice(0, index), ...widgets.slice(index + 1)]\n return [{ ...document, widgets: next }, step, { type: 'add', widget, index }]\n }\n const old = widget as unknown as Record<string, unknown>\n const changed: Record<string, unknown> = { ...old }\n const patch: Record<string, unknown> = {}\n const undo: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(step.patch)) {\n if (PROTECTED.has(key))\n throw new Error(`[@dou-ui/dashboard] a widget's \"${key}\" cannot be changed`)\n if (sameValue(old[key], value)) continue\n patch[key] = value\n undo[key] = old[key]\n if (value === undefined) delete changed[key]\n else changed[key] = value\n }\n if (!Object.keys(patch).length) return null\n const next = widgets.slice()\n next[index] = changed as unknown as DashboardWidget\n return [\n { ...document, widgets: next },\n { type: 'update', id: step.id, patch: patch as DashboardWidgetPatch },\n { type: 'update', id: step.id, patch: undo as DashboardWidgetPatch },\n ]\n}\n\n/**\n * Changes to a dashboard, made step by step and then handed to the\n * dashboard's `dispatch` as one change (one undo step). A step that changes\n * nothing (a widget that isn't there, a patch to the same values, an id\n * already in use) is left out.\n *\n * ```ts\n * const tr = dashboard.value.transaction()\n * tr.update('orders', { title: '本周订单' }).remove('memo')\n * dashboard.value.dispatch(tr)\n * ```\n */\nexport class DashboardTransaction {\n /** The document after the steps so far. */\n doc: DashboardDocument\n readonly steps: DashboardStep[] = []\n /** The steps that undo the ones so far, last one first. */\n readonly inverse: DashboardStep[] = []\n readonly meta: DashboardTransactionMeta = {}\n\n constructor(\n /** The document the transaction starts from. */\n readonly before: DashboardDocument,\n ) {\n this.doc = before\n }\n\n /** Whether any step was made. */\n get changed() {\n return this.steps.length > 0\n }\n\n /** Apply a step as it is. */\n step(step: DashboardStep) {\n const result = applyStep(this.doc, step)\n if (!result) return this\n this.doc = result[0]\n this.steps.push(result[1])\n this.inverse.unshift(result[2])\n return this\n }\n\n /** Put a widget into the document at `index` (the end by default). */\n add(widget: DashboardWidget, index = Infinity) {\n return this.step({ type: 'add', widget, index })\n }\n\n /** Take a widget out. */\n remove(id: string) {\n return this.step({ type: 'remove', id })\n }\n\n /** Set keys of a widget, e.g. `{ title }`, `{ props }` or `{ place }`; `undefined` removes a key. */\n update(id: string, patch: DashboardWidgetPatch) {\n return this.step({ type: 'update', id, patch })\n }\n\n setMeta(key: string, value: unknown) {\n this.meta[key] = value\n return this\n }\n}\n\n/**\n * Applies steps to a document in one go, giving the transaction: its `doc`\n * (unchanged widgets are kept as they are) and the `inverse` steps that\n * undo it. For the server, tests, or saved steps.\n */\nexport function applySteps(\n document: DashboardDocument,\n steps: readonly DashboardStep[],\n meta: DashboardTransactionMeta = {},\n): DashboardTransaction {\n const tr = new DashboardTransaction(document)\n Object.assign(tr.meta, meta)\n for (const step of steps) tr.step(step)\n return tr\n}\n\n/** A deep copy of a JSON value (also of a reactive one). */\nexport const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T\n\n/** The indexes of the longest strictly increasing run of `values`. */\nfunction longestRun(values: readonly number[]): Set<number> {\n const tails: number[] = []\n const previous: number[] = []\n values.forEach((value, index) => {\n let low = 0\n let high = tails.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (values[tails[mid]!]! < value) low = mid + 1\n else high = mid\n }\n previous[index] = low > 0 ? tails[low - 1]! : -1\n tails[low] = index\n })\n const kept = new Set<number>()\n let index = tails.length ? tails[tails.length - 1]! : -1\n while (index >= 0) {\n kept.add(index)\n index = previous[index]!\n }\n return kept\n}\n\n/**\n * Adds the steps that turn the transaction's document into `next`: widgets\n * that are gone are removed, new ones added, changed keys updated, and the\n * fewest widgets taken out and put back to match the order. Unchanged\n * widgets keep their objects.\n *\n * A widget that stays keeps its `comments`, the discussions on it, as they\n * are: restoring a default layout doesn't take them off. Pass\n * `{ keepComments: false }` for the exact steps, e.g. to apply a shared\n * document.\n */\nexport function replaceWidgets(\n tr: DashboardTransaction,\n next: DashboardDocument,\n options: ReplaceWidgetsOptions = {},\n) {\n const keep = options.keepComments !== false\n const wanted = new Map(next.widgets.map((widget, index) => [widget.id, index]))\n for (const widget of tr.doc.widgets) {\n const index = wanted.get(widget.id)\n // a widget whose type changed is a new widget\n if (index === undefined || next.widgets[index]!.type !== widget.type) tr.remove(widget.id)\n }\n const current = tr.doc.widgets\n const kept = longestRun(current.map((widget) => wanted.get(widget.id)!))\n for (const [index, widget] of current.entries()) if (!kept.has(index)) tr.remove(widget.id)\n next.widgets.forEach((widget, index) => {\n const there = tr.doc.widgets[index]\n if (there?.id !== widget.id) return void tr.add(widget, index)\n const patch: Record<string, unknown> = {}\n const old = there as unknown as Record<string, unknown>\n const item = widget as unknown as Record<string, unknown>\n for (const key of new Set([...Object.keys(old), ...Object.keys(item)]))\n if (!PROTECTED.has(key) && !(keep && key === 'comments')) patch[key] = item[key]\n tr.update(widget.id, patch as DashboardWidgetPatch)\n })\n return tr\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import comment_button_vue_vue_type_script_setup_true_lang_default from "./comment-button.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/dashboard/comment-button.vue
|
|
3
|
+
var comment_button_default = comment_button_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { comment_button_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=comment-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comment-button.js","names":["_sfc_main"],"sources":["../../src/dashboard/comment-button.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { MessageCircleIcon } from '@dou-ui/icons'\nimport { DouButton, DouTooltip, type Placement } from 'dou-ui'\nimport { computed } from 'vue'\nimport { format, type DashboardMessages } from '../locale'\nimport type { DashboardComments } from './comments'\n\n/*\n * A widget's comment button, in its title bar and its full screen view:\n * with how many open threads there are, opening them or the box to start one.\n */\n\nconst props = withDefaults(\n defineProps<{\n id: string\n comments: DashboardComments\n messages: DashboardMessages\n /** Where the tooltip goes: in the full screen view, clear of the dialog's close button above. */\n tooltipPlacement?: Placement\n }>(),\n { tooltipPlacement: 'top' },\n)\n\nconst count = computed(() => props.comments.openOn.value.get(props.id)?.length ?? 0)\n/** Shown with something to open, or a thread to start. */\nconst shown = computed(\n () => props.comments.enabled.value && (props.comments.writable.value || count.value > 0),\n)\nconst expanded = computed(() => props.comments.current.value === props.id)\nconst label = computed(() =>\n count.value ? format(props.messages.comments, { count: count.value }) : props.messages.comment,\n)\n\nfunction toggle(event: MouseEvent) {\n if (expanded.value) return props.comments.close()\n // pressed from the keyboard (no clicks counted): straight into the reply box;\n // a click or tap only shows the threads, without bringing up a phone's keyboard\n props.comments.start(props.id, event.detail === 0)\n}\n</script>\n\n<template>\n <DouTooltip v-if=\"shown\" :content=\"label\" :placement=\"tooltipPlacement\">\n <DouButton\n size=\"sm\"\n variant=\"ghost\"\n :class=\"['dou-dashboard__comment', count && 'has-threads']\"\n :aria-label=\"label\"\n :aria-expanded=\"expanded\"\n :data-dou-comment=\"id\"\n @click=\"toggle\"\n >\n <template v-if=\"count\" #leading><MessageCircleIcon /></template>\n <template v-if=\"count\">{{ count }}</template>\n <MessageCircleIcon v-else />\n </DouButton>\n </DouTooltip>\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { format } from "../locale.js";
|
|
2
|
+
import { DouButton, DouTooltip } from "dou-ui";
|
|
3
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createSlots, createTextVNode, createVNode, defineComponent, normalizeClass, openBlock, toDisplayString, unref, withCtx } from "vue";
|
|
4
|
+
import { MessageCircleIcon } from "@dou-ui/icons";
|
|
5
|
+
// src/dashboard/comment-button.vue?vue&type=script&setup=true&lang.ts
|
|
6
|
+
var comment_button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
7
|
+
__name: "comment-button",
|
|
8
|
+
props: {
|
|
9
|
+
id: {},
|
|
10
|
+
comments: {},
|
|
11
|
+
messages: {},
|
|
12
|
+
tooltipPlacement: { default: "top" }
|
|
13
|
+
},
|
|
14
|
+
setup(__props) {
|
|
15
|
+
const props = __props;
|
|
16
|
+
const count = computed(() => props.comments.openOn.value.get(props.id)?.length ?? 0);
|
|
17
|
+
/** Shown with something to open, or a thread to start. */
|
|
18
|
+
const shown = computed(() => props.comments.enabled.value && (props.comments.writable.value || count.value > 0));
|
|
19
|
+
const expanded = computed(() => props.comments.current.value === props.id);
|
|
20
|
+
const label = computed(() => count.value ? format(props.messages.comments, { count: count.value }) : props.messages.comment);
|
|
21
|
+
function toggle(event) {
|
|
22
|
+
if (expanded.value) return props.comments.close();
|
|
23
|
+
props.comments.start(props.id, event.detail === 0);
|
|
24
|
+
}
|
|
25
|
+
return (_ctx, _cache) => {
|
|
26
|
+
return shown.value ? (openBlock(), createBlock(unref(DouTooltip), {
|
|
27
|
+
key: 0,
|
|
28
|
+
content: label.value,
|
|
29
|
+
placement: __props.tooltipPlacement
|
|
30
|
+
}, {
|
|
31
|
+
default: withCtx(() => [createVNode(unref(DouButton), {
|
|
32
|
+
size: "sm",
|
|
33
|
+
variant: "ghost",
|
|
34
|
+
class: normalizeClass(["dou-dashboard__comment", count.value && "has-threads"]),
|
|
35
|
+
"aria-label": label.value,
|
|
36
|
+
"aria-expanded": expanded.value,
|
|
37
|
+
"data-dou-comment": __props.id,
|
|
38
|
+
onClick: toggle
|
|
39
|
+
}, createSlots({
|
|
40
|
+
default: withCtx(() => [count.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(count.value), 1)], 64)) : (openBlock(), createBlock(unref(MessageCircleIcon), { key: 1 }))]),
|
|
41
|
+
_: 2
|
|
42
|
+
}, [count.value ? {
|
|
43
|
+
name: "leading",
|
|
44
|
+
fn: withCtx(() => [createVNode(unref(MessageCircleIcon))]),
|
|
45
|
+
key: "0"
|
|
46
|
+
} : void 0]), 1032, [
|
|
47
|
+
"class",
|
|
48
|
+
"aria-label",
|
|
49
|
+
"aria-expanded",
|
|
50
|
+
"data-dou-comment"
|
|
51
|
+
])]),
|
|
52
|
+
_: 1
|
|
53
|
+
}, 8, ["content", "placement"])) : createCommentVNode("", true);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
export { comment_button_vue_vue_type_script_setup_true_lang_default as default };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=comment-button.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comment-button.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createBlock","_unref","_withCtx","_createVNode","_normalizeClass","_createSlots","_createElementBlock","_Fragment","_createTextVNode","_toDisplayString","_createCommentVNode"],"sources":["../../src/dashboard/comment-button.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { MessageCircleIcon } from '@dou-ui/icons'\nimport { DouButton, DouTooltip, type Placement } from 'dou-ui'\nimport { computed } from 'vue'\nimport { format, type DashboardMessages } from '../locale'\nimport type { DashboardComments } from './comments'\n\n/*\n * A widget's comment button, in its title bar and its full screen view:\n * with how many open threads there are, opening them or the box to start one.\n */\n\nconst props = withDefaults(\n defineProps<{\n id: string\n comments: DashboardComments\n messages: DashboardMessages\n /** Where the tooltip goes: in the full screen view, clear of the dialog's close button above. */\n tooltipPlacement?: Placement\n }>(),\n { tooltipPlacement: 'top' },\n)\n\nconst count = computed(() => props.comments.openOn.value.get(props.id)?.length ?? 0)\n/** Shown with something to open, or a thread to start. */\nconst shown = computed(\n () => props.comments.enabled.value && (props.comments.writable.value || count.value > 0),\n)\nconst expanded = computed(() => props.comments.current.value === props.id)\nconst label = computed(() =>\n count.value ? format(props.messages.comments, { count: count.value }) : props.messages.comment,\n)\n\nfunction toggle(event: MouseEvent) {\n if (expanded.value) return props.comments.close()\n // pressed from the keyboard (no clicks counted): straight into the reply box;\n // a click or tap only shows the threads, without bringing up a phone's keyboard\n props.comments.start(props.id, event.detail === 0)\n}\n</script>\n\n<template>\n <DouTooltip v-if=\"shown\" :content=\"label\" :placement=\"tooltipPlacement\">\n <DouButton\n size=\"sm\"\n variant=\"ghost\"\n :class=\"['dou-dashboard__comment', count && 'has-threads']\"\n :aria-label=\"label\"\n :aria-expanded=\"expanded\"\n :data-dou-comment=\"id\"\n @click=\"toggle\"\n >\n <template v-if=\"count\" #leading><MessageCircleIcon /></template>\n <template v-if=\"count\">{{ count }}</template>\n <MessageCircleIcon v-else />\n </DouButton>\n </DouTooltip>\n</template>\n"],"mappings":";;;;;;;;;;;;;;EAYA,MAAM,QAAQ;EAWd,MAAM,QAAQ,eAAe,MAAM,SAAS,OAAO,MAAM,IAAI,MAAM,EAAE,CAAC,EAAE,UAAU,CAAC;;EAEnF,MAAM,QAAQ,eACN,MAAM,SAAS,QAAQ,UAAU,MAAM,SAAS,SAAS,SAAS,MAAM,QAAQ,EACxF;EACA,MAAM,WAAW,eAAe,MAAM,SAAS,QAAQ,UAAU,MAAM,EAAE;EACzE,MAAM,QAAQ,eACZ,MAAM,QAAQ,OAAO,MAAM,SAAS,UAAU,EAAE,OAAO,MAAM,MAAM,CAAC,IAAI,MAAM,SAAS,OACzF;EAEA,SAAS,OAAO,OAAmB;GACjC,IAAI,SAAS,OAAO,OAAO,MAAM,SAAS,MAAM;GAGhD,MAAM,SAAS,MAAM,MAAM,IAAI,MAAM,WAAW,CAAC;EACnD;;GAIoB,OAAA,MAAA,SAAlBA,UAAA,GAAAC,YAcaC,MAAA,UAAA,GAAA;;IAda,SAAS,MAAA;IAAQ,WAAW,QAAA;;IACpD,SAAAC,cAYY,CAZZC,YAYYF,MAAA,SAAA,GAAA;KAXV,MAAK;KACL,SAAQ;KACP,OAAKG,eAAA,CAAA,0BAA6B,MAAA,SAAK,aAAA,CAAA;KACvC,cAAY,MAAA;KACZ,iBAAe,SAAA;KACf,oBAAkB,QAAA;KAClB,SAAO;;KAGR,SAAAF,cAA6C,CAA7B,MAAA,SAAhBH,UAAA,GAAAO,mBAA6CC,UAAA,EAAA,KAAA,EAAA,GAAA,CAAnBC,gBAAAC,gBAAA,MAAA,KAAK,GAAA,CAAA,C,WAC/BV,UAAA,GAAAC,YAA4BC,MAAA,iBAAA,GAAA,EAAA,KAAA,EAAA,CAAA,E;;I,IAFZ,MAAA,Q;KAAQ,MAAA;KAAQ,IAAAC,cAAqB,CAArBC,YAAqBF,MAAA,iBAAA,CAAA,C"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import comment_card_vue_vue_type_script_setup_true_lang_default from "./comment-card.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/dashboard/comment-card.vue
|
|
3
|
+
var comment_card_default = comment_card_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { comment_card_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=comment-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comment-card.js","names":["_sfc_main"],"sources":["../../src/dashboard/comment-card.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouCommentThread, useFloating, useLayer, useZIndex, type VirtualElement } from 'dou-ui'\nimport { computed, nextTick, onBeforeUnmount, shallowRef, useTemplateRef, watch } from 'vue'\nimport type { DashboardMessages } from '../locale'\nimport type { DashboardComments } from './comments'\n\n/*\n * The card with a widget's discussions, by the comment button in its title\n * bar: every open thread there, or the box for a thread being started.\n */\n\nconst props = defineProps<{\n comments: DashboardComments\n messages: DashboardMessages\n /** The comment button of a widget, to put the card by and give the focus back to. */\n buttonOf: (widget: string) => HTMLElement | null\n /** Where the card goes: the button, or the widget while its button isn't drawn yet. */\n placeOf: (widget: string) => HTMLElement | null\n}>()\n\nconst { comments } = props\nconst panel = shallowRef<HTMLElement | null>(null)\nconst open = computed(() => !!comments.current.value)\n\nconst anchor: VirtualElement = {\n getBoundingClientRect() {\n const widget = comments.current.value\n const el = widget && props.placeOf(widget)\n return el ? el.getBoundingClientRect() : new DOMRect()\n },\n}\n\nconst { style, placement, update } = useFloating(() => (open.value ? anchor : null), panel, {\n open,\n placement: 'bottom-end',\n offset: 6,\n})\nconst { next } = useZIndex()\nconst zIndex = shallowRef(0)\nwatch(open, (value) => value && (zIndex.value = next()), { immediate: true })\n\n// a widget far from the view is drawn once it is scrolled to: then the card moves to its button\nlet frame = 0\nwatch(\n () => comments.current.value,\n (widget) => {\n cancelAnimationFrame(frame)\n if (!widget || props.buttonOf(widget)) return\n let tries = 60\n const wait = () => {\n if (comments.current.value !== widget) return\n if (props.buttonOf(widget)) return update()\n update()\n if (--tries > 0) frame = requestAnimationFrame(wait)\n }\n frame = requestAnimationFrame(wait)\n },\n { flush: 'post' },\n)\nonBeforeUnmount(() => cancelAnimationFrame(frame))\n\nlet lastWidget: string | null = null\nwatch(comments.current, (widget) => widget && (lastWidget = widget), { immediate: true })\n\nfunction restore() {\n const el = lastWidget ? props.buttonOf(lastWidget) : null\n el?.focus({ preventScroll: true })\n}\n\nconst focusLost = () => {\n const el = document.activeElement\n return !el || el === document.body || !!panel.value?.contains(el)\n}\n\nfunction close() {\n const refocus = focusLost()\n comments.close()\n if (refocus) restore()\n}\n\nuseLayer({\n open,\n elements: () => {\n const widget = comments.current.value\n return [panel.value, widget ? props.buttonOf(widget) : null]\n },\n onEscape: () => close(),\n onOutside: () => comments.close(),\n})\n\nconst views = useTemplateRef<InstanceType<typeof DouCommentThread>[]>('views')\n// asked to (the shortcut, the button, a thread just started): the reply box\nwatch(\n () => comments.focusRequest.value,\n async () => {\n await nextTick()\n await nextTick()\n const id = comments.active.value\n const index = comments.shown.value.findIndex((t) => t.id === id)\n views.value?.[Math.max(0, index)]?.focus()\n },\n)\n\n// a thread resolved or removed from the card: back to the button, once the card is gone\nfunction onClosed() {\n const el = document.activeElement\n if (!el || el === document.body) restore()\n}\n</script>\n\n<template>\n <Transition name=\"dou-floating\" @after-leave=\"onClosed\">\n <div\n v-if=\"open\"\n ref=\"panel\"\n class=\"dou-floating dou-dashboard__comment-card\"\n role=\"dialog\"\n :aria-label=\"messages.comment\"\n :style=\"[style, { zIndex }]\"\n :data-placement=\"placement\"\n >\n <DouCommentThread\n v-if=\"comments.pending.value\"\n ref=\"views\"\n key=\"new\"\n :thread=\"null\"\n :author=\"comments.author.value\"\n autofocus\n @action=\"comments.apply\"\n @cancel=\"close\"\n />\n <template v-else>\n <DouCommentThread\n v-for=\"thread in comments.shown.value\"\n :key=\"thread.id\"\n ref=\"views\"\n :thread=\"thread\"\n :author=\"comments.author.value\"\n :readonly=\"!comments.writable.value\"\n @action=\"comments.apply\"\n @cancel=\"close\"\n />\n </template>\n </div>\n </Transition>\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { DouCommentThread, useFloating, useLayer, useZIndex } from "dou-ui";
|
|
2
|
+
import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, defineComponent, nextTick, normalizeStyle, onBeforeUnmount, openBlock, renderList, shallowRef, unref, useTemplateRef, watch, withCtx } from "vue";
|
|
3
|
+
// src/dashboard/comment-card.vue?vue&type=script&setup=true&lang.ts
|
|
4
|
+
var _hoisted_1 = ["aria-label", "data-placement"];
|
|
5
|
+
var comment_card_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
6
|
+
__name: "comment-card",
|
|
7
|
+
props: {
|
|
8
|
+
comments: {},
|
|
9
|
+
messages: {},
|
|
10
|
+
buttonOf: { type: Function },
|
|
11
|
+
placeOf: { type: Function }
|
|
12
|
+
},
|
|
13
|
+
setup(__props) {
|
|
14
|
+
const props = __props;
|
|
15
|
+
const { comments } = props;
|
|
16
|
+
const panel = shallowRef(null);
|
|
17
|
+
const open = computed(() => !!comments.current.value);
|
|
18
|
+
const anchor = { getBoundingClientRect() {
|
|
19
|
+
const widget = comments.current.value;
|
|
20
|
+
const el = widget && props.placeOf(widget);
|
|
21
|
+
return el ? el.getBoundingClientRect() : new DOMRect();
|
|
22
|
+
} };
|
|
23
|
+
const { style, placement, update } = useFloating(() => open.value ? anchor : null, panel, {
|
|
24
|
+
open,
|
|
25
|
+
placement: "bottom-end",
|
|
26
|
+
offset: 6
|
|
27
|
+
});
|
|
28
|
+
const { next } = useZIndex();
|
|
29
|
+
const zIndex = shallowRef(0);
|
|
30
|
+
watch(open, (value) => value && (zIndex.value = next()), { immediate: true });
|
|
31
|
+
let frame = 0;
|
|
32
|
+
watch(() => comments.current.value, (widget) => {
|
|
33
|
+
cancelAnimationFrame(frame);
|
|
34
|
+
if (!widget || props.buttonOf(widget)) return;
|
|
35
|
+
let tries = 60;
|
|
36
|
+
const wait = () => {
|
|
37
|
+
if (comments.current.value !== widget) return;
|
|
38
|
+
if (props.buttonOf(widget)) return update();
|
|
39
|
+
update();
|
|
40
|
+
if (--tries > 0) frame = requestAnimationFrame(wait);
|
|
41
|
+
};
|
|
42
|
+
frame = requestAnimationFrame(wait);
|
|
43
|
+
}, { flush: "post" });
|
|
44
|
+
onBeforeUnmount(() => cancelAnimationFrame(frame));
|
|
45
|
+
let lastWidget = null;
|
|
46
|
+
watch(comments.current, (widget) => widget && (lastWidget = widget), { immediate: true });
|
|
47
|
+
function restore() {
|
|
48
|
+
(lastWidget ? props.buttonOf(lastWidget) : null)?.focus({ preventScroll: true });
|
|
49
|
+
}
|
|
50
|
+
const focusLost = () => {
|
|
51
|
+
const el = document.activeElement;
|
|
52
|
+
return !el || el === document.body || !!panel.value?.contains(el);
|
|
53
|
+
};
|
|
54
|
+
function close() {
|
|
55
|
+
const refocus = focusLost();
|
|
56
|
+
comments.close();
|
|
57
|
+
if (refocus) restore();
|
|
58
|
+
}
|
|
59
|
+
useLayer({
|
|
60
|
+
open,
|
|
61
|
+
elements: () => {
|
|
62
|
+
const widget = comments.current.value;
|
|
63
|
+
return [panel.value, widget ? props.buttonOf(widget) : null];
|
|
64
|
+
},
|
|
65
|
+
onEscape: () => close(),
|
|
66
|
+
onOutside: () => comments.close()
|
|
67
|
+
});
|
|
68
|
+
const views = useTemplateRef("views");
|
|
69
|
+
watch(() => comments.focusRequest.value, async () => {
|
|
70
|
+
await nextTick();
|
|
71
|
+
await nextTick();
|
|
72
|
+
const id = comments.active.value;
|
|
73
|
+
const index = comments.shown.value.findIndex((t) => t.id === id);
|
|
74
|
+
views.value?.[Math.max(0, index)]?.focus();
|
|
75
|
+
});
|
|
76
|
+
function onClosed() {
|
|
77
|
+
const el = document.activeElement;
|
|
78
|
+
if (!el || el === document.body) restore();
|
|
79
|
+
}
|
|
80
|
+
return (_ctx, _cache) => {
|
|
81
|
+
return openBlock(), createBlock(Transition, {
|
|
82
|
+
name: "dou-floating",
|
|
83
|
+
onAfterLeave: onClosed
|
|
84
|
+
}, {
|
|
85
|
+
default: withCtx(() => [open.value ? (openBlock(), createElementBlock("div", {
|
|
86
|
+
key: 0,
|
|
87
|
+
ref_key: "panel",
|
|
88
|
+
ref: panel,
|
|
89
|
+
class: "dou-floating dou-dashboard__comment-card",
|
|
90
|
+
role: "dialog",
|
|
91
|
+
"aria-label": __props.messages.comment,
|
|
92
|
+
style: normalizeStyle([unref(style), { zIndex: zIndex.value }]),
|
|
93
|
+
"data-placement": unref(placement)
|
|
94
|
+
}, [unref(comments).pending.value ? (openBlock(), createBlock(unref(DouCommentThread), {
|
|
95
|
+
ref_key: "views",
|
|
96
|
+
ref: views,
|
|
97
|
+
key: "new",
|
|
98
|
+
thread: null,
|
|
99
|
+
author: unref(comments).author.value,
|
|
100
|
+
autofocus: "",
|
|
101
|
+
onAction: unref(comments).apply,
|
|
102
|
+
onCancel: close
|
|
103
|
+
}, null, 8, ["author", "onAction"])) : (openBlock(true), createElementBlock(Fragment, { key: 1 }, renderList(unref(comments).shown.value, (thread) => {
|
|
104
|
+
return openBlock(), createBlock(unref(DouCommentThread), {
|
|
105
|
+
key: thread.id,
|
|
106
|
+
ref_for: true,
|
|
107
|
+
ref_key: "views",
|
|
108
|
+
ref: views,
|
|
109
|
+
thread,
|
|
110
|
+
author: unref(comments).author.value,
|
|
111
|
+
readonly: !unref(comments).writable.value,
|
|
112
|
+
onAction: unref(comments).apply,
|
|
113
|
+
onCancel: close
|
|
114
|
+
}, null, 8, [
|
|
115
|
+
"thread",
|
|
116
|
+
"author",
|
|
117
|
+
"readonly",
|
|
118
|
+
"onAction"
|
|
119
|
+
]);
|
|
120
|
+
}), 128))], 12, _hoisted_1)) : createCommentVNode("", true)]),
|
|
121
|
+
_: 1
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
export { comment_card_vue_vue_type_script_setup_true_lang_default as default };
|
|
127
|
+
|
|
128
|
+
//# sourceMappingURL=comment-card.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comment-card.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createBlock","_Transition","_withCtx","_createElementBlock","_normalizeStyle","_unref","_Fragment","_renderList","_createCommentVNode"],"sources":["../../src/dashboard/comment-card.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouCommentThread, useFloating, useLayer, useZIndex, type VirtualElement } from 'dou-ui'\nimport { computed, nextTick, onBeforeUnmount, shallowRef, useTemplateRef, watch } from 'vue'\nimport type { DashboardMessages } from '../locale'\nimport type { DashboardComments } from './comments'\n\n/*\n * The card with a widget's discussions, by the comment button in its title\n * bar: every open thread there, or the box for a thread being started.\n */\n\nconst props = defineProps<{\n comments: DashboardComments\n messages: DashboardMessages\n /** The comment button of a widget, to put the card by and give the focus back to. */\n buttonOf: (widget: string) => HTMLElement | null\n /** Where the card goes: the button, or the widget while its button isn't drawn yet. */\n placeOf: (widget: string) => HTMLElement | null\n}>()\n\nconst { comments } = props\nconst panel = shallowRef<HTMLElement | null>(null)\nconst open = computed(() => !!comments.current.value)\n\nconst anchor: VirtualElement = {\n getBoundingClientRect() {\n const widget = comments.current.value\n const el = widget && props.placeOf(widget)\n return el ? el.getBoundingClientRect() : new DOMRect()\n },\n}\n\nconst { style, placement, update } = useFloating(() => (open.value ? anchor : null), panel, {\n open,\n placement: 'bottom-end',\n offset: 6,\n})\nconst { next } = useZIndex()\nconst zIndex = shallowRef(0)\nwatch(open, (value) => value && (zIndex.value = next()), { immediate: true })\n\n// a widget far from the view is drawn once it is scrolled to: then the card moves to its button\nlet frame = 0\nwatch(\n () => comments.current.value,\n (widget) => {\n cancelAnimationFrame(frame)\n if (!widget || props.buttonOf(widget)) return\n let tries = 60\n const wait = () => {\n if (comments.current.value !== widget) return\n if (props.buttonOf(widget)) return update()\n update()\n if (--tries > 0) frame = requestAnimationFrame(wait)\n }\n frame = requestAnimationFrame(wait)\n },\n { flush: 'post' },\n)\nonBeforeUnmount(() => cancelAnimationFrame(frame))\n\nlet lastWidget: string | null = null\nwatch(comments.current, (widget) => widget && (lastWidget = widget), { immediate: true })\n\nfunction restore() {\n const el = lastWidget ? props.buttonOf(lastWidget) : null\n el?.focus({ preventScroll: true })\n}\n\nconst focusLost = () => {\n const el = document.activeElement\n return !el || el === document.body || !!panel.value?.contains(el)\n}\n\nfunction close() {\n const refocus = focusLost()\n comments.close()\n if (refocus) restore()\n}\n\nuseLayer({\n open,\n elements: () => {\n const widget = comments.current.value\n return [panel.value, widget ? props.buttonOf(widget) : null]\n },\n onEscape: () => close(),\n onOutside: () => comments.close(),\n})\n\nconst views = useTemplateRef<InstanceType<typeof DouCommentThread>[]>('views')\n// asked to (the shortcut, the button, a thread just started): the reply box\nwatch(\n () => comments.focusRequest.value,\n async () => {\n await nextTick()\n await nextTick()\n const id = comments.active.value\n const index = comments.shown.value.findIndex((t) => t.id === id)\n views.value?.[Math.max(0, index)]?.focus()\n },\n)\n\n// a thread resolved or removed from the card: back to the button, once the card is gone\nfunction onClosed() {\n const el = document.activeElement\n if (!el || el === document.body) restore()\n}\n</script>\n\n<template>\n <Transition name=\"dou-floating\" @after-leave=\"onClosed\">\n <div\n v-if=\"open\"\n ref=\"panel\"\n class=\"dou-floating dou-dashboard__comment-card\"\n role=\"dialog\"\n :aria-label=\"messages.comment\"\n :style=\"[style, { zIndex }]\"\n :data-placement=\"placement\"\n >\n <DouCommentThread\n v-if=\"comments.pending.value\"\n ref=\"views\"\n key=\"new\"\n :thread=\"null\"\n :author=\"comments.author.value\"\n autofocus\n @action=\"comments.apply\"\n @cancel=\"close\"\n />\n <template v-else>\n <DouCommentThread\n v-for=\"thread in comments.shown.value\"\n :key=\"thread.id\"\n ref=\"views\"\n :thread=\"thread\"\n :author=\"comments.author.value\"\n :readonly=\"!comments.writable.value\"\n @action=\"comments.apply\"\n @cancel=\"close\"\n />\n </template>\n </div>\n </Transition>\n</template>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { applyCommentAction } from "dou-ui";
|
|
2
|
+
import { computed, shallowRef } from "vue";
|
|
3
|
+
// src/dashboard/comments.ts
|
|
4
|
+
/** The thread ids on a widget; nothing when a hand-written document has something else there. */
|
|
5
|
+
var commentIdsOf = (widget) => Array.isArray(widget.comments) ? widget.comments.filter((id) => typeof id === "string") : [];
|
|
6
|
+
/**
|
|
7
|
+
* The comments of a dashboard: which widget each thread is about, the thread
|
|
8
|
+
* open now, and the one being started.
|
|
9
|
+
*/
|
|
10
|
+
function useDashboardComments(options) {
|
|
11
|
+
const { props, emit, model, run, carry } = options;
|
|
12
|
+
const threads = computed(() => props.comments ?? []);
|
|
13
|
+
const enabled = computed(() => props.comments !== void 0);
|
|
14
|
+
const writable = computed(() => enabled.value && !!props.author && props.commentable !== false);
|
|
15
|
+
const innerActive = shallowRef(null);
|
|
16
|
+
const active = computed(() => props.activeComment === void 0 ? innerActive.value : props.activeComment);
|
|
17
|
+
/** The last thread this dashboard opened, so a `v-model:activeComment` echoing it isn't taken for one from outside. */
|
|
18
|
+
let ownOpen = null;
|
|
19
|
+
function open(id) {
|
|
20
|
+
if (id === active.value) return;
|
|
21
|
+
innerActive.value = id;
|
|
22
|
+
ownOpen = id;
|
|
23
|
+
emit("update:activeComment", id);
|
|
24
|
+
}
|
|
25
|
+
const openedHere = (id) => id === ownOpen;
|
|
26
|
+
/** The widget a thread is being started on, until its first comment is sent. */
|
|
27
|
+
const pending = shallowRef(null);
|
|
28
|
+
/** Goes up when the open card should take the focus, ready for a reply. */
|
|
29
|
+
const focusRequest = shallowRef(0);
|
|
30
|
+
/** The open threads on each widget, oldest first. */
|
|
31
|
+
const openOn = computed(() => {
|
|
32
|
+
const live = new Set(threads.value.filter((t) => !t.resolved).map((t) => t.id));
|
|
33
|
+
const out = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const widget of model.value.widgets) {
|
|
35
|
+
const ids = commentIdsOf(widget).filter((id) => live.has(id));
|
|
36
|
+
if (ids.length) out.set(widget.id, ids);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
});
|
|
40
|
+
function widgetOf(id) {
|
|
41
|
+
return model.value.widgets.find((widget) => commentIdsOf(widget).includes(id))?.id ?? null;
|
|
42
|
+
}
|
|
43
|
+
/** The widget the card is for: the one a thread is being started on, or the open thread's. */
|
|
44
|
+
const current = computed(() => {
|
|
45
|
+
if (pending.value) return pending.value;
|
|
46
|
+
const id = active.value;
|
|
47
|
+
return id ? widgetOf(id) : null;
|
|
48
|
+
});
|
|
49
|
+
/** The threads the card shows: every open one on the widget, or the open one if it is resolved. */
|
|
50
|
+
const shown = computed(() => {
|
|
51
|
+
const widget = current.value;
|
|
52
|
+
if (!widget || pending.value) return [];
|
|
53
|
+
const ids = openOn.value.get(widget) ?? [];
|
|
54
|
+
const list = active.value && !ids.includes(active.value) ? [...ids, active.value] : ids;
|
|
55
|
+
const byId = new Map(threads.value.map((t) => [t.id, t]));
|
|
56
|
+
return list.flatMap((id) => byId.get(id) ?? []);
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Opens the discussions on a widget, or the box to start one. `focus`
|
|
60
|
+
* puts the focus in the reply box: from the keyboard, not from a tap,
|
|
61
|
+
* which would bring up the keyboard on a phone.
|
|
62
|
+
*/
|
|
63
|
+
function start(widget, focus = true) {
|
|
64
|
+
if (!enabled.value || !model.value.widgets.some((w) => w.id === widget)) return false;
|
|
65
|
+
const [first] = openOn.value.get(widget) ?? [];
|
|
66
|
+
if (first) {
|
|
67
|
+
pending.value = null;
|
|
68
|
+
open(first);
|
|
69
|
+
if (focus) focusRequest.value++;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (!writable.value) return false;
|
|
73
|
+
open(null);
|
|
74
|
+
pending.value = widget;
|
|
75
|
+
focusRequest.value++;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
function close() {
|
|
79
|
+
pending.value = null;
|
|
80
|
+
open(null);
|
|
81
|
+
}
|
|
82
|
+
/** Adds or takes away a thread's id on its widget; not an undo step. */
|
|
83
|
+
function mark(widget, id, add) {
|
|
84
|
+
const found = model.value.widgets.find((w) => w.id === widget);
|
|
85
|
+
if (!found) return;
|
|
86
|
+
const had = commentIdsOf(found);
|
|
87
|
+
const next = add ? [...had.filter((x) => x !== id), id] : had.filter((x) => x !== id);
|
|
88
|
+
const patch = { comments: next.length ? next : void 0 };
|
|
89
|
+
run([{
|
|
90
|
+
type: "update",
|
|
91
|
+
id: widget,
|
|
92
|
+
patch
|
|
93
|
+
}], {
|
|
94
|
+
history: false,
|
|
95
|
+
origin: "comment"
|
|
96
|
+
});
|
|
97
|
+
carry(widget, patch);
|
|
98
|
+
}
|
|
99
|
+
function apply(action) {
|
|
100
|
+
if (action.type === "create") {
|
|
101
|
+
const widget = pending.value;
|
|
102
|
+
if (!widget) return;
|
|
103
|
+
mark(widget, action.thread.id, true);
|
|
104
|
+
pending.value = null;
|
|
105
|
+
} else if (action.type === "remove") {
|
|
106
|
+
const widget = widgetOf(action.threadId);
|
|
107
|
+
if (widget) mark(widget, action.threadId, false);
|
|
108
|
+
}
|
|
109
|
+
emit("update:comments", applyCommentAction(threads.value, action));
|
|
110
|
+
emit("commentAction", action);
|
|
111
|
+
if (action.type === "create") {
|
|
112
|
+
open(action.thread.id);
|
|
113
|
+
focusRequest.value++;
|
|
114
|
+
} else if (action.type === "remove" || action.type === "resolve") open(null);
|
|
115
|
+
}
|
|
116
|
+
function anchors() {
|
|
117
|
+
return Object.fromEntries(threads.value.map((thread) => {
|
|
118
|
+
const widget = widgetOf(thread.id);
|
|
119
|
+
return [thread.id, widget ? { widget } : null];
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
enabled,
|
|
124
|
+
writable,
|
|
125
|
+
threads,
|
|
126
|
+
author: computed(() => props.author),
|
|
127
|
+
active,
|
|
128
|
+
pending,
|
|
129
|
+
focusRequest,
|
|
130
|
+
openOn,
|
|
131
|
+
current,
|
|
132
|
+
shown,
|
|
133
|
+
start,
|
|
134
|
+
apply,
|
|
135
|
+
open,
|
|
136
|
+
close,
|
|
137
|
+
openedHere,
|
|
138
|
+
anchors,
|
|
139
|
+
widgetOf
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export { commentIdsOf, useDashboardComments };
|
|
143
|
+
|
|
144
|
+
//# sourceMappingURL=comments.js.map
|