@formicoidea/labre-core 0.52.0 → 0.54.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/dist/_pkgs/affine-block-data-view/data-view-block.js +1 -1
- package/dist/_pkgs/affine-block-database/components/title/index.js +7 -2
- package/dist/_pkgs/affine-block-database/database-block-styles.js +9 -1
- package/dist/_pkgs/affine-block-embed-doc/page-drop/page-drop.d.ts +70 -0
- package/dist/_pkgs/affine-block-embed-doc/page-drop/page-drop.js +196 -0
- package/dist/_pkgs/affine-block-embed-doc/view.js +3 -0
- package/dist/_pkgs/affine-block-frame/frame-template.d.ts +11 -0
- package/dist/_pkgs/affine-block-frame/frame-template.js +24 -4
- package/dist/_pkgs/affine-block-frame/translations.d.ts +3 -0
- package/dist/_pkgs/affine-block-frame/translations.js +7 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/render.js +9 -3
- package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +9 -5
- package/dist/_pkgs/affine-block-surface/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-surface/index.js +1 -0
- package/dist/_pkgs/affine-block-surface/renderer/canvas-renderer.d.ts +55 -3
- package/dist/_pkgs/affine-block-surface/renderer/canvas-renderer.js +139 -6
- package/dist/_pkgs/affine-block-surface/renderer/notation-tint.d.ts +92 -0
- package/dist/_pkgs/affine-block-surface/renderer/notation-tint.js +113 -0
- package/dist/_pkgs/affine-block-surface/surface-block.js +2 -2
- package/dist/_pkgs/affine-components/color-picker/styles.js +1 -1
- package/dist/_pkgs/affine-components/context-menu/input.js +2 -1
- package/dist/_pkgs/affine-components/context-menu/menu-renderer.d.ts +7 -0
- package/dist/_pkgs/affine-components/context-menu/menu-renderer.js +53 -13
- package/dist/_pkgs/affine-components/drop-indicator/file-drop-manager.js +15 -0
- package/dist/_pkgs/affine-gfx-shape/element-renderer/shape/index.js +4 -2
- package/dist/_pkgs/affine-inline-link/link-node/link-popup/styles.js +1 -1
- package/dist/_pkgs/affine-inline-reference/reference-node/reference-popup/reference-popup.js +9 -9
- package/dist/_pkgs/affine-shared/consts/notation.d.ts +1 -0
- package/dist/_pkgs/affine-shared/consts/notation.js +29 -0
- package/dist/_pkgs/affine-shared/services/frame-template-service.d.ts +5 -0
- package/dist/_pkgs/affine-shared/services/index.d.ts +1 -0
- package/dist/_pkgs/affine-shared/services/index.js +1 -0
- package/dist/_pkgs/affine-shared/services/page-drop-service.d.ts +61 -0
- package/dist/_pkgs/affine-shared/services/page-drop-service.js +30 -0
- package/dist/_pkgs/affine-shared/services/thread-marks-service.d.ts +32 -1
- package/dist/_pkgs/affine-shared/styles/scrollbar-style.js +1 -1
- package/dist/_pkgs/affine-widget-edgeless-auto-connect/index.js +5 -5
- package/dist/_pkgs/affine-widget-edgeless-selected-rect/edgeless-auto-complete.js +2 -2
- package/dist/_pkgs/affine-widget-keyboard-toolbar/config.js +6 -6
- package/dist/_pkgs/affine-widget-keyboard-toolbar/styles.js +1 -1
- package/dist/_pkgs/affine-widget-linked-doc/config.d.ts +8 -0
- package/dist/_pkgs/affine-widget-linked-doc/config.js +8 -1
- package/dist/_pkgs/affine-widget-linked-doc/linked-doc-popover.js +40 -32
- package/dist/_pkgs/affine-widget-linked-doc/styles.js +1 -1
- package/dist/_pkgs/affine-widget-linked-doc/translations.d.ts +2 -0
- package/dist/_pkgs/affine-widget-linked-doc/translations.js +6 -0
- package/dist/_pkgs/affine-widget-thread-marks/excerpt.d.ts +6 -2
- package/dist/_pkgs/affine-widget-thread-marks/excerpt.js +8 -0
- package/dist/_pkgs/affine-widget-thread-marks/index.d.ts +1 -0
- package/dist/_pkgs/affine-widget-thread-marks/index.js +1 -0
- package/dist/_pkgs/affine-widget-thread-marks/text-range.d.ts +26 -0
- package/dist/_pkgs/affine-widget-thread-marks/text-range.js +59 -0
- package/dist/_pkgs/affine-widget-thread-marks/toolbar.d.ts +4 -3
- package/dist/_pkgs/affine-widget-thread-marks/toolbar.js +24 -8
- package/dist/_pkgs/affine-widget-thread-marks/widget.d.ts +33 -2
- package/dist/_pkgs/affine-widget-thread-marks/widget.js +121 -5
- package/dist/_pkgs/affine-widget-toolbar/utils.d.ts +2 -1
- package/dist/_pkgs/affine-widget-toolbar/utils.js +5 -2
- package/dist/_pkgs/data-view/core/common/properties.js +2 -2
- package/dist/_pkgs/data-view/core/common/property-menu.d.ts +3 -3
- package/dist/_pkgs/data-view/core/common/property-menu.js +7 -7
- package/dist/_pkgs/data-view/core/component/overflow/overflow.js +4 -1
- package/dist/_pkgs/data-view/core/group-by/renderer/base.d.ts +4 -1
- package/dist/_pkgs/data-view/core/group-by/renderer/base.js +17 -2
- package/dist/_pkgs/data-view/core/group-by/renderer/number-group.js +1 -1
- package/dist/_pkgs/data-view/core/group-by/renderer/select-group.d.ts +0 -3
- package/dist/_pkgs/data-view/core/group-by/renderer/select-group.js +63 -118
- package/dist/_pkgs/data-view/core/group-by/renderer/string-group.js +1 -1
- package/dist/_pkgs/data-view/core/group-by/setting.js +2 -2
- package/dist/_pkgs/data-view/core/utils/wording.d.ts +6 -2
- package/dist/_pkgs/data-view/core/utils/wording.js +5 -3
- package/dist/_pkgs/data-view/core/view/default-name.d.ts +6 -0
- package/dist/_pkgs/data-view/core/view/default-name.js +8 -0
- package/dist/_pkgs/data-view/translations.d.ts +47 -0
- package/dist/_pkgs/data-view/translations.js +124 -0
- package/dist/_pkgs/data-view/view-presets/table/mobile/column-header.d.ts +3 -1
- package/dist/_pkgs/data-view/view-presets/table/mobile/column-header.js +102 -124
- package/dist/_pkgs/data-view/view-presets/table/mobile/group.js +4 -2
- package/dist/_pkgs/data-view/view-presets/table/mobile/header.js +17 -9
- package/dist/_pkgs/data-view/view-presets/table/mobile/row.js +20 -11
- package/dist/_pkgs/data-view/view-presets/table/mobile/table-view-style.d.ts +17 -0
- package/dist/_pkgs/data-view/view-presets/table/mobile/table-view-style.js +31 -6
- package/dist/_pkgs/data-view/view-presets/table/mobile/table-view-ui-logic.js +2 -2
- package/dist/_pkgs/data-view/view-presets/table/pc/group.js +4 -2
- package/dist/_pkgs/data-view/widget-presets/quick-setting-bar/filter/list-view.d.ts +3 -1
- package/dist/_pkgs/data-view/widget-presets/quick-setting-bar/filter/list-view.js +23 -3
- package/dist/_pkgs/data-view/widget-presets/tools/presets/search/search.d.ts +3 -0
- package/dist/_pkgs/data-view/widget-presets/tools/presets/search/search.js +37 -5
- package/dist/_pkgs/data-view/widget-presets/tools/presets/table-add-row/add-row.d.ts +3 -0
- package/dist/_pkgs/data-view/widget-presets/tools/presets/table-add-row/add-row.js +99 -35
- package/dist/_pkgs/data-view/widget-presets/tools/presets/view-options/view-options.js +30 -22
- package/dist/_pkgs/data-view/widget-presets/tools/presets/view-options/view-settings.js +8 -6
- package/dist/_pkgs/data-view/widget-presets/views-bar/views-view.js +15 -0
- package/dist/host-panels.d.ts +1 -1
- package/dist/host-panels.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ThreadTextRange } from '../affine-shared/services/index.js';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
/** Where a range sits in its block's text today, by index. */
|
|
4
|
+
export interface ResolvedThreadRange {
|
|
5
|
+
index: number;
|
|
6
|
+
length: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The anchor of `length` characters from `index` in `yText` (ADR 0038,
|
|
10
|
+
* amendment of 2026-10-15), or `null` for an empty run or one outside the
|
|
11
|
+
* text.
|
|
12
|
+
*
|
|
13
|
+
* `start` sticks to the first selected character (`assoc = 0`) and `end` to
|
|
14
|
+
* the last one (`assoc = -1`), so typing right after the run does not grow
|
|
15
|
+
* it. Creating a relative position only reads the item ids: nothing is
|
|
16
|
+
* written to the document (held by `text-range.unit.spec.ts`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function encodeThreadRange(yText: Y.Text, index: number, length: number): ThreadTextRange | null;
|
|
19
|
+
/**
|
|
20
|
+
* Where `range` sits in `yText` now, or `null` when it cannot say: the text
|
|
21
|
+
* is detached, a string is not a position, a position belongs to another
|
|
22
|
+
* text, or the run collapsed because its text was deleted. Never throws: a
|
|
23
|
+
* host's stored anchor may be anything, and an unresolvable range simply is
|
|
24
|
+
* not drawn.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveThreadRange(yText: Y.Text, range: ThreadTextRange): ResolvedThreadRange | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
function toBase64(bytes) {
|
|
3
|
+
let binary = '';
|
|
4
|
+
for (const byte of bytes)
|
|
5
|
+
binary += String.fromCharCode(byte);
|
|
6
|
+
return btoa(binary);
|
|
7
|
+
}
|
|
8
|
+
function fromBase64(text) {
|
|
9
|
+
return Uint8Array.from(atob(text), char => char.charCodeAt(0));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The anchor of `length` characters from `index` in `yText` (ADR 0038,
|
|
13
|
+
* amendment of 2026-10-15), or `null` for an empty run or one outside the
|
|
14
|
+
* text.
|
|
15
|
+
*
|
|
16
|
+
* `start` sticks to the first selected character (`assoc = 0`) and `end` to
|
|
17
|
+
* the last one (`assoc = -1`), so typing right after the run does not grow
|
|
18
|
+
* it. Creating a relative position only reads the item ids: nothing is
|
|
19
|
+
* written to the document (held by `text-range.unit.spec.ts`).
|
|
20
|
+
*/
|
|
21
|
+
export function encodeThreadRange(yText, index, length) {
|
|
22
|
+
if (!(length > 0) || index < 0 || index + length > yText.length) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const start = Y.createRelativePositionFromTypeIndex(yText, index, 0);
|
|
26
|
+
const end = Y.createRelativePositionFromTypeIndex(yText, index + length, -1);
|
|
27
|
+
return {
|
|
28
|
+
start: toBase64(Y.encodeRelativePosition(start)),
|
|
29
|
+
end: toBase64(Y.encodeRelativePosition(end)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function absoluteIndex(encoded, yText, doc) {
|
|
33
|
+
const position = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(fromBase64(encoded)), doc);
|
|
34
|
+
// A position of another text (a host that mixed up two anchors) is not
|
|
35
|
+
// this block's run.
|
|
36
|
+
return position && position.type === yText ? position.index : null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Where `range` sits in `yText` now, or `null` when it cannot say: the text
|
|
40
|
+
* is detached, a string is not a position, a position belongs to another
|
|
41
|
+
* text, or the run collapsed because its text was deleted. Never throws: a
|
|
42
|
+
* host's stored anchor may be anything, and an unresolvable range simply is
|
|
43
|
+
* not drawn.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveThreadRange(yText, range) {
|
|
46
|
+
const doc = yText.doc;
|
|
47
|
+
if (!doc)
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const start = absoluteIndex(range.start, yText, doc);
|
|
51
|
+
const end = absoluteIndex(range.end, yText, doc);
|
|
52
|
+
if (start === null || end === null || end <= start)
|
|
53
|
+
return null;
|
|
54
|
+
return { index: start, length: end - start };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -9,15 +9,16 @@ import { type ThreadTarget } from './excerpt.js';
|
|
|
9
9
|
* - On the canvas, exactly one selected model: a canvas element (shape,
|
|
10
10
|
* connector, group, framework board) is an `element`, a canvas block (note,
|
|
11
11
|
* frame, image, embed) a `block`.
|
|
12
|
-
* - A text selection
|
|
13
|
-
*
|
|
12
|
+
* - A text selection: the block it starts in, with the selected run of that
|
|
13
|
+
* block's text (ADR 0038, amendment of 2026-10-15).
|
|
14
14
|
* - One selected block, or the block a hover toolbar is drawn for.
|
|
15
15
|
*/
|
|
16
16
|
export declare function threadTargetOf(ctx: ToolbarContext): ThreadTarget | null;
|
|
17
17
|
/**
|
|
18
18
|
* Ask the host to open the discussion of `target`, with the excerpt the
|
|
19
19
|
* library computes. Nothing is written: no prop, no id in the document.
|
|
20
|
-
* Without the seam, nothing happens.
|
|
20
|
+
* Without the seam, nothing happens. `range` is handed over only when there
|
|
21
|
+
* is one, so a host comparing requests sees the first shape for a whole block.
|
|
21
22
|
*/
|
|
22
23
|
export declare function openThreadOn(std: BlockStdScope, target: ThreadTarget): void;
|
|
23
24
|
export declare const THREAD_DISCUSS_ACTION_ID = "g.thread-discuss";
|
|
@@ -3,6 +3,7 @@ import { ToolbarModuleExtension, toolbarModuleKey, ThreadMarksProvider, } from '
|
|
|
3
3
|
import { BlockFlavourIdentifier, BlockSelection, TextSelection, } from '../std/index.js';
|
|
4
4
|
import { GfxPrimitiveElementModel } from '../std/gfx/index.js';
|
|
5
5
|
import { threadExcerpt } from './excerpt.js';
|
|
6
|
+
import { encodeThreadRange } from './text-range.js';
|
|
6
7
|
import { THREAD_DISCUSS } from './translations.js';
|
|
7
8
|
/** Containers a discussion never anchors to: the page and the canvas itself. */
|
|
8
9
|
const NOT_A_TARGET = new Set(['affine:page', 'affine:surface']);
|
|
@@ -14,6 +15,22 @@ function blockTarget(id, ctx) {
|
|
|
14
15
|
return null;
|
|
15
16
|
return { targetType: 'block', targetId: model.id };
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The block a text selection starts in, with the selected run of it. A
|
|
20
|
+
* selection over several blocks anchors to the first one, on the part of it
|
|
21
|
+
* that is selected (ADR 0038, amendment of 2026-10-15). A run that cannot be
|
|
22
|
+
* encoded (an empty one, a block with no text) leaves the block alone.
|
|
23
|
+
*/
|
|
24
|
+
function textTarget(text, ctx) {
|
|
25
|
+
const target = blockTarget(text.from.blockId, ctx);
|
|
26
|
+
if (!target)
|
|
27
|
+
return null;
|
|
28
|
+
const yText = ctx.store.getModelById(target.targetId)?.text?.yText;
|
|
29
|
+
const range = yText
|
|
30
|
+
? encodeThreadRange(yText, text.from.index, text.from.length)
|
|
31
|
+
: null;
|
|
32
|
+
return range ? { ...target, range } : target;
|
|
33
|
+
}
|
|
17
34
|
/**
|
|
18
35
|
* The ONE target a toolbar row speaks for, or `null` when it speaks for
|
|
19
36
|
* several (a discussion anchors to one block or one element, labre ADR-0048).
|
|
@@ -21,8 +38,8 @@ function blockTarget(id, ctx) {
|
|
|
21
38
|
* - On the canvas, exactly one selected model: a canvas element (shape,
|
|
22
39
|
* connector, group, framework board) is an `element`, a canvas block (note,
|
|
23
40
|
* frame, image, embed) a `block`.
|
|
24
|
-
* - A text selection
|
|
25
|
-
*
|
|
41
|
+
* - A text selection: the block it starts in, with the selected run of that
|
|
42
|
+
* block's text (ADR 0038, amendment of 2026-10-15).
|
|
26
43
|
* - One selected block, or the block a hover toolbar is drawn for.
|
|
27
44
|
*/
|
|
28
45
|
export function threadTargetOf(ctx) {
|
|
@@ -38,11 +55,8 @@ export function threadTargetOf(ctx) {
|
|
|
38
55
|
: blockTarget(model.id, ctx);
|
|
39
56
|
}
|
|
40
57
|
const text = ctx.selection.find(TextSelection);
|
|
41
|
-
if (text)
|
|
42
|
-
|
|
43
|
-
const to = text.to?.blockId ?? from;
|
|
44
|
-
return from === to ? blockTarget(from, ctx) : null;
|
|
45
|
-
}
|
|
58
|
+
if (text)
|
|
59
|
+
return textTarget(text, ctx);
|
|
46
60
|
if (ctx.selection.filter(BlockSelection).length > 1)
|
|
47
61
|
return null;
|
|
48
62
|
return blockTarget(ctx.getCurrentModelBy(BlockSelection)?.id, ctx);
|
|
@@ -50,7 +64,8 @@ export function threadTargetOf(ctx) {
|
|
|
50
64
|
/**
|
|
51
65
|
* Ask the host to open the discussion of `target`, with the excerpt the
|
|
52
66
|
* library computes. Nothing is written: no prop, no id in the document.
|
|
53
|
-
* Without the seam, nothing happens.
|
|
67
|
+
* Without the seam, nothing happens. `range` is handed over only when there
|
|
68
|
+
* is one, so a host comparing requests sees the first shape for a whole block.
|
|
54
69
|
*/
|
|
55
70
|
export function openThreadOn(std, target) {
|
|
56
71
|
const service = std.getOptional(ThreadMarksProvider);
|
|
@@ -61,6 +76,7 @@ export function openThreadOn(std, target) {
|
|
|
61
76
|
targetType: target.targetType,
|
|
62
77
|
targetId: target.targetId,
|
|
63
78
|
excerpt: threadExcerpt(std, target),
|
|
79
|
+
...(target.range ? { range: target.range } : {}),
|
|
64
80
|
});
|
|
65
81
|
}
|
|
66
82
|
export const THREAD_DISCUSS_ACTION_ID = 'g.thread-discuss';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type SelectionRect } from '../affine-shared/commands/index.js';
|
|
1
2
|
import { type ThreadMark } from '../affine-shared/services/index.js';
|
|
2
3
|
import { WidgetComponent } from '../std/index.js';
|
|
4
|
+
import { nothing } from 'lit';
|
|
3
5
|
export declare const AFFINE_THREAD_MARKS_WIDGET = "affine-thread-marks-widget";
|
|
4
6
|
/**
|
|
5
7
|
* Where a mark sits, relative to the widget's own origin: beside a block of
|
|
@@ -15,6 +17,8 @@ export interface ThreadMarkPlace {
|
|
|
15
17
|
export interface PlacedThreadMark extends ThreadMarkPlace {
|
|
16
18
|
mark: ThreadMark;
|
|
17
19
|
}
|
|
20
|
+
/** Is the point (viewport coordinates) inside one of the rects? */
|
|
21
|
+
export declare function hitsThreadRange(rects: readonly SelectionRect[], x: number, y: number): boolean;
|
|
18
22
|
/**
|
|
19
23
|
* The marks to draw, from what the host says and where the editor finds each
|
|
20
24
|
* target. Pure, so the "ignore what is absent" rule is held by a test:
|
|
@@ -22,7 +26,9 @@ export interface PlacedThreadMark extends ThreadMarkPlace {
|
|
|
22
26
|
* - a target `locate` cannot find (deleted, of the other view, hidden for this
|
|
23
27
|
* viewer) is skipped, never an error: an orphan anchor has no mark;
|
|
24
28
|
* - a mark at `count <= 0` is not drawn;
|
|
25
|
-
* - two marks on one target are one mark: counts add up, unread wins
|
|
29
|
+
* - two marks on one target are one mark: counts add up, unread wins, and
|
|
30
|
+
* the run of text it opens is kept only when every merged mark names the
|
|
31
|
+
* same one (a mark that stands for several discussions opens the block).
|
|
26
32
|
*/
|
|
27
33
|
export declare function placeThreadMarks(marks: readonly ThreadMark[], locate: (mark: ThreadMark) => ThreadMarkPlace | null): PlacedThreadMark[];
|
|
28
34
|
/**
|
|
@@ -31,6 +37,15 @@ export declare function placeThreadMarks(marks: readonly ThreadMark[], locate: (
|
|
|
31
37
|
* on the canvas on the corner of each marked element or block. A click asks
|
|
32
38
|
* the host to open the discussion, exactly as "Discuss" does.
|
|
33
39
|
*
|
|
40
|
+
* A mark that carries a `range` also highlights that run of its block's text
|
|
41
|
+
* in the page view (ADR 0038, amendment of 2026-10-15). The run is resolved
|
|
42
|
+
* from its Yjs relative positions on every render and drawn as the range's
|
|
43
|
+
* client rects (the technique of `doc-remote-selection`), never as a mark in
|
|
44
|
+
* the text. The highlight takes no pointer event, so nothing blocks placing
|
|
45
|
+
* the caret or selecting; a click that lands on it with nothing selected
|
|
46
|
+
* opens its discussion. A run that no longer resolves is not drawn, and the
|
|
47
|
+
* block's own mark stays.
|
|
48
|
+
*
|
|
34
49
|
* It reads the seam and the layout, and writes nothing. Positions are
|
|
35
50
|
* client rects taken relative to the widget's own box and recomputed, once a
|
|
36
51
|
* frame at most, when the marks, the document, the canvas or the window
|
|
@@ -41,8 +56,24 @@ export declare class ThreadMarksWidget extends WidgetComponent {
|
|
|
41
56
|
private readonly _refresh;
|
|
42
57
|
private get _edgeless();
|
|
43
58
|
connectedCallback(): void;
|
|
59
|
+
/**
|
|
60
|
+
* A click on a highlighted run opens its discussion. The click is not
|
|
61
|
+
* consumed: the caret lands where it was put. A click that ends a drag
|
|
62
|
+
* (something is selected) is a selection, not a request.
|
|
63
|
+
*/
|
|
64
|
+
private readonly _onClick;
|
|
65
|
+
/** Open the discussion of the run under the point, if there is one. */
|
|
66
|
+
private _openRangeAt;
|
|
67
|
+
private _resolve;
|
|
68
|
+
/**
|
|
69
|
+
* The runs to highlight, in viewport coordinates, page view only: on the
|
|
70
|
+
* canvas a note's text may sit under a shape, and this overlay would paint
|
|
71
|
+
* over it. `ponytail:` a run in a canvas note keeps only its block's mark;
|
|
72
|
+
* drawing it there needs the canvas stacking, if a reader asks for it.
|
|
73
|
+
*/
|
|
74
|
+
private _ranges;
|
|
44
75
|
private _locate;
|
|
45
|
-
render():
|
|
76
|
+
render(): import("lit-html").TemplateResult<1> | typeof nothing;
|
|
46
77
|
}
|
|
47
78
|
declare global {
|
|
48
79
|
interface HTMLElementTagNameMap {
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { CommentIcon } from '@blocksuite/icons/lit';
|
|
2
2
|
import { isPainted } from '../affine-block-surface/index.js';
|
|
3
|
+
import { getRangeRects, } from '../affine-shared/commands/index.js';
|
|
3
4
|
import { ThreadMarksProvider, translateKey, } from '../affine-shared/services/index.js';
|
|
4
5
|
import { isInsideEdgelessEditor, requestThrottledConnectedFrame, } from '../affine-shared/utils/index.js';
|
|
5
|
-
import { WidgetComponent } from '../std/index.js';
|
|
6
|
+
import { TextSelection, WidgetComponent } from '../std/index.js';
|
|
6
7
|
import { GfxBlockElementModel, GfxControllerIdentifier, GfxPrimitiveElementModel, } from '../std/gfx/index.js';
|
|
7
8
|
import { effect } from '@preact/signals-core';
|
|
8
9
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
9
10
|
import { css, html, nothing, unsafeCSS } from 'lit';
|
|
10
11
|
import { repeat } from 'lit/directives/repeat.js';
|
|
11
12
|
import { styleMap } from 'lit/directives/style-map.js';
|
|
13
|
+
import { resolveThreadRange } from './text-range.js';
|
|
12
14
|
import { openThreadOn } from './toolbar.js';
|
|
13
15
|
import { THREAD_DISCUSS } from './translations.js';
|
|
14
16
|
export const AFFINE_THREAD_MARKS_WIDGET = 'affine-thread-marks-widget';
|
|
17
|
+
/** Is the point (viewport coordinates) inside one of the rects? */
|
|
18
|
+
export function hitsThreadRange(rects, x, y) {
|
|
19
|
+
return rects.some(rect => x >= rect.left &&
|
|
20
|
+
x <= rect.left + rect.width &&
|
|
21
|
+
y >= rect.top &&
|
|
22
|
+
y <= rect.top + rect.height);
|
|
23
|
+
}
|
|
24
|
+
const sameRange = (a, b) => a?.start === b?.start && a?.end === b?.end;
|
|
15
25
|
/**
|
|
16
26
|
* The marks to draw, from what the host says and where the editor finds each
|
|
17
27
|
* target. Pure, so the "ignore what is absent" rule is held by a test:
|
|
@@ -19,7 +29,9 @@ export const AFFINE_THREAD_MARKS_WIDGET = 'affine-thread-marks-widget';
|
|
|
19
29
|
* - a target `locate` cannot find (deleted, of the other view, hidden for this
|
|
20
30
|
* viewer) is skipped, never an error: an orphan anchor has no mark;
|
|
21
31
|
* - a mark at `count <= 0` is not drawn;
|
|
22
|
-
* - two marks on one target are one mark: counts add up, unread wins
|
|
32
|
+
* - two marks on one target are one mark: counts add up, unread wins, and
|
|
33
|
+
* the run of text it opens is kept only when every merged mark names the
|
|
34
|
+
* same one (a mark that stands for several discussions opens the block).
|
|
23
35
|
*/
|
|
24
36
|
export function placeThreadMarks(marks, locate) {
|
|
25
37
|
const merged = new Map();
|
|
@@ -33,6 +45,7 @@ export function placeThreadMarks(marks, locate) {
|
|
|
33
45
|
...seen,
|
|
34
46
|
count: seen.count + mark.count,
|
|
35
47
|
unread: seen.unread || mark.unread,
|
|
48
|
+
range: sameRange(seen.range, mark.range) ? seen.range : undefined,
|
|
36
49
|
}
|
|
37
50
|
: mark);
|
|
38
51
|
}
|
|
@@ -50,6 +63,15 @@ export function placeThreadMarks(marks, locate) {
|
|
|
50
63
|
* on the canvas on the corner of each marked element or block. A click asks
|
|
51
64
|
* the host to open the discussion, exactly as "Discuss" does.
|
|
52
65
|
*
|
|
66
|
+
* A mark that carries a `range` also highlights that run of its block's text
|
|
67
|
+
* in the page view (ADR 0038, amendment of 2026-10-15). The run is resolved
|
|
68
|
+
* from its Yjs relative positions on every render and drawn as the range's
|
|
69
|
+
* client rects (the technique of `doc-remote-selection`), never as a mark in
|
|
70
|
+
* the text. The highlight takes no pointer event, so nothing blocks placing
|
|
71
|
+
* the caret or selecting; a click that lands on it with nothing selected
|
|
72
|
+
* opens its discussion. A run that no longer resolves is not drawn, and the
|
|
73
|
+
* block's own mark stays.
|
|
74
|
+
*
|
|
53
75
|
* It reads the seam and the layout, and writes nothing. Positions are
|
|
54
76
|
* client rects taken relative to the widget's own box and recomputed, once a
|
|
55
77
|
* frame at most, when the marks, the document, the canvas or the window
|
|
@@ -61,6 +83,16 @@ export class ThreadMarksWidget extends WidgetComponent {
|
|
|
61
83
|
this._refresh = requestThrottledConnectedFrame(() => {
|
|
62
84
|
this.requestUpdate();
|
|
63
85
|
}, this);
|
|
86
|
+
/**
|
|
87
|
+
* A click on a highlighted run opens its discussion. The click is not
|
|
88
|
+
* consumed: the caret lands where it was put. A click that ends a drag
|
|
89
|
+
* (something is selected) is a selection, not a request.
|
|
90
|
+
*/
|
|
91
|
+
this._onClick = (event) => {
|
|
92
|
+
if (window.getSelection()?.isCollapsed === false)
|
|
93
|
+
return;
|
|
94
|
+
this._openRangeAt(event.clientX, event.clientY);
|
|
95
|
+
};
|
|
64
96
|
}
|
|
65
97
|
static { this.styles = css `
|
|
66
98
|
:host {
|
|
@@ -88,6 +120,16 @@ export class ThreadMarksWidget extends WidgetComponent {
|
|
|
88
120
|
pointer-events: auto;
|
|
89
121
|
}
|
|
90
122
|
|
|
123
|
+
.thread-range {
|
|
124
|
+
position: absolute;
|
|
125
|
+
top: 0;
|
|
126
|
+
left: 0;
|
|
127
|
+
background-color: ${unsafeCSS(cssVarV2('block/comment/highlightDefault'))};
|
|
128
|
+
border-bottom: 2px solid
|
|
129
|
+
${unsafeCSS(cssVarV2('block/comment/highlightUnderline'))};
|
|
130
|
+
pointer-events: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
91
133
|
.thread-mark[data-unread] {
|
|
92
134
|
color: ${unsafeCSS(cssVarV2('text/emphasis'))};
|
|
93
135
|
}
|
|
@@ -116,6 +158,12 @@ export class ThreadMarksWidget extends WidgetComponent {
|
|
|
116
158
|
_disposables.addFromEvent(window, 'resize', this._refresh);
|
|
117
159
|
// Capture: the page view scrolls in a container, not the window.
|
|
118
160
|
_disposables.addFromEvent(window, 'scroll', this._refresh, true);
|
|
161
|
+
// A highlighted run moves with every keystroke, local or remote, and a
|
|
162
|
+
// text edit is no block update: follow the document itself.
|
|
163
|
+
const doc = std.store.spaceDoc;
|
|
164
|
+
doc.on('update', this._refresh);
|
|
165
|
+
_disposables.add(() => doc.off('update', this._refresh));
|
|
166
|
+
_disposables.addFromEvent(std.host, 'click', this._onClick);
|
|
119
167
|
const gfx = std.getOptional(GfxControllerIdentifier);
|
|
120
168
|
if (gfx) {
|
|
121
169
|
_disposables.add(gfx.viewport.viewportUpdated.subscribe(this._refresh));
|
|
@@ -134,6 +182,62 @@ export class ThreadMarksWidget extends WidgetComponent {
|
|
|
134
182
|
}));
|
|
135
183
|
}
|
|
136
184
|
}
|
|
185
|
+
/** Open the discussion of the run under the point, if there is one. */
|
|
186
|
+
_openRangeAt(x, y) {
|
|
187
|
+
const service = this.std.getOptional(ThreadMarksProvider);
|
|
188
|
+
if (!service)
|
|
189
|
+
return;
|
|
190
|
+
const hit = this._ranges(service.marks$.peek()).find(({ rects }) => hitsThreadRange(rects, x, y));
|
|
191
|
+
if (!hit)
|
|
192
|
+
return;
|
|
193
|
+
openThreadOn(this.std, {
|
|
194
|
+
targetType: 'block',
|
|
195
|
+
targetId: hit.mark.targetId,
|
|
196
|
+
range: hit.mark.range,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
_resolve(mark) {
|
|
200
|
+
if (mark.targetType !== 'block' || !mark.range)
|
|
201
|
+
return null;
|
|
202
|
+
const yText = this.std.store.getModelById(mark.targetId)?.text?.yText;
|
|
203
|
+
return yText ? resolveThreadRange(yText, mark.range) : null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The runs to highlight, in viewport coordinates, page view only: on the
|
|
207
|
+
* canvas a note's text may sit under a shape, and this overlay would paint
|
|
208
|
+
* over it. `ponytail:` a run in a canvas note keeps only its block's mark;
|
|
209
|
+
* drawing it there needs the canvas stacking, if a reader asks for it.
|
|
210
|
+
*/
|
|
211
|
+
_ranges(marks) {
|
|
212
|
+
if (this._edgeless)
|
|
213
|
+
return [];
|
|
214
|
+
const drawn = [];
|
|
215
|
+
for (const mark of marks) {
|
|
216
|
+
if (!(mark.count > 0))
|
|
217
|
+
continue;
|
|
218
|
+
const run = this._resolve(mark);
|
|
219
|
+
if (!run)
|
|
220
|
+
continue;
|
|
221
|
+
let range;
|
|
222
|
+
try {
|
|
223
|
+
range = this.std.range.textSelectionToRange(this.std.selection.create(TextSelection, {
|
|
224
|
+
from: { blockId: mark.targetId, ...run },
|
|
225
|
+
to: null,
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// The inline editor has not rendered the latest text yet: the next
|
|
230
|
+
// frame draws it.
|
|
231
|
+
range = null;
|
|
232
|
+
}
|
|
233
|
+
if (!range)
|
|
234
|
+
continue;
|
|
235
|
+
const rects = getRangeRects(range, null);
|
|
236
|
+
if (rects.length > 0)
|
|
237
|
+
drawn.push({ mark, rects });
|
|
238
|
+
}
|
|
239
|
+
return drawn;
|
|
240
|
+
}
|
|
137
241
|
_locate(mark, origin) {
|
|
138
242
|
const edgeless = this._edgeless;
|
|
139
243
|
if (edgeless) {
|
|
@@ -180,10 +284,21 @@ export class ThreadMarksWidget extends WidgetComponent {
|
|
|
180
284
|
return nothing;
|
|
181
285
|
const origin = this.getBoundingClientRect();
|
|
182
286
|
const placed = placeThreadMarks(marks, mark => this._locate(mark, origin));
|
|
183
|
-
|
|
287
|
+
const ranges = this._ranges(marks);
|
|
288
|
+
if (placed.length === 0 && ranges.length === 0)
|
|
184
289
|
return nothing;
|
|
185
290
|
const label = translateKey(this.std, ...THREAD_DISCUSS);
|
|
186
|
-
|
|
291
|
+
const highlights = ranges.flatMap(({ mark, rects }) => rects.map(rect => html `<div
|
|
292
|
+
class="thread-range"
|
|
293
|
+
data-testid="thread-range"
|
|
294
|
+
data-target-id=${mark.targetId}
|
|
295
|
+
style=${styleMap({
|
|
296
|
+
width: `${rect.width}px`,
|
|
297
|
+
height: `${rect.height}px`,
|
|
298
|
+
transform: `translate(${rect.left - origin.left}px, ${rect.top - origin.top}px)`,
|
|
299
|
+
})}
|
|
300
|
+
></div>`));
|
|
301
|
+
return html `${highlights}${repeat(placed, ({ mark }) => `${mark.targetType}:${mark.targetId}`, ({ mark, x, y, anchor }) => html `<icon-button
|
|
187
302
|
class="thread-mark"
|
|
188
303
|
data-testid="thread-mark"
|
|
189
304
|
data-target-type=${mark.targetType}
|
|
@@ -206,9 +321,10 @@ export class ThreadMarksWidget extends WidgetComponent {
|
|
|
206
321
|
openThreadOn(this.std, {
|
|
207
322
|
targetType: mark.targetType,
|
|
208
323
|
targetId: mark.targetId,
|
|
324
|
+
range: mark.range,
|
|
209
325
|
});
|
|
210
326
|
}}
|
|
211
327
|
>${CommentIcon({ width: '16px', height: '16px' })}</icon-button
|
|
212
|
-
>`)
|
|
328
|
+
>`)}`;
|
|
213
329
|
}
|
|
214
330
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EditorToolbar } from '../affine-components/toolbar/index.js';
|
|
2
|
-
import { ActionPlacement, type ToolbarActions, type ToolbarContext, type ToolbarLayoutStep, type ToolbarPlacement } from '../affine-shared/services/index.js';
|
|
2
|
+
import { ActionPlacement, type ToolbarAction, type ToolbarActions, type ToolbarContext, type ToolbarLayoutStep, type ToolbarPlacement } from '../affine-shared/services/index.js';
|
|
3
3
|
import type { AutoUpdateOptions, ReferenceElement, SideObject } from '@floating-ui/dom';
|
|
4
4
|
export declare const sideMap: Map<string, {
|
|
5
5
|
top: number;
|
|
@@ -33,6 +33,7 @@ export declare function combine(actions: ToolbarActions, context: ToolbarContext
|
|
|
33
33
|
content?: ((cx: ToolbarContext) => import("lit-html").TemplateResult | null) | (import("lit-html").TemplateResult | null);
|
|
34
34
|
run?: (cx: ToolbarContext) => void;
|
|
35
35
|
}[];
|
|
36
|
+
export declare function group(actions: ToolbarAction[]): ToolbarAction[];
|
|
36
37
|
/**
|
|
37
38
|
* How much of the row an entry has given up, decided by {@link ToolbarFitter}
|
|
38
39
|
* and re-applied by every render until the room changes again.
|
|
@@ -210,7 +210,7 @@ export function combine(actions, context) {
|
|
|
210
210
|
});
|
|
211
211
|
return filtered;
|
|
212
212
|
}
|
|
213
|
-
function group(actions) {
|
|
213
|
+
export function group(actions) {
|
|
214
214
|
const grouped = groupBy(actions, a => a.id);
|
|
215
215
|
const paired = toPairs(grouped).map(([_, items]) => {
|
|
216
216
|
if (items.length === 1)
|
|
@@ -222,7 +222,10 @@ function group(actions) {
|
|
|
222
222
|
});
|
|
223
223
|
return paired;
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
// Only a group's `actions` is a list of entries to merge by id; any other
|
|
226
|
+
// array (a `labelWording` tuple) is a value, and grouping it turned the
|
|
227
|
+
// wording into a keyless object the host's i18next choked on.
|
|
228
|
+
const merge = (a, b) => mergeWith(a, b, (obj, src, key) => key === 'actions' && Array.isArray(obj) ? group(obj.concat(src)) : src);
|
|
226
229
|
export function emptyToolbarLayout() {
|
|
227
230
|
return { shrunk: new Set(), collapsed: new Set() };
|
|
228
231
|
}
|
|
@@ -147,11 +147,11 @@ let DataViewPropertiesSettingView = (() => {
|
|
|
147
147
|
width: 4px;
|
|
148
148
|
height: 12px;
|
|
149
149
|
border-radius: 1px;
|
|
150
|
-
background-color:
|
|
150
|
+
background-color: var(--affine-border-color);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
.property-item:hover .property-item-drag-bar {
|
|
154
|
-
background-color:
|
|
154
|
+
background-color: var(--affine-placeholder-color);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
.property-item-icon {
|
|
@@ -2,7 +2,7 @@ import { type MenuConfig, type PopupTarget } from '../../../affine-components/co
|
|
|
2
2
|
import { type Wording } from '../utils/wording.js';
|
|
3
3
|
import type { Property } from '../view-manager/property.js';
|
|
4
4
|
import type { SingleView } from '../view-manager/single-view.js';
|
|
5
|
-
export declare const inputConfig: (property: Property) => (menu: import("../../../affine-components/context-menu/menu.js").Menu) => import("lit-html").TemplateResult<1> | undefined;
|
|
5
|
+
export declare const inputConfig: (property: Property, wording?: Wording) => (menu: import("../../../affine-components/context-menu/menu.js").Menu) => import("lit-html").TemplateResult<1> | undefined;
|
|
6
6
|
export declare const typeConfig: (property: Property, wording?: Wording) => (menu: import("../../../affine-components/context-menu/menu.js").Menu, index: number) => import("lit-html").TemplateResult | undefined;
|
|
7
7
|
/** What a property menu's opener adds around the shared entries. */
|
|
8
8
|
export type PropertyMenuSlots = {
|
|
@@ -28,8 +28,8 @@ export type PropertyMenuSlots = {
|
|
|
28
28
|
* ponytail: "Number Format" stays a table slot (its bar is a table/pc
|
|
29
29
|
* element, behind `enable_number_formatting`, off in Labre), so the
|
|
30
30
|
* properties list does not offer it. Move the bar to core and the entry into
|
|
31
|
-
* this list when the flag ships. The virtual
|
|
32
|
-
*
|
|
31
|
+
* this list when the flag ships. The virtual table header still spells its
|
|
32
|
+
* own copy of these entries; point it here in a dedicated pass.
|
|
33
33
|
*/
|
|
34
34
|
export declare const propertyMenuItems: (property: Property, slots?: PropertyMenuSlots) => MenuConfig[];
|
|
35
35
|
export declare const popPropertyMenu: (target: PopupTarget, property: Property, slots?: PropertyMenuSlots) => import("../../../affine-components/context-menu/menu-renderer.js").MenuHandler;
|
|
@@ -2,10 +2,10 @@ import { menu, popMenu, } from '../../../affine-components/context-menu/index.js
|
|
|
2
2
|
import { TOOLBAR_DELETE, TOOLBAR_DUPLICATE, } from '../../../affine-shared/services/index.js';
|
|
3
3
|
import { DeleteIcon, DuplicateIcon, ViewIcon } from '@blocksuite/icons/lit';
|
|
4
4
|
import { html } from 'lit/static-html.js';
|
|
5
|
-
import { DATA_VIEW_PROPERTY_HIDE_IN_VIEW, DATA_VIEW_PROPERTY_TYPE, } from '../../translations.js';
|
|
5
|
+
import { DATA_VIEW_PROPERTY_HIDE_IN_VIEW, DATA_VIEW_PROPERTY_NAME, DATA_VIEW_PROPERTY_TYPE, DATA_VIEW_PROPERTY_TYPE_ENTRY, } from '../../translations.js';
|
|
6
6
|
import { renderUniLit } from '../utils/uni-component/index.js';
|
|
7
7
|
import { englishWording } from '../utils/wording.js';
|
|
8
|
-
export const inputConfig = (property) => {
|
|
8
|
+
export const inputConfig = (property, wording = englishWording) => {
|
|
9
9
|
return menu.input({
|
|
10
10
|
prefix: html `
|
|
11
11
|
<div class="affine-database-column-type-menu-icon">
|
|
@@ -13,7 +13,7 @@ export const inputConfig = (property) => {
|
|
|
13
13
|
</div>
|
|
14
14
|
`,
|
|
15
15
|
initialValue: property.name$.value,
|
|
16
|
-
placeholder:
|
|
16
|
+
placeholder: wording(DATA_VIEW_PROPERTY_NAME),
|
|
17
17
|
// Persist on blur so the typed name survives the menu being dismissed by
|
|
18
18
|
// a click outside, which never fires the completion path.
|
|
19
19
|
onBlur: text => {
|
|
@@ -25,7 +25,7 @@ export const typeConfig = (property, wording = englishWording) => {
|
|
|
25
25
|
return menu.group({
|
|
26
26
|
items: [
|
|
27
27
|
menu.subMenu({
|
|
28
|
-
name:
|
|
28
|
+
name: wording(DATA_VIEW_PROPERTY_TYPE_ENTRY),
|
|
29
29
|
hide: () => !property.typeCanSet,
|
|
30
30
|
postfix: html ` <div
|
|
31
31
|
class="affine-database-column-type-icon"
|
|
@@ -75,13 +75,13 @@ export const typeConfig = (property, wording = englishWording) => {
|
|
|
75
75
|
* ponytail: "Number Format" stays a table slot (its bar is a table/pc
|
|
76
76
|
* element, behind `enable_number_formatting`, off in Labre), so the
|
|
77
77
|
* properties list does not offer it. Move the bar to core and the entry into
|
|
78
|
-
* this list when the flag ships. The virtual
|
|
79
|
-
*
|
|
78
|
+
* this list when the flag ships. The virtual table header still spells its
|
|
79
|
+
* own copy of these entries; point it here in a dedicated pass.
|
|
80
80
|
*/
|
|
81
81
|
export const propertyMenuItems = (property, slots = {}) => {
|
|
82
82
|
const wording = slots.wording ?? englishWording;
|
|
83
83
|
return [
|
|
84
|
-
inputConfig(property),
|
|
84
|
+
inputConfig(property, wording),
|
|
85
85
|
typeConfig(property, wording),
|
|
86
86
|
...(slots.afterType ?? []),
|
|
87
87
|
menu.group({
|
|
@@ -136,7 +136,10 @@ let Overflow = (() => {
|
|
|
136
136
|
const moreWidth = moreButton?.getBoundingClientRect().width;
|
|
137
137
|
if (moreWidth !== undefined)
|
|
138
138
|
this.moreExtraWidth = moreWidth;
|
|
139
|
-
|
|
139
|
+
// The natural width: a host may let a drawn item give way (the views
|
|
140
|
+
// bar clips its pinned tab), and a shrunk width fed back here would make
|
|
141
|
+
// room that is not there, then take it back, pass after pass.
|
|
142
|
+
const widths = Array.from(this.items, item => item.scrollWidth);
|
|
140
143
|
const drawn = fitOverflow({
|
|
141
144
|
widths,
|
|
142
145
|
container: this.getBoundingClientRect().width,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { ShadowlessElement } from '../../../../std/index.js';
|
|
1
|
+
import { type BlockStdScope, ShadowlessElement } from '../../../../std/index.js';
|
|
2
2
|
import type { Group } from '../trait.js';
|
|
3
3
|
import type { GroupRenderProps } from '../types.js';
|
|
4
4
|
declare const BaseGroup_base: typeof ShadowlessElement & import("../../../../global/utils/types.js").Constructor<import("../../../../global/lit/with-disposable.js").DisposableClass>;
|
|
5
5
|
export declare class BaseGroup<JsonValue, Data extends Record<string, unknown>> extends BaseGroup_base implements GroupRenderProps<JsonValue, Data> {
|
|
6
6
|
accessor group: Group<unknown, JsonValue, Data>;
|
|
7
7
|
accessor readonly: boolean;
|
|
8
|
+
/** Absent outside an editor host: the wordings read English. */
|
|
9
|
+
accessor std: BlockStdScope;
|
|
10
|
+
get ungrouped(): string;
|
|
8
11
|
updateData(data: Data): void;
|
|
9
12
|
updateValue(value: JsonValue): void;
|
|
10
13
|
get value(): JsonValue;
|