@formicoidea/labre-core 0.45.0 → 0.46.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-callout/commands/turn-callout-into.d.ts +43 -0
- package/dist/_pkgs/affine-block-callout/commands/turn-callout-into.js +109 -0
- package/dist/_pkgs/affine-block-callout/configs/toolbar.d.ts +65 -0
- package/dist/_pkgs/affine-block-callout/configs/toolbar.js +178 -0
- package/dist/_pkgs/affine-block-callout/view.js +7 -1
- package/dist/_pkgs/affine-block-frame/commands.d.ts +27 -0
- package/dist/_pkgs/affine-block-frame/commands.js +49 -0
- package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +11 -6
- package/dist/_pkgs/affine-block-frame/frame-manager.js +10 -5
- package/dist/_pkgs/affine-block-frame/frame-order.d.ts +43 -0
- package/dist/_pkgs/affine-block-frame/frame-order.js +93 -0
- package/dist/_pkgs/affine-block-frame/index.d.ts +2 -0
- package/dist/_pkgs/affine-block-frame/index.js +2 -0
- package/dist/_pkgs/affine-block-frame/present/frame-order-menu.d.ts +10 -2
- package/dist/_pkgs/affine-block-frame/present/frame-order-menu.js +32 -18
- package/dist/_pkgs/affine-block-frame/view.js +6 -0
- package/dist/_pkgs/affine-block-note/configs/toolbar.d.ts +19 -0
- package/dist/_pkgs/affine-block-note/configs/toolbar.js +38 -23
- package/dist/_pkgs/affine-block-note/translations.d.ts +2 -0
- package/dist/_pkgs/affine-block-note/translations.js +14 -1
- package/dist/_pkgs/affine-block-root/configs/toolbar.js +31 -2
- package/dist/_pkgs/affine-block-root/translations.d.ts +7 -2
- package/dist/_pkgs/affine-block-root/translations.js +10 -13
- package/dist/_pkgs/affine-block-surface/extensions/selection-pane/actions.d.ts +20 -9
- package/dist/_pkgs/affine-block-surface/extensions/selection-pane/actions.js +57 -20
- package/dist/_pkgs/affine-block-surface/extensions/selection-pane/commands.d.ts +6 -8
- package/dist/_pkgs/affine-block-surface/extensions/selection-pane/commands.js +21 -8
- package/dist/_pkgs/affine-block-surface/extensions/selection-pane/index.d.ts +1 -1
- package/dist/_pkgs/affine-block-surface/extensions/selection-pane/index.js +1 -1
- package/dist/_pkgs/affine-components/portal/portal.d.ts +1 -1
- package/dist/_pkgs/affine-foundation/view.d.ts +8 -8
- package/dist/_pkgs/affine-fragment-frame-panel/body/frame-panel-body.d.ts +34 -2
- package/dist/_pkgs/affine-fragment-frame-panel/body/frame-panel-body.js +194 -81
- package/dist/_pkgs/affine-fragment-frame-panel/card/frame-card.d.ts +7 -4
- package/dist/_pkgs/affine-fragment-frame-panel/card/frame-card.js +9 -24
- package/dist/_pkgs/affine-fragment-frame-panel/index.d.ts +1 -0
- package/dist/_pkgs/affine-fragment-frame-panel/index.js +1 -0
- package/dist/_pkgs/affine-model/utils/types.d.ts +2 -2
- package/dist/_pkgs/affine-shared/services/edit-props-store.d.ts +4 -4
- package/dist/_pkgs/affine-shared/services/font-loader/config.d.ts +2 -2
- package/dist/_pkgs/affine-shared/services/font-loader/font-loader-service.d.ts +3 -3
- package/dist/_pkgs/affine-shared/services/index.d.ts +1 -1
- package/dist/_pkgs/affine-shared/services/index.js +1 -1
- package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-storage.d.ts +4 -4
- package/dist/_pkgs/affine-shared/services/outline-panel-service.d.ts +31 -0
- package/dist/_pkgs/affine-shared/services/outline-panel-service.js +17 -0
- package/dist/_pkgs/affine-shared/services/translation-service/chrome.d.ts +11 -0
- package/dist/_pkgs/affine-shared/services/translation-service/chrome.js +19 -0
- package/dist/_pkgs/affine-shared/utils/index.d.ts +1 -0
- package/dist/_pkgs/affine-shared/utils/index.js +1 -0
- package/dist/_pkgs/affine-shared/utils/panel-reorder-drag.d.ts +85 -0
- package/dist/_pkgs/affine-shared/utils/panel-reorder-drag.js +162 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/drop.d.ts +7 -6
- package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/drop.js +11 -8
- package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/selection-pane-widget.d.ts +31 -15
- package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/selection-pane-widget.js +178 -142
- package/dist/_pkgs/std/scope/std-scope.d.ts +1 -1
- package/dist/commands.js +2 -0
- package/dist/host-panels.d.ts +90 -0
- package/dist/host-panels.js +111 -0
- package/package.json +5 -1
- package/dist/_pkgs/affine-fragment-frame-panel/utils/drag.d.ts +0 -27
- package/dist/_pkgs/affine-fragment-frame-panel/utils/drag.js +0 -96
- package/dist/_pkgs/affine-shared/services/sidebar-service.d.ts +0 -8
- package/dist/_pkgs/affine-shared/services/sidebar-service.js +0 -7
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CalloutBlockModel } from '../../affine-model/index.js';
|
|
2
|
+
import { type Command } from '../../std/index.js';
|
|
3
|
+
import { type BlockModel, type Store } from '../../store/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The selected callouts "Turn into → <text kind>" would unwrap, or none at all.
|
|
6
|
+
*
|
|
7
|
+
* Decided BEFORE anything is written, and all-or-nothing: if one callout
|
|
8
|
+
* cannot be converted, none is (#468). A callout cannot be when its parent
|
|
9
|
+
* does not accept the target flavour (a code block under a database row),
|
|
10
|
+
* or when a grandchild could not live under the block its own parent becomes
|
|
11
|
+
* — and a code block keeps no children at all, so a callout whose content
|
|
12
|
+
* has nested blocks is not turned into code (the merge would drop them).
|
|
13
|
+
* Anything that is not a callout is skipped, and a callout nested in another
|
|
14
|
+
* selected callout moves with it rather than being converted twice.
|
|
15
|
+
*/
|
|
16
|
+
export declare function calloutUnwrapTargets(store: Store, models: BlockModel[], flavour: string): CalloutBlockModel[];
|
|
17
|
+
/**
|
|
18
|
+
* "Turn into → <text kind>" on a selected callout (#468): the exact inverse of
|
|
19
|
+
* `turnIntoCalloutCommand`. The callout is a HUB whose own `text` is unused
|
|
20
|
+
* and whose words live in its children, so the conversion UNWRAPS: each child
|
|
21
|
+
* becomes a block of the target kind at the callout's place, in order, its
|
|
22
|
+
* own children moved under it; a target of `affine:code` merges a callout's
|
|
23
|
+
* children into one code block, the way `mergeToCodeModel` merges a
|
|
24
|
+
* selection; an empty callout yields one empty block. The callout's emoji is
|
|
25
|
+
* dropped with it — it belongs to the frame being removed.
|
|
26
|
+
*
|
|
27
|
+
* Add first, prove with `getModelById`, delete only after (#418, #423): every
|
|
28
|
+
* new block is built before anything moves, and if one cannot be, the ones
|
|
29
|
+
* already built are deleted and every callout stays as it was. Only then are
|
|
30
|
+
* the grandchildren moved (validated up front by `calloutUnwrapTargets`) and
|
|
31
|
+
* the callouts deleted. The lifted blocks are therefore NEW blocks carrying
|
|
32
|
+
* the text, like the paragraph `turnIntoCalloutCommand` creates, while the
|
|
33
|
+
* grandchildren keep their identity.
|
|
34
|
+
*/
|
|
35
|
+
export declare const turnCalloutIntoCommand: Command<{
|
|
36
|
+
models: BlockModel[];
|
|
37
|
+
flavour: string;
|
|
38
|
+
props?: {
|
|
39
|
+
type?: string;
|
|
40
|
+
};
|
|
41
|
+
}, {
|
|
42
|
+
updatedBlocks: BlockModel[];
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CalloutBlockModel } from '../../affine-model/index.js';
|
|
2
|
+
import { matchModels } from '../../affine-shared/utils/index.js';
|
|
3
|
+
import { BlockSelection } from '../../std/index.js';
|
|
4
|
+
import { Text } from '../../store/index.js';
|
|
5
|
+
const isAncestor = (store, ancestor, block) => {
|
|
6
|
+
for (let at = store.getParent(block); at; at = store.getParent(at)) {
|
|
7
|
+
if (at === ancestor)
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The selected callouts "Turn into → <text kind>" would unwrap, or none at all.
|
|
14
|
+
*
|
|
15
|
+
* Decided BEFORE anything is written, and all-or-nothing: if one callout
|
|
16
|
+
* cannot be converted, none is (#468). A callout cannot be when its parent
|
|
17
|
+
* does not accept the target flavour (a code block under a database row),
|
|
18
|
+
* or when a grandchild could not live under the block its own parent becomes
|
|
19
|
+
* — and a code block keeps no children at all, so a callout whose content
|
|
20
|
+
* has nested blocks is not turned into code (the merge would drop them).
|
|
21
|
+
* Anything that is not a callout is skipped, and a callout nested in another
|
|
22
|
+
* selected callout moves with it rather than being converted twice.
|
|
23
|
+
*/
|
|
24
|
+
export function calloutUnwrapTargets(store, models, flavour) {
|
|
25
|
+
const { schema } = store;
|
|
26
|
+
const callouts = models.filter((model) => matchModels(model, [CalloutBlockModel]));
|
|
27
|
+
const outermost = callouts.filter(callout => !callouts.some(other => isAncestor(store, other, callout)));
|
|
28
|
+
const feasible = outermost.every(callout => {
|
|
29
|
+
const parent = store.getParent(callout);
|
|
30
|
+
if (!parent || !schema.isValid(flavour, parent.flavour))
|
|
31
|
+
return false;
|
|
32
|
+
return callout.children.every(child => flavour === 'affine:code'
|
|
33
|
+
? child.children.length === 0
|
|
34
|
+
: child.children.every(grandchild => schema.isValid(grandchild.flavour, flavour)));
|
|
35
|
+
});
|
|
36
|
+
return feasible ? outermost : [];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* "Turn into → <text kind>" on a selected callout (#468): the exact inverse of
|
|
40
|
+
* `turnIntoCalloutCommand`. The callout is a HUB whose own `text` is unused
|
|
41
|
+
* and whose words live in its children, so the conversion UNWRAPS: each child
|
|
42
|
+
* becomes a block of the target kind at the callout's place, in order, its
|
|
43
|
+
* own children moved under it; a target of `affine:code` merges a callout's
|
|
44
|
+
* children into one code block, the way `mergeToCodeModel` merges a
|
|
45
|
+
* selection; an empty callout yields one empty block. The callout's emoji is
|
|
46
|
+
* dropped with it — it belongs to the frame being removed.
|
|
47
|
+
*
|
|
48
|
+
* Add first, prove with `getModelById`, delete only after (#418, #423): every
|
|
49
|
+
* new block is built before anything moves, and if one cannot be, the ones
|
|
50
|
+
* already built are deleted and every callout stays as it was. Only then are
|
|
51
|
+
* the grandchildren moved (validated up front by `calloutUnwrapTargets`) and
|
|
52
|
+
* the callouts deleted. The lifted blocks are therefore NEW blocks carrying
|
|
53
|
+
* the text, like the paragraph `turnIntoCalloutCommand` creates, while the
|
|
54
|
+
* grandchildren keep their identity.
|
|
55
|
+
*/
|
|
56
|
+
export const turnCalloutIntoCommand = (ctx, next) => {
|
|
57
|
+
const { std, models, flavour, props } = ctx;
|
|
58
|
+
const store = std.store;
|
|
59
|
+
if (store.readonly)
|
|
60
|
+
return;
|
|
61
|
+
const callouts = calloutUnwrapTargets(store, models, flavour);
|
|
62
|
+
if (callouts.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
store.captureSync();
|
|
65
|
+
const created = [];
|
|
66
|
+
const adoptions = [];
|
|
67
|
+
const add = (callout, text) => {
|
|
68
|
+
const parent = store.getParent(callout);
|
|
69
|
+
// Inserted right before the callout, so the blocks keep their order.
|
|
70
|
+
const block = parent
|
|
71
|
+
? store.getModelById(store.addBlock(flavour, { ...props, ...(text && { text }) }, parent, parent.children.indexOf(callout)))
|
|
72
|
+
: null;
|
|
73
|
+
if (block)
|
|
74
|
+
created.push(block);
|
|
75
|
+
return block;
|
|
76
|
+
};
|
|
77
|
+
// `addBlock` returns an id even for a block the store refused, so only
|
|
78
|
+
// `getModelById` proves the block exists (`add` above).
|
|
79
|
+
const build = (callout) => {
|
|
80
|
+
const sources = callout.children;
|
|
81
|
+
if (flavour === 'affine:code') {
|
|
82
|
+
const joined = sources
|
|
83
|
+
.map(source => source.text?.toString())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join('\n');
|
|
86
|
+
return add(callout, new Text(joined)) !== null;
|
|
87
|
+
}
|
|
88
|
+
if (sources.length === 0)
|
|
89
|
+
return add(callout) !== null;
|
|
90
|
+
return sources.every(source => {
|
|
91
|
+
const block = add(callout, source.text?.clone());
|
|
92
|
+
if (block && source.children.length > 0)
|
|
93
|
+
adoptions.push([source.children, block]);
|
|
94
|
+
return block !== null;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
if (!callouts.every(build)) {
|
|
98
|
+
created.forEach(block => store.deleteBlock(block));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
adoptions.forEach(([children, block]) => store.moveBlocks(children, block));
|
|
102
|
+
callouts.forEach(callout => store.deleteBlock(callout));
|
|
103
|
+
// The gesture started from a block selection (the callout's handle), so it
|
|
104
|
+
// ends on one; deferred a frame so the new blocks have rendered.
|
|
105
|
+
requestAnimationFrame(() => {
|
|
106
|
+
std.selection.setGroup('note', created.map(block => std.selection.create(BlockSelection, { blockId: block.id })));
|
|
107
|
+
});
|
|
108
|
+
return next({ updatedBlocks: created });
|
|
109
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ActionPlacement, type ToolbarContext } from '../../affine-shared/services/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The callout's block toolbar, opened by its drag handle (#468). Without a
|
|
4
|
+
* module for `affine:callout` the toolbar widget shows no row for a selected
|
|
5
|
+
* callout at all, so a callout could not be converted back. Copy, duplicate
|
|
6
|
+
* and delete are the note row's, restricted to the block selection — except
|
|
7
|
+
* that they take the selection in `flat` mode, descendants included, where
|
|
8
|
+
* the note row's Duplicate takes `highest`: a callout is a hub, and drafted
|
|
9
|
+
* without its children it would copy as an empty frame.
|
|
10
|
+
*
|
|
11
|
+
* Centred above the block (`top`) like the note row a paragraph or a code
|
|
12
|
+
* block opens; without a placement of its own the widget sets a block's row
|
|
13
|
+
* at `top-start`, which left the callout's row hanging off its left edge.
|
|
14
|
+
*/
|
|
15
|
+
export declare const calloutToolbarConfig: {
|
|
16
|
+
readonly placement: "top";
|
|
17
|
+
readonly actions: [{
|
|
18
|
+
readonly id: "a.conversions";
|
|
19
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
20
|
+
readonly content: (ctx: ToolbarContext) => import("lit-html").TemplateResult<1> | null;
|
|
21
|
+
}, {
|
|
22
|
+
readonly variant?: "destructive";
|
|
23
|
+
readonly content?: ((cx: ToolbarContext) => import("lit-html").TemplateResult | null) | (import("lit-html").TemplateResult | null);
|
|
24
|
+
readonly label?: string;
|
|
25
|
+
readonly icon?: import("lit-html").TemplateResult;
|
|
26
|
+
readonly active?: ((cx: ToolbarContext) => boolean) | boolean;
|
|
27
|
+
readonly disabled?: ((cx: ToolbarContext) => boolean) | boolean;
|
|
28
|
+
readonly placement?: ActionPlacement;
|
|
29
|
+
readonly tooltip?: string | import("lit-html").TemplateResult;
|
|
30
|
+
readonly score?: number;
|
|
31
|
+
readonly when?: ((cx: ToolbarContext) => boolean) | boolean;
|
|
32
|
+
readonly priority?: number;
|
|
33
|
+
readonly labelWording?: import("../../affine-shared/services/index.js").ChromeWording;
|
|
34
|
+
readonly showLabel?: boolean;
|
|
35
|
+
readonly tooltipWording?: import("../../affine-shared/services/index.js").ChromeWording;
|
|
36
|
+
readonly run?: (cx: ToolbarContext) => void;
|
|
37
|
+
readonly id: "g.comment";
|
|
38
|
+
}, {
|
|
39
|
+
readonly placement: ActionPlacement.More;
|
|
40
|
+
readonly id: "a.clipboard";
|
|
41
|
+
readonly actions: [{
|
|
42
|
+
readonly id: "copy";
|
|
43
|
+
readonly labelWording: import("../../affine-shared/services/index.js").ChromeWording;
|
|
44
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
45
|
+
readonly run: ({ chain, host, std }: ToolbarContext) => void;
|
|
46
|
+
}, {
|
|
47
|
+
readonly id: "duplicate";
|
|
48
|
+
readonly labelWording: import("../../affine-shared/services/index.js").ChromeWording;
|
|
49
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
50
|
+
readonly run: (ctx: ToolbarContext) => void;
|
|
51
|
+
}];
|
|
52
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
53
|
+
}, {
|
|
54
|
+
readonly placement: ActionPlacement.More;
|
|
55
|
+
readonly id: "c.delete";
|
|
56
|
+
readonly actions: [{
|
|
57
|
+
readonly id: "delete";
|
|
58
|
+
readonly labelWording: import("../../affine-shared/services/index.js").ChromeWording;
|
|
59
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
60
|
+
readonly variant: "destructive";
|
|
61
|
+
readonly run: ({ chain }: ToolbarContext) => void;
|
|
62
|
+
}];
|
|
63
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
64
|
+
}];
|
|
65
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { toast } from '../../affine-components/toast/index.js';
|
|
2
|
+
import { EditorChevronDown } from '../../affine-components/toolbar/index.js';
|
|
3
|
+
import { CalloutBlockModel } from '../../affine-model/index.js';
|
|
4
|
+
import { textConversionConfigs } from '../../affine-rich-text/index.js';
|
|
5
|
+
import { copySelectedModelsCommand, deleteSelectedModelsCommand, draftSelectedModelsCommand, duplicateSelectedModelsCommand, getSelectedModelsCommand, } from '../../affine-shared/commands/index.js';
|
|
6
|
+
import { ActionPlacement, blockCommentToolbarButton, TOAST_COPIED_TO_CLIPBOARD, TOOLBAR_CONVERSIONS_ARIA, TOOLBAR_COPY, TOOLBAR_DELETE, TOOLBAR_DUPLICATE, TOOLBAR_TURN_INTO, translateKey, } from '../../affine-shared/services/index.js';
|
|
7
|
+
import { matchModels } from '../../affine-shared/utils/index.js';
|
|
8
|
+
import { CopyIcon, DeleteIcon, DuplicateIcon, FontIcon, } from '@blocksuite/icons/lit';
|
|
9
|
+
import { html } from 'lit';
|
|
10
|
+
import { repeat } from 'lit/directives/repeat.js';
|
|
11
|
+
import { calloutUnwrapTargets, turnCalloutIntoCommand, } from '../commands/turn-callout-into.js';
|
|
12
|
+
/** The block-selected callouts — the row only opens on a block selection. */
|
|
13
|
+
const selectedCallouts = ({ chain }) => {
|
|
14
|
+
const [ok, { selectedModels = [] }] = chain
|
|
15
|
+
.pipe(getSelectedModelsCommand, { types: ['block'], mode: 'highest' })
|
|
16
|
+
.run();
|
|
17
|
+
return ok
|
|
18
|
+
? selectedModels.filter(model => matchModels(model, [CalloutBlockModel]))
|
|
19
|
+
: [];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* "Turn into" on a callout (#468): the text kinds of the note's own row, minus
|
|
23
|
+
* the divider (which is not a conversion of a block's words), each offered
|
|
24
|
+
* only when `calloutUnwrapTargets` says the whole conversion can complete —
|
|
25
|
+
* a refusal is an absent entry, never a click that does nothing.
|
|
26
|
+
*
|
|
27
|
+
* Like the note row, which offers "Turn into" on a paragraph or a code block
|
|
28
|
+
* only once it holds words (`isFormatSupported`), the entry waits for the
|
|
29
|
+
* callout to hold some: an empty callout shows the "⋮" menu alone (PO
|
|
30
|
+
* recette of #468, for one behaviour across blocks).
|
|
31
|
+
*/
|
|
32
|
+
const conversionsAction = {
|
|
33
|
+
id: 'a.conversions',
|
|
34
|
+
when: ctx => !ctx.readonly,
|
|
35
|
+
content(ctx) {
|
|
36
|
+
const { std, store } = ctx;
|
|
37
|
+
const callouts = selectedCallouts(ctx);
|
|
38
|
+
if (callouts.length === 0)
|
|
39
|
+
return null;
|
|
40
|
+
const holdsWords = callouts.some(callout => callout.children.some(child => (child.text?.length ?? 0) > 0));
|
|
41
|
+
if (!holdsWords)
|
|
42
|
+
return null;
|
|
43
|
+
const conversions = textConversionConfigs.filter(({ flavour }) => flavour !== 'affine:divider' &&
|
|
44
|
+
calloutUnwrapTargets(store, callouts, flavour).length > 0);
|
|
45
|
+
if (conversions.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
return html `
|
|
48
|
+
<editor-menu-button
|
|
49
|
+
.contentPadding="${'8px'}"
|
|
50
|
+
.button=${html `
|
|
51
|
+
<editor-icon-button
|
|
52
|
+
aria-label="${translateKey(std, ...TOOLBAR_CONVERSIONS_ARIA)}"
|
|
53
|
+
.tooltip="${translateKey(std, ...TOOLBAR_TURN_INTO)}"
|
|
54
|
+
>
|
|
55
|
+
${FontIcon()} ${EditorChevronDown}
|
|
56
|
+
</editor-icon-button>
|
|
57
|
+
`}
|
|
58
|
+
>
|
|
59
|
+
<div data-size="large" data-orientation="vertical">
|
|
60
|
+
${repeat(conversions, item => item.name, ({ flavour, type, name, nameWording, icon }) => {
|
|
61
|
+
const label = nameWording
|
|
62
|
+
? translateKey(std, ...nameWording)
|
|
63
|
+
: name;
|
|
64
|
+
return html `
|
|
65
|
+
<editor-menu-action
|
|
66
|
+
aria-label=${label}
|
|
67
|
+
@click=${() => std.command.exec(turnCalloutIntoCommand, {
|
|
68
|
+
models: callouts,
|
|
69
|
+
flavour,
|
|
70
|
+
...(type && { props: { type } }),
|
|
71
|
+
})}
|
|
72
|
+
>
|
|
73
|
+
${icon}<span class="label">${label}</span>
|
|
74
|
+
</editor-menu-action>
|
|
75
|
+
`;
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
</editor-menu-button>
|
|
79
|
+
`;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The callout's block toolbar, opened by its drag handle (#468). Without a
|
|
84
|
+
* module for `affine:callout` the toolbar widget shows no row for a selected
|
|
85
|
+
* callout at all, so a callout could not be converted back. Copy, duplicate
|
|
86
|
+
* and delete are the note row's, restricted to the block selection — except
|
|
87
|
+
* that they take the selection in `flat` mode, descendants included, where
|
|
88
|
+
* the note row's Duplicate takes `highest`: a callout is a hub, and drafted
|
|
89
|
+
* without its children it would copy as an empty frame.
|
|
90
|
+
*
|
|
91
|
+
* Centred above the block (`top`) like the note row a paragraph or a code
|
|
92
|
+
* block opens; without a placement of its own the widget sets a block's row
|
|
93
|
+
* at `top-start`, which left the callout's row hanging off its left edge.
|
|
94
|
+
*/
|
|
95
|
+
export const calloutToolbarConfig = {
|
|
96
|
+
placement: 'top',
|
|
97
|
+
actions: [
|
|
98
|
+
conversionsAction,
|
|
99
|
+
{
|
|
100
|
+
id: 'g.comment',
|
|
101
|
+
...blockCommentToolbarButton,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
placement: ActionPlacement.More,
|
|
105
|
+
id: 'a.clipboard',
|
|
106
|
+
actions: [
|
|
107
|
+
{
|
|
108
|
+
id: 'copy',
|
|
109
|
+
labelWording: TOOLBAR_COPY,
|
|
110
|
+
icon: CopyIcon(),
|
|
111
|
+
run({ chain, host, std }) {
|
|
112
|
+
// `flat`, not `highest`: the callout's words are its children,
|
|
113
|
+
// and the draft keeps only the children that are selected too.
|
|
114
|
+
const [ok] = chain
|
|
115
|
+
.pipe(getSelectedModelsCommand, {
|
|
116
|
+
types: ['block'],
|
|
117
|
+
mode: 'flat',
|
|
118
|
+
})
|
|
119
|
+
.pipe(draftSelectedModelsCommand)
|
|
120
|
+
.pipe(copySelectedModelsCommand)
|
|
121
|
+
.run();
|
|
122
|
+
if (!ok)
|
|
123
|
+
return;
|
|
124
|
+
toast(host, translateKey(std, ...TOAST_COPIED_TO_CLIPBOARD));
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'duplicate',
|
|
129
|
+
labelWording: TOOLBAR_DUPLICATE,
|
|
130
|
+
icon: DuplicateIcon(),
|
|
131
|
+
run(ctx) {
|
|
132
|
+
const { chain, store } = ctx;
|
|
133
|
+
// The copy lands right after the (last) callout. Said explicitly:
|
|
134
|
+
// the command's default anchors on the LAST selected model, which
|
|
135
|
+
// in `flat` mode is one of the callout's descendants.
|
|
136
|
+
const last = selectedCallouts(ctx).at(-1);
|
|
137
|
+
const parentModel = last && store.getParent(last);
|
|
138
|
+
if (!last || !parentModel)
|
|
139
|
+
return;
|
|
140
|
+
store.captureSync();
|
|
141
|
+
// `flat`, not `highest`, for the same reason as Copy: a callout
|
|
142
|
+
// drafted without its children duplicates as an empty frame.
|
|
143
|
+
chain
|
|
144
|
+
.pipe(getSelectedModelsCommand, {
|
|
145
|
+
types: ['block'],
|
|
146
|
+
mode: 'flat',
|
|
147
|
+
})
|
|
148
|
+
.pipe(duplicateSelectedModelsCommand, {
|
|
149
|
+
parentModel,
|
|
150
|
+
index: parentModel.children.indexOf(last) + 1,
|
|
151
|
+
})
|
|
152
|
+
.run();
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
when: ctx => !ctx.flags.isNative(),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
placement: ActionPlacement.More,
|
|
160
|
+
id: 'c.delete',
|
|
161
|
+
actions: [
|
|
162
|
+
{
|
|
163
|
+
id: 'delete',
|
|
164
|
+
labelWording: TOOLBAR_DELETE,
|
|
165
|
+
icon: DeleteIcon(),
|
|
166
|
+
variant: 'destructive',
|
|
167
|
+
run({ chain }) {
|
|
168
|
+
chain
|
|
169
|
+
.pipe(getSelectedModelsCommand, { types: ['block'] })
|
|
170
|
+
.pipe(deleteSelectedModelsCommand)
|
|
171
|
+
.run();
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
when: ctx => !ctx.flags.isNative(),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ViewExtensionProvider, } from '../affine-ext-loader/index.js';
|
|
2
2
|
import { TextConversionEntryExtension } from '../affine-rich-text/index.js';
|
|
3
|
+
import { ToolbarModuleExtension } from '../affine-shared/services/index.js';
|
|
3
4
|
import { SlashMenuConfigExtension } from '../affine-widget-slash-menu/index.js';
|
|
4
|
-
import { BlockViewExtension, FlavourExtension } from '../std/index.js';
|
|
5
|
+
import { BlockFlavourIdentifier, BlockViewExtension, FlavourExtension, } from '../std/index.js';
|
|
5
6
|
import { literal } from 'lit/static-html.js';
|
|
6
7
|
import { CalloutKeymapExtension } from './callout-keymap.js';
|
|
7
8
|
import { calloutSlashMenuConfig } from './configs/slash-menu.js';
|
|
9
|
+
import { calloutToolbarConfig } from './configs/toolbar.js';
|
|
8
10
|
import { calloutTurnIntoEntry } from './configs/turn-into.js';
|
|
9
11
|
import { effects } from './effects.js';
|
|
10
12
|
export class CalloutViewExtension extends ViewExtensionProvider {
|
|
@@ -24,6 +26,10 @@ export class CalloutViewExtension extends ViewExtensionProvider {
|
|
|
24
26
|
CalloutKeymapExtension,
|
|
25
27
|
SlashMenuConfigExtension('affine:callout', calloutSlashMenuConfig),
|
|
26
28
|
TextConversionEntryExtension(calloutTurnIntoEntry),
|
|
29
|
+
ToolbarModuleExtension({
|
|
30
|
+
id: BlockFlavourIdentifier('affine:callout'),
|
|
31
|
+
config: calloutToolbarConfig,
|
|
32
|
+
}),
|
|
27
33
|
]);
|
|
28
34
|
}
|
|
29
35
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AnyCommandDescriptor } from '../std/index.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* The frames' commands, `owner: 'core'`: a frame belongs to no framework, and
|
|
5
|
+
* `'core'` owners are exempt from the id-prefix rule (ADR 0008).
|
|
6
|
+
*
|
|
7
|
+
* They live here and not beside the selection pane's: `blocks/surface` cannot
|
|
8
|
+
* import `blocks/frame` (the dependency runs the other way), and the action
|
|
9
|
+
* reads the frame manager's presentation order.
|
|
10
|
+
*/
|
|
11
|
+
export declare const reorderFramesParams: z.ZodObject<{
|
|
12
|
+
/** The frames to move, as one block, keeping their relative order. */
|
|
13
|
+
ids: z.ZodArray<z.ZodString, "many">;
|
|
14
|
+
/**
|
|
15
|
+
* The frame they land directly BEFORE in the presentation order; `null`
|
|
16
|
+
* puts them at the end.
|
|
17
|
+
*/
|
|
18
|
+
before: z.ZodNullable<z.ZodString>;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
before: string | null;
|
|
21
|
+
ids: string[];
|
|
22
|
+
}, {
|
|
23
|
+
before: string | null;
|
|
24
|
+
ids: string[];
|
|
25
|
+
}>;
|
|
26
|
+
export type ReorderFramesParams = z.infer<typeof reorderFramesParams>;
|
|
27
|
+
export declare const frameCommands: AnyCommandDescriptor[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { reorderFramePresentation } from './frame-order.js';
|
|
3
|
+
/**
|
|
4
|
+
* The frames' commands, `owner: 'core'`: a frame belongs to no framework, and
|
|
5
|
+
* `'core'` owners are exempt from the id-prefix rule (ADR 0008).
|
|
6
|
+
*
|
|
7
|
+
* They live here and not beside the selection pane's: `blocks/surface` cannot
|
|
8
|
+
* import `blocks/frame` (the dependency runs the other way), and the action
|
|
9
|
+
* reads the frame manager's presentation order.
|
|
10
|
+
*/
|
|
11
|
+
export const reorderFramesParams = z.object({
|
|
12
|
+
/** The frames to move, as one block, keeping their relative order. */
|
|
13
|
+
ids: z.array(z.string().min(1)).min(1),
|
|
14
|
+
/**
|
|
15
|
+
* The frame they land directly BEFORE in the presentation order; `null`
|
|
16
|
+
* puts them at the end.
|
|
17
|
+
*/
|
|
18
|
+
before: z.string().min(1).nullable(),
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Reorder the presentation (ADR 0034). The frame panel's drag, the
|
|
22
|
+
* presentation toolbar's order menu and a host's own slide panel all run it
|
|
23
|
+
* through `runCommand`, so the read-only refusal, the no-op check and the one
|
|
24
|
+
* undo step live in the action, once.
|
|
25
|
+
*/
|
|
26
|
+
const reorderFrames = {
|
|
27
|
+
id: 'canvas.frame.reorder',
|
|
28
|
+
owner: 'core',
|
|
29
|
+
kind: 'action',
|
|
30
|
+
labelKey: 'com.labre.command.canvas.frame.reorder',
|
|
31
|
+
labelFallback: 'Reorder frames',
|
|
32
|
+
descriptionKey: 'com.labre.command.canvas.frame.reorder.description',
|
|
33
|
+
descriptionFallback: 'Move frames before another frame in the presentation order, or to its end.',
|
|
34
|
+
// Agent only: without an explicit target there is nothing to say where.
|
|
35
|
+
surfaces: ['agent'],
|
|
36
|
+
scope: 'edgeless',
|
|
37
|
+
defaultKeys: { mac: [], other: [] },
|
|
38
|
+
availability: 'editable',
|
|
39
|
+
params: reorderFramesParams,
|
|
40
|
+
run: (std, _invocation, params) => {
|
|
41
|
+
const parsed = reorderFramesParams.safeParse(params);
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
console.error('canvas.frame.reorder: invalid params', parsed.error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
reorderFramePresentation(std, parsed.data.ids, parsed.data.before);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export const frameCommands = [reorderFrames];
|
|
@@ -24,9 +24,9 @@ export declare class EdgelessFrameManager extends GfxExtension {
|
|
|
24
24
|
private readonly _pendingAdoptions;
|
|
25
25
|
private _pendingAdoptionTicks;
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
* the legacy
|
|
29
|
-
*
|
|
27
|
+
* Every frame, in presentation order (`framePresentationComparator`): by
|
|
28
|
+
* `presentationIndex`, and the legacy frames that have none AFTER the keyed
|
|
29
|
+
* ones, ordered among themselves by their stacking `index`.
|
|
30
30
|
*/
|
|
31
31
|
get frames(): FrameBlockModel[];
|
|
32
32
|
constructor(gfx: GfxController);
|
|
@@ -35,7 +35,7 @@ export declare class EdgelessFrameManager extends GfxExtension {
|
|
|
35
35
|
index: string;
|
|
36
36
|
presentationIndex?: string;
|
|
37
37
|
};
|
|
38
|
-
}>(a: T, b: T):
|
|
38
|
+
}>(a: T, b: T): 1 | 0 | -1;
|
|
39
39
|
private _addChildrenToLegacyFrame;
|
|
40
40
|
private _addFrameBlock;
|
|
41
41
|
/**
|
|
@@ -124,8 +124,13 @@ export declare class EdgelessFrameManager extends GfxExtension {
|
|
|
124
124
|
getFrameFromPoint([x, y]: IVec, ignoreFrames?: FrameBlockModel[]): FrameBlockModel | null;
|
|
125
125
|
getParentFrame(element: GfxModel): FrameBlockModel | null;
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
127
|
+
* Give a `presentationIndex` to legacy frames, keeping their order — but
|
|
128
|
+
* only on a document where NO frame has one yet. The comparator sorts
|
|
129
|
+
* keyless frames AFTER keyed ones, so as soon as one frame is keyed the first
|
|
130
|
+
* frame is keyed and this returns at once: a mix of keyed and keyless frames
|
|
131
|
+
* is left as it is. In practice a loaded frame always carries a key (the
|
|
132
|
+
* schema's default is written on load), so this only acts on a hand-built
|
|
133
|
+
* store. Idempotent; writes through the props proxy.
|
|
129
134
|
*/
|
|
130
135
|
refreshLegacyFrameOrder(): void;
|
|
131
136
|
removeAllChildrenFromFrame(frame: FrameBlockModel): void;
|
|
@@ -90,9 +90,9 @@ export const EdgelessFrameManagerIdentifier = GfxExtensionIdentifier('frame-mana
|
|
|
90
90
|
export class EdgelessFrameManager extends GfxExtension {
|
|
91
91
|
static { this.key = 'frame-manager'; }
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
* the legacy
|
|
95
|
-
*
|
|
93
|
+
* Every frame, in presentation order (`framePresentationComparator`): by
|
|
94
|
+
* `presentationIndex`, and the legacy frames that have none AFTER the keyed
|
|
95
|
+
* ones, ordered among themselves by their stacking `index`.
|
|
96
96
|
*/
|
|
97
97
|
get frames() {
|
|
98
98
|
return Object.values(this.gfx.doc.blocks.value)
|
|
@@ -484,8 +484,13 @@ export class EdgelessFrameManager extends GfxExtension {
|
|
|
484
484
|
return container && isFrameBlock(container) ? container : null;
|
|
485
485
|
}
|
|
486
486
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
487
|
+
* Give a `presentationIndex` to legacy frames, keeping their order — but
|
|
488
|
+
* only on a document where NO frame has one yet. The comparator sorts
|
|
489
|
+
* keyless frames AFTER keyed ones, so as soon as one frame is keyed the first
|
|
490
|
+
* frame is keyed and this returns at once: a mix of keyed and keyless frames
|
|
491
|
+
* is left as it is. In practice a loaded frame always carries a key (the
|
|
492
|
+
* schema's default is written on load), so this only acts on a hand-built
|
|
493
|
+
* store. Idempotent; writes through the props proxy.
|
|
489
494
|
*/
|
|
490
495
|
refreshLegacyFrameOrder() {
|
|
491
496
|
const frames = this.frames.splice(0, this.frames.length);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BlockStdScope } from '../std/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The one write of the frames' PRESENTATION order (ADR 0034): the
|
|
4
|
+
* order a presentation walks the frames in, held by each frame's
|
|
5
|
+
* `presentationIndex` — a fractional key, never a position.
|
|
6
|
+
*
|
|
7
|
+
* Why one function: two sites wrote that order by hand — the frame panel's
|
|
8
|
+
* drag and the presentation toolbar's order menu — each its own way, neither
|
|
9
|
+
* refusing a read-only document, and the panel taking its undo checkpoint
|
|
10
|
+
* AFTER the write, so the reorder merged into whatever gesture came before.
|
|
11
|
+
* Both now reach this through `canvas.frame.reorder`, and so does a host's own
|
|
12
|
+
* slide panel: the guards below live here, once.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors the selection pane's `reorderPaneElement`
|
|
15
|
+
* (`blocks/surface/src/extensions/selection-pane/actions.ts`):
|
|
16
|
+
*
|
|
17
|
+
* - refuses on a read-only document before reading anything else;
|
|
18
|
+
* - writes nothing when nothing would change — an unchanged write is an empty
|
|
19
|
+
* undo step, and an undo that "does nothing" reads as a broken undo;
|
|
20
|
+
* - takes ONE `captureSync()` first, so one gesture is one undo step, the
|
|
21
|
+
* legacy keys `refreshLegacyFrameOrder` may write included (it writes in
|
|
22
|
+
* the same capture window, so it joins the gesture's step);
|
|
23
|
+
* - writes through `gfx.updateElement`, which is `store.updateBlock` for a
|
|
24
|
+
* frame — not the CRUD `updateElement`, which would also record the moved
|
|
25
|
+
* frame's props as the flavour's last-used props.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Move the frames `ids` so they sit, as one block, directly BEFORE `before` in
|
|
29
|
+
* the presentation order — `null` puts them at the end. The moved frames keep
|
|
30
|
+
* their current relative order, whatever order `ids` lists them in. Answers
|
|
31
|
+
* whether anything was written.
|
|
32
|
+
*
|
|
33
|
+
* Refused, with nothing written: a read-only document; an id that is not a
|
|
34
|
+
* frame of this document; a `before` that is one of the moved frames, or not
|
|
35
|
+
* a frame; and a target that is where the frames already are.
|
|
36
|
+
*
|
|
37
|
+
* Each moved frame gets a key between its new neighbours, so only the moved
|
|
38
|
+
* frames are written and nothing else shifts. Two neighbours with equal (or
|
|
39
|
+
* inverted) keys cannot be split — `generateKeyBetweenV2` throws on them, and
|
|
40
|
+
* the model's default key is random per frame, so a pasted, imported or
|
|
41
|
+
* hand-made document can carry them: refusing beats throwing in a drop.
|
|
42
|
+
*/
|
|
43
|
+
export declare function reorderFramePresentation(std: BlockStdScope, ids: readonly string[], before: string | null): boolean;
|