Skip to content

Commit 4b36780

Browse files
authored
feat(images): allow drag-and-drop for images in editor (#2227)
* feat(images): allow drag-and-drop for images in editor * chore: review fixes
1 parent 2c6da10 commit 4b36780

9 files changed

Lines changed: 1263 additions & 211 deletions

File tree

packages/layout-engine/measuring/dom/src/index.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,26 @@ function calculateTypographyMetrics(
448448
};
449449
}
450450

451+
/**
452+
* Wraps `calculateTypographyMetrics` and applies inline-image height override.
453+
*
454+
* Typography metrics (ascent, descent) stay text-based so the baseline doesn't
455+
* shift. When the line contains an inline image taller than the text line height,
456+
* lineHeight is expanded to the image height — matching Word's behaviour where
457+
* the text baseline stays fixed and the image occupies exactly its own height.
458+
*/
459+
function finalizeLineMetrics(
460+
line: { maxFontSize: number; maxFontInfo?: FontInfo; maxImageHeight?: number },
461+
spacing?: ParagraphSpacing,
462+
): { ascent: number; descent: number; lineHeight: number } {
463+
const metrics = calculateTypographyMetrics(line.maxFontSize, spacing, line.maxFontInfo);
464+
const imageH = line.maxImageHeight ?? 0;
465+
if (imageH > metrics.lineHeight) {
466+
metrics.lineHeight = imageH;
467+
}
468+
return metrics;
469+
}
470+
451471
/**
452472
* Calculates typography metrics for empty paragraphs.
453473
*
@@ -1048,6 +1068,8 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
10481068
maxFontSize: number;
10491069
/** Font info for the run with maxFontSize, used for accurate typography metrics */
10501070
maxFontInfo?: FontInfo;
1071+
/** Tallest inline image on this line (pixels) */
1072+
maxImageHeight?: number;
10511073
maxWidth: number;
10521074
segments: Line['segments'];
10531075
leaders?: Line['leaders'];
@@ -1275,7 +1297,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
12751297

