fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
287 lines (286 loc) • 11.7 kB
JavaScript
import { _defineProperty } from "../../../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs";
import { CHANGED, NONE } from "../../constants.mjs";
import { Point } from "../../Point.mjs";
import { getDocumentFromElement } from "../../util/dom_misc.mjs";
import { cloneStyles } from "../../util/internals/cloneStyles.mjs";
import { setStyle } from "../../util/internals/dom_style.mjs";
//#region src/shapes/IText/DraggableTextDelegate.ts
/**
* #### Dragging IText/Textbox Lifecycle
* - {@link start} is called from `mousedown` {@link IText#_mouseDownHandler} and determines if dragging should start by testing {@link isPointerOverSelection}
* - if true `mousedown` {@link IText#_mouseDownHandler} is blocked to keep selection
* - if the pointer moves, canvas fires numerous mousemove {@link Canvas#_onMouseMove} that we make sure **aren't** prevented ({@link IText#shouldStartDragging}) in order for the window to start a drag session
* - once/if the session starts canvas calls {@link onDragStart} on the active object to determine if dragging should occur
* - canvas fires relevant drag events that are handled by the handlers defined in this scope
* - {@link end} is called from `mouseup` {@link IText#mouseUpHandler}, blocking IText default click behavior
* - in case the drag session didn't occur, {@link end} handles a click, since logic to do so was blocked during `mousedown`
*/
var DraggableTextDelegate = class {
constructor(target) {
_defineProperty(this, "target", void 0);
_defineProperty(this, "__mouseDownInPlace", false);
_defineProperty(this, "__dragStartFired", false);
_defineProperty(this, "__isDraggingOver", false);
_defineProperty(this, "__dragStartSelection", void 0);
_defineProperty(this, "__dragImageDisposer", void 0);
_defineProperty(this, "_dispose", void 0);
this.target = target;
const disposers = [
this.target.on("dragenter", this.dragEnterHandler.bind(this)),
this.target.on("dragover", this.dragOverHandler.bind(this)),
this.target.on("dragleave", this.dragLeaveHandler.bind(this)),
this.target.on("dragend", this.dragEndHandler.bind(this)),
this.target.on("drop", this.dropHandler.bind(this))
];
this._dispose = () => {
disposers.forEach((d) => d());
this._dispose = void 0;
};
}
isPointerOverSelection(e) {
const target = this.target;
const newSelection = target.getSelectionStartFromPointer(e);
return target.isEditing && newSelection >= target.selectionStart && newSelection <= target.selectionEnd && target.selectionStart < target.selectionEnd;
}
/**
* @public override this method to disable dragging and default to mousedown logic
*/
start(e) {
return this.__mouseDownInPlace = this.isPointerOverSelection(e);
}
/**
* @public override this method to disable dragging without discarding selection
*/
isActive() {
return this.__mouseDownInPlace;
}
/**
* Ends interaction and sets cursor in case of a click
* @returns true if was active
*/
end(e) {
const active = this.isActive();
if (active && !this.__dragStartFired) {
this.target.setCursorByClick(e);
this.target.initDelayedCursor(true);
}
this.__mouseDownInPlace = false;
this.__dragStartFired = false;
this.__isDraggingOver = false;
return active;
}
getDragStartSelection() {
return this.__dragStartSelection;
}
/**
* Override to customize the drag image
* https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage
*/
setDragImage(e, { selectionStart, selectionEnd }) {
var _e$dataTransfer;
const target = this.target;
const canvas = target.canvas;
const flipFactor = new Point(target.flipX ? -1 : 1, target.flipY ? -1 : 1);
const boundaries = target._getCursorBoundaries(selectionStart);
const pos = new Point(boundaries.left + boundaries.leftOffset, boundaries.top + boundaries.topOffset).multiply(flipFactor).transform(target.calcTransformMatrix());
const diff = canvas.getScenePoint(e).subtract(pos);
const retinaScaling = target.getCanvasRetinaScaling();
const bbox = target.getBoundingRect();
const correction = pos.subtract(new Point(bbox.left, bbox.top));
const vpt = canvas.viewportTransform;
const offset = correction.add(diff).transform(vpt, true);
const bgc = target.backgroundColor;
const styles = cloneStyles(target.styles);
target.backgroundColor = "";
const styleOverride = {
stroke: "transparent",
fill: "transparent",
textBackgroundColor: "transparent"
};
target.setSelectionStyles(styleOverride, 0, selectionStart);
target.setSelectionStyles(styleOverride, selectionEnd, target.text.length);
target.dirty = true;
const dragImage = target.toCanvasElement({
enableRetinaScaling: canvas.enableRetinaScaling,
viewportTransform: true
});
target.backgroundColor = bgc;
target.styles = styles;
target.dirty = true;
setStyle(dragImage, {
position: "fixed",
left: `${-dragImage.width}px`,
border: NONE,
width: `${dragImage.width / retinaScaling}px`,
height: `${dragImage.height / retinaScaling}px`
});
this.__dragImageDisposer && this.__dragImageDisposer();
this.__dragImageDisposer = () => {
dragImage.remove();
};
getDocumentFromElement(e.target || this.target.hiddenTextarea).body.appendChild(dragImage);
(_e$dataTransfer = e.dataTransfer) === null || _e$dataTransfer === void 0 || _e$dataTransfer.setDragImage(dragImage, offset.x, offset.y);
}
/**
* @returns {boolean} determines whether {@link target} should/shouldn't become a drag source
*/
onDragStart(e) {
this.__dragStartFired = true;
const target = this.target;
const active = this.isActive();
if (active && e.dataTransfer) {
const selection = this.__dragStartSelection = {
selectionStart: target.selectionStart,
selectionEnd: target.selectionEnd
};
const value = target._text.slice(selection.selectionStart, selection.selectionEnd).join("");
const data = {
text: target.text,
value,
...selection
};
e.dataTransfer.setData("text/plain", value);
e.dataTransfer.setData("application/fabric", JSON.stringify({
value,
styles: target.getSelectionStyles(selection.selectionStart, selection.selectionEnd, true)
}));
e.dataTransfer.effectAllowed = "copyMove";
this.setDragImage(e, data);
}
target.abortCursorAnimation();
return active;
}
/**
* use {@link targetCanDrop} to respect overriding
* @returns {boolean} determines whether {@link target} should/shouldn't become a drop target
*/
canDrop(e) {
if (this.target.editable && !this.target.getActiveControl() && !e.defaultPrevented) {
if (this.isActive() && this.__dragStartSelection) {
const index = this.target.getSelectionStartFromPointer(e);
const dragStartSelection = this.__dragStartSelection;
return index < dragStartSelection.selectionStart || index > dragStartSelection.selectionEnd;
}
return true;
}
return false;
}
/**
* in order to respect overriding {@link IText#canDrop} we call that instead of calling {@link canDrop} directly
*/
targetCanDrop(e) {
return this.target.canDrop(e);
}
dragEnterHandler({ e }) {
const canDrop = this.targetCanDrop(e);
if (!this.__isDraggingOver && canDrop) this.__isDraggingOver = true;
}
dragOverHandler(ev) {
const { e } = ev;
const canDrop = this.targetCanDrop(e);
if (!this.__isDraggingOver && canDrop) this.__isDraggingOver = true;
else if (this.__isDraggingOver && !canDrop) this.__isDraggingOver = false;
if (this.__isDraggingOver) {
e.preventDefault();
ev.canDrop = true;
ev.dropTarget = this.target;
}
}
dragLeaveHandler() {
if (this.__isDraggingOver || this.isActive()) this.__isDraggingOver = false;
}
/**
* Override the `text/plain | application/fabric` types of {@link DragEvent#dataTransfer}
* in order to change the drop value or to customize styling respectively, by listening to the `drop:before` event
* https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#performing_a_drop
*/
dropHandler(ev) {
var _e$dataTransfer2;
const { e } = ev;
const didDrop = e.defaultPrevented;
this.__isDraggingOver = false;
e.preventDefault();
let insert = (_e$dataTransfer2 = e.dataTransfer) === null || _e$dataTransfer2 === void 0 ? void 0 : _e$dataTransfer2.getData("text/plain");
if (insert && !didDrop) {
const target = this.target;
const canvas = target.canvas;
let insertAt = target.getSelectionStartFromPointer(e);
const { styles } = e.dataTransfer.types.includes("application/fabric") ? JSON.parse(e.dataTransfer.getData("application/fabric")) : {};
const trailing = insert[Math.max(0, insert.length - 1)];
const selectionStartOffset = 0;
if (this.__dragStartSelection) {
const selectionStart = this.__dragStartSelection.selectionStart;
const selectionEnd = this.__dragStartSelection.selectionEnd;
if (insertAt > selectionStart && insertAt <= selectionEnd) insertAt = selectionStart;
else if (insertAt > selectionEnd) insertAt -= selectionEnd - selectionStart;
target.removeChars(selectionStart, selectionEnd);
delete this.__dragStartSelection;
}
if (target._reNewline.test(trailing) && (target._reNewline.test(target._text[insertAt]) || insertAt === target._text.length)) insert = insert.trimEnd();
ev.didDrop = true;
ev.dropTarget = target;
target.insertChars(insert, styles, insertAt);
canvas.setActiveObject(target);
target.enterEditing(e);
target.selectionStart = Math.min(insertAt + selectionStartOffset, target._text.length);
target.selectionEnd = Math.min(target.selectionStart + insert.length, target._text.length);
target.hiddenTextarea.value = target.text;
target._updateTextarea();
target.hiddenTextarea.focus();
target.fire(CHANGED, {
index: insertAt + selectionStartOffset,
action: "drop"
});
canvas.fire("text:changed", { target });
canvas.contextTopDirty = true;
canvas.requestRenderAll();
}
}
/**
* fired only on the drag source after drop (if occurred)
* handle changes to the drag source in case of a drop on another object or a cancellation
* https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#finishing_a_drag
*/
dragEndHandler({ e }) {
if (this.isActive() && this.__dragStartFired) {
if (this.__dragStartSelection) {
var _e$dataTransfer3;
const target = this.target;
const canvas = this.target.canvas;
const { selectionStart, selectionEnd } = this.__dragStartSelection;
const dropEffect = ((_e$dataTransfer3 = e.dataTransfer) === null || _e$dataTransfer3 === void 0 ? void 0 : _e$dataTransfer3.dropEffect) || "none";
if (dropEffect === "none") {
target.selectionStart = selectionStart;
target.selectionEnd = selectionEnd;
target._updateTextarea();
target.hiddenTextarea.focus();
} else {
target.clearContextTop();
if (dropEffect === "move") {
target.removeChars(selectionStart, selectionEnd);
target.selectionStart = target.selectionEnd = selectionStart;
target.hiddenTextarea && (target.hiddenTextarea.value = target.text);
target._updateTextarea();
target.fire(CHANGED, {
index: selectionStart,
action: "dragend"
});
canvas.fire("text:changed", { target });
canvas.requestRenderAll();
}
target.exitEditing();
}
}
}
this.__dragImageDisposer && this.__dragImageDisposer();
delete this.__dragImageDisposer;
delete this.__dragStartSelection;
this.__isDraggingOver = false;
}
dispose() {
this._dispose && this._dispose();
}
};
//#endregion
export { DraggableTextDelegate };
//# sourceMappingURL=DraggableTextDelegate.mjs.map