fix: a page-placed frame is measured from the page's corner
Word's positionH never counts from the anchor character, and the page a frame lands on changes as pagination inserts spacers above it. placeFromPage measures the frame's static position and turns both offsets into the margins that reach the page corner — x from the text column, y from the page top — and every pagination pass re-places them. A run-through text box can now sit in front of the text (inFront) instead of always behind it.
This commit is contained in:
1 parent
280726f730
commit
1789505431
4 files changed
+60
-21
No files matched your search
@@ -3,7 +3,7 @@
|
||||
Covers `src/lib/editor/extensions/image.ts` and `textBox.ts` — node views, resize/rotate
|
||||
handles, wrap modes, frame offsets — and their ODF/DOCX legs.
|
||||
|
||||
- **`image.ts`** (`Image`) — inline, as-character image atom (Word's "in line with text"). Attrs: `src` (data-URI), `alt`, `width`/`height` (**unscaled doc px @96dpi**, like `rowHeight`, and **fractional** — rounding a page-wide picture to whole px costs 0.3mm of height; `framePx` snaps back to a whole one within 0.05px, which is where our own export's 0.001cm quantization lands, so the round trip stays exact), `rotation` (CW degrees). The node view nests an `<img>` in a rotated "rotor" inside an axis-aligned wrapper that reserves the rotated bounding box (so text reflows around it). The rotor's transform makes it a stacking context, so a selected `.image-node` is given a `z-index` above the header/footer layer to keep handles that poke into a margin grabbable. It has eight zoom-aware resize handles — corners aspect-locked, edges single-axis (width- or height-only) — plus a rotation-arrow grip, and shows a live "Width … × Height …" (cm) badge while resizing; resize deltas are un-rotated onto the image's own axes so handles track the pointer at any angle (drag math mirrors `tableRowResize.ts`; width cap = the cell or page-text-column, height cap = the page text height). `setImage` command inserts it; the toolbar button (ToolbarExpanded), drag-drop and paste (Editor.svelte) read the file via `FileReader` → data-URI and clamp the initial size to the page text box. A picture that arrives by **URL** (an `<img>` pasted or dropped from a web page) is fetched into a data-URI right after the paste (`inlineRemoteImages`); one the site withholds is dropped with a message, since only a data-URI ever reaches a file. **Text wrap:** the `wrap` attr (`'inline'|'left'|'right'|'topBottom'|'through'`) makes the image a *floating* frame — `ImageToolbar.svelte` (a per-image floating toolbar wired in `Editor.svelte` like `TableToolbar`) sets the mode; `left`/`right` float the wrapper at its anchor paragraph (text flows on the open side via CSS `float`), `topBottom` is a **full-width `float`** (text only above/below), and `through` — Word's in-front-of / behind-text (`wp:wrapNone` + `behindDoc`), ODF's `style:wrap="run-through"` — leaves the flow entirely: `position: absolute` with no offsets keeps the static position it was anchored at, so it reserves neither width nor height, and `inFront` picks the side of the text it lands on. Read as a side float instead it reserved a near-column-wide picture's whole height, which cost the image fixture a page and ~50 reports. A text box in that mode always sits behind the text — the mode has no toolbar entry yet, so only a file can carry it in. All modes use `float` deliberately: an in-flow `display:block` on an inline-atom node view splits the paragraph's inline content into anonymous block boxes, which desyncs ProseMirror's inline view descriptor and makes it drop the following page-break spacer widget; a float is out-of-flow and avoids that. Dragging an **inline** image runs ProseMirror's native node move (the `dropCursor` plugin shows the caret). Dragging a **floating** image runs a custom drag (`ImageView.startReposition`) that **live re-anchors** the node to the text position under the cursor, so the float and text reflow in real time as you drag; it's rAF-throttled and only moves when the cursor's line changes (a float can't move within a line), and stays a single undo step (first move records history, later moves are `addToHistory:false` and get rebased on undo). A paragraph that contains an image paginates **atomically** (`pageBreaks.ts` — pushed whole to the next page, spacer placed before it) so a page break never splits the paragraph next to the image's float (where ProseMirror drops the spacer widget). **Wrap distance:** `wrapDist` is the gap the file keeps beside a float, in cm (Word's `distL`/`distR`, ODF's `fo:margin-left`/`-right` on the graphic style) — only the side the text flows on, since the other one is the frame's own offset. A frame that declares none reserves **nothing**, beside it or above/below it: probed, LibreOffice wraps flush against it, and a gap of our own put every figure in a text box 3mm too tall and broke a line a word early. `setImageWrap` gives a frame the file never floated the 0.32cm a word processor's own wrap command writes. **Vertical alignment (as-char):** `vAlign` carries ODF's `style:vertical-pos`/`-rel` pair for an inline frame — `null` (LibreOffice's `top`/`baseline` and Word's only mode) stands the frame on the text baseline, `middle`/`below` centre it on the baseline or hang it below, and `text-top`/`text-middle`/`text-bottom` measure against the character area instead (CSS `vertical-align: text-top`/`text-bottom`; nothing in CSS centres on that area, so `text-middle` uses 0.36em for half of ascent − descent, which Liberation Serif/Sans, Times and Arial share). `from-top` becomes `vAlign: 'offset'` and puts the frame's top `wrapOffsetY` cm below the baseline whatever the relation says — all probed against Line truncated
|
||||
- **`image.ts`** (`Image`) — inline, as-character image atom (Word's "in line with text"). Attrs: `src` (data-URI), `alt`, `width`/`height` (**unscaled doc px @96dpi**, like `rowHeight`, and **fractional** — rounding a page-wide picture to whole px costs 0.3mm of height; `framePx` snaps back to a whole one within 0.05px, which is where our own export's 0.001cm quantization lands, so the round trip stays exact), `rotation` (CW degrees). The node view nests an `<img>` in a rotated "rotor" inside an axis-aligned wrapper that reserves the rotated bounding box (so text reflows around it). The rotor's transform makes it a stacking context, so a selected `.image-node` is given a `z-index` above the header/footer layer to keep handles that poke into a margin grabbable. It has eight zoom-aware resize handles — corners aspect-locked, edges single-axis (width- or height-only) — plus a rotation-arrow grip, and shows a live "Width … × Height …" (cm) badge while resizing; resize deltas are un-rotated onto the image's own axes so handles track the pointer at any angle (drag math mirrors `tableRowResize.ts`; width cap = the cell or page-text-column, height cap = the page text height). `setImage` command inserts it; the toolbar button (ToolbarExpanded), drag-drop and paste (Editor.svelte) read the file via `FileReader` → data-URI and clamp the initial size to the page text box. A picture that arrives by **URL** (an `<img>` pasted or dropped from a web page) is fetched into a data-URI right after the paste (`inlineRemoteImages`); one the site withholds is dropped with a message, since only a data-URI ever reaches a file. **Text wrap:** the `wrap` attr (`'inline'|'left'|'right'|'topBottom'|'through'`) makes the image a *floating* frame — `ImageToolbar.svelte` (a per-image floating toolbar wired in `Editor.svelte` like `TableToolbar`) sets the mode; `left`/`right` float the wrapper at its anchor paragraph (text flows on the open side via CSS `float`), `topBottom` is a **full-width `float`** (text only above/below), and `through` — Word's in-front-of / behind-text (`wp:wrapNone` + `behindDoc`), ODF's `style:wrap="run-through"` — leaves the flow entirely: `position: absolute` with no offsets keeps the static position it was anchored at, so it reserves neither width nor height, and `inFront` picks the side of the text it lands on. A frame the file places against the **page** takes both offsets from the page's corner instead (see `wrapFromPage`). Read as a side float instead it reserved a near-column-wide picture's whole height, which cost the image fixture a page and ~50 reports. A text box in that mode sits behind the text unless the file puts it in front (`inFront`, z-index 1 against its −2) — the mode has no toolbar entry yet, so only a file can carry it in. All modes use `float` deliberately: an in-flow `display:block` on an inline-atom node view splits the paragraph's inline content into anonymous block boxes, which desyncs ProseMirror's inline view descriptor and makes it drop the following page-break spacer widget; a float is out-of-flow and avoids that. Dragging an **inline** image runs ProseMirror's native node move (the `dropCursor` plugin shows the caret). Dragging a **floating** image runs a custom drag (`ImageView.startReposition`) that **live re-anchors** the node to the text position under the cursor, so the float and text reflow in real time as you drag; it's rAF-throttled and only moves when the cursor's line changes (a float can't move within a line), and stays a single undo step (first move records history, later moves are `addToHistory:false` and get rebased on undo). A paragraph that contains an image paginates **atomically** (`pageBreaks.ts` — pushed whole to the next page, spacer placed before it) so a page break never splits the paragraph next to the image's float (where ProseMirror drops the spacer widget). **Wrap distance:** `wrapDist` is the gap the file keeps beside a float, in cm (Word's `distL`/`distR`, ODF's `fo:margin-left`/`-right` on the graphic style) — only the side the text flows on, since the other one is the frame's own offset. A frame that declares none reserves **nothing**, beside it or above/below it: probed, LibreOffice wraps flush against it, and a gap of our own put every figure in a text box 3mm too tall and broke a line a word early. `setImageWrap` gives a frame the file never floated the 0.32cm a word processor's own wrap command writes. **Vertical alignment (as-char):** `vAlign` carries ODF's `style:vertical-pos`/`-rel` pair for an inline frame — `null` (LibreOffice's `top`/`baseline` and Word's only mode) stands the frame on the text baseline, `middle`/`below` centre it on the baseline or hang it below, and `text-top`/`text-middle`/`text-bottom` measure against the character area instead (CSS `vertical-align: text-top`/`text-bottom`; nothing in CSS centres on that area, so `text-middle` uses 0.36em for half of ascent − descent, which Liberation SeLine truncated
|
||||
- **`textBox.ts`** (`TextBox`) — an **inline** text box / basic shape carrying editable block content (`group: 'inline'`, `inline: true`, `content: '(paragraph|heading|bulletList|orderedList)+ | table'`, `isolating`) — the frame both formats write, a character in a run that holds paragraphs. It rides a paragraph, so `inline` really is in the line, and a box reaches a table cell or a list item through their paragraphs, as one does in Word. Attrs: `width`/`height` (px @96dpi; height renders as **min-height** — content grows the box), `rotation` (CW deg), `wrap` (image's `WrapMode`), `shapeKind` (see the shape table below), `fillColor`/`strokeColor` (`null` = transparent/no border), `strokeWidthPt`. Padding is the fixed `TEXTBOX_PADDING_CM` (0.15cm). **Wrap** follows `ImageView.applyWrap` exactly: `inline` is an `inline-block` in the line, `left`/`right` are floats, `topBottom` a **full-width float** whose wrapper spans the column while `placeInBand` moves the rotor across it, `through` leaves the flow. Never `display:block` — that splits the paragraph's inline content into anonymous block boxes and ProseMirror drops the following page-break spacer. A paragraph holding a box paginates **atomically** (`pageBreaks.ts`), whichever way the box wraps. `TextBoxView` mirrors `ImageView` (wrapper reserving the rotated bbox → rotor carrying fill/stroke/rotation → `contentDOM`); the rotor is out of flow and auto-grows, so a ResizeObserver refits the wrapper. **Click model (Word-like):** `stopEvent`/`isFrameHit` claim mouse events on a ~6px frame ring (rotation-/zoom-aware) → NodeSelection + handles (the shared `image-*` handle classes/CSS); clicks further inside pass to ProseMirror (caret). The box is browser-`draggable` only while node-selected, so PM's native drag moves it without hijacking text selection. **Reaching the position beside the box:** a frame nobody is editing is an atom to the browser — a `contenteditable="false"` node decoration, dropped while the caret is in its text — and `ArrowLeft`/`ArrowRight` at the box's first/last text position step out to `before`/`after` the node. Without both, a box that starts its paragraph is a trap: there is no text position beside it, so the browser pulls the caret meant for the box's own place in the line into the frame and everything typed there lands inside it. The frame then has to place the caret itself on the click that enters it (`onFrameMouseDown`, `stopEvent`), since a non-editable one gets none from the browser. A nested `contentEditable` island is not the way — ProseMirror's `hasFocus` is `activeElement == view.dom`, and an island holding focus makes it read no selection at all. Commands: `insertTextBox()` (at the caret, cursor inside; refused inside a box), `setTextBoxAttrs()` (NodeSelection *or* cursor inside; helper `findTextBox`), `setTextBoxAlign()`. **Two guards** the schema cannot express live in the extension's plugin: a `filterTransaction` refuses a box inside a box — both word processors refuse it and neither format's writer has a place for one — and an `appendTransaction` grows a selection ending inside a box it did not start in out to the box's own bounds, since deleting such a range dissolves the frame and spills its text into the body. `TextBoxToolbar.svelte` (wired in `Editor.svelte` like the image toolbar, also shown while the caret is inside a box) sets wrap/shape/fill/stroke, the alignment and `textVertical` — the box's text running **top-to-bottom, right-to-left** (`writing-mode: vertical-rl` on the content area, which is all the browser needs). It round-trips as the style's own writing mode in ODF — a plain box is a Writer text frame (its `TbxFr` style chains to `Frame`; see `src/lib/export/CLAUDE.md`) and takes it in the graphic properties, a drawing *shape* only in its style's paragraph properties (both probed) — and Word's `w:bodyPr vert`; ODF's bottom-to-top and Word's `vert270` read as the same one direction the browser lays out. **DOCX autofit:** the shape body writes `<a:noAutofit/>`, which is what LibreOffice writes for the same frame — read as `spAutoFit` it gives the frame `fo:min-height="0"` and draws its text detached from the shape, over whatever follows (probed). The cost is the ODF leg's auto-grow: content taller than the declared height is clipped there, DOCX having no `fo:min-height`. **LibreOffice puts a `wrapTopAndBottom` anchor's band above its anchor paragraph** whatever the anchor says — probed against `posOffset` 0/635, `align top`, `relativeFrom` line/column, `allowOverlap`, and against a file LibreOffice exported itself; `wp:inline` is the one form it places right, and a band-wrapped box keeps `wp:anchor` anyway (Word is the reference for `.docx`). **Where it sits across the column:** only a **band-wrapped** box has a place of its own — `wrapAlign` (`null`/`center`/`right`, drawn by `placeInBand`), and the toolbar's three buttons show for that mode alone, dropping any `wrapOffset`Line truncated
|
||||
|
||||
## A floating table
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
import type { EditorView } from '@tiptap/pm/view';
|
||||
import { dropCursor } from '@tiptap/pm/dropcursor';
|
||||
import { cmToPx } from '../../storage/pageMargins';
|
||||
import { readVerticalMargins, topInEditor } from './pageBreaks';
|
||||
import { readVerticalMargins, placeFromPage } from './pageBreaks';
|
||||
|
||||
// Inline, as-character image, or a floating text-wrapped frame (wrap = flow mode);
|
||||
// width/height are doc px @96dpi, rotation CW degrees. Export → cm + ODF
|
||||
@@ -103,22 +103,15 @@ export function frameMargins(wrap: WrapMode, offsetCm: unknown, boxWidthPx: numb
|
||||
// Word's behind-text / in-front-of-text, ODF run-through: the text runs over or under
|
||||
// the frame, so it reserves nothing. Absolute with no offsets keeps the static position
|
||||
// it was anchored at; the file's own offsets ride as margins from there.
|
||||
export function applyRunThrough(el: HTMLElement, offsetCm: unknown, offsetYCm: unknown, inFront: boolean): void {
|
||||
export function applyRunThrough(el: HTMLElement, offsetCm: unknown, offsetYCm: unknown, inFront: boolean, fromPage = false): void {
|
||||
const px = (cm: unknown) => (typeof cm === 'number' ? Math.round(cmToPx(cm)) : 0);
|
||||
el.style.position = 'absolute';
|
||||
el.style.margin = `${px(offsetYCm)}px 0 0 ${px(offsetCm)}px`;
|
||||
el.style.zIndex = inFront ? '1' : '-1';
|
||||
}
|
||||
|
||||
// Word's positionV relativeFrom="page" / ODF's style:vertical-rel="page": the offset
|
||||
// counts from the top of the page the anchor lands on, not from the anchor itself. The
|
||||
// frame keeps the x its static position gives it and takes its y off the page grid.
|
||||
export function sinkToPageTop(view: EditorView, el: HTMLElement, offsetYCm: unknown): void {
|
||||
el.style.marginTop = '0px';
|
||||
const { grid } = readVerticalMargins(view.dom as HTMLElement);
|
||||
const top = topInEditor(view, el);
|
||||
const y = typeof offsetYCm === 'number' ? cmToPx(offsetYCm) : 0;
|
||||
el.style.marginTop = `${Math.round(grid.topOf(grid.pageAt(top)) + y - top)}px`;
|
||||
// A page-placed frame states its corner instead: placeFromPage turns the pair into
|
||||
// the margins that reach it, and pagination re-places it from these same numbers.
|
||||
if (fromPage) { el.dataset.pageX = String(px(offsetCm)); el.dataset.pageY = String(px(offsetYCm)); }
|
||||
else { delete el.dataset.pageX; delete el.dataset.pageY; }
|
||||
}
|
||||
|
||||
// Where an as-char frame sits against the line (ODF style:vertical-pos/-rel, probed
|
||||
@@ -548,10 +541,10 @@ class ImageView {
|
||||
}
|
||||
delete d.dataset.anchorPage;
|
||||
if (wrap === 'through') {
|
||||
applyRunThrough(d, a.wrapOffset, a.wrapOffsetY, a.inFront === true);
|
||||
applyRunThrough(d, a.wrapOffset, a.wrapOffsetY, a.inFront === true, a.wrapFromPage === true);
|
||||
// Deferred like sinkToOffset: the frame has to be laid out before its own page
|
||||
// can be read off the grid.
|
||||
if (a.wrapFromPage) requestAnimationFrame(() => sinkToPageTop(this.view, d, a.wrapOffsetY));
|
||||
if (a.wrapFromPage) requestAnimationFrame(() => placeFromPage(this.view, d));
|
||||
return;
|
||||
}
|
||||
if (wrap === 'left' || wrap === 'right') {
|
||||
|
||||
@@ -291,6 +291,38 @@ export function topInEditor(view: EditorView, el: HTMLElement): number {
|
||||
return top;
|
||||
}
|
||||
|
||||
// Its left, likewise — the same chain, so a frame reads both in one pass.
|
||||
export function leftInEditor(view: EditorView, el: HTMLElement): number {
|
||||
const tiptap = view.dom as HTMLElement;
|
||||
let left = 0;
|
||||
for (let n: HTMLElement | null = el; n && n !== tiptap; n = n.offsetParent as HTMLElement | null) {
|
||||
left += n.offsetLeft;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// A run-through frame the file places against the page, not against its anchor (Word's
|
||||
// positionH/V relativeFrom="page"): x counts from the text column, y from the top of the
|
||||
// page the anchor lands on. Both are written as margins off the frame's static position,
|
||||
// which is where the anchor character sits — so each is measured, not assumed.
|
||||
export function placeFromPage(view: EditorView, el: HTMLElement, grid?: PageGrid): void {
|
||||
const g = grid ?? readVerticalMargins(view.dom as HTMLElement).grid;
|
||||
el.style.marginTop = '0px';
|
||||
el.style.marginLeft = '0px';
|
||||
const top = topInEditor(view, el);
|
||||
const left = leftInEditor(view, el);
|
||||
const column = parseFloat(getComputedStyle(view.dom as HTMLElement).paddingLeft) || 0;
|
||||
el.style.marginTop = `${Math.round(g.topOf(g.pageAt(top)) + (Number(el.dataset.pageY) || 0) - top)}px`;
|
||||
el.style.marginLeft = `${Math.round(column + (Number(el.dataset.pageX) || 0) - left)}px`;
|
||||
}
|
||||
|
||||
// Pagination moves the page grid under those frames, so every pass re-places them.
|
||||
export function placePageFrames(view: EditorView, grid: PageGrid): void {
|
||||
for (const el of Array.from((view.dom as HTMLElement).querySelectorAll<HTMLElement>('[data-page-y]'))) {
|
||||
placeFromPage(view, el, grid);
|
||||
}
|
||||
}
|
||||
|
||||
function pageContentStart(page: number, marginTop: number, grid: PageGrid): number {
|
||||
return grid.topOf(page) + marginTop;
|
||||
}
|
||||
@@ -1980,6 +2012,10 @@ export const PageBreaks = Extension.create({
|
||||
isUpdating = false;
|
||||
lastBottom = contentBottom();
|
||||
|
||||
// The spacers this pass placed moved every page-placed frame's anchor; the
|
||||
// frames sit out of the flow, so re-placing them changes no measurement.
|
||||
placePageFrames(editorView, vm.grid);
|
||||
|
||||
// A per-page restart counts within the page each anchor landed on, which only
|
||||
// this pass knows (notes.ts). Renumbering can rewrap, so it takes a pass of its
|
||||
// own — on the same budget, since a label that moves its anchor never settles.
|
||||
|
||||
@@ -9,7 +9,8 @@ import { NodeSelection, Selection, TextSelection, Plugin } from '@tiptap/pm/stat
|
||||
import type { EditorState } from '@tiptap/pm/state';
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
import type { EditorView } from '@tiptap/pm/view';
|
||||
import { HANDLES, MIN_SIZE_PX, clamp, parsePx, frameMargins, pageContentHeightPx, applyRunThrough, sinkToPageTop, type WrapMode } from './image';
|
||||
import { placeFromPage } from './pageBreaks';
|
||||
import { HANDLES, MIN_SIZE_PX, clamp, parsePx, frameMargins, pageContentHeightPx, applyRunThrough, type WrapMode } from './image';
|
||||
import { SHAPES, shapePath, linePaths, arrowHeadPx, isShapeKind, isLineKind, type ShapeKind } from '../../utils/shapes';
|
||||
import { cmToPx } from '../../storage/pageMargins';
|
||||
|
||||
@@ -48,6 +49,7 @@ export interface TextBoxAttrs {
|
||||
wrapOffset: number | null; // cm from the text column's left edge
|
||||
wrapOffsetY: number | null; // cm below the anchor paragraph
|
||||
wrapFromPage: boolean; // …or below the top of the page the anchor lands on
|
||||
inFront: boolean; // over the text rather than behind it (run-through only)
|
||||
wrapDist: number | null; // cm of gap to the text beside it
|
||||
wrapAlign: string | null; // 'center'/'right' = set against the middle/far end
|
||||
paddingCm: number; // inset ring around the text (ODF fo:padding)
|
||||
@@ -196,6 +198,13 @@ export const TextBox = Node.create({
|
||||
parseHTML: el => parseCmAttr((el as HTMLElement).getAttribute('data-wrap-offset-y')),
|
||||
renderHTML: () => ({}),
|
||||
},
|
||||
// In front of the text rather than behind it (Word's behindDoc="0", ODF
|
||||
// style:run-through="foreground") — as on an image.
|
||||
inFront: {
|
||||
default: false,
|
||||
parseHTML: el => (el as HTMLElement).hasAttribute('data-in-front'),
|
||||
renderHTML: () => ({}),
|
||||
},
|
||||
// Whether wrapOffsetY counts from the top of the frame's page — as on an image.
|
||||
wrapFromPage: {
|
||||
default: false,
|
||||
@@ -300,6 +309,7 @@ export const TextBox = Node.create({
|
||||
style,
|
||||
...(a.rotation ? { 'data-rotation': String(a.rotation) } : {}),
|
||||
...(a.wrap !== 'inline' ? { 'data-wrap': a.wrap } : {}),
|
||||
...(a.inFront ? { 'data-in-front': '' } : {}),
|
||||
...(a.wrapFromPage ? { 'data-wrap-from-page': '' } : {}),
|
||||
...(a.shapeKind !== 'textbox' ? { 'data-shape': a.shapeKind } : {}),
|
||||
...(a.shapePath ? { 'data-shape-path': a.shapePath } : {}),
|
||||
@@ -718,11 +728,11 @@ class TextBoxView {
|
||||
} else if (a.wrap === 'through') {
|
||||
// Behind the text, which is what a shape with no run-through of its own exports as
|
||||
// — and under a picture behind the text too (-1), which is the order LibreOffice
|
||||
// paints a cover page in whatever stacking order the file names.
|
||||
applyRunThrough(d, a.wrapOffset, a.wrapOffsetY, false);
|
||||
d.style.zIndex = '-2';
|
||||
// paints a cover page in; a box the file puts in front of the text sits above both.
|
||||
applyRunThrough(d, a.wrapOffset, a.wrapOffsetY, a.inFront === true, a.wrapFromPage === true);
|
||||
if (a.inFront !== true) d.style.zIndex = '-2';
|
||||
// Deferred: the frame has to be laid out before its own page can be read.
|
||||
if (a.wrapFromPage) requestAnimationFrame(() => sinkToPageTop(this.editor.view, d, a.wrapOffsetY));
|
||||
if (a.wrapFromPage) requestAnimationFrame(() => placeFromPage(this.editor.view, d));
|
||||
} else if (a.wrap === 'topBottom') {
|
||||
// A full-width float, as on an image: text may only flow above and below it, and
|
||||
// a block box on an inline node view splits the paragraph's inline content into
|
||||
|
||||
Reference in new issue
Block a user