12761298
if ((run as Run).kind === 'break') {
12771299
if (currentLine) {
1278-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
1300+
const metrics = finalizeLineMetrics(currentLine, spacing);
12791301
const lineBase = currentLine;
12801302
const completedLine: Line = { ...lineBase, ...metrics };
12811303
addBarTabsToLine(completedLine);
@@ -1307,7 +1329,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
13071329
// For leading line breaks (before any text), use fallback font info for accurate height calculation
13081330
const lineBreakFontInfo = hasSeenTextRun ? undefined : fallbackFontInfo;
13091331
if (currentLine) {
1310-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
1332+
const metrics = finalizeLineMetrics(currentLine, spacing);
13111333
const completedLine: Line = {
13121334
...currentLine,
13131335
...metrics,
@@ -1489,7 +1511,8 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
14891511
toRun: runIndex,
14901512
toChar: 1, // Images are treated as single atomic units
14911513
width: imageWidth,
1492-
maxFontSize: imageHeight, // Use image height for line height calculation
1514+
maxFontSize: 0,
1515+
maxImageHeight: imageHeight,
14931516
maxWidth: getEffectiveWidth(lines.length === 0 ? initialAvailableWidth : bodyContentWidth),
14941517
spaceCount: 0,
14951518
segments: [
@@ -1518,7 +1541,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
15181541
if (!skipFitCheck && currentLine.width + imageWidth > currentLine.maxWidth && currentLine.width > 0) {
15191542
// Image doesn't fit - finish current line and start new line with image
15201543
trimTrailingWrapSpaces(currentLine);
1521-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
1544+
const metrics = finalizeLineMetrics(currentLine, spacing);
15221545
const lineBase = currentLine;
15231546
const completedLine: Line = {
15241547
...lineBase,
@@ -1538,7 +1561,8 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
15381561
toRun: runIndex,
15391562
toChar: 1,
15401563
width: imageWidth,
1541-
maxFontSize: imageHeight,
1564+
maxFontSize: 0,
1565+
maxImageHeight: imageHeight,
15421566
maxWidth: getEffectiveWidth(bodyContentWidth),
15431567
spaceCount: 0,
15441568
segments: [
@@ -1555,7 +1579,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
15551579
currentLine.toRun = runIndex;
15561580
currentLine.toChar = 1;
15571581
currentLine.width = roundValue(currentLine.width + imageWidth);
1558-
currentLine.maxFontSize = Math.max(currentLine.maxFontSize, imageHeight);
1582+
currentLine.maxImageHeight = Math.max(currentLine.maxImageHeight ?? 0, imageHeight);
15591583
if (!currentLine.segments) currentLine.segments = [];
15601584
currentLine.segments.push({
15611585
runIndex,
@@ -1668,7 +1692,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
16681692
if (currentLine.width + annotationWidth > currentLine.maxWidth && currentLine.width > 0) {
16691693
// Doesn't fit - finish current line and start new one
16701694
trimTrailingWrapSpaces(currentLine);
1671-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
1695+
const metrics = finalizeLineMetrics(currentLine, spacing);
16721696
const lineBase = currentLine;
16731697
const completedLine: Line = {
16741698
...lineBase,
@@ -1772,7 +1796,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
17721796
currentLine.width > 0
17731797
) {
17741798
trimTrailingWrapSpaces(currentLine);
1775-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
1799+
const metrics = finalizeLineMetrics(currentLine, spacing);
17761800
const lineBase = currentLine;
17771801
const completedLine: Line = {
17781802
...lineBase,
@@ -1886,7 +1910,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
18861910
) {
18871911
// Space doesn't fit - finish current line and start new one with the space
18881912
trimTrailingWrapSpaces(currentLine);
1889-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
1913+
const metrics = finalizeLineMetrics(currentLine, spacing);
18901914
const lineBase = currentLine;
18911915
const completedLine: Line = {
18921916
...lineBase,
@@ -1969,7 +1993,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
19691993
// long word can use the pending tab alignment.
19701994
if (currentLine && currentLine.width > 0 && currentLine.segments && currentLine.segments.length > 0) {
19711995
trimTrailingWrapSpaces(currentLine);
1972-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
1996+
const metrics = finalizeLineMetrics(currentLine, spacing);
19731997
const lineBase = currentLine;
19741998
const completedLine: Line = {
19751999
...lineBase,
@@ -2036,7 +2060,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
20362060
} else {
20372061
// More chunks to come - finish this line and push it
20382062
trimTrailingWrapSpaces(currentLine);
2039-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
2063+
const metrics = finalizeLineMetrics(currentLine, spacing);
20402064
const lineBase = currentLine;
20412065
const completedLine: Line = {
20422066
...lineBase,
@@ -2182,7 +2206,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
21822206

21832207
if (shouldBreak) {
21842208
trimTrailingWrapSpaces(currentLine);
2185-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
2209+
const metrics = finalizeLineMetrics(currentLine, spacing);
21862210
const lineBase = currentLine;
21872211
const completedLine: Line = {
21882212
...lineBase,
@@ -2247,7 +2271,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
22472271
appendSegment(currentLine.segments, runIndex, wordStartChar, wordEndNoSpace, wordOnlyWidth, explicitXHere);
22482272
// finish current line and start a new one on next iteration
22492273
trimTrailingWrapSpaces(currentLine);
2250-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
2274+
const metrics = finalizeLineMetrics(currentLine, spacing);
22512275
const lineBase = currentLine;
22522276
const completedLine: Line = { ...lineBase, ...metrics };
22532277
addBarTabsToLine(completedLine);
@@ -2386,7 +2410,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
23862410
}
23872411

23882412
if (currentLine) {
2389-
const metrics = calculateTypographyMetrics(currentLine.maxFontSize, spacing, currentLine.maxFontInfo);
2413+
const metrics = finalizeLineMetrics(currentLine, spacing);
23902414
const lineBase = currentLine;
23912415
const finalLine: Line = {
23922416
...lineBase,

packages/super-editor/src/components/toolbar/super-toolbar.js

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@ import { getActiveFormatting } from '@core/helpers/getActiveFormatting.js';
66
import { findParentNode } from '@helpers/index.js';
77
import { vClickOutside } from '@superdoc/common';
88
import Toolbar from './Toolbar.vue';
9-
import {
10-
checkAndProcessImage,
11-
replaceSelectionWithImagePlaceholder,
12-
uploadAndInsertImage,
13-
getFileOpener,
14-
} from '../../extensions/image/imageHelpers/index.js';
9+
import { getFileOpener, processAndInsertImageFile } from '../../extensions/image/imageHelpers/index.js';
1510
import { toolbarIcons } from './toolbarIcons.js';
1611
import { toolbarTexts } from './toolbarTexts.js';
1712
import { getQuickFormatList } from '@extensions/linked-styles/index.js';
@@ -459,29 +454,12 @@ export class SuperToolbar extends EventEmitter {
459454
return;
460455
}
461456

462-
const { size, file } = await checkAndProcessImage({
457+
await processAndInsertImageFile({
463458
file: result.file,
464-
getMaxContentSize: () => this.activeEditor.getMaxContentSize(),
465-
});
466-
467-
if (!file) {
468-
return;
469-
}
470-
471-
const id = {};
472-
473-
replaceSelectionWithImagePlaceholder({
474-
view: this.activeEditor.view,
475-
editorOptions: this.activeEditor.options,
476-
id,
477-
});
478-
479-
await uploadAndInsertImage({
480459
editor: this.activeEditor,
481460
view: this.activeEditor.view,
482-
file,
483-
size,
484-
id,
461+
editorOptions: this.activeEditor.options,
462+
getMaxContentSize: () => this.activeEditor.getMaxContentSize(),
485463
});
486464
} catch (error) {
487465
const err = new Error('[super-toolbar 🎨] Image upload failed');

packages/super-editor/src/core/presentation-editor/PresentationEditor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
shouldUseCellSelection as shouldUseCellSelectionFromHelper,
6666
} from './tables/TableSelectionUtilities.js';
6767
import { DragDropManager } from './input/DragDropManager.js';
68+
import { processAndInsertImageFile } from '@extensions/image/imageHelpers/processAndInsertImageFile.js';
6869
import { HeaderFooterSessionManager } from './header-footer/HeaderFooterSessionManager.js';
6970
import { decodeRPrFromMarks } from '../super-converter/styles.js';
7071
import { halfPointToPoints } from '../super-converter/helpers.js';
@@ -2764,7 +2765,7 @@ export class PresentationEditor extends EventEmitter {
27642765
}
27652766

27662767
/**
2767-
* Sets up drag and drop handlers for field annotations.
2768+
* Sets up drag and drop handlers for field annotations and image files.
27682769
*/
27692770
#setupDragHandlers() {
27702771
// Clean up any existing manager
@@ -2777,6 +2778,7 @@ export class PresentationEditor extends EventEmitter {
27772778
scheduleSelectionUpdate: () => this.#scheduleSelectionUpdate(),
27782779
getViewportHost: () => this.#viewportHost,
27792780
getPainterHost: () => this.#painterHost,
2781+
insertImageFile: (params) => processAndInsertImageFile(params),
27802782
});
27812783
this.#dragDropManager.bind();
27822784
}

0 commit comments

Comments
 (0)