@formicoidea/labre-core 0.57.0 → 0.59.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/architecture/code.c4 +94 -65
- package/architecture/domain.c4 +2510 -0
- package/architecture/likec4.config.json +3 -3
- package/dist/_pkgs/affine-block-attachment/edgeless-clipboard-config.d.ts +3 -2
- package/dist/_pkgs/affine-block-attachment/edgeless-clipboard-config.js +18 -13
- package/dist/_pkgs/affine-block-callout/adapters/markdown.js +4 -1
- package/dist/_pkgs/affine-block-code/adapters/html.js +1 -1
- package/dist/_pkgs/affine-block-code/adapters/markdown/preprocessor.js +13 -1
- package/dist/_pkgs/affine-block-code/adapters/notion-html.js +1 -1
- package/dist/_pkgs/affine-block-code/code-block.d.ts +17 -2
- package/dist/_pkgs/affine-block-code/code-block.js +28 -23
- package/dist/_pkgs/affine-block-code/code-toolbar/components/lang-button.js +17 -4
- package/dist/_pkgs/affine-block-code/code-toolbar/components/preview-button.js +1 -1
- package/dist/_pkgs/affine-block-data-view/data-source.js +12 -5
- package/dist/_pkgs/affine-block-edgeless-text/edgeless-clipboard-config.d.ts +3 -2
- package/dist/_pkgs/affine-block-edgeless-text/edgeless-clipboard-config.js +8 -5
- package/dist/_pkgs/affine-block-image/asset-picker.d.ts +23 -2
- package/dist/_pkgs/affine-block-image/asset-picker.js +52 -12
- package/dist/_pkgs/affine-block-image/configs/replace-action.d.ts +8 -5
- package/dist/_pkgs/affine-block-image/configs/replace-action.js +8 -8
- package/dist/_pkgs/affine-block-image/configs/toolbar.js +2 -1
- package/dist/_pkgs/affine-block-image/crop/crop-controller.d.ts +15 -0
- package/dist/_pkgs/affine-block-image/crop/crop-controller.js +40 -2
- package/dist/_pkgs/affine-block-image/crop/crop-math.d.ts +22 -0
- package/dist/_pkgs/affine-block-image/crop/crop-math.js +55 -2
- package/dist/_pkgs/affine-block-image/crop/crop-overlay.d.ts +17 -3
- package/dist/_pkgs/affine-block-image/crop/crop-overlay.js +64 -6
- package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.d.ts +3 -2
- package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.js +20 -14
- package/dist/_pkgs/affine-block-image/image-block.js +1 -0
- package/dist/_pkgs/affine-block-image/image-edgeless-block.js +3 -0
- package/dist/_pkgs/affine-block-image/index.d.ts +2 -0
- package/dist/_pkgs/affine-block-image/index.js +2 -0
- package/dist/_pkgs/affine-block-image/replace/commands.d.ts +68 -0
- package/dist/_pkgs/affine-block-image/replace/commands.js +200 -0
- package/dist/_pkgs/affine-block-image/replace/replace-model.d.ts +44 -0
- package/dist/_pkgs/affine-block-image/replace/replace-model.js +168 -0
- package/dist/_pkgs/affine-block-image/replace/toolbar.d.ts +21 -0
- package/dist/_pkgs/affine-block-image/replace/toolbar.js +159 -0
- package/dist/_pkgs/affine-block-image/translations.d.ts +15 -2
- package/dist/_pkgs/affine-block-image/translations.js +35 -2
- package/dist/_pkgs/affine-block-image/utils.d.ts +7 -0
- package/dist/_pkgs/affine-block-image/utils.js +13 -0
- package/dist/_pkgs/affine-block-image/view.js +7 -0
- package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.d.ts +14 -5
- package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.js +18 -11
- package/dist/_pkgs/affine-block-note/config.d.ts +9 -2
- package/dist/_pkgs/affine-block-note/edgeless-clipboard-config.d.ts +3 -2
- package/dist/_pkgs/affine-block-note/edgeless-clipboard-config.js +8 -5
- package/dist/_pkgs/affine-block-paragraph/adapters/markdown.js +11 -0
- package/dist/_pkgs/affine-block-root/commands/invisible-characters-commands.d.ts +17 -0
- package/dist/_pkgs/affine-block-root/commands/invisible-characters-commands.js +123 -0
- package/dist/_pkgs/affine-block-root/edgeless/clipboard/command.js +82 -65
- package/dist/_pkgs/affine-block-root/edgeless/edgeless-keyboard.js +2 -0
- package/dist/_pkgs/affine-block-root/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-root/index.js +1 -0
- package/dist/_pkgs/affine-block-root/translations.d.ts +6 -0
- package/dist/_pkgs/affine-block-root/translations.js +10 -0
- package/dist/_pkgs/affine-block-root/view.js +6 -1
- package/dist/_pkgs/affine-block-surface/commands/index.d.ts +1 -1
- package/dist/_pkgs/affine-block-surface/commands/index.js +1 -1
- package/dist/_pkgs/affine-block-surface/commands/reassociate-connectors.d.ts +9 -0
- package/dist/_pkgs/affine-block-surface/commands/reassociate-connectors.js +21 -12
- package/dist/_pkgs/affine-block-surface/extensions/clipboard-config.d.ts +33 -0
- package/dist/_pkgs/affine-block-surface/extensions/clipboard-config.js +53 -0
- package/dist/_pkgs/affine-block-surface/extensions/reading-toolbar.js +12 -12
- package/dist/_pkgs/affine-block-surface/framework-background/background-view.js +2 -1
- package/dist/_pkgs/affine-block-surface/framework-background/band-widths.d.ts +104 -0
- package/dist/_pkgs/affine-block-surface/framework-background/band-widths.js +156 -0
- package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +87 -4
- package/dist/_pkgs/affine-block-surface/framework-background/def.js +26 -7
- package/dist/_pkgs/affine-block-surface/framework-background/ellipsis.d.ts +58 -0
- package/dist/_pkgs/affine-block-surface/framework-background/ellipsis.js +110 -0
- package/dist/_pkgs/affine-block-surface/framework-background/index.d.ts +3 -0
- package/dist/_pkgs/affine-block-surface/framework-background/index.js +3 -0
- package/dist/_pkgs/affine-block-surface/framework-background/labels.d.ts +12 -2
- package/dist/_pkgs/affine-block-surface/framework-background/labels.js +29 -5
- package/dist/_pkgs/affine-block-surface/framework-background/measure-text.d.ts +14 -0
- package/dist/_pkgs/affine-block-surface/framework-background/measure-text.js +31 -0
- package/dist/_pkgs/affine-block-surface/framework-background/renderer.d.ts +14 -1
- package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +84 -31
- package/dist/_pkgs/affine-block-table/adapters/html.js +26 -14
- package/dist/_pkgs/affine-block-table/adapters/utils.d.ts +1 -0
- package/dist/_pkgs/affine-block-table/adapters/utils.js +23 -5
- package/dist/_pkgs/affine-block-table/header-menu.d.ts +13 -0
- package/dist/_pkgs/affine-block-table/header-menu.js +25 -0
- package/dist/_pkgs/affine-block-table/selection-controller.d.ts +15 -0
- package/dist/_pkgs/affine-block-table/selection-controller.js +62 -0
- package/dist/_pkgs/affine-block-table/table-cell-css.d.ts +7 -0
- package/dist/_pkgs/affine-block-table/table-cell-css.js +10 -0
- package/dist/_pkgs/affine-block-table/table-cell.d.ts +8 -0
- package/dist/_pkgs/affine-block-table/table-cell.js +35 -3
- package/dist/_pkgs/affine-block-table/table-data-manager.d.ts +14 -0
- package/dist/_pkgs/affine-block-table/table-data-manager.js +44 -3
- package/dist/_pkgs/affine-block-table/translations.d.ts +14 -0
- package/dist/_pkgs/affine-block-table/translations.js +22 -0
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
- package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.d.ts +3 -1
- package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.js +15 -2
- package/dist/_pkgs/affine-gfx-mindmap/toolbar/mindmap-tool-button.d.ts +2 -2
- package/dist/_pkgs/affine-gfx-template/builtin/other.d.ts +4 -0
- package/dist/_pkgs/affine-gfx-template/builtin/other.js +1 -1
- package/dist/_pkgs/affine-gfx-template/builtin/slides/build.d.ts +81 -0
- package/dist/_pkgs/affine-gfx-template/builtin/slides/build.js +276 -0
- package/dist/_pkgs/affine-gfx-template/builtin/slides/consts.d.ts +126 -0
- package/dist/_pkgs/affine-gfx-template/builtin/slides/consts.js +101 -0
- package/dist/_pkgs/affine-gfx-template/builtin/slides/index.d.ts +8 -0
- package/dist/_pkgs/affine-gfx-template/builtin/slides/index.js +27 -0
- package/dist/_pkgs/affine-gfx-template/builtin/slides/masks.d.ts +83 -0
- package/dist/_pkgs/affine-gfx-template/builtin/slides/masks.js +674 -0
- package/dist/_pkgs/affine-gfx-template/contribute.js +12 -17
- package/dist/_pkgs/affine-gfx-template/make-snapshot.d.ts +14 -2
- package/dist/_pkgs/affine-gfx-template/make-snapshot.js +6 -2
- package/dist/_pkgs/affine-gfx-template/services/template-middlewares.d.ts +12 -0
- package/dist/_pkgs/affine-gfx-template/services/template-middlewares.js +48 -0
- package/dist/_pkgs/affine-gfx-template/services/template.js +2 -1
- package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.d.ts +15 -1
- package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.js +33 -37
- package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +6 -0
- package/dist/_pkgs/affine-gfx-template/translations.d.ts +94 -0
- package/dist/_pkgs/affine-gfx-template/translations.js +441 -2
- package/dist/_pkgs/affine-model/blocks/table/table-model.d.ts +4 -0
- package/dist/_pkgs/affine-model/blocks/table/table-model.js +4 -0
- package/dist/_pkgs/affine-model/elements/bpmn/node.d.ts +46 -3
- package/dist/_pkgs/affine-model/elements/bpmn/node.js +47 -4
- package/dist/_pkgs/affine-model/elements/bpmn/pool.d.ts +20 -0
- package/dist/_pkgs/affine-model/elements/bpmn/pool.js +33 -4
- package/dist/_pkgs/affine-shared/services/edit-props-store.d.ts +3 -16
- package/dist/_pkgs/affine-shared/services/edit-props-store.js +16 -16
- package/dist/_pkgs/affine-shared/services/editor-setting-service.d.ts +3 -0
- package/dist/_pkgs/affine-shared/services/editor-setting-service.js +4 -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/invisible-characters.d.ts +33 -0
- package/dist/_pkgs/affine-shared/services/invisible-characters.js +63 -0
- package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-cache.d.ts +0 -9
- package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-cache.js +3 -9
- package/dist/_pkgs/affine-shared/services/telemetry-service/lifecycle.d.ts +26 -0
- package/dist/_pkgs/affine-shared/services/telemetry-service/telemetry-service.d.ts +4 -3
- package/dist/_pkgs/affine-shared/services/telemetry-service/types.d.ts +11 -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/invisible-characters.d.ts +42 -0
- package/dist/_pkgs/affine-shared/utils/invisible-characters.js +81 -0
- package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/index.js +26 -26
- package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/zoom-toolbar.js +13 -2
- package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
- package/dist/_pkgs/affine-widget-page-dragging-area/index.js +10 -10
- package/dist/_pkgs/affine-widget-viewport-overlay/index.js +13 -13
- package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
- package/dist/_pkgs/data-view/view-presets/table/pc/header/column-header.js +6 -0
- package/dist/_pkgs/data-view/view-presets/table/pc/table-view-ui-logic.js +5 -2
- package/dist/_pkgs/std/clipboard/clipboard.d.ts +15 -1
- package/dist/_pkgs/std/clipboard/clipboard.js +18 -0
- package/dist/_pkgs/std/inline/components/embed-gap.js +1 -1
- package/dist/_pkgs/std/inline/components/v-line.js +1 -1
- package/dist/_pkgs/std/inline/components/v-text.d.ts +15 -2
- package/dist/_pkgs/std/inline/components/v-text.js +147 -15
- package/dist/_pkgs/std/inline/consts.d.ts +5 -0
- package/dist/_pkgs/std/inline/consts.js +5 -0
- package/dist/_pkgs/std/inline/utils/index.d.ts +1 -0
- package/dist/_pkgs/std/inline/utils/index.js +1 -0
- package/dist/_pkgs/std/inline/utils/invisible-characters.d.ts +57 -0
- package/dist/_pkgs/std/inline/utils/invisible-characters.js +162 -0
- package/dist/_pkgs/std/inline/utils/point-conversion.js +3 -3
- package/dist/_pkgs/std/inline/utils/text.d.ts +8 -0
- package/dist/_pkgs/std/inline/utils/text.js +11 -2
- package/dist/_pkgs/store/model/store/crud.d.ts +28 -0
- package/dist/_pkgs/store/model/store/crud.js +97 -30
- package/dist/_pkgs/store/model/store/query.js +3 -2
- package/dist/_pkgs/store/model/store/store.d.ts +21 -0
- package/dist/_pkgs/store/model/store/store.js +88 -21
- package/dist/_pkgs/store/transformer/transformer.d.ts +46 -0
- package/dist/_pkgs/store/transformer/transformer.js +72 -0
- package/dist/commands.js +6 -1
- package/dist/translations.js +2 -0
- package/package.json +3 -4
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { ImageBlockModel, ShapeType } from '../../affine-model/index.js';
|
|
2
|
+
import { TelemetryProvider } from '../../affine-shared/services/index.js';
|
|
3
|
+
import { GfxControllerIdentifier } from '../../std/gfx/index.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { pickImageSource, replaceImageSource } from '../asset-picker.js';
|
|
6
|
+
import { IMAGE_REPLACE_PICTURE_DESCRIPTION, IMAGE_REPLACE_WITH_IMAGE, IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION, IMAGE_REPLACE_WITH_SHAPE, IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION, } from '../translations.js';
|
|
7
|
+
import { canReplaceImageWithShape, canReplaceShapeWithImage, replaceImageWithShape, replaceShapeWithImage, } from './replace-model.js';
|
|
8
|
+
/**
|
|
9
|
+
* The replace commands (ADR 0044), `owner: 'core'`: a shape and an image
|
|
10
|
+
* belong to no framework. Registered from the image view extension, so they
|
|
11
|
+
* go with the `image` flag; the toolbar entries invoke them through
|
|
12
|
+
* `runCommand`, so the refusal, the one undo step and the telemetry live
|
|
13
|
+
* here, once.
|
|
14
|
+
*
|
|
15
|
+
* They EMIT FROM THEIR BODY (listed in `registry.unit.spec.ts`'s
|
|
16
|
+
* self-emitting commands): `CommandDescriptor.telemetry` maps onto the
|
|
17
|
+
* framework creation events, and whether anything was replaced is a fact of
|
|
18
|
+
* the invocation — a cancelled file chooser replaces nothing.
|
|
19
|
+
*/
|
|
20
|
+
export const replaceElementParams = z.object({
|
|
21
|
+
// The element to replace; absent, the single selected one.
|
|
22
|
+
elementId: z.string().min(1).optional(),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* « Replace with shape » takes the shape picked in its sub-menu (ADR 0044,
|
|
26
|
+
* amendment of 2026-10-27). Both optional: absent, a plain rectangle, so the
|
|
27
|
+
* palette and the agent keep the v1 behaviour.
|
|
28
|
+
*/
|
|
29
|
+
export const replaceWithShapeParams = replaceElementParams.extend({
|
|
30
|
+
shapeType: z.nativeEnum(ShapeType).optional(),
|
|
31
|
+
// The corner radius as a fraction of the box: 0.1 is the rounded rectangle.
|
|
32
|
+
radius: z.number().min(0).max(0.5).optional(),
|
|
33
|
+
});
|
|
34
|
+
function target(std, params) {
|
|
35
|
+
const parsed = replaceElementParams.safeParse(params ?? {});
|
|
36
|
+
if (!parsed.success)
|
|
37
|
+
return null;
|
|
38
|
+
const gfx = std.get(GfxControllerIdentifier);
|
|
39
|
+
const { elementId } = parsed.data;
|
|
40
|
+
if (elementId !== undefined)
|
|
41
|
+
return gfx.getElementById(elementId);
|
|
42
|
+
const selected = gfx.selection.selectedElements;
|
|
43
|
+
return selected.length === 1 ? selected[0] : null;
|
|
44
|
+
}
|
|
45
|
+
/** The shape the command would replace, or `null`. */
|
|
46
|
+
export function replaceableShape(std, params) {
|
|
47
|
+
const model = target(std, params);
|
|
48
|
+
return canReplaceShapeWithImage(std, model) ? model : null;
|
|
49
|
+
}
|
|
50
|
+
/** The canvas image the command would replace, or `null`. */
|
|
51
|
+
export function replaceableImage(std, params) {
|
|
52
|
+
const model = target(std, params);
|
|
53
|
+
return model instanceof ImageBlockModel &&
|
|
54
|
+
canReplaceImageWithShape(std, model)
|
|
55
|
+
? model
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The canvas image whose PICTURE the command would swap, or `null`. Looser
|
|
60
|
+
* than {@link replaceableImage}: the block stays (same id, caption and
|
|
61
|
+
* comments kept), so a captioned image may change its picture; a locked one
|
|
62
|
+
* may not.
|
|
63
|
+
*/
|
|
64
|
+
export function replaceablePicture(std, params) {
|
|
65
|
+
const model = target(std, params);
|
|
66
|
+
if (std.store.readonly || !(model instanceof ImageBlockModel))
|
|
67
|
+
return null;
|
|
68
|
+
if (model.parent?.flavour !== 'affine:surface')
|
|
69
|
+
return null;
|
|
70
|
+
return model.isLocked() ? null : model;
|
|
71
|
+
}
|
|
72
|
+
function track(std, invocation, from, to) {
|
|
73
|
+
std.getOptional(TelemetryProvider)?.track('ElementReplaced', {
|
|
74
|
+
page: 'whiteboard editor',
|
|
75
|
+
from,
|
|
76
|
+
to,
|
|
77
|
+
control: invocation.source,
|
|
78
|
+
module: invocation.surface,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function select(std, id) {
|
|
82
|
+
std.get(GfxControllerIdentifier).selection.set({
|
|
83
|
+
elements: [id],
|
|
84
|
+
editing: false,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export const REPLACE_SHAPE_WITH_IMAGE = 'canvas.shape.replaceWithImage';
|
|
88
|
+
export const REPLACE_IMAGE_WITH_SHAPE = 'canvas.image.replaceWithShape';
|
|
89
|
+
export const REPLACE_IMAGE_WITH_IMAGE = 'canvas.image.replaceWithImage';
|
|
90
|
+
export const replaceShapeWithImageCommand = {
|
|
91
|
+
id: REPLACE_SHAPE_WITH_IMAGE,
|
|
92
|
+
owner: 'core',
|
|
93
|
+
kind: 'action',
|
|
94
|
+
labelKey: IMAGE_REPLACE_WITH_IMAGE[0],
|
|
95
|
+
labelFallback: IMAGE_REPLACE_WITH_IMAGE[1],
|
|
96
|
+
descriptionKey: IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION[0],
|
|
97
|
+
descriptionFallback: IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION[1],
|
|
98
|
+
keywords: ['replace', 'image', 'picture', 'shape'],
|
|
99
|
+
// Not `agent`: the picture comes from a dialog a person answers.
|
|
100
|
+
surfaces: ['contextual-toolbar', 'palette'],
|
|
101
|
+
scope: 'edgeless',
|
|
102
|
+
defaultKeys: { mac: [], other: [] },
|
|
103
|
+
availability: 'editable',
|
|
104
|
+
when: std => replaceableShape(std) !== null,
|
|
105
|
+
params: replaceElementParams,
|
|
106
|
+
run: async (std, invocation, params) => {
|
|
107
|
+
if (!replaceableShape(std, params))
|
|
108
|
+
return;
|
|
109
|
+
const image = await pickImageSource(std);
|
|
110
|
+
if (!image)
|
|
111
|
+
return;
|
|
112
|
+
// Re-read: the dialog took time, the shape may be gone, locked or the
|
|
113
|
+
// document read-only by now.
|
|
114
|
+
const shape = replaceableShape(std, params);
|
|
115
|
+
if (!shape)
|
|
116
|
+
return;
|
|
117
|
+
const id = replaceShapeWithImage(std, shape, image);
|
|
118
|
+
if (!id)
|
|
119
|
+
return;
|
|
120
|
+
select(std, id);
|
|
121
|
+
std.getOptional(TelemetryProvider)?.track('BlockCreated', {
|
|
122
|
+
page: 'whiteboard editor',
|
|
123
|
+
blockType: 'affine:image',
|
|
124
|
+
control: invocation.source,
|
|
125
|
+
module: invocation.surface,
|
|
126
|
+
});
|
|
127
|
+
track(std, invocation, 'shape', 'image');
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
export const replaceImageWithShapeCommand = {
|
|
131
|
+
id: REPLACE_IMAGE_WITH_SHAPE,
|
|
132
|
+
owner: 'core',
|
|
133
|
+
kind: 'action',
|
|
134
|
+
labelKey: IMAGE_REPLACE_WITH_SHAPE[0],
|
|
135
|
+
labelFallback: IMAGE_REPLACE_WITH_SHAPE[1],
|
|
136
|
+
descriptionKey: IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION[0],
|
|
137
|
+
descriptionFallback: IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION[1],
|
|
138
|
+
keywords: ['replace', 'shape', 'rectangle', 'placeholder', 'image'],
|
|
139
|
+
surfaces: ['contextual-toolbar', 'palette', 'agent'],
|
|
140
|
+
scope: 'edgeless',
|
|
141
|
+
defaultKeys: { mac: [], other: [] },
|
|
142
|
+
availability: 'editable',
|
|
143
|
+
when: std => replaceableImage(std) !== null,
|
|
144
|
+
params: replaceWithShapeParams,
|
|
145
|
+
run: (std, invocation, params) => {
|
|
146
|
+
const image = replaceableImage(std, params);
|
|
147
|
+
if (!image)
|
|
148
|
+
return;
|
|
149
|
+
const parsed = replaceWithShapeParams.safeParse(params ?? {});
|
|
150
|
+
if (!parsed.success)
|
|
151
|
+
return;
|
|
152
|
+
const { shapeType, radius } = parsed.data;
|
|
153
|
+
const id = replaceImageWithShape(std, image, { shapeType, radius });
|
|
154
|
+
if (!id)
|
|
155
|
+
return;
|
|
156
|
+
select(std, id);
|
|
157
|
+
track(std, invocation, 'image', 'shape');
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* « Replace with image » on a canvas IMAGE (ADR 0044, amendment of
|
|
162
|
+
* 2026-10-27): another picture in the same block, from the host's library
|
|
163
|
+
* when there is one, from the system file chooser otherwise — so the entry
|
|
164
|
+
* exists without a host. The block keeps its id, corner, width and caption
|
|
165
|
+
* (`replaceImageSource`); it reports `ElementReplaced { image → image }`.
|
|
166
|
+
*/
|
|
167
|
+
export const replaceImageWithImageCommand = {
|
|
168
|
+
id: REPLACE_IMAGE_WITH_IMAGE,
|
|
169
|
+
owner: 'core',
|
|
170
|
+
kind: 'action',
|
|
171
|
+
labelKey: IMAGE_REPLACE_WITH_IMAGE[0],
|
|
172
|
+
labelFallback: IMAGE_REPLACE_WITH_IMAGE[1],
|
|
173
|
+
descriptionKey: IMAGE_REPLACE_PICTURE_DESCRIPTION[0],
|
|
174
|
+
descriptionFallback: IMAGE_REPLACE_PICTURE_DESCRIPTION[1],
|
|
175
|
+
keywords: ['replace', 'image', 'picture', 'library', 'swap'],
|
|
176
|
+
// Not `agent`: the picture comes from a dialog a person answers.
|
|
177
|
+
surfaces: ['contextual-toolbar', 'palette'],
|
|
178
|
+
scope: 'edgeless',
|
|
179
|
+
defaultKeys: { mac: [], other: [] },
|
|
180
|
+
availability: 'editable',
|
|
181
|
+
when: std => replaceablePicture(std) !== null,
|
|
182
|
+
params: replaceElementParams,
|
|
183
|
+
run: async (std, invocation, params) => {
|
|
184
|
+
if (!replaceablePicture(std, params))
|
|
185
|
+
return;
|
|
186
|
+
const source = await pickImageSource(std);
|
|
187
|
+
if (!source)
|
|
188
|
+
return;
|
|
189
|
+
// Re-read after the dialog, like the shape's.
|
|
190
|
+
const image = replaceablePicture(std, params);
|
|
191
|
+
if (!image || !replaceImageSource(image, source))
|
|
192
|
+
return;
|
|
193
|
+
track(std, invocation, 'image', 'image');
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
export const imageReplaceCommands = [
|
|
197
|
+
replaceShapeWithImageCommand,
|
|
198
|
+
replaceImageWithShapeCommand,
|
|
199
|
+
replaceImageWithImageCommand,
|
|
200
|
+
];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ImageBlockModel, type ImageBlockProps, ShapeElementModel, ShapeType } from '../../affine-model/index.js';
|
|
2
|
+
import type { BlockStdScope } from '../../std/index.js';
|
|
3
|
+
import type { ImageBlockSource } from '../utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Whether a shape may be replaced by an image. Refused: a read-only store, a
|
|
6
|
+
* locked shape, a NOTATION shape (it carries a role its framework reads), a
|
|
7
|
+
* shape with text (v1 never drops what the author typed) and a mind-map node
|
|
8
|
+
* (the mind map owns its tree).
|
|
9
|
+
*/
|
|
10
|
+
export declare function canReplaceShapeWithImage(std: BlockStdScope, shape: unknown): shape is ShapeElementModel;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a canvas image may be replaced by a shape. Refused: a read-only
|
|
13
|
+
* store, an image that is not on the canvas, a locked image, and one with a
|
|
14
|
+
* caption or comments (v1 never drops what the author wrote).
|
|
15
|
+
*/
|
|
16
|
+
export declare function canReplaceImageWithShape(std: BlockStdScope, image: ImageBlockModel | null | undefined): image is ImageBlockModel;
|
|
17
|
+
/**
|
|
18
|
+
* The crop that cuts `image` to the proportions of `box`, as stored props:
|
|
19
|
+
* nothing when the picture already has them (the whole image is the absent
|
|
20
|
+
* key, ADR 0037).
|
|
21
|
+
*/
|
|
22
|
+
export declare function cropToBox(box: {
|
|
23
|
+
w: number;
|
|
24
|
+
h: number;
|
|
25
|
+
}, image: Pick<ImageBlockSource, 'width' | 'height'>): Pick<ImageBlockProps, 'crop'>;
|
|
26
|
+
/**
|
|
27
|
+
* Put `image` in place of `shape`, cut to the shape's box. Answers the new
|
|
28
|
+
* image block's id, or `null` when the gesture is refused (nothing written).
|
|
29
|
+
*/
|
|
30
|
+
export declare function replaceShapeWithImage(std: BlockStdScope, shape: ShapeElementModel, image: ImageBlockSource): string | null;
|
|
31
|
+
/** Which shape takes the image's place: a rectangle unless told otherwise. */
|
|
32
|
+
export type ReplacementShape = {
|
|
33
|
+
shapeType?: ShapeType;
|
|
34
|
+
/** The corner radius, a fraction of the box (the rounded rectangle's 0.1). */
|
|
35
|
+
radius?: number;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Put a shape in place of `image` — a rectangle by default, or the
|
|
39
|
+
* `shapeType` and `radius` picked in « Replace with shape » (ADR 0044,
|
|
40
|
+
* amendment of 2026-10-27) — at its box, rotation, depth and layer.
|
|
41
|
+
* The blob stays in the asset store (undo brings the image back, and other
|
|
42
|
+
* pages may show it). Answers the new shape's id, or `null` when refused.
|
|
43
|
+
*/
|
|
44
|
+
export declare function replaceImageWithShape(std: BlockStdScope, image: ImageBlockModel, shape?: ReplacementShape): string | null;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { applyCreationLayer, reassociateConnectors, sourceLayerOf, } from '../../affine-block-surface/index.js';
|
|
2
|
+
import { ImageBlockSchema, isWholeImage, MindmapElementModel, ShapeElementModel, ShapeType, } from '../../affine-model/index.js';
|
|
3
|
+
import { Bound } from '../../global/gfx/index.js';
|
|
4
|
+
import { GfxControllerIdentifier } from '../../std/gfx/index.js';
|
|
5
|
+
import { ratioCrop } from '../crop/crop-math.js';
|
|
6
|
+
/**
|
|
7
|
+
* « Replace with image » and « Replace with shape » (ADR 0044): a model of
|
|
8
|
+
* the other kind at the old one's place, then the old one deleted. A shape is
|
|
9
|
+
* a surface ELEMENT and an image a BLOCK, so this is not a morph: the id
|
|
10
|
+
* changes, on purpose.
|
|
11
|
+
*
|
|
12
|
+
* What crosses over, in ONE undo step (one `captureSync`): `xywh`,
|
|
13
|
+
* `rotate`, `index`, the user layer (`sourceLayerOf`, ADR 0031 §6), every
|
|
14
|
+
* connector end bound to the old id, and the old model's place in its group
|
|
15
|
+
* or frame. Nothing is written when the gesture is refused, so a refusal
|
|
16
|
+
* never leaves an empty undo step.
|
|
17
|
+
*/
|
|
18
|
+
function surfaceOf(std) {
|
|
19
|
+
return (std.get(GfxControllerIdentifier).surface ??
|
|
20
|
+
null);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether a shape may be replaced by an image. Refused: a read-only store, a
|
|
24
|
+
* locked shape, a NOTATION shape (it carries a role its framework reads), a
|
|
25
|
+
* shape with text (v1 never drops what the author typed) and a mind-map node
|
|
26
|
+
* (the mind map owns its tree).
|
|
27
|
+
*/
|
|
28
|
+
export function canReplaceShapeWithImage(std, shape) {
|
|
29
|
+
if (std.store.readonly)
|
|
30
|
+
return false;
|
|
31
|
+
if (!(shape instanceof ShapeElementModel))
|
|
32
|
+
return false;
|
|
33
|
+
if (shape.isLocked())
|
|
34
|
+
return false;
|
|
35
|
+
if (shape.role !== undefined)
|
|
36
|
+
return false;
|
|
37
|
+
if ((shape.text?.length ?? 0) > 0)
|
|
38
|
+
return false;
|
|
39
|
+
if (shape.group instanceof MindmapElementModel)
|
|
40
|
+
return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether a canvas image may be replaced by a shape. Refused: a read-only
|
|
45
|
+
* store, an image that is not on the canvas, a locked image, and one with a
|
|
46
|
+
* caption or comments (v1 never drops what the author wrote).
|
|
47
|
+
*/
|
|
48
|
+
export function canReplaceImageWithShape(std, image) {
|
|
49
|
+
if (std.store.readonly || !image)
|
|
50
|
+
return false;
|
|
51
|
+
if (image.flavour !== ImageBlockSchema.model.flavour)
|
|
52
|
+
return false;
|
|
53
|
+
if (image.parent?.flavour !== 'affine:surface')
|
|
54
|
+
return false;
|
|
55
|
+
if (image.isLocked())
|
|
56
|
+
return false;
|
|
57
|
+
if (image.props.caption)
|
|
58
|
+
return false;
|
|
59
|
+
if (Object.keys(image.props.comments ?? {}).length > 0)
|
|
60
|
+
return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Move `oldModel`'s connectors and container membership onto `newModel`,
|
|
65
|
+
* then delete `oldModel`. Runs inside the caller's transaction. The new
|
|
66
|
+
* model joins the container BEFORE the old one leaves it, so a group is never
|
|
67
|
+
* seen empty.
|
|
68
|
+
*/
|
|
69
|
+
function handOver(std, surface, oldModel, newModel) {
|
|
70
|
+
reassociateConnectors(surface, oldModel.id, newModel.id);
|
|
71
|
+
const container = oldModel.group;
|
|
72
|
+
if (container) {
|
|
73
|
+
container.addChild(newModel);
|
|
74
|
+
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
|
75
|
+
container.removeChild(oldModel);
|
|
76
|
+
}
|
|
77
|
+
if (surface.hasElementById(oldModel.id)) {
|
|
78
|
+
surface.deleteElement(oldModel.id);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
std.store.deleteBlock(oldModel);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The crop that cuts `image` to the proportions of `box`, as stored props:
|
|
86
|
+
* nothing when the picture already has them (the whole image is the absent
|
|
87
|
+
* key, ADR 0037).
|
|
88
|
+
*/
|
|
89
|
+
export function cropToBox(box, image) {
|
|
90
|
+
if (!(box.w > 0 && box.h > 0))
|
|
91
|
+
return {};
|
|
92
|
+
const rect = ratioCrop(box.w / box.h, image.width, image.height);
|
|
93
|
+
return isWholeImage(rect) ? {} : { crop: rect };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Put `image` in place of `shape`, cut to the shape's box. Answers the new
|
|
97
|
+
* image block's id, or `null` when the gesture is refused (nothing written).
|
|
98
|
+
*/
|
|
99
|
+
export function replaceShapeWithImage(std, shape, image) {
|
|
100
|
+
const surface = surfaceOf(std);
|
|
101
|
+
if (!surface || !canReplaceShapeWithImage(std, shape))
|
|
102
|
+
return null;
|
|
103
|
+
if (!surface.hasElementById(shape.id))
|
|
104
|
+
return null;
|
|
105
|
+
if (!image.sourceId || !(image.width > 0 && image.height > 0))
|
|
106
|
+
return null;
|
|
107
|
+
const bound = Bound.deserialize(shape.xywh);
|
|
108
|
+
const props = {
|
|
109
|
+
sourceId: image.sourceId,
|
|
110
|
+
size: image.size,
|
|
111
|
+
// On the canvas `width`/`height` are the WHOLE picture's natural size
|
|
112
|
+
// and `xywh` the visible box (ADR 0037).
|
|
113
|
+
width: image.width,
|
|
114
|
+
height: image.height,
|
|
115
|
+
xywh: shape.xywh,
|
|
116
|
+
rotate: shape.rotate ?? 0,
|
|
117
|
+
index: shape.index,
|
|
118
|
+
layer: sourceLayerOf(shape),
|
|
119
|
+
...cropToBox(bound, image),
|
|
120
|
+
};
|
|
121
|
+
applyCreationLayer(std, props);
|
|
122
|
+
const { store } = std;
|
|
123
|
+
store.captureSync();
|
|
124
|
+
// The block is created OUTSIDE the hand-over's transaction: a block added
|
|
125
|
+
// inside one has no model until it ends, and the container needs the
|
|
126
|
+
// model. Both writes land in the undo step `captureSync` just opened.
|
|
127
|
+
const id = store.addBlock(ImageBlockSchema.model.flavour, props, surface);
|
|
128
|
+
const created = store.getModelById(id);
|
|
129
|
+
if (created)
|
|
130
|
+
store.transact(() => handOver(std, surface, shape, created));
|
|
131
|
+
return id;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Put a shape in place of `image` — a rectangle by default, or the
|
|
135
|
+
* `shapeType` and `radius` picked in « Replace with shape » (ADR 0044,
|
|
136
|
+
* amendment of 2026-10-27) — at its box, rotation, depth and layer.
|
|
137
|
+
* The blob stays in the asset store (undo brings the image back, and other
|
|
138
|
+
* pages may show it). Answers the new shape's id, or `null` when refused.
|
|
139
|
+
*/
|
|
140
|
+
export function replaceImageWithShape(std, image, shape = {}) {
|
|
141
|
+
const surface = surfaceOf(std);
|
|
142
|
+
if (!surface || !canReplaceImageWithShape(std, image))
|
|
143
|
+
return null;
|
|
144
|
+
if (!std.store.getBlock(image.id))
|
|
145
|
+
return null;
|
|
146
|
+
const props = {
|
|
147
|
+
type: 'shape',
|
|
148
|
+
shapeType: shape.shapeType ?? ShapeType.Rect,
|
|
149
|
+
radius: shape.radius ?? 0,
|
|
150
|
+
xywh: image.xywh,
|
|
151
|
+
rotate: image.props.rotate ?? 0,
|
|
152
|
+
index: image.index,
|
|
153
|
+
layer: sourceLayerOf(image),
|
|
154
|
+
};
|
|
155
|
+
// The surface's `beforeAdd` middleware stamps the layer too; resolving it
|
|
156
|
+
// here keeps the rule when no middleware is mounted.
|
|
157
|
+
applyCreationLayer(std, props);
|
|
158
|
+
const { store } = std;
|
|
159
|
+
let id = null;
|
|
160
|
+
store.captureSync();
|
|
161
|
+
store.transact(() => {
|
|
162
|
+
id = surface.addElement(props);
|
|
163
|
+
const created = surface.getElementById(id);
|
|
164
|
+
if (created)
|
|
165
|
+
handOver(std, surface, image, created);
|
|
166
|
+
});
|
|
167
|
+
return id;
|
|
168
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ToolbarAction } from '../../affine-shared/services/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* « Replace » on a canvas image's toolbar (ADR 0044, amendment of
|
|
4
|
+
* 2026-10-27): ONE menu where there were two buttons, because the product
|
|
5
|
+
* owner wants the choice in one place — « Replace with image » (another
|
|
6
|
+
* picture, from the host's library or the system file chooser,
|
|
7
|
+
* `canvas.image.replaceWithImage`) and « Replace with shape », a sub-menu of
|
|
8
|
+
* the shape tool's shapes (`canvas.image.replaceWithShape` with the picked
|
|
9
|
+
* `shapeType` and `radius`). It keeps the id `a.replace`, so it sits where the
|
|
10
|
+
* old « Replace » did. Each entry shows only when its command would act.
|
|
11
|
+
*/
|
|
12
|
+
export declare const imageReplaceMenuToolbarAction: ToolbarAction;
|
|
13
|
+
/**
|
|
14
|
+
* « Replace with image » on the SHAPE toolbar, registered from this package:
|
|
15
|
+
* `gfx/shape` never imports the image block. The bare
|
|
16
|
+
* `affine:surface:shape` key belongs to `shapeToolbarExtension`, so this
|
|
17
|
+
* second module carries an owner (`toolbarModuleKey`) and the registry hands
|
|
18
|
+
* it to the same row. Replacing a shape with ANOTHER shape is the shape
|
|
19
|
+
* toolbar's own « Switch shape type », not repeated here.
|
|
20
|
+
*/
|
|
21
|
+
export declare const shapeReplaceWithImageToolbarExtension: import("../../store/index.js").ExtensionType;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { ShapeComponentConfig } from '../../affine-gfx-shape/index.js';
|
|
2
|
+
import { EditorChevronDown, renderPickerMenuItems, } from '../../affine-components/toolbar/index.js';
|
|
3
|
+
import { getShapeRadius, getShapeType, ShapeElementModel, } from '../../affine-model/index.js';
|
|
4
|
+
import { ToolbarModuleExtension, toolbarModuleKey, translateKey, } from '../../affine-shared/services/index.js';
|
|
5
|
+
import { ArrowRightSmallIcon, ImageIcon, ReplaceIcon, ShapeIcon, } from '@blocksuite/icons/lit';
|
|
6
|
+
import { BlockFlavourIdentifier, runCommand } from '../../std/index.js';
|
|
7
|
+
import { html, nothing } from 'lit';
|
|
8
|
+
import { createRef, ref } from 'lit/directives/ref.js';
|
|
9
|
+
import { IMAGE_REPLACE_WITH_IMAGE, IMAGE_REPLACE_WITH_SHAPE, IMAGE_TOOLBAR_REPLACE, } from '../translations.js';
|
|
10
|
+
import { replaceableImage, replaceablePicture, replaceableShape, replaceImageWithImageCommand, replaceImageWithShapeCommand, replaceShapeWithImageCommand, } from './commands.js';
|
|
11
|
+
const FROM_TOOLBAR = {
|
|
12
|
+
surface: 'contextual-toolbar',
|
|
13
|
+
source: 'toolbar:general',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A click inside the shape sub-menu must not reach `document`: every
|
|
17
|
+
* `editor-menu-button` closes on a click outside its own trigger
|
|
18
|
+
* (`listenClickAway`), and the sub-menu's trigger and row are outside the
|
|
19
|
+
* Replace menu's trigger, so the outer menu would close — and take the
|
|
20
|
+
* sub-menu, which it contains, with it — on the very click that opens it.
|
|
21
|
+
*/
|
|
22
|
+
const keepOpen = (event) => event.stopPropagation();
|
|
23
|
+
/**
|
|
24
|
+
* Beside the menu, as a sub-menu opens. One object, not a literal per render:
|
|
25
|
+
* `editor-menu-button` rebuilds its popper whenever `popperOptions` changes
|
|
26
|
+
* identity, which would drop an open sub-menu on any toolbar re-render.
|
|
27
|
+
*/
|
|
28
|
+
const SUBMENU_POPPER = {
|
|
29
|
+
allowedPlacements: ['right-start', 'left-start'],
|
|
30
|
+
mainAxis: 8,
|
|
31
|
+
};
|
|
32
|
+
function renderShapeSubmenu(ctx, outer) {
|
|
33
|
+
const label = translateKey(ctx.std, ...IMAGE_REPLACE_WITH_SHAPE);
|
|
34
|
+
const onPick = (name) => {
|
|
35
|
+
if (!name)
|
|
36
|
+
return;
|
|
37
|
+
outer.value?.hide();
|
|
38
|
+
runCommand(ctx.std, replaceImageWithShapeCommand, FROM_TOOLBAR, {
|
|
39
|
+
shapeType: getShapeType(name),
|
|
40
|
+
radius: getShapeRadius(name),
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
// The shape tool's own list — names, icons and wordings — so the sub-menu
|
|
44
|
+
// offers exactly the shapes a user can draw, in the same order.
|
|
45
|
+
const items = ShapeComponentConfig.map(item => ({
|
|
46
|
+
key: translateKey(ctx.std, ...item.tooltipWording),
|
|
47
|
+
value: item.name,
|
|
48
|
+
icon: item.generalIcon,
|
|
49
|
+
disabled: item.disabled,
|
|
50
|
+
}));
|
|
51
|
+
return html `
|
|
52
|
+
<div @click=${keepOpen}>
|
|
53
|
+
<editor-menu-button
|
|
54
|
+
data-testid="image-replace-shape"
|
|
55
|
+
.contentPadding=${'4px 6px'}
|
|
56
|
+
.popperOptions=${SUBMENU_POPPER}
|
|
57
|
+
.button=${html `
|
|
58
|
+
<editor-icon-button
|
|
59
|
+
aria-label=${label}
|
|
60
|
+
data-testid="image-replace-shape-button"
|
|
61
|
+
.justify=${'space-between'}
|
|
62
|
+
.iconContainerPadding=${[4, 8]}
|
|
63
|
+
.iconContainerWidth=${'100%'}
|
|
64
|
+
>
|
|
65
|
+
${ShapeIcon()}
|
|
66
|
+
<span class="label">${label}</span>
|
|
67
|
+
${ArrowRightSmallIcon()}
|
|
68
|
+
</editor-icon-button>
|
|
69
|
+
`}
|
|
70
|
+
>
|
|
71
|
+
<div>
|
|
72
|
+
${renderPickerMenuItems(items, null, onPick, 'image-replace-shape')}
|
|
73
|
+
</div>
|
|
74
|
+
</editor-menu-button>
|
|
75
|
+
</div>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* « Replace » on a canvas image's toolbar (ADR 0044, amendment of
|
|
80
|
+
* 2026-10-27): ONE menu where there were two buttons, because the product
|
|
81
|
+
* owner wants the choice in one place — « Replace with image » (another
|
|
82
|
+
* picture, from the host's library or the system file chooser,
|
|
83
|
+
* `canvas.image.replaceWithImage`) and « Replace with shape », a sub-menu of
|
|
84
|
+
* the shape tool's shapes (`canvas.image.replaceWithShape` with the picked
|
|
85
|
+
* `shapeType` and `radius`). It keeps the id `a.replace`, so it sits where the
|
|
86
|
+
* old « Replace » did. Each entry shows only when its command would act.
|
|
87
|
+
*/
|
|
88
|
+
export const imageReplaceMenuToolbarAction = {
|
|
89
|
+
id: 'a.replace',
|
|
90
|
+
when: ctx => !ctx.readonly &&
|
|
91
|
+
(replaceablePicture(ctx.std) !== null ||
|
|
92
|
+
replaceableImage(ctx.std) !== null),
|
|
93
|
+
content(ctx) {
|
|
94
|
+
const picture = replaceablePicture(ctx.std) !== null;
|
|
95
|
+
const toShape = replaceableImage(ctx.std) !== null;
|
|
96
|
+
if (!picture && !toShape)
|
|
97
|
+
return null;
|
|
98
|
+
const label = translateKey(ctx.std, ...IMAGE_TOOLBAR_REPLACE);
|
|
99
|
+
const withImage = translateKey(ctx.std, ...IMAGE_REPLACE_WITH_IMAGE);
|
|
100
|
+
const outer = createRef();
|
|
101
|
+
return html `
|
|
102
|
+
<editor-menu-button
|
|
103
|
+
${ref(outer)}
|
|
104
|
+
data-testid="image-replace-menu"
|
|
105
|
+
.contentPadding=${'8px'}
|
|
106
|
+
.button=${html `
|
|
107
|
+
<editor-icon-button
|
|
108
|
+
aria-label=${label}
|
|
109
|
+
data-testid="image-replace-button"
|
|
110
|
+
.tooltip=${label}
|
|
111
|
+
>
|
|
112
|
+
${ReplaceIcon()} ${EditorChevronDown}
|
|
113
|
+
</editor-icon-button>
|
|
114
|
+
`}
|
|
115
|
+
>
|
|
116
|
+
<div data-size="large" data-orientation="vertical">
|
|
117
|
+
${picture
|
|
118
|
+
? html `
|
|
119
|
+
<editor-menu-action
|
|
120
|
+
aria-label=${withImage}
|
|
121
|
+
data-testid="image-replace-with-image"
|
|
122
|
+
@click=${() => runCommand(ctx.std, replaceImageWithImageCommand, FROM_TOOLBAR)}
|
|
123
|
+
>
|
|
124
|
+
${ImageIcon()}
|
|
125
|
+
<span class="label">${withImage}</span>
|
|
126
|
+
</editor-menu-action>
|
|
127
|
+
`
|
|
128
|
+
: nothing}
|
|
129
|
+
${toShape ? renderShapeSubmenu(ctx, outer) : nothing}
|
|
130
|
+
</div>
|
|
131
|
+
</editor-menu-button>
|
|
132
|
+
`;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const shapeReplaceToolbarConfig = {
|
|
136
|
+
actions: [
|
|
137
|
+
{
|
|
138
|
+
id: 'h.replace-with-image',
|
|
139
|
+
tooltip: IMAGE_REPLACE_WITH_IMAGE[1],
|
|
140
|
+
tooltipWording: IMAGE_REPLACE_WITH_IMAGE,
|
|
141
|
+
icon: ImageIcon(),
|
|
142
|
+
when: ctx => !ctx.readonly && replaceableShape(ctx.std) !== null,
|
|
143
|
+
run: ctx => runCommand(ctx.std, replaceShapeWithImageCommand, FROM_TOOLBAR),
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
when: ctx => ctx.getSurfaceModelsByType(ShapeElementModel).length === 1,
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* « Replace with image » on the SHAPE toolbar, registered from this package:
|
|
150
|
+
* `gfx/shape` never imports the image block. The bare
|
|
151
|
+
* `affine:surface:shape` key belongs to `shapeToolbarExtension`, so this
|
|
152
|
+
* second module carries an owner (`toolbarModuleKey`) and the registry hands
|
|
153
|
+
* it to the same row. Replacing a shape with ANOTHER shape is the shape
|
|
154
|
+
* toolbar's own « Switch shape type », not repeated here.
|
|
155
|
+
*/
|
|
156
|
+
export const shapeReplaceWithImageToolbarExtension = ToolbarModuleExtension({
|
|
157
|
+
id: BlockFlavourIdentifier(toolbarModuleKey('affine:surface:shape', 'image-replace')),
|
|
158
|
+
config: shapeReplaceToolbarConfig,
|
|
159
|
+
});
|
|
@@ -36,10 +36,23 @@ export declare const IMAGE_TOAST_DOWNLOADING: ChromeWording;
|
|
|
36
36
|
export declare const IMAGE_TOAST_COPIED: ChromeWording;
|
|
37
37
|
export declare const IMAGE_TOAST_READ_SIZE_FAILED: ChromeWording;
|
|
38
38
|
/**
|
|
39
|
-
* The image toolbar's "Replace" (`configs/replace-action.ts`)
|
|
40
|
-
* with
|
|
39
|
+
* The image toolbar's "Replace": the page button (`configs/replace-action.ts`)
|
|
40
|
+
* and the canvas menu that holds « Replace with image » and « Replace with
|
|
41
|
+
* shape » (`replace/toolbar.ts`, ADR 0044 amendment of 2026-10-27). Offered
|
|
42
|
+
* with or without a host: without one, the system file chooser.
|
|
41
43
|
*/
|
|
42
44
|
export declare const IMAGE_TOOLBAR_REPLACE: ChromeWording;
|
|
45
|
+
/**
|
|
46
|
+
* « Replace with image » on a shape and « Replace with shape » on a canvas
|
|
47
|
+
* image (ADR 0044): the toolbar entries and the commands they run share these
|
|
48
|
+
* keys, so the palette and the toolbar say the same words.
|
|
49
|
+
*/
|
|
50
|
+
export declare const IMAGE_REPLACE_WITH_IMAGE: ChromeWording;
|
|
51
|
+
export declare const IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION: ChromeWording;
|
|
52
|
+
/** `canvas.image.replaceWithImage`: the same label, said of an image. */
|
|
53
|
+
export declare const IMAGE_REPLACE_PICTURE_DESCRIPTION: ChromeWording;
|
|
54
|
+
export declare const IMAGE_REPLACE_WITH_SHAPE: ChromeWording;
|
|
55
|
+
export declare const IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION: ChromeWording;
|
|
43
56
|
/**
|
|
44
57
|
* This package's contribution to the translation-key manifest, listed in
|
|
45
58
|
* `PACKAGE_WORDINGS` under source `chrome`
|
|
@@ -78,13 +78,41 @@ export const IMAGE_TOAST_READ_SIZE_FAILED = [
|
|
|
78
78
|
'Failed to read image size, please try another image',
|
|
79
79
|
];
|
|
80
80
|
/**
|
|
81
|
-
* The image toolbar's "Replace" (`configs/replace-action.ts`)
|
|
82
|
-
* with
|
|
81
|
+
* The image toolbar's "Replace": the page button (`configs/replace-action.ts`)
|
|
82
|
+
* and the canvas menu that holds « Replace with image » and « Replace with
|
|
83
|
+
* shape » (`replace/toolbar.ts`, ADR 0044 amendment of 2026-10-27). Offered
|
|
84
|
+
* with or without a host: without one, the system file chooser.
|
|
83
85
|
*/
|
|
84
86
|
export const IMAGE_TOOLBAR_REPLACE = [
|
|
85
87
|
'com.labre.image.toolbar.replace',
|
|
86
88
|
'Replace',
|
|
87
89
|
];
|
|
90
|
+
/**
|
|
91
|
+
* « Replace with image » on a shape and « Replace with shape » on a canvas
|
|
92
|
+
* image (ADR 0044): the toolbar entries and the commands they run share these
|
|
93
|
+
* keys, so the palette and the toolbar say the same words.
|
|
94
|
+
*/
|
|
95
|
+
export const IMAGE_REPLACE_WITH_IMAGE = [
|
|
96
|
+
'com.labre.image.replace.with-image',
|
|
97
|
+
'Replace with image',
|
|
98
|
+
];
|
|
99
|
+
export const IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION = [
|
|
100
|
+
'com.labre.image.replace.with-image.description',
|
|
101
|
+
'Put a picture in place of this shape, cut to its box.',
|
|
102
|
+
];
|
|
103
|
+
/** `canvas.image.replaceWithImage`: the same label, said of an image. */
|
|
104
|
+
export const IMAGE_REPLACE_PICTURE_DESCRIPTION = [
|
|
105
|
+
'com.labre.image.replace.picture.description',
|
|
106
|
+
'Put another picture in this image, from the library or a file.',
|
|
107
|
+
];
|
|
108
|
+
export const IMAGE_REPLACE_WITH_SHAPE = [
|
|
109
|
+
'com.labre.image.replace.with-shape',
|
|
110
|
+
'Replace with shape',
|
|
111
|
+
];
|
|
112
|
+
export const IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION = [
|
|
113
|
+
'com.labre.image.replace.with-shape.description',
|
|
114
|
+
'Put a rectangle in place of this image.',
|
|
115
|
+
];
|
|
88
116
|
/**
|
|
89
117
|
* This package's contribution to the translation-key manifest, listed in
|
|
90
118
|
* `PACKAGE_WORDINGS` under source `chrome`
|
|
@@ -108,4 +136,9 @@ export const IMAGE_WORDINGS = [
|
|
|
108
136
|
IMAGE_TOAST_COPIED,
|
|
109
137
|
IMAGE_TOAST_READ_SIZE_FAILED,
|
|
110
138
|
IMAGE_TOOLBAR_REPLACE,
|
|
139
|
+
IMAGE_REPLACE_WITH_IMAGE,
|
|
140
|
+
IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION,
|
|
141
|
+
IMAGE_REPLACE_PICTURE_DESCRIPTION,
|
|
142
|
+
IMAGE_REPLACE_WITH_SHAPE,
|
|
143
|
+
IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION,
|
|
111
144
|
];
|