import { prefix, triggerEvent, fillParams, fillEndParams, calculatePosition, fillCSSObject, catchEvent, getComputedStyle, } from "../utils"; import { Renderer, RoundableProps, OnRoundStart, RoundableState, OnRound, ControlPose, OnRoundEnd, MoveableManagerInterface, OnRoundGroup, MoveableGroupInterface, OnRoundGroupStart, OnRoundGroupEnd, } from "../types"; import { splitSpace } from "@daybrush/utils"; import { setDragStart, getDragDist, calculatePointerDist } from "../gesto/GestoUtils"; import { minus, plus } from "@scena/matrix"; import { getRadiusValues, getRadiusStyles, splitRadiusPoses, } from "./roundable/borderRadius"; import { fillChildEvents } from "../groupUtils"; function addBorderRadiusByLine( controlPoses: ControlPose[], lineIndex: number, distX: number, distY: number, ) { // lineIndex // 0 top // 1 right // 2 bottom // 3 left const horizontalsLength = controlPoses.filter(({ virtual, horizontal }) => horizontal && !virtual).length; const verticalsLength = controlPoses.filter(({ virtual, vertical }) => vertical && !virtual).length; let controlIndex = -1; //top if (lineIndex === 0) { if (horizontalsLength === 0) { controlIndex = 0; } else if (horizontalsLength === 1) { controlIndex = 1; } } // bottom if (lineIndex === 2) { if (horizontalsLength <= 2) { controlIndex = 2; } else if (horizontalsLength <= 3) { controlIndex = 3; } } // left if (lineIndex === 3) { if (verticalsLength === 0) { controlIndex = 4; } else if (verticalsLength < 4) { controlIndex = 7; } } // right if (lineIndex === 1) { if (verticalsLength <= 1) { controlIndex = 5; } else if (verticalsLength <= 2) { controlIndex = 6; } } if (controlIndex === -1 || !controlPoses[controlIndex].virtual) { return; } const controlPoseInfo = controlPoses[controlIndex]; addBorderRadius(controlPoses, controlIndex); if (controlIndex < 4) { controlPoseInfo.pos[0] = distX; } else { controlPoseInfo.pos[1] = distY; } } function addBorderRadius( controlPoses: ControlPose[], index: number, ) { if (index < 4) { controlPoses.slice(0, index + 1).forEach(info => { info.virtual = false; }); } else { if (controlPoses[0].virtual) { controlPoses[0].virtual = false; } controlPoses.slice(4, index + 1).forEach(info => { info.virtual = false; }); } } function removeBorderRadius( controlPoses: ControlPose[], index: number, ) { if (index < 4) { controlPoses.slice(index, 4).forEach(info => { info.virtual = true; }); } else { controlPoses.slice(index).forEach(info => { info.virtual = true; }); } } function getBorderRadius( borderRadius: string, width: number, height: number, minCounts: number[] = [0, 0], full?: boolean, ) { let values: string[] = []; if (!borderRadius || borderRadius === "0px") { values = []; } else { values = splitSpace(borderRadius); } return getRadiusValues(values, width, height, 0, 0, minCounts, full); } function triggerRoundEvent( moveable: MoveableManagerInterface, e: any, dist: number[], delta: number[], nextPoses: ControlPose[], ) { const state = moveable.state; const { width, height, } = state; const { raws, styles, radiusPoses, } = getRadiusStyles( nextPoses, moveable.props.roundRelative!, width, height, ); const { horizontals, verticals, } = splitRadiusPoses(radiusPoses, raws); const borderRadius = styles.join(" "); state.borderRadiusState = borderRadius; const params = fillParams(moveable, e, { horizontals, verticals, borderRadius, width, height, delta, dist, ...fillCSSObject({ borderRadius, }, e), }); triggerEvent(moveable, "onRound", params); return params; } function getStyleBorderRadius(moveable: MoveableManagerInterface) { const { style, } = moveable.getState(); let borderRadius = style.borderRadius || ""; if (!borderRadius && moveable.props.groupable) { const firstMoveable = moveable.moveables![0]; const firstTarget = moveable.getTargets()[0]; if (firstTarget) { if (firstMoveable?.props.target === firstTarget) { borderRadius = moveable.moveables![0]?.state.style.borderRadius ?? ""; style.borderRadius = borderRadius; } else { borderRadius = getComputedStyle(firstTarget).borderRadius; style.borderRadius = borderRadius; } } } return borderRadius; } /** * @namespace Moveable.Roundable * @description Whether to show and drag or double click border-radius */ export default { name: "roundable", props: [ "roundable", "roundRelative", "minRoundControls", "maxRoundControls", "roundClickable", "roundPadding", "isDisplayShadowRoundControls", ] as const, events: [ "roundStart", "round", "roundEnd", "roundGroupStart", "roundGroup", "roundGroupEnd", ] as const, css: [ `.control.border-radius { background: #d66; cursor: pointer; z-index: 3; }`, `.control.border-radius.vertical { background: #d6d; z-index: 2; }`, `.control.border-radius.virtual { opacity: 0.5; z-index: 1; }`, `:host.round-line-clickable .line.direction { cursor: pointer; }`, ], className(moveable: MoveableManagerInterface) { const roundClickable = moveable.props.roundClickable; return roundClickable === true || roundClickable === "line" ? prefix("round-line-clickable") : ""; }, requestStyle(): Array { return ["borderRadius"]; }, requestChildStyle(): Array { return ["borderRadius"]; }, render(moveable: MoveableManagerInterface, React: Renderer): any { const { target, width, height, allMatrix, is3d, left, top, borderRadiusState, } = moveable.getState(); const { minRoundControls = [0, 0], maxRoundControls = [4, 4], zoom, roundPadding = 0, isDisplayShadowRoundControls, groupable, } = moveable.props; if (!target) { return null; } const borderRadius = borderRadiusState || getStyleBorderRadius(moveable); const n = is3d ? 4 : 3; const radiusValues = getBorderRadius( borderRadius, width, height, minRoundControls, true, ); if (!radiusValues) { return null; } let verticalCount = 0; let horizontalCount = 0; const basePos = groupable ? [0, 0] : [left, top]; return radiusValues.map((v, i) => { const horizontal = v.horizontal; const vertical = v.vertical; const direction = v.direction || ""; const originalPos = [...v.pos]; horizontalCount += Math.abs(horizontal); verticalCount += Math.abs(vertical); if (horizontal && direction.indexOf("n") > -1) { originalPos[1] -= roundPadding; } if (vertical && direction.indexOf("w") > -1) { originalPos[0] -= roundPadding; } if (horizontal && direction.indexOf("s") > -1) { originalPos[1] += roundPadding; } if (vertical && direction.indexOf("e") > -1) { originalPos[0] += roundPadding; } const pos = minus(calculatePosition(allMatrix, originalPos, n), basePos); const isDisplayVerticalShadow = isDisplayShadowRoundControls && isDisplayShadowRoundControls !== "horizontal"; const isDisplay = v.vertical ? verticalCount <= maxRoundControls[1] && (isDisplayVerticalShadow || !v.virtual) : horizontalCount <= maxRoundControls[0] && (isDisplayShadowRoundControls || !v.virtual); return
; }); }, dragControlCondition(moveable: any, e: any) { if (!e.inputEvent || e.isRequest) { return false; } const className = (e.inputEvent.target.getAttribute("class") || ""); return className.indexOf("border-radius") > -1 || (className.indexOf("moveable-line") > -1 && className.indexOf("moveable-direction") > -1); }, dragGroupControlCondition(moveable: any, e: any) { return this.dragControlCondition(moveable, e); }, dragControlStart(moveable: MoveableManagerInterface, e: any) { const { inputEvent, datas } = e; const inputTarget = inputEvent.target; const className = (inputTarget.getAttribute("class") || ""); const isControl = className.indexOf("border-radius") > -1; const isLine = className.indexOf("moveable-line") > -1 && className.indexOf("moveable-direction") > -1; const controlIndex = isControl ? parseInt(inputTarget.getAttribute("data-radius-index"), 10) : -1; let lineIndex = -1; if (isLine) { const indexAttr = inputTarget.getAttribute("data-line-key")! || ""; if (indexAttr) { lineIndex = parseInt(indexAttr.replace(/render-line-/g, ""), 10); if (isNaN(lineIndex)) { lineIndex = -1; } } } if (!isControl && !isLine) { return false; } const params = fillParams(moveable, e, {}); const result = triggerEvent( moveable, "onRoundStart", params); if (result === false) { return false; } datas.lineIndex = lineIndex; datas.controlIndex = controlIndex; datas.isControl = isControl; datas.isLine = isLine; setDragStart(moveable, e); const { roundRelative, minRoundControls = [0, 0], } = moveable.props; const state = moveable.state; const { width, height, } = state; datas.isRound = true; datas.prevDist = [0, 0]; const borderRadius = getStyleBorderRadius(moveable); const controlPoses = getBorderRadius( borderRadius || "", width, height, minRoundControls, true, ) || []; datas.controlPoses = controlPoses; state.borderRadiusState = getRadiusStyles( controlPoses, roundRelative!, width, height, ).styles.join(" "); return params; }, dragControl(moveable: MoveableManagerInterface, e: any) { const { datas } = e; const controlPoses = datas.controlPoses as ControlPose[]; if (!datas.isRound || !datas.isControl || !controlPoses.length) { return false; } const index = datas.controlIndex as number; const [distX, distY] = getDragDist(e); const dist = [distX, distY]; const delta = minus(dist, datas.prevDist); const { maxRoundControls = [4, 4], } = moveable.props; const { width, height } = moveable.state; const selectedControlPose = controlPoses[index]; const selectedVertical = selectedControlPose.vertical; const selectedHorizontal = selectedControlPose.horizontal; // 0: [0, 1, 2, 3] maxCount === 1 // 0: [0, 2] maxCount === 2 // 1: [1, 3] maxCount === 2 // 0: [0] maxCount === 3 // 1: [1, 3] maxCount === 3 const dists = controlPoses.map(pose => { const { horizontal, vertical } = pose; const poseDist = [ horizontal * selectedHorizontal * dist[0], vertical * selectedVertical * dist[1], ]; if (horizontal) { if (maxRoundControls[0] === 1) { return poseDist; } else if (maxRoundControls[0] < 4 && horizontal !== selectedHorizontal) { return poseDist; } } else if (maxRoundControls[1] === 0) { poseDist[1] = vertical * selectedHorizontal * dist[0] / width * height; return poseDist; } else if (selectedVertical) { if (maxRoundControls[1] === 1) { return poseDist; } else if (maxRoundControls[1] < 4 && vertical !== selectedVertical) { return poseDist; } } return [0, 0]; }); dists[index] = dist; const nextPoses = controlPoses.map((info, i) => { return { ...info, pos: plus(info.pos, dists[i]), }; }); if (index < 4) { nextPoses.slice(0, index + 1).forEach(info => { info.virtual = false; }); } else { nextPoses.slice(4, index + 1).forEach(info => { info.virtual = false; }); } datas.prevDist = [distX, distY]; return triggerRoundEvent( moveable, e, dist, delta, nextPoses, ); }, dragControlEnd(moveable: MoveableManagerInterface, e: any) { const state = moveable.state; state.borderRadiusState = ""; const { datas, isDouble } = e; if (!datas.isRound) { return false; } const { isControl, controlIndex, isLine, lineIndex, } = datas; const controlPoses = datas.controlPoses as ControlPose[]; const length = controlPoses.filter(({ virtual }) => virtual).length; const { roundClickable = true, } = moveable.props; if (isDouble && roundClickable) { if (isControl && (roundClickable === true || roundClickable === "control")) { removeBorderRadius(controlPoses, controlIndex); } else if (isLine && (roundClickable === true || roundClickable === "line")) { const [distX, distY] = calculatePointerDist(moveable, e); addBorderRadiusByLine(controlPoses, lineIndex, distX, distY); } if (length !== controlPoses.filter(({ virtual }) => virtual).length) { triggerRoundEvent( moveable, e, [0, 0], [0, 0], controlPoses, ); } } const params = fillEndParams(moveable, e, {}); triggerEvent(moveable, "onRoundEnd", params); state.borderRadiusState = ""; return params; }, dragGroupControlStart(moveable: MoveableGroupInterface, e: any) { const result = this.dragControlStart(moveable, e); if (!result) { return false; } const moveables = moveable.moveables; const targets = moveable.props.targets!; const events = fillChildEvents(moveable, "roundable", e); const nextParams: OnRoundGroupStart = { targets: moveable.props.targets!, events: events.map((ev, i) => { return { ...ev, target: targets[i], moveable: moveables[i], currentTarget: moveables[i], }; }), ...result, }; triggerEvent(moveable, "onRoundGroupStart", nextParams); return result; }, dragGroupControl(moveable: MoveableGroupInterface, e: any) { const result = this.dragControl(moveable, e); if (!result) { return false; } const moveables = moveable.moveables; const targets = moveable.props.targets!; const events = fillChildEvents(moveable, "roundable", e); const nextParams: OnRoundGroup = { targets: moveable.props.targets!, events: events.map((ev, i) => { return { ...ev, target: targets[i], moveable: moveables[i], currentTarget: moveables[i], ...fillCSSObject({ borderRadius: result.borderRadius, }, ev), }; }), ...result, }; triggerEvent(moveable, "onRoundGroup", nextParams); return nextParams; }, dragGroupControlEnd(moveable: MoveableGroupInterface, e: any) { const moveables = moveable.moveables; const targets = moveable.props.targets!; const events = fillChildEvents(moveable, "roundable", e); catchEvent(moveable, "onRound", parentEvent => { const nextParams: OnRoundGroup = { targets: moveable.props.targets!, events: events.map((ev, i) => { return { ...ev, target: targets[i], moveable: moveables[i], currentTarget: moveables[i], ...fillCSSObject({ borderRadius: parentEvent.borderRadius, }, ev), }; }), ...parentEvent, }; triggerEvent(moveable, "onRoundGroup", nextParams); }); const result = this.dragControlEnd(moveable, e); if (!result) { return false; } const nextParams: OnRoundGroupEnd = { targets: moveable.props.targets!, events: events.map((ev, i) => { return { ...ev, target: targets[i], moveable: moveables[i], currentTarget: moveables[i], lastEvent: ev.datas?.lastEvent, }; }), ...result, }; triggerEvent(moveable, "onRoundGroupEnd", nextParams); return nextParams; }, unset(moveable: MoveableManagerInterface) { moveable.state.borderRadiusState = ""; }, }; /** * Whether to show and drag or double click border-radius, (default: false) * @name Moveable.Roundable#roundable * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundRelative: false, * }); * moveable.on("roundStart", e => { * console.log(e); * }).on("round", e => { * e.target.style.borderRadius = e.borderRadius; * }).on("roundEnd", e => { * console.log(e); * }); */ /** * % Can be used instead of the absolute px * @name Moveable.Roundable#roundRelative * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundRelative: false, * }); * moveable.on("roundStart", e => { * console.log(e); * }).on("round", e => { * e.target.style.borderRadius = e.borderRadius; * }).on("roundEnd", e => { * console.log(e); * }); */ /** * Minimum number of round controls. It moves in proportion by control. [horizontal, vertical] (default: [0, 0]) * @name Moveable.Roundable#minRoundControls * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundRelative: false, * minRoundControls: [0, 0], * }); * moveable.minRoundControls = [1, 0]; */ /** * Maximum number of round controls. It moves in proportion by control. [horizontal, vertical] (default: [4, 4]) * @name Moveable.Roundable#maxRoundControls * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundRelative: false, * maxRoundControls: [4, 4], * }); * moveable.maxRoundControls = [1, 0]; */ /** * Whether you can add/delete round controls by double-clicking a line or control. * @name Moveable.Roundable#roundClickable * @default true * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundRelative: false, * roundClickable: true, * }); * moveable.roundClickable = false; */ /** * Whether to show a round control that does not actually exist as a shadow * @name Moveable.Roundable#isDisplayShadowRoundControls * @default false * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * isDisplayShadowRoundControls: false, * }); * moveable.isDisplayShadowRoundControls = true; */ /** * The padding value of the position of the round control * @name Moveable.Roundable#roundPadding * @default false * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundPadding: 0, * }); * moveable.roundPadding = 15; */ /** * When drag start the clip area or controls, the `roundStart` event is called. * @memberof Moveable.Roundable * @event roundStart * @param {Moveable.Roundable.OnRoundStart} - Parameters for the `roundStart` event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundRelative: false, * }); * moveable.on("roundStart", e => { * console.log(e); * }).on("round", e => { * e.target.style.borderRadius = e.borderRadius; * }).on("roundEnd", e => { * console.log(e); * }); */ /** * When drag or double click the border area or controls, the `round` event is called. * @memberof Moveable.Roundable * @event round * @param {Moveable.Roundable.OnRound} - Parameters for the `round` event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundRelative: false, * }); * moveable.on("roundStart", e => { * console.log(e); * }).on("round", e => { * e.target.style.borderRadius = e.borderRadius; * }).on("roundEnd", e => { * console.log(e); * }); */ /** * When drag end the border area or controls, the `roundEnd` event is called. * @memberof Moveable.Roundable * @event roundEnd * @param {Moveable.Roundable.onRoundEnd} - Parameters for the `roundEnd` event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * roundable: true, * roundRelative: false, * }); * moveable.on("roundStart", e => { * console.log(e); * }).on("round", e => { * e.target.style.borderRadius = e.borderRadius; * }).on("roundEnd", e => { * console.log(e); * }); */ /** * When drag start the clip area or controls, the `roundGroupStart` event is called. * @memberof Moveable.Roundable * @event roundGroupStart * @param {Moveable.Roundable.OnRoundGroupStart} - Parameters for the `roundGroupStart` event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * targets: [target1, target2, target3], * roundable: true, * }); * moveable.on("roundGroupStart", e => { * console.log(e.targets); * }).on("roundGroup", e => { * e.events.forEach(ev => { * ev.target.style.cssText += ev.cssText; * }); * }).on("roundGroupEnd", e => { * console.log(e); * }); */ /** * When drag or double click the border area or controls, the `roundGroup` event is called. * @memberof Moveable.Roundable * @event roundGroup * @param {Moveable.Roundable.OnRoundGroup} - Parameters for the `roundGroup` event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * targets: [target1, target2, target3], * roundable: true, * }); * moveable.on("roundGroupStart", e => { * console.log(e.targets); * }).on("roundGroup", e => { * e.events.forEach(ev => { * ev.target.style.cssText += ev.cssText; * }); * }).on("roundGroupEnd", e => { * console.log(e); * }); */ /** * When drag end the border area or controls, the `roundGroupEnd` event is called. * @memberof Moveable.Roundable * @event roundGroupEnd * @param {Moveable.Roundable.onRoundGroupEnd} - Parameters for the `roundGroupEnd` event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * targets: [target1, target2, target3], * roundable: true, * }); * moveable.on("roundGroupStart", e => { * console.log(e.targets); * }).on("roundGroup", e => { * e.events.forEach(ev => { * ev.target.style.cssText += ev.cssText; * }); * }).on("roundGroupEnd", e => { * console.log(e); * }); */