Skip to content

Commit d5b9af5

Browse files
author
jay
committed
fix: 修复拖拽bug
1 parent aebe9d9 commit d5b9af5

6 files changed

Lines changed: 135 additions & 21 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ the Release, rather than merely pushing the tag, starts the package workflow.
124124
The current version can also be published manually from `main` with:
125125

126126
```bash
127-
gh workflow run publish.yml --ref main
128-
gh run watch
127+
npm run publish:github
128+
npm run publish:watch
129129
```
130130

131131
## Package API

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"start": "node ./bin/htmleditor.js ./samples/demo.html --no-open",
3838
"test": "node --test",
3939
"pack:check": "npm pack --dry-run",
40+
"publish:github": "gh workflow run publish.yml --ref main",
41+
"publish:watch": "gh run watch",
4042
"prepublishOnly": "npm test"
4143
},
4244
"engines": {

public/app.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,9 @@ function injectEditorLayer() {
772772
const snapshot = refreshSelectionSnapshot(range);
773773
state.canvasEditor?.updateSelection(snapshot, range);
774774
},
775+
onInteractionStart() {
776+
stopInlineTextEdit({ commit: true });
777+
},
775778
onMoveStart(element) {
776779
state.suppressPreviewClick = true;
777780
applyDragMarker(element, "dragging");

public/canvasEditorMath.js

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ export function resizeFromHandle({
5757
};
5858
}
5959

60+
export function resizeMaxWidthForViewport({
61+
side,
62+
startWidth,
63+
rectLeft,
64+
rectRight,
65+
viewportWidth,
66+
margin = 4,
67+
}) {
68+
const edge = Math.max(0, finiteNumber(margin, 4));
69+
const availableWidth = side === "left"
70+
? finiteNumber(rectRight) - edge
71+
: finiteNumber(viewportWidth) - finiteNumber(rectLeft) - edge;
72+
73+
// When the authored box already exceeds the visible viewport, treating the
74+
// smaller available width as a maximum makes the first pointer move jump or
75+
// appear frozen. Leave that axis unconstrained so both shrinking and growing
76+
// continue from the actual rendered width.
77+
return availableWidth >= finiteNumber(startWidth)
78+
? availableWidth
79+
: Number.POSITIVE_INFINITY;
80+
}
81+
6082
export function moveFromPointer({
6183
startLeft,
6284
startTop,
@@ -76,18 +98,34 @@ export function moveFromPointer({
7698
.every((value) => Number.isFinite(Number(value)));
7799
if (canBound) {
78100
const edge = Math.max(0, finiteNumber(margin, 4));
79-
const visualLeft = clamp(
80-
finiteNumber(startRectLeft) + appliedX,
81-
edge,
82-
finiteNumber(viewportWidth) - finiteNumber(width) - edge,
83-
);
84-
const visualTop = clamp(
85-
finiteNumber(startRectTop) + appliedY,
86-
edge,
87-
finiteNumber(viewportHeight) - finiteNumber(height) - edge,
88-
);
89-
appliedX = visualLeft - finiteNumber(startRectLeft);
90-
appliedY = visualTop - finiteNumber(startRectTop);
101+
const viewport = {
102+
width: finiteNumber(viewportWidth),
103+
height: finiteNumber(viewportHeight),
104+
};
105+
const size = {
106+
width: finiteNumber(width),
107+
height: finiteNumber(height),
108+
};
109+
110+
// A box larger than the viewport cannot satisfy both edge constraints.
111+
// Keep that axis following the pointer instead of collapsing its range and
112+
// pinning the element to zero while the snap guides continue to move.
113+
if (size.width <= viewport.width - edge * 2) {
114+
const visualLeft = clamp(
115+
finiteNumber(startRectLeft) + appliedX,
116+
edge,
117+
viewport.width - size.width - edge,
118+
);
119+
appliedX = visualLeft - finiteNumber(startRectLeft);
120+
}
121+
if (size.height <= viewport.height - edge * 2) {
122+
const visualTop = clamp(
123+
finiteNumber(startRectTop) + appliedY,
124+
edge,
125+
viewport.height - size.height - edge,
126+
);
127+
appliedY = visualTop - finiteNumber(startRectTop);
128+
}
91129
}
92130
return {
93131
left: finiteNumber(startLeft) + appliedX,

public/canvasTextEditor.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
moveHandlePlacement,
1616
positionStart,
1717
resizeFromHandle,
18+
resizeMaxWidthForViewport,
1819
toggleDecoration,
1920
toggleFontStyle,
2021
toggleFontWeight,
@@ -436,6 +437,7 @@ export function createCanvasTextEditor({
436437
onDelete = () => {},
437438
onSelectionChange = () => {},
438439
onTextRangeChange = () => {},
440+
onInteractionStart = () => {},
439441
onMoveStart = () => {},
440442
onMoveEnd = () => {},
441443
}) {
@@ -662,6 +664,7 @@ export function createCanvasTextEditor({
662664
if (!selected) return;
663665
event.preventDefault();
664666
event.stopPropagation();
667+
onInteractionStart(selected);
665668
const handle = captureTarget?.setPointerCapture ? captureTarget : event.currentTarget;
666669
handle?.setPointerCapture?.(event.pointerId);
667670
const listenerTarget = doc;
@@ -779,8 +782,10 @@ export function createCanvasTextEditor({
779782
if (!selected) return;
780783
event.preventDefault();
781784
event.stopPropagation();
785+
onInteractionStart(selected);
782786
onBeforeChange();
783787
const handle = event.currentTarget;
788+
const listenerTarget = doc;
784789
const side = handle.dataset.handle;
785790
handle.setPointerCapture(event.pointerId);
786791
const startX = event.clientX;
@@ -796,6 +801,14 @@ export function createCanvasTextEditor({
796801
});
797802
const wasStatic = computed.position === "static";
798803
const wasInline = computed.display === "inline";
804+
const maxWidth = resizeMaxWidthForViewport({
805+
side,
806+
startWidth,
807+
rectLeft: rect.left,
808+
rectRight: rect.right,
809+
viewportWidth: win.innerWidth,
810+
margin: 4,
811+
});
799812
if (side === "left" && wasStatic) selected.style.position = "relative";
800813
if (wasInline) selected.style.display = "inline-block";
801814
selected.style.boxSizing = "border-box";
@@ -811,16 +824,16 @@ export function createCanvasTextEditor({
811824
startWidth,
812825
startLeft,
813826
deltaX: moveEvent.clientX - startX,
814-
maxWidth: side === "left" ? rect.right - 4 : win.innerWidth - rect.left - 4,
827+
maxWidth,
815828
});
816829
selected.style.width = `${latest.width}px`;
817830
if (side === "left") selected.style.left = `${latest.left}px`;
818831
refresh();
819832
};
820833
const finish = () => {
821-
handle.removeEventListener("pointermove", move);
822-
handle.removeEventListener("pointerup", finish);
823-
handle.removeEventListener("pointercancel", finish);
834+
listenerTarget.removeEventListener("pointermove", move);
835+
listenerTarget.removeEventListener("pointerup", finish);
836+
listenerTarget.removeEventListener("pointercancel", finish);
824837
const changes = [
825838
{ property: "box-sizing", value: "border-box" },
826839
{ property: "max-width", value: "none" },
@@ -836,9 +849,9 @@ export function createCanvasTextEditor({
836849
onSelectionChange(selected);
837850
sync();
838851
};
839-
handle.addEventListener("pointermove", move);
840-
handle.addEventListener("pointerup", finish);
841-
handle.addEventListener("pointercancel", finish);
852+
listenerTarget.addEventListener("pointermove", move);
853+
listenerTarget.addEventListener("pointerup", finish);
854+
listenerTarget.addEventListener("pointercancel", finish);
842855
}
843856

844857
root.querySelector('[data-handle="move"]').addEventListener("pointerdown", beginMove);

test/canvasEditorMath.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,45 @@ test("resize respects the available viewport width", () => {
4444
);
4545
});
4646

47+
test("resize viewport limit keeps normal elements inside the visible area", () => {
48+
assert.equal(
49+
math.resizeMaxWidthForViewport({
50+
side: "right",
51+
startWidth: 200,
52+
rectLeft: 50,
53+
rectRight: 250,
54+
viewportWidth: 320,
55+
margin: 4,
56+
}),
57+
266,
58+
);
59+
assert.equal(
60+
math.resizeMaxWidthForViewport({
61+
side: "left",
62+
startWidth: 200,
63+
rectLeft: 50,
64+
rectRight: 250,
65+
viewportWidth: 320,
66+
margin: 4,
67+
}),
68+
246,
69+
);
70+
});
71+
72+
test("resize viewport limit does not lock an already overflowing element", () => {
73+
assert.equal(
74+
math.resizeMaxWidthForViewport({
75+
side: "right",
76+
startWidth: 300,
77+
rectLeft: 50,
78+
rectRight: 350,
79+
viewportWidth: 320,
80+
margin: 4,
81+
}),
82+
Number.POSITIVE_INFINITY,
83+
);
84+
});
85+
4786
test("move applies pointer deltas to the starting position", () => {
4887
assert.equal(typeof math.moveFromPointer, "function");
4988
assert.deepEqual(
@@ -71,6 +110,25 @@ test("move keeps the visual element inside viewport margins", () => {
71110
);
72111
});
73112

113+
test("move keeps oversized elements following the pointer on unconstrainable axes", () => {
114+
assert.deepEqual(
115+
math.moveFromPointer({
116+
startLeft: 98,
117+
startTop: -32,
118+
deltaX: 50,
119+
deltaY: 25,
120+
startRectLeft: 98,
121+
startRectTop: 228,
122+
width: 1050,
123+
height: 700,
124+
viewportWidth: 1012,
125+
viewportHeight: 600,
126+
margin: 0,
127+
}),
128+
{ left: 148, top: -7 },
129+
);
130+
});
131+
74132
test("move handle flips inside when the selected box touches the viewport top", () => {
75133
assert.equal(typeof math.moveHandlePlacement, "function");
76134
assert.equal(math.moveHandlePlacement({ top: 0 }), "inside");

0 commit comments

Comments
 (0)