diff --git a/package.json b/package.json index c44ca78b88..ef6463cabd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@print-one/grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.21.16", + "version": "0.21.17", "author": "Print.one", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", diff --git a/src/canvas/index.ts b/src/canvas/index.ts index fcab42499a..0cb9d33436 100644 --- a/src/canvas/index.ts +++ b/src/canvas/index.ts @@ -43,6 +43,7 @@ import Frame from './model/Frame'; import { CanvasEvents, ToWorldOption } from './types'; import CanvasView, { FitViewportOptions } from './view/CanvasView'; import FrameView from './view/FrameView'; +import { rotateCoordinate } from '../utils/Rotator'; export type CanvasEvent = `${CanvasEvents}`; @@ -488,9 +489,26 @@ export default class CanvasModule extends Module { const zoom = this.getZoomDecimal(); const zoomOffset = 1 / zoom; + let y = (e.clientY + addTop - yOffset) * zoomOffset; + let x = (e.clientX + addLeft - xOffset) * zoomOffset; + + const rotated = rotateCoordinate( + { + l: x, + t: y, + }, + { + l: 0, + t: 0, + w: frame?.model?.width ?? 0, + h: frame?.model?.height ?? 0, + r: -this.getRotationAngle(), + } + ); + return { - y: (e.clientY + addTop - yOffset) * zoomOffset, - x: (e.clientX + addLeft - xOffset) * zoomOffset, + y: rotated.t, + x: rotated.l, }; } @@ -596,6 +614,14 @@ export default class CanvasModule extends Module { return parseFloat(this.canvas.get('zoom')); } + getRotationAngle() { + return this.canvas.get('rotationAngle'); + } + + setRotationAngle(value: number) { + this.canvas.set('rotationAngle', value); + } + /** * Set canvas position coordinates * @param {Number} x Horizontal position diff --git a/src/canvas/model/Canvas.ts b/src/canvas/model/Canvas.ts index 6993847b15..90f0396e56 100644 --- a/src/canvas/model/Canvas.ts +++ b/src/canvas/model/Canvas.ts @@ -13,6 +13,7 @@ export default class Canvas extends ModuleModel { frames: [], rulers: false, zoom: 100, + rotationAngle: 0, x: 0, y: 0, // Scripts to apply on all frames diff --git a/src/canvas/view/CanvasView.ts b/src/canvas/view/CanvasView.ts index 4db50d807c..e2d9a49ce6 100644 --- a/src/canvas/view/CanvasView.ts +++ b/src/canvas/view/CanvasView.ts @@ -19,6 +19,7 @@ import Frame from '../model/Frame'; import { GetBoxRectOptions, ToWorldOption } from '../types'; import FrameView from './FrameView'; import FramesView from './FramesView'; +import { rotateCoordinate } from '../../utils/Rotator'; export interface MarginPaddingOffsets { marginTop?: number; @@ -36,6 +37,8 @@ export type ElementPosOpts = { avoidFrameZoom?: boolean; noScroll?: boolean; nativeBoundingRect?: boolean; + avoidRotate?: boolean; + overrideRect?: ElementRect; }; export interface FitViewportOptions { @@ -252,8 +255,9 @@ export default class CanvasView extends ModuleView { if (framesArea) { const { x, y } = model.attributes; const zoomDc = module.getZoomDecimal(); + const rotation = module.getRotationAngle(); - framesArea.style.transform = `scale(${zoomDc}) translate(${x * mpl}px, ${y * mpl}px)`; + framesArea.style.transform = `scale(${zoomDc}) translate(${x * mpl}px, ${y * mpl}px) rotate(${rotation}deg)`; } if (cvStyle) { @@ -455,7 +459,26 @@ export default class CanvasView extends ModuleView { const frame = this.frame?.el; const winEl = el?.ownerDocument.defaultView; const frEl = winEl ? (winEl.frameElement as HTMLElement) : frame; - this.frmOff = this.offset(frEl || frame, { nativeBoundingRect: true }); + const zoom = this.module.getZoomDecimal(); + //Native offset has been transformed by the zoom and rotation + const nativeOffset = this.offset(frEl || frame, { nativeBoundingRect: true }); + //Original offset of the element has not been transformed + const originalOffset = this.offset(frEl || frame, { nativeBoundingRect: false }); + + //We want to get the offset without the rotation + const middle = { + x: nativeOffset.left + nativeOffset.width / 2, + y: nativeOffset.top + nativeOffset.height / 2, + }; + + let width = originalOffset.width * zoom; + let height = originalOffset.height * zoom; + this.frmOff = { + width: width, + height: height, + top: middle.y - height / 2, + left: middle.x - width / 2, + }; } return this.frmOff; } @@ -482,12 +505,29 @@ export default class CanvasView extends ModuleView { const frameOffset = this.getFrameOffset(el); const canvasEl = this.el; const canvasOffset = this.getCanvasOffset(); - const elRect = this.offset(el, opts); + const elRect = opts.overrideRect ?? this.offset(el, opts); const frameTop = opts.avoidFrameOffset ? 0 : frameOffset.top; const frameLeft = opts.avoidFrameOffset ? 0 : frameOffset.left; - const elTop = opts.avoidFrameZoom ? elRect.top : elRect.top * zoom; - const elLeft = opts.avoidFrameZoom ? elRect.left : elRect.left * zoom; + const rotated = rotateCoordinate( + { + l: elRect.left + elRect.width / 2, + t: elRect.top + elRect.height / 2, + }, + { + l: 0, + t: 0, + w: this.frame?.model?.width ?? 0, + h: this.frame?.model?.height ?? 0, + r: opts.avoidRotate ? 0 : this.module.getRotationAngle(), + } + ); + + rotated.l -= elRect.width / 2; + rotated.t -= elRect.height / 2; + + const elTop = opts.avoidFrameZoom ? rotated.t : rotated.t * zoom; + const elLeft = opts.avoidFrameZoom ? rotated.l : rotated.l * zoom; const top = opts.avoidFrameOffset ? elTop : elTop + frameTop - canvasOffset.top + canvasEl.scrollTop; const left = opts.avoidFrameOffset ? elLeft : elLeft + frameLeft - canvasOffset.left + canvasEl.scrollLeft; diff --git a/src/commands/view/ComponentDrag.ts b/src/commands/view/ComponentDrag.ts index 1874db9769..c25603ea82 100644 --- a/src/commands/view/ComponentDrag.ts +++ b/src/commands/view/ComponentDrag.ts @@ -210,7 +210,12 @@ export default { }, getElementPos(el: HTMLElement) { - return this.editor.Canvas.getElementPos(el, { noScroll: 1 }); + return this.editor.Canvas.getElementPos(el, { + noScroll: 1, + avoidRotate: 1, + avoidFrameOffset: 1, + avoidFrameZoom: 1, + }); }, getElementGuides(el: HTMLElement) { @@ -428,14 +433,30 @@ export default { const statEdge2Raw = isY ? rect.left + rect.width : rect.top + rect.height; const posFirst = isY ? item.y : item.x; const posSecond = isEdge1 ? statEdge2 : origEdge2; - const pos2 = `${posFirst}px`; const size = isEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2; const sizeRaw = isEdge1 ? origEdge1Raw - statEdge2Raw : statEdge1Raw - origEdge2Raw; + + const position = this.editor.Canvas.getElementPos(origin, { + noScroll: 1, + overrideRect: { + top: isY ? posFirst : posSecond, + left: isY ? posSecond : posFirst, + width: isY ? size : 0, + height: isY ? 0 : size, + }, + }); + + const rotationAngle = this.canvas.getRotationAngle(); + const zoom = this.canvas.getZoomDecimal(); + guideInfoStyle.display = ''; - guideInfoStyle[isY ? 'top' : 'left'] = pos2; - guideInfoStyle[isY ? 'left' : 'top'] = `${posSecond}px`; - guideInfoStyle[isY ? 'width' : 'height'] = `${size}px`; + guideInfoStyle.top = `${position.top}px`; + guideInfoStyle.left = `${position.left}px`; + guideInfoStyle.rotate = `${rotationAngle}deg`; + guideInfoStyle[isY ? 'width' : 'height'] = `${size * zoom}px`; elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`; + elGuideInfoCnt.style.rotate = `${-rotationAngle}deg`; + elGuideInfoCnt.style.transformOrigin = isY ? '50% 100%' : '100% 50%'; this.em.trigger(`${evName}:active`, { ...this.getEventOpts(), guide: item, diff --git a/src/commands/view/Resize.ts b/src/commands/view/Resize.ts index 07f429d2b2..313d0af1cb 100644 --- a/src/commands/view/Resize.ts +++ b/src/commands/view/Resize.ts @@ -11,6 +11,7 @@ export default { prefix: editor.getConfig().stylePrefix, posFetcher: canvasView.getElementPos.bind(canvasView), mousePosFetcher: canvas.getMouseRelativePos.bind(canvas), + rotationAngle: canvas.getRotationAngle(), ...(opt.options || {}), }; let { canvasResizer } = this; diff --git a/src/commands/view/Rotate.ts b/src/commands/view/Rotate.ts index 121397a8cf..ec16ab7816 100644 --- a/src/commands/view/Rotate.ts +++ b/src/commands/view/Rotate.ts @@ -11,6 +11,7 @@ export default { prefix: editor.getConfig().stylePrefix, posFetcher: canvasView.getElementPos.bind(canvasView), mousePosFetcher: (ev: MouseEvent) => canvas.getMouseRelativeCanvas(ev, {}), + rotationAngle: canvas.getRotationAngle(), ...(opt.options || {}), }; let { canvasRotator } = this; diff --git a/src/commands/view/SelectComponent.ts b/src/commands/view/SelectComponent.ts index f428367fa2..47e8b104f6 100644 --- a/src/commands/view/SelectComponent.ts +++ b/src/commands/view/SelectComponent.ts @@ -743,6 +743,7 @@ export default { style.width = pos.width + unit; style.height = pos.height + unit; style.rotate = window.getComputedStyle(el).getPropertyValue('rotate'); + style.transform = `rotate(${this.canvas.getRotationAngle()}deg)`; this._trgToolUp('local', { component, @@ -794,6 +795,7 @@ export default { style.width = pos.width + unit; style.height = pos.height + unit; style.rotate = window.getComputedStyle(el).getPropertyValue('rotate'); + style.transform = `rotate(${this.canvas.getRotationAngle()}deg)`; this.updateToolbarPos({ top: targetToElem.top, left: targetToElem.left }); this._trgToolUp('global', { diff --git a/src/utils/Resizer.ts b/src/utils/Resizer.ts index 6b18a45743..dd40fa202a 100644 --- a/src/utils/Resizer.ts +++ b/src/utils/Resizer.ts @@ -3,6 +3,7 @@ import { ElementPosOpts } from '../canvas/view/CanvasView'; import { Position } from '../common'; import { off, on } from './dom'; import { normalizeFloat } from './mixins'; +import { rotateCoordinate } from './Rotator'; type RectDim = { t: number; @@ -26,7 +27,7 @@ type CallbackOptions = { resizer: Resizer; }; -type Coordinate = Pick; +type Coordinate = Pick; export interface ResizerOptions { /** @@ -212,17 +213,33 @@ export interface ResizerOptions { * Where to append resize container (default body element). */ appendTo?: HTMLElement; + + rotationAngle?: number; } -type Handlers = Record; +const cursors = { + 0: 'nwse-resize', + 45: 'ns-resize', + 90: 'nesw-resize', + 135: 'ew-resize', + 180: 'nwse-resize', + 225: 'ns-resize', + 270: 'nesw-resize', + 315: 'ew-resize', +} as Record; + +const rotations = { + tl: 0, + tc: 45, + tr: 90, + cl: 315, + cr: 135, + bl: 270, + bc: 225, + br: 180, +} as const; -const createHandler = (name: string, opts: { prefix?: string } = {}) => { - var pfx = opts.prefix || ''; - var el = document.createElement('i'); - el.className = pfx + 'resizer-h ' + pfx + 'resizer-h-' + name; - el.setAttribute('data-' + pfx + 'handler', name); - return el; -}; +type Handlers = Record; const getBoundingRect = (el: HTMLElement, win?: Window): BoundingRect => { var w = win || window; @@ -260,6 +277,20 @@ export default class Resizer { onEnd?: ResizerOptions['onEnd']; onUpdateContainer?: ResizerOptions['onUpdateContainer']; + private createHandler(name: string, opts: { prefix?: string } = {}) { + var pfx = opts.prefix || ''; + var el = document.createElement('i'); + el.className = pfx + 'resizer-h ' + pfx + 'resizer-h-' + name; + el.setAttribute('data-' + pfx + 'handler', name); + + let rot = rotations[name as keyof typeof rotations]; + rot += Math.round(this.totalRotation / 45) * 45 + 3600; + rot %= 360; + el.style.cursor = cursors[rot]; + + return el; + } + /** * Init the Resizer with options * @param {Object} options @@ -316,6 +347,16 @@ export default class Resizer { this.setup(); } + get totalRotation() { + let r = 0; + for (let el = this.container; el; el = el?.parentElement ?? undefined) { + const _rotate = getComputedStyle(el).rotate; + const rotate = (Number((_rotate === 'none' ? '0deg' : _rotate).replace('deg', '')) + 360) % 360; + r += rotate; + } + return r + (this.opts.rotationAngle ?? 0); + } + /** * Setup resizer */ @@ -341,7 +382,7 @@ export default class Resizer { const handlers: Handlers = {}; ['tl', 'tc', 'tr', 'cl', 'cr', 'bl', 'bc', 'br'].forEach( // @ts-ignore - hdl => (handlers[hdl] = opts[hdl] ? createHandler(hdl, opts) : null) + hdl => (handlers[hdl] = opts[hdl] ? this.createHandler(hdl, opts) : null) ); for (let n in handlers) { @@ -447,29 +488,38 @@ export default class Resizer { } /** - * Get any of the 8 handlers from the rectangle, + * Get any of the 8 handlers from the rectangle, * and get it's coordinates based on zero degrees rotation. */ private getRectCoordiante(handler: string, rect: RectDim): Coordinate { switch (handler) { - case 'tl': return { t: rect.t, l: rect.l }; - case 'tr': return { t: rect.t, l: rect.l + rect.w }; - case 'bl': return { t: rect.t + rect.h, l: rect.l }; - case 'br': return { t: rect.t + rect.h, l: rect.l + rect.w }; - case 'tc': return { t: rect.t, l: rect.l + (rect.w / 2) }; - case 'cr': return { t: rect.t + (rect.h / 2), l: rect.l + rect.w }; - case 'bc': return { t: rect.t + rect.h, l: rect.l + (rect.w / 2) }; - case 'cl': return { t: rect.t + (rect.h / 2), l: rect.l }; - default: throw new Error('Invalid handler ' + handler); + case 'tl': + return { t: rect.t, l: rect.l }; + case 'tr': + return { t: rect.t, l: rect.l + rect.w }; + case 'bl': + return { t: rect.t + rect.h, l: rect.l }; + case 'br': + return { t: rect.t + rect.h, l: rect.l + rect.w }; + case 'tc': + return { t: rect.t, l: rect.l + rect.w / 2 }; + case 'cr': + return { t: rect.t + rect.h / 2, l: rect.l + rect.w }; + case 'bc': + return { t: rect.t + rect.h, l: rect.l + rect.w / 2 }; + case 'cl': + return { t: rect.t + rect.h / 2, l: rect.l }; + default: + throw new Error('Invalid handler ' + handler); } - } + } /** * Get opposite coordinate on rectangle based on distance to center */ private getOppositeRectCoordinate(coordinate: Coordinate, rect: RectDim): Coordinate { - const cx = rect.l + (rect.w / 2); - const cy = rect.t + (rect.h / 2); + const cx = rect.l + rect.w / 2; + const cy = rect.t + rect.h / 2; const dx = cx - coordinate.l; const dy = cy - coordinate.t; @@ -477,26 +527,7 @@ export default class Resizer { const nx = cx + dx; const ny = cy + dy; - return { l: nx, t: ny } - } - - /** - * Rotate a rectangle coordinate around it's center and given rectangle rotation - */ - private rotateCoordinate(coordinate: Coordinate, rect: RectDim): Coordinate { - const cx = rect.l + (rect.w / 2); - const cy = rect.t + (rect.h / 2); - - const a = ((rect.r)); - const theta = a * (Math.PI / 180); - - const x = coordinate.l; - const y = coordinate.t; - - const rx = (x - cx) * Math.cos(theta) - (y - cy) * Math.sin(theta) + cx; - const ry = (x - cx) * Math.sin(theta) + (y - cy) * Math.cos(theta) + cy; - - return { l: rx, t: ry } + return { l: nx, t: ny }; } /** @@ -529,7 +560,7 @@ export default class Resizer { l: Number.parseFloat(el?.computedStyleMap().get('left')?.toString() ?? '0'), w: rect.width, h: rect.height, - r: rotation + r: rotation, }; this.rectDim = { ...this.startDim, @@ -545,7 +576,7 @@ export default class Resizer { l: parentRect.left, w: parentRect.width, h: parentRect.height, - r: 0 + r: 0, }; // Listen events @@ -585,7 +616,7 @@ export default class Resizer { this.delta = { x: cx - sx, - y: cy - sy + y: cy - sy, }; this.keys = { shift: e.shiftKey, @@ -630,10 +661,10 @@ export default class Resizer { // Calculate difference between locking point after new dimensions const coordiantes = [this.startDim!, rect].map(rect => { - const handlerCoordinate = this.getRectCoordiante(this.handlerAttr!, rect); + const handlerCoordinate = this.getRectCoordiante(this.handlerAttr!, rect); const oppositeCoordinate = this.getOppositeRectCoordinate(handlerCoordinate, rect); - return this.rotateCoordinate(oppositeCoordinate, rect); - }) + return rotateCoordinate(oppositeCoordinate, rect); + }); const diffX = coordiantes[0].l - coordiantes[1].l; const diffY = coordiantes[0].t - coordiantes[1].t; @@ -736,7 +767,7 @@ export default class Resizer { l: startDim.l, w: startW, h: startH, - r: startDim.r + r: startDim.r, }; if (!data) return; diff --git a/src/utils/Rotator.ts b/src/utils/Rotator.ts index fe191dfe2e..07798cc06d 100644 --- a/src/utils/Rotator.ts +++ b/src/utils/Rotator.ts @@ -24,6 +24,25 @@ type CallbackOptionsRotator = { rotator: Rotator; }; +export function rotateCoordinate( + coordinate: Pick, + rect: RectDimRotator +): Pick { + const cx = rect.l + rect.w / 2; + const cy = rect.t + rect.h / 2; + + const a = rect.r; + const theta = a * (Math.PI / 180); + + const x = coordinate.l; + const y = coordinate.t; + + const rx = (x - cx) * Math.cos(theta) - (y - cy) * Math.sin(theta) + cx; + const ry = (x - cx) * Math.sin(theta) + (y - cy) * Math.cos(theta) + cy; + + return { l: rx, t: ry }; +} + export interface RotatorOptions { /** * Function which returns custom X and Y coordinates of the mouse. @@ -118,6 +137,8 @@ export interface RotatorOptions { * @default 45 */ snapPoints?: number; + + rotationAngle?: number; } const getBoundingRect = (el: HTMLElement, win?: Window): BoundingRectRotator => { @@ -353,7 +374,7 @@ export default class Rotator { l: rect.left, w: rect.width, h: rect.height, - r: rectRotation, + r: rectRotation - (this.opts.rotationAngle ?? 0), }; this.rectDim = { t: rect.top, @@ -414,7 +435,7 @@ export default class Rotator { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, - } + }; this.rectDim = this.calc(this); this.updateRect(false); @@ -534,7 +555,7 @@ export default class Rotator { if (!data) return; - const angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI + 90; + const angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI + 90 - (this.opts.rotationAngle ?? 0); box.r = angle;