Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support rotating of elements #5559

Closed
2 changes: 1 addition & 1 deletion dist/css/grapes.min.css

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions src/canvas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,13 +498,14 @@ export default class CanvasModule extends Module<CanvasConfig> {
* @return {Object}
* @private
*/
getMouseRelativeCanvas(ev: MouseEvent, opts: any) {
getMouseRelativeCanvas(ev: MouseEvent) {
const zoom = this.getZoomDecimal();
const { top = 0, left = 0 } = this.getCanvasView().getPosition(opts) ?? {};
const zoomOffset = 1 / zoom;
const { top: frameTop = 0, left: frameLeft = 0 } = this.getCanvasView().getFrameOffset() ?? {};

return {
y: ev.clientY * zoom + top,
x: ev.clientX * zoom + left,
y: (ev.clientY - frameTop) * zoomOffset,
x: (ev.clientX - frameLeft) * zoomOffset,
};
}

Expand Down
8 changes: 5 additions & 3 deletions src/canvas/view/CanvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type ElementPosOpts = {
avoidFrameOffset?: boolean;
avoidFrameZoom?: boolean;
noScroll?: boolean;
nativeBoundingRect?: boolean;
};

export interface FitViewportOptions {
Expand Down Expand Up @@ -340,9 +341,10 @@ export default class CanvasView extends ModuleView<Canvas> {
* @return { {top: number, left: number, width: number, height: number} }
*/
offset(el?: HTMLElement, opts: ElementPosOpts = {}) {
const { noScroll } = opts;
const rect = getElRect(el);
const { noScroll, nativeBoundingRect } = opts;
const rect = getElRect(el, nativeBoundingRect);
const scroll = noScroll ? { x: 0, y: 0 } : getDocumentScroll(el);
const docBody = el?.ownerDocument.body;

return {
top: rect.top + scroll.y,
Expand Down Expand Up @@ -454,7 +456,7 @@ export default class CanvasView extends ModuleView<Canvas> {
const frame = this.frame?.el;
const winEl = el?.ownerDocument.defaultView;
const frEl = winEl ? (winEl.frameElement as HTMLElement) : frame;
this.frmOff = this.offset(frEl || frame);
this.frmOff = this.offset(frEl || frame, { nativeBoundingRect: true });
}
return this.frmOff;
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type CommandEvent = 'run' | 'stop' | `run:${string}` | `stop:${string}` |
const commandsDef = [
['preview', 'Preview', 'preview'],
['resize', 'Resize', 'resize'],
['rotate', 'Rotate', 'rotate'],
['fullscreen', 'Fullscreen', 'fullscreen'],
['copy', 'CopyComponent'],
['paste', 'PasteComponent'],
Expand Down
33 changes: 33 additions & 0 deletions src/commands/view/Rotate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Rotator from '../../utils/Rotator';
import { CommandObject } from './CommandAbstract';

export default {
run(editor, sender, opts) {
const opt = opts || {};
const canvas = editor.Canvas;
const canvasView = canvas.getCanvasView();
const options = {
appendTo: canvas.getResizerEl(),
prefix: editor.getConfig().stylePrefix,
posFetcher: canvasView.getElementPos.bind(canvasView),
mousePosFetcher: canvas.getMouseRelativeCanvas.bind(canvas),
...(opt.options || {}),
};
let { canvasRotator } = this;

// Create the rotator for the canvas if not yet created
if (!canvasRotator || opt.forceNew) {
this.canvasRotator = new editor.Utils.Rotator(options);
canvasRotator = this.canvasRotator;
}

canvasRotator.setOptions(options, true);
canvasRotator.blur();
canvasRotator.focus(opt.el);
return canvasRotator;
},

stop() {
this.canvasRotator?.blur();
},
} as CommandObject<{ options?: {}; forceNew?: boolean; el: HTMLElement }, { canvasRotator?: Rotator }>;
116 changes: 113 additions & 3 deletions src/commands/view/SelectComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Component from '../../dom_components/model/Component';
import Toolbar from '../../dom_components/model/Toolbar';
import ToolbarView from '../../dom_components/view/ToolbarView';
import { isDoc, isTaggableNode, isVisible, off, on } from '../../utils/dom';
import { getComponentModel, getComponentView, getUnitFromValue, getViewEl, hasWin, isObject } from '../../utils/mixins';
import { getComponentModel, getComponentView, getRotation, getUnitFromValue, getViewEl, hasWin, isObject } from '../../utils/mixins';
import { CommandObject } from './CommandAbstract';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import { ResizerOptions } from '../../utils/Resizer';
Expand Down Expand Up @@ -86,10 +86,11 @@ export default {
methods[method](win, 'resize', this.onFrameResize);
};
methods[method](window, 'resize', this.onFrameUpdated);
methods[method](window, 'rotate', this.onFrameUpdated);
methods[method](listenToEl, 'scroll', this.onContainerChange);
em[method]('component:toggled component:update undo redo', this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this);
em[method]('component:resize styleable:change component:input', this.updateGlobalPos, this);
em[method]('component:resize component:rotate styleable:change component:input', this.updateGlobalPos, this);
em[method]('component:update:toolbar', this._upToolbar, this);
em[method]('change:canvasOffset', this.updateAttached, this);
em[method]('frame:updated', this.onFrameUpdated, this);
Expand Down Expand Up @@ -184,6 +185,7 @@ export default {
// This will hide some elements from the select component
this.updateLocalPos(result);
this.initResize(component);
this.initRotate(component);
},

updateGlobalPos() {
Expand Down Expand Up @@ -317,6 +319,7 @@ export default {
if (!model) return;
this.editor.select(model, { event, useValid: true });
this.initResize(model);
this.initRotate(model);
},

/**
Expand Down Expand Up @@ -497,6 +500,110 @@ export default {
}
},

/**
* Init rotator on the element if possible
* @param {HTMLElement|Component} elem
* @private
*/
initRotate(elem: HTMLElement) {
const { em, canvas } = this;
const editor = em?.Editor;
const config = em?.config;
const pfx = config.stylePrefix || '';
const rotateClass = `${pfx}rotating`;
const model = !isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected();
const rotatable = model && model.get('rotatable');

if (!editor || !rotatable) {
editor.stopCommand('rotate');
this.rotator = null;
return;
}

let options = {};
let modelToStyle: any;

var toggleBodyClass = (method: string, e: any, opts: any) => {
const docs = opts.docs;
docs &&
docs.forEach((doc: Document) => {
const body = doc.body;
const cls = body.className || '';
body.className = (method == 'add' ? `${cls} ${rotateClass}` : cls.replace(rotateClass, '')).trim();
});
};

const el = isElement(elem) ? elem : model.getEl();
options = {
// Here the rotator is updated with the current element height and width
onStart(e: Event, opts: any = {}) {
const { el, rotator } = opts;
toggleBodyClass('add', e, opts);
modelToStyle = em.Styles.getModelToStyle(model);
canvas.toggleFramesEvents(false);
const computedStyle = getComputedStyle(el);
const modelStyle = modelToStyle.getStyle();

let currentWidth = modelStyle['width'];
if (isNaN(parseFloat(currentWidth))) {
currentWidth = computedStyle['width'];
}

let currentHeight = modelStyle['height'];
if (isNaN(parseFloat(currentHeight))) {
currentHeight = computedStyle['height'];
}

rotator.startDim.r = getRotation(computedStyle);
showOffsets = false;
},

// Update all positioned elements (eg. component toolbar)
onMove() {
editor.trigger('component:rotate');
},

onEnd(e: Event, opts: any) {
toggleBodyClass('remove', e, opts);
editor.trigger('component:rotate');
canvas.toggleFramesEvents(true);
showOffsets = true;
},

updateTarget(el: any, rect: any, options: any = {}) {
if (!modelToStyle) {
return;
}

const { store, config } = options;
const { keyHeight, keyWidth } = config;
const style: any = {};

if (em.getDragMode(model)) {
style.rotate = `${rect.r}deg`;
}

modelToStyle.addStyle(
{
...style,
// value for the partial update
__p: !store ? 1 : '',
},
{ avoidStore: !store }
);
const updateEvent = 'update:component:style';
const eventToListen = `${updateEvent}:${keyHeight} ${updateEvent}:${keyWidth}`;
em && em.trigger(eventToListen, null, null, { noEmit: 1 });
},
};

if (typeof rotatable == 'object') {
options = { ...options, ...rotatable, parent: options };
}

this.rotator = editor.runCommand('rotate', { el, options, force: 1 });
},

/**
* Update toolbar if the component has one
* @param {Object} mod
Expand Down Expand Up @@ -630,6 +737,7 @@ export default {
style.left = leftOff + unit;
style.width = pos.width + unit;
style.height = pos.height + unit;
style.rotate = window.getComputedStyle(el).getPropertyValue('rotate');

this._trgToolUp('local', {
component,
Expand Down Expand Up @@ -680,6 +788,7 @@ export default {
style.left = leftOff + unit;
style.width = pos.width + unit;
style.height = pos.height + unit;
style.rotate = window.getComputedStyle(el).getPropertyValue('rotate');

this.updateToolbarPos({ top: targetToElem.top, left: targetToElem.left });
this._trgToolUp('global', {
Expand Down Expand Up @@ -712,7 +821,7 @@ export default {
* @private
*/
getElementPos(el: HTMLElement) {
return this.canvas.getCanvasView().getElementPos(el, { noScroll: true });
return this.canvas.getCanvasView().getElementPos(el, { noScroll: true, nativeBoundingRect: false });
},

/**
Expand Down Expand Up @@ -759,5 +868,6 @@ export default {
!opts.preserveSelected && em.setSelected();
this.toggleToolsEl();
editor && editor.stopCommand('resize');
editor && editor.stopCommand('rotate');
},
} as CommandObject<any, { [k: string]: any }>;
2 changes: 2 additions & 0 deletions src/dom_components/model/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* @property {Boolean} [highlightable=true] It can be highlighted with 'dotted' borders if true. Default: `true`
* @property {Boolean} [copyable=true] True if it's possible to clone the component. Default: `true`
* @property {Boolean} [resizable=false] Indicates if it's possible to resize the component. It's also possible to pass an object as [options for the Resizer](https://github.com/GrapesJS/grapesjs/blob/master/src/utils/Resizer.js). Default: `false`
* @property {Boolean} [rotatable=false] Indicates if it's possible to rotate the component. Default: `false`
* @property {Boolean} [editable=false] Allow to edit the content of the component (used on Text components). Default: `false`
* @property {Boolean} [layerable=true] Set to `false` if you need to hide the component inside Layers. Default: `true`
* @property {Boolean} [selectable=true] Allow component to be selected when clicked. Default: `true`
Expand Down Expand Up @@ -134,6 +135,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
highlightable: true,
copyable: true,
resizable: false,
rotatable: false,
editable: false,
layerable: true,
selectable: true,
Expand Down
13 changes: 13 additions & 0 deletions src/styles/scss/_gjs_canvas.scss
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,19 @@ $guide_pad: 5px !default;
cursor: nwse-resize;
}

.#{$app-prefix}rotator-c {
pointer-events: all;
position: absolute;
border: 3px solid $colorBlue;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #fff;
margin: $hndlMargin;
top: -20px;
left: 50%;
}

.#{$pn-prefix}panel {
.#{$app-prefix}resizer-h {
background-color: rgba(0, 0, 0, 0.2);
Expand Down
Loading