@malva-ui/editor 0.1.14 → 0.1.15
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.
|
@@ -3274,13 +3274,12 @@ function insertAnchor(doc, from, to) {
|
|
|
3274
3274
|
? childStart(doc, to) + doc.child(to).nodeSize
|
|
3275
3275
|
: childStart(doc, to);
|
|
3276
3276
|
}
|
|
3277
|
-
/** @internal
|
|
3278
|
-
function
|
|
3277
|
+
/** @internal Measures the mount layer's frame from a single rect read. */
|
|
3278
|
+
function mountFrame(mount) {
|
|
3279
|
+
const rect = mount.getBoundingClientRect();
|
|
3279
3280
|
const width = mount.offsetWidth;
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
const scaled = mount.getBoundingClientRect().width;
|
|
3283
|
-
return scaled > 0 ? scaled / width : 1;
|
|
3281
|
+
const scale = width && rect.width > 0 ? rect.width / width : 1;
|
|
3282
|
+
return { top: rect.top, scale };
|
|
3284
3283
|
}
|
|
3285
3284
|
/**
|
|
3286
3285
|
* @internal Top-level child index for one rendered child of the editor DOM, or
|
|
@@ -3315,6 +3314,51 @@ function topLevelIndexOfDom(view, element) {
|
|
|
3315
3314
|
return null;
|
|
3316
3315
|
return element.contains(view.nodeDOM(childStart(doc, index))) ? index : null;
|
|
3317
3316
|
}
|
|
3317
|
+
/**
|
|
3318
|
+
* @internal The hit one rendered child stands for, or null when that child is
|
|
3319
|
+
* not a top-level block.
|
|
3320
|
+
*
|
|
3321
|
+
* A widget decoration owns no top-level node, and its box is a sliver rather
|
|
3322
|
+
* than a block's. Letting one stand in for a block would pair a neighbouring
|
|
3323
|
+
* block's index with the widget's geometry, so `dropTargetIndex` would take
|
|
3324
|
+
* that block's midpoint from the wrong box and land the drop on the wrong side
|
|
3325
|
+
* of it — silently, in one undo step. `topLevelIndexOfDom` answering null is
|
|
3326
|
+
* what makes that a *category* rejection rather than a class-name test, so a
|
|
3327
|
+
* block-level decoration added later is covered without touching this file.
|
|
3328
|
+
*
|
|
3329
|
+
* Both edges come off one rect read, so nothing can pair a `top` from one
|
|
3330
|
+
* layout with a `bottom` from another.
|
|
3331
|
+
*/
|
|
3332
|
+
function blockHit(view, element) {
|
|
3333
|
+
const index = topLevelIndexOfDom(view, element);
|
|
3334
|
+
if (index === null)
|
|
3335
|
+
return null;
|
|
3336
|
+
const rect = element.getBoundingClientRect();
|
|
3337
|
+
return { index, top: rect.top, bottom: rect.bottom };
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* @internal One rendered child at or before `from` — but never before `floor` —
|
|
3341
|
+
* that is a top-level block, paired with its own child index. Null when that
|
|
3342
|
+
* span holds nothing but widgets.
|
|
3343
|
+
*
|
|
3344
|
+
* This is the only way the search below reaches a rect, and that is the point:
|
|
3345
|
+
* **no geometry is ever read off a child that owns no top-level node.** A
|
|
3346
|
+
* widget's box is a sliver where a block's is a block, and one taken out of
|
|
3347
|
+
* flow reports a box that says nothing about where it sits — `display: none`
|
|
3348
|
+
* gives an all-zero rect, and `prosemirror-gapcursor`'s shipped stylesheet
|
|
3349
|
+
* makes exactly that: `position: absolute; display: none`, lifted to `block`
|
|
3350
|
+
* only under `.ProseMirror-focused`, so a gap-cursor selection surviving a blur
|
|
3351
|
+
* parks an all-zero-rect child at top level. Comparing a pointer against that
|
|
3352
|
+
* box would drag a search past real blocks.
|
|
3353
|
+
*/
|
|
3354
|
+
function blockAtOrBefore(view, children, from, floor) {
|
|
3355
|
+
for (let i = from; i >= floor; i -= 1) {
|
|
3356
|
+
const hit = blockHit(view, children[i]);
|
|
3357
|
+
if (hit)
|
|
3358
|
+
return { childIndex: i, hit };
|
|
3359
|
+
}
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3318
3362
|
/**
|
|
3319
3363
|
* @internal Top-level block the given viewport y falls on, or the nearest one
|
|
3320
3364
|
* above it. Null for an empty document, or for a point the view cannot map.
|
|
@@ -3327,49 +3371,85 @@ function topLevelIndexOfDom(view, element) {
|
|
|
3327
3371
|
* test and returns null for every point outside it, in a real browser as much
|
|
3328
3372
|
* as under a test DOM.
|
|
3329
3373
|
*
|
|
3330
|
-
*
|
|
3331
|
-
*
|
|
3332
|
-
* one
|
|
3374
|
+
* **Assumes the top-level blocks are vertically ordered and non-overlapping** —
|
|
3375
|
+
* `top` non-decreasing down the blocks, and no block's box reaching into the
|
|
3376
|
+
* next one's. Normal flow guarantees it for blocks, which are the only children
|
|
3377
|
+
* this reads a box from. Violating it (a float, `position: absolute`, a
|
|
3378
|
+
* negative margin on a *block*) resolves the handle to a neighbouring block
|
|
3379
|
+
* rather than throwing. `slotAt` assumes the same, and so did the scan this
|
|
3380
|
+
* replaced, which stopped at the first child starting below the pointer.
|
|
3381
|
+
*
|
|
3382
|
+
* Under that ordering the answer is *the last block starting at or above the
|
|
3383
|
+
* pointer*, clamped to the first block when every block starts below it. The
|
|
3384
|
+
* search probes child indices, but every comparison is against a **block**: a
|
|
3385
|
+
* probe landing on a widget resolves back to the nearest block at or before it,
|
|
3386
|
+
* and that block's own child index — not the widget's — bounds the next step.
|
|
3387
|
+
* Widget geometry therefore cannot move the search, which is the same category
|
|
3388
|
+
* rejection `blockHit` performs, extended to the probe itself. Pairing a
|
|
3389
|
+
* block's index with a widget's box is what that prevents: `dropTargetIndex`
|
|
3390
|
+
* would take the deciding midpoint from the wrong box and land the drop on the
|
|
3391
|
+
* wrong side of its neighbour — silently, in one undo step.
|
|
3333
3392
|
*
|
|
3334
|
-
*
|
|
3335
|
-
*
|
|
3336
|
-
*
|
|
3337
|
-
*
|
|
3338
|
-
*
|
|
3339
|
-
*
|
|
3340
|
-
*
|
|
3341
|
-
*
|
|
3342
|
-
* *hovered* child, so it is deep hovering in a long document that degrades.
|
|
3343
|
-
* Restoring the linear scan without losing the category skip means deferring
|
|
3344
|
-
* the mapping until a winner is picked and walking back over the few
|
|
3345
|
-
* consecutive widgets — the clamp above the first block is what makes that
|
|
3346
|
-
* more than a one-liner, so it is deliberately left as follow-up.
|
|
3393
|
+
* Cost, against the mapping-per-child scan this replaced. `posAtDOM`, `nodeDOM`
|
|
3394
|
+
* and `childStart` each walk linearly to the child they address, so mapping the
|
|
3395
|
+
* child at index `k` is Θ(k) and scanning down to it was Θ(k²) — 500 mappings
|
|
3396
|
+
* and ~125k node walks for a hover at the bottom of a 500-block document, per
|
|
3397
|
+
* `mousemove`. Here it is Θ(log n) mappings, one per probe plus the widgets a
|
|
3398
|
+
* probe walks over, and ~4.5k node walks at the same size. Deferring to a
|
|
3399
|
+
* single mapping overall is only possible by trusting widget geometry, which
|
|
3400
|
+
* is the trade this deliberately refuses.
|
|
3347
3401
|
*/
|
|
3348
3402
|
function blockAtPoint(view, clientY) {
|
|
3349
3403
|
const children = view.dom.children;
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
const
|
|
3359
|
-
if (
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
candidate
|
|
3366
|
-
|
|
3404
|
+
// `high` is -1 for a view that renders no children at all, so both loops
|
|
3405
|
+
// below are skipped and the answer is null without a bounds check.
|
|
3406
|
+
let low = 0;
|
|
3407
|
+
let high = children.length - 1;
|
|
3408
|
+
let best = null;
|
|
3409
|
+
let bestChild = -1;
|
|
3410
|
+
while (low <= high) {
|
|
3411
|
+
const middle = (low + high) >> 1;
|
|
3412
|
+
const found = blockAtOrBefore(view, children, middle, low);
|
|
3413
|
+
if (found === null) {
|
|
3414
|
+
// Nothing but widgets in [low, middle]: every block still in range
|
|
3415
|
+
// starts after it. Discarding the span is what bounds the walk-backs.
|
|
3416
|
+
low = middle + 1;
|
|
3417
|
+
}
|
|
3418
|
+
else if (found.hit.top <= clientY) {
|
|
3419
|
+
// A candidate, and every block before it is one too; only a later block
|
|
3420
|
+
// can improve on it.
|
|
3421
|
+
best = found.hit;
|
|
3422
|
+
bestChild = found.childIndex;
|
|
3423
|
+
low = found.childIndex + 1;
|
|
3424
|
+
}
|
|
3425
|
+
else {
|
|
3426
|
+
high = found.childIndex - 1;
|
|
3367
3427
|
}
|
|
3368
|
-
candidate = { index, top: rect.top, bottom: rect.bottom };
|
|
3369
|
-
if (clientY <= rect.bottom)
|
|
3370
|
-
break;
|
|
3371
3428
|
}
|
|
3372
|
-
|
|
3429
|
+
if (best === null) {
|
|
3430
|
+
// Every block starts below the pointer, which is the clamp: offer the
|
|
3431
|
+
// first block rather than blink the handle out. Null only for a view
|
|
3432
|
+
// rendering no top-level block at all.
|
|
3433
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
3434
|
+
const hit = blockHit(view, children[i]);
|
|
3435
|
+
if (hit)
|
|
3436
|
+
return hit;
|
|
3437
|
+
}
|
|
3438
|
+
return null;
|
|
3439
|
+
}
|
|
3440
|
+
// A pointer exactly on a block's leading edge belongs to the block above it
|
|
3441
|
+
// when the two boxes touch, which is where the scan this replaced ended up:
|
|
3442
|
+
// it broke on the first box whose `bottom` reached the pointer. The shipped
|
|
3443
|
+
// stylesheet keeps `0.75rem` between paragraph border boxes so nothing can
|
|
3444
|
+
// land here, but the answer is a silent one to change. The predecessor is
|
|
3445
|
+
// only resolved for a pointer exactly on `best.top`, so a well-formed
|
|
3446
|
+
// document pays for it on one pixel per block and never elsewhere.
|
|
3447
|
+
if (clientY === best.top && bestChild > 0) {
|
|
3448
|
+
const previous = blockAtOrBefore(view, children, bestChild - 1, 0);
|
|
3449
|
+
if (previous !== null && previous.hit.bottom >= clientY)
|
|
3450
|
+
return previous.hit;
|
|
3451
|
+
}
|
|
3452
|
+
return best;
|
|
3373
3453
|
}
|
|
3374
3454
|
/**
|
|
3375
3455
|
* @internal Pre-move `to` index for dropping the block at `from` onto `hit`,
|
|
@@ -3405,15 +3485,16 @@ function dropTargetIndex(hit, clientY, from) {
|
|
|
3405
3485
|
* space, while `getBoundingClientRect()` reports scaled screen pixels.
|
|
3406
3486
|
*/
|
|
3407
3487
|
function snapshotBlocks(view, mount) {
|
|
3408
|
-
const scale =
|
|
3409
|
-
const mountTop = mount.getBoundingClientRect().top;
|
|
3488
|
+
const { top: mountTop, scale } = mountFrame(mount);
|
|
3410
3489
|
const slots = [];
|
|
3411
3490
|
const children = view.dom.children;
|
|
3412
3491
|
for (let i = 0; i < children.length; i += 1) {
|
|
3413
3492
|
const element = children[i];
|
|
3414
|
-
// Same category check the hover
|
|
3493
|
+
// Same category check the hover path uses: a widget decoration owns no
|
|
3415
3494
|
// top-level node and must not occupy a slot, or the partition would shift
|
|
3416
|
-
// it as though it were a block.
|
|
3495
|
+
// it as though it were a block. Mapping every child once here is what the
|
|
3496
|
+
// drag pays instead of the hover path's deferred single mapping — the
|
|
3497
|
+
// partition and the settle need a slot per block regardless.
|
|
3417
3498
|
const index = topLevelIndexOfDom(view, element);
|
|
3418
3499
|
if (index === null)
|
|
3419
3500
|
continue;
|
|
@@ -3432,9 +3513,10 @@ function snapshotBlocks(view, mount) {
|
|
|
3432
3513
|
*
|
|
3433
3514
|
* Reproduces `blockAtPoint`'s clamping exactly — above the first block resolves
|
|
3434
3515
|
* to the first, a point in the gap between two blocks resolves to the
|
|
3435
|
-
* preceding one, below the last resolves to the last
|
|
3436
|
-
*
|
|
3437
|
-
*
|
|
3516
|
+
* preceding one, below the last resolves to the last. Both are the same
|
|
3517
|
+
* "last block starting at or above the pointer" search on the same ordering
|
|
3518
|
+
* assumption; this one runs against an array the drag already mapped, so it
|
|
3519
|
+
* needs no widget walk-back and reads no rect at all.
|
|
3438
3520
|
*/
|
|
3439
3521
|
function slotAt(slots, y) {
|
|
3440
3522
|
if (slots.length === 0)
|
|
@@ -3444,6 +3526,9 @@ function slotAt(slots, y) {
|
|
|
3444
3526
|
let low = 0;
|
|
3445
3527
|
let high = slots.length - 1;
|
|
3446
3528
|
while (low < high) {
|
|
3529
|
+
// `ceil`, not `floor`: this converges by raising `low` *to* the midpoint,
|
|
3530
|
+
// so a `floor` midpoint of `low` for an adjacent pair would set `low` to
|
|
3531
|
+
// itself and spin forever.
|
|
3447
3532
|
const middle = Math.ceil((low + high) / 2);
|
|
3448
3533
|
if (slots[middle].top <= y)
|
|
3449
3534
|
low = middle;
|
|
@@ -3674,7 +3759,30 @@ const MlvEditorBlockHandle = Extension.create({
|
|
|
3674
3759
|
// mount is a sibling of `view.dom`, so no serializer can reach it.
|
|
3675
3760
|
const indicator = createIndicatorElement();
|
|
3676
3761
|
mount.appendChild(indicator);
|
|
3677
|
-
|
|
3762
|
+
/**
|
|
3763
|
+
* What the handle currently publishes, or null while it is
|
|
3764
|
+
* retracted.
|
|
3765
|
+
*
|
|
3766
|
+
* Comparing against this is what keeps a pointer move that resolves
|
|
3767
|
+
* an unchanged hit from touching the DOM at all, and the write it
|
|
3768
|
+
* saves is the smaller half of the point: an attribute write on the
|
|
3769
|
+
* handle dirties the layout tree, so the *next* move's rect reads
|
|
3770
|
+
* have to force a fresh layout before they can answer. Skipping the
|
|
3771
|
+
* writes leaves layout clean, and a hover that stays on one block
|
|
3772
|
+
* then forces no layout at all.
|
|
3773
|
+
*
|
|
3774
|
+
* Keyed on the values written rather than on `hit.index`, because
|
|
3775
|
+
* two of the three can change while the index does not: scrolling
|
|
3776
|
+
* moves the block under a stationary pointer, and `label()` is a
|
|
3777
|
+
* signal read that answers differently on a locale change.
|
|
3778
|
+
*/
|
|
3779
|
+
let published = null;
|
|
3780
|
+
const hide = () => {
|
|
3781
|
+
if (published === null)
|
|
3782
|
+
return;
|
|
3783
|
+
published = null;
|
|
3784
|
+
handle.setAttribute('data-visible', 'false');
|
|
3785
|
+
};
|
|
3678
3786
|
const onMouseMove = (event) => {
|
|
3679
3787
|
if (!options.enabled())
|
|
3680
3788
|
return hide();
|
|
@@ -3683,14 +3791,22 @@ const MlvEditorBlockHandle = Extension.create({
|
|
|
3683
3791
|
return hide();
|
|
3684
3792
|
// `getBoundingClientRect()` reports scaled screen pixels, but a
|
|
3685
3793
|
// CSS `top` on a child of the scaled layer is applied in the
|
|
3686
|
-
// layer's own unscaled space. Convert once,
|
|
3687
|
-
//
|
|
3688
|
-
|
|
3689
|
-
const
|
|
3690
|
-
const
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3794
|
+
// layer's own unscaled space. Convert once, off one rect read of
|
|
3795
|
+
// the mount.
|
|
3796
|
+
const { top: mountTop, scale } = mountFrame(mount);
|
|
3797
|
+
const top = `${(hit.top - mountTop) / scale}px`;
|
|
3798
|
+
const index = String(hit.index);
|
|
3799
|
+
const label = options.label();
|
|
3800
|
+
if (published !== null &&
|
|
3801
|
+
published.top === top &&
|
|
3802
|
+
published.index === index &&
|
|
3803
|
+
published.label === label) {
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3806
|
+
published = { top, index, label };
|
|
3807
|
+
handle.style.top = top;
|
|
3808
|
+
handle.dataset['index'] = index;
|
|
3809
|
+
handle.title = label;
|
|
3694
3810
|
handle.setAttribute('data-visible', 'true');
|
|
3695
3811
|
};
|
|
3696
3812
|
/** Top-level index being dragged, or null when no drag is active. */
|
|
@@ -3703,8 +3819,14 @@ const MlvEditorBlockHandle = Extension.create({
|
|
|
3703
3819
|
let ghost = null;
|
|
3704
3820
|
/** Gap the partition is currently opened at, or null when closed. */
|
|
3705
3821
|
let gap = null;
|
|
3706
|
-
/**
|
|
3707
|
-
|
|
3822
|
+
/**
|
|
3823
|
+
* Converts a viewport y into the mount's own unscaled space, off
|
|
3824
|
+
* one rect read — this runs on every `dragover`.
|
|
3825
|
+
*/
|
|
3826
|
+
const toMountSpace = (clientY) => {
|
|
3827
|
+
const { top, scale } = mountFrame(mount);
|
|
3828
|
+
return (clientY - top) / scale;
|
|
3829
|
+
};
|
|
3708
3830
|
/**
|
|
3709
3831
|
* Gap a resolved target index sits on. `dropTargetIndex` folds the
|
|
3710
3832
|
* `slots.length + 1` gaps onto `moveBlock`'s pre-move convention,
|
|
@@ -3786,8 +3908,7 @@ const MlvEditorBlockHandle = Extension.create({
|
|
|
3786
3908
|
order.splice(from, 1);
|
|
3787
3909
|
order.splice(to, 0, from);
|
|
3788
3910
|
const doc = view.state.doc;
|
|
3789
|
-
const scale =
|
|
3790
|
-
const mountTop = mount.getBoundingClientRect().top;
|
|
3911
|
+
const { top: mountTop, scale } = mountFrame(mount);
|
|
3791
3912
|
const first = Math.min(from, to);
|
|
3792
3913
|
const last = Math.max(from, to);
|
|
3793
3914
|
for (let index = first; index <= last; index += 1) {
|
|
@@ -3849,7 +3970,7 @@ const MlvEditorBlockHandle = Extension.create({
|
|
|
3849
3970
|
// Cloned *before* the source is dimmed: the clone carries
|
|
3850
3971
|
// resolved computed styles, so dimming first would bake the
|
|
3851
3972
|
// reduced opacity into the drag image.
|
|
3852
|
-
ghost = createGhostElement(slot.element,
|
|
3973
|
+
ghost = createGhostElement(slot.element, mountFrame(mount).scale);
|
|
3853
3974
|
event.dataTransfer?.setDragImage(ghost, getComputedStyle(view.dom).direction === 'rtl'
|
|
3854
3975
|
? ghost.offsetWidth
|
|
3855
3976
|
: 0, 0);
|