Browse Source

Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into dynamic-values-improvements

pull/6419/head
mohamedsalem401 2 years ago
parent
commit
6e266a2136
  1. 24
      packages/core/src/canvas/index.ts
  2. 11
      packages/core/src/canvas/model/Canvas.ts
  3. 11
      packages/core/src/canvas/view/CanvasView.ts
  4. 92
      packages/core/src/canvas/view/FrameView.ts
  5. 16
      packages/core/src/canvas/view/FrameWrapView.ts
  6. 1
      packages/core/src/commands/view/SelectComponent.ts
  7. 2
      packages/core/src/common/index.ts
  8. 5
      packages/core/src/device_manager/model/Device.ts
  9. 20
      packages/core/src/editor/model/Editor.ts
  10. 35
      packages/core/src/pages/index.ts
  11. 2
      packages/core/src/pages/model/Pages.ts
  12. 103
      packages/core/src/utils/AutoScroller.ts
  13. 82
      packages/core/test/specs/editor/index.ts

24
packages/core/src/canvas/index.ts

@ -44,10 +44,12 @@ import { CanvasEvents, CanvasRefreshOptions, ToWorldOption } from './types';
import CanvasView, { FitViewportOptions } from './view/CanvasView';
import FrameView from './view/FrameView';
import { DragSource } from '../utils/sorter/types';
import AutoScroller from '../utils/AutoScroller';
export type CanvasEvent = `${CanvasEvents}`;
export default class CanvasModule extends Module<CanvasConfig> {
autoScroller: AutoScroller;
/**
* Get configuration object
* @name getConfig
@ -83,6 +85,8 @@ export default class CanvasModule extends Module<CanvasConfig> {
this.model = this.canvas;
this.startAutoscroll = this.startAutoscroll.bind(this);
this.stopAutoscroll = this.stopAutoscroll.bind(this);
this.autoScroller = new AutoScroller();
return this;
}
@ -418,11 +422,9 @@ export default class CanvasModule extends Module<CanvasConfig> {
const canvasOffset = opts.canvasOff || this.canvasRectOffset(el, elRect);
const targetHeight = targetEl.offsetHeight || 0;
const targetWidth = targetEl.offsetWidth || 0;
const elementRight = elRect.left + elRect.width;
const canvasView = this.getCanvasView();
const { scrollTop: canvasScrollTop, scrollLeft: canvasScrollLeft } = canvasView.getCanvasScroll();
const canvasRect = canvasView.getPosition();
const frameOffset = canvasView.getFrameOffset(el);
const { event } = opts;
const defaultLeftOffset = elRect.width - targetWidth;
@ -432,7 +434,7 @@ export default class CanvasModule extends Module<CanvasConfig> {
const canvasLiftLimit = Math.max(-elRect.left + canvasScrollLeft, 0);
left = Math.max(left, canvasLiftLimit);
const elementRightLimit = elementRight - targetWidth;
const elementRightLimit = elRect.width - targetWidth;
left = Math.min(left, elementRightLimit);
const canvasRightLimit = canvasRect.width + canvasScrollLeft - targetWidth - elRect.left;
@ -443,14 +445,15 @@ export default class CanvasModule extends Module<CanvasConfig> {
if (targetReachesCanvasTop) {
const fullHeight = elRect.height + targetHeight;
const elementIsShorterThanFrame = fullHeight < frameOffset.height;
const elementIsShorterThanFrame = fullHeight < canvasRect.height;
// Scroll with the window if the top edge is reached and the
// element is bigger than the canvas
if (elementIsShorterThanFrame) {
top = top + fullHeight;
} else {
top = -canvasOffset.top < elRect.height ? -canvasOffset.top : elRect.height;
const canvasRelativeTop = -canvasOffset.top + canvasScrollTop;
top = canvasRelativeTop < elRect.height ? canvasRelativeTop : elRect.height;
}
}
@ -607,6 +610,13 @@ export default class CanvasModule extends Module<CanvasConfig> {
startAutoscroll(frame?: Frame) {
const fr = (frame && frame.view) || this.em.getCurrentFrame();
fr && fr.startAutoscroll();
if (this.config.scrollableCanvas) {
const el = this.getCanvasView().el;
this.autoScroller.start(el, el, {
zoom: this.em.getZoomDecimal(),
});
}
}
/**
@ -616,6 +626,10 @@ export default class CanvasModule extends Module<CanvasConfig> {
stopAutoscroll(frame?: Frame) {
const fr = (frame && frame.view) || this.em.getCurrentFrame();
fr && fr.stopAutoscroll();
if (this.config.scrollableCanvas) {
this.autoScroller.stop();
}
}
/**

11
packages/core/src/canvas/model/Canvas.ts

@ -4,6 +4,7 @@ import { Coordinates, CoordinatesTypes, DEFAULT_COORDS } from '../../common';
import { evUpdate as evDeviceUpdate } from '../../device_manager';
import Page from '../../pages/model/Page';
import PagesEvents from '../../pages/types';
import Frame from './Frame';
import Frames from './Frames';
export default class Canvas extends ModuleModel<CanvasModule> {
@ -56,14 +57,14 @@ export default class Canvas extends ModuleModel<CanvasModule> {
this.updateDevice({ frame: page.getMainFrame() });
}
updateDevice(opts: any = {}) {
updateDevice(opts: { frame?: Frame } = {}) {
const { em } = this;
const device = em.getDeviceModel();
const model = opts.frame || em.getCurrentFrameModel();
const frame = opts.frame || em.getCurrentFrameModel();
if (model && device) {
const { width, height } = device.attributes;
model.set({ width, height }, { noUndo: 1 });
if (frame && device) {
const { width, height, minHeight } = device.attributes;
frame.set({ width, height, minHeight }, { noUndo: 1 });
}
}

11
packages/core/src/canvas/view/CanvasView.ts

@ -47,6 +47,7 @@ export interface FitViewportOptions {
gap?: number | { x: number; y: number };
ignoreHeight?: boolean;
el?: HTMLElement;
zoom?: number;
}
export default class CanvasView extends ModuleView<Canvas> {
@ -83,7 +84,7 @@ export default class CanvasView extends ModuleView<Canvas> {
frames!: FramesView;
frame?: FrameView;
private timerZoom?: number;
private timerZoom?: NodeJS.Timeout;
private frmOff?: { top: number; left: number; width: number; height: number };
private cvsOff?: { top: number; left: number; width: number; height: number };
@ -134,6 +135,7 @@ export default class CanvasView extends ModuleView<Canvas> {
}
remove(...args: any) {
clearTimeout(this.timerZoom);
this.frames?.remove();
//@ts-ignore
this.frames = undefined;
@ -241,12 +243,12 @@ export default class CanvasView extends ModuleView<Canvas> {
this.clearOff();
toolsWrpEl.style.display = 'none';
em.trigger('canvas:update', ev);
this.timerZoom && clearTimeout(this.timerZoom);
clearTimeout(this.timerZoom);
this.timerZoom = setTimeout(() => {
em.stopDefault(defOpts);
em.runDefault(defOpts);
toolsWrpEl.style.display = '';
}, 300) as any;
}, 300);
}
updateFramesArea() {
@ -269,6 +271,7 @@ export default class CanvasView extends ModuleView<Canvas> {
fitViewport(opts: FitViewportOptions = {}) {
const { em, module, model } = this;
this.clearOff();
const canvasRect = this.getCanvasOffset();
const { el } = opts;
const elFrame = el && getComponentView(el)?.frameView;
@ -303,7 +306,7 @@ export default class CanvasView extends ModuleView<Canvas> {
const zoomRatio = noHeight ? widthRatio : Math.min(widthRatio, heightRatio);
const zoom = zoomRatio * 100;
module.setZoom(zoom);
module.setZoom(opts.zoom ?? zoom);
// check for the frame witdh is necessary as we're centering the frame via CSS
const coordX = -boxRect.x + (frame.width >= canvasWidth ? canvasWidth / 2 - boxWidth / 2 : -gapX);

92
packages/core/src/canvas/view/FrameView.ts

@ -6,21 +6,13 @@ import ComponentWrapperView from '../../dom_components/view/ComponentWrapperView
import ComponentView from '../../dom_components/view/ComponentView';
import { type as typeHead } from '../../dom_components/model/ComponentHead';
import Droppable from '../../utils/Droppable';
import {
append,
appendVNodes,
createCustomEvent,
createEl,
getPointerEvent,
motionsEv,
off,
on,
} from '../../utils/dom';
import { append, appendVNodes, createCustomEvent, createEl, motionsEv, off, on } from '../../utils/dom';
import { hasDnd, setViewEl } from '../../utils/mixins';
import Canvas from '../model/Canvas';
import Frame from '../model/Frame';
import FrameWrapView from './FrameWrapView';
import CanvasEvents from '../types';
import AutoScroller from '../../utils/AutoScroller';
export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
/** @ts-ignore */
@ -39,6 +31,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
lastClientY?: number;
lastMaxHeight = 0;
private autoScroller: AutoScroller;
private jsContainer?: HTMLElement;
private tools: { [key: string]: HTMLElement } = {};
private wrapper?: ComponentWrapperView;
@ -47,7 +40,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
constructor(model: Frame, view?: FrameWrapView) {
super({ model });
bindAll(this, 'updateClientY', 'stopAutoscroll', 'autoscroll', '_emitUpdate');
bindAll(this, 'startAutoscroll', 'stopAutoscroll', '_emitUpdate');
const { el } = this;
//@ts-ignore
this.module._config = {
@ -63,6 +56,17 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
this.listenTo(cvModel, 'change:styles', this.renderStyles);
model.view = this;
setViewEl(el, this);
this.autoScroller = new AutoScroller(this.config.autoscrollLimit, {
rectIsInScrollIframe: true,
onScroll: () => {
const toolsEl = this.getGlobalToolsEl();
toolsEl.style.opacity = '0';
this.showGlobalTools();
this.em.Canvas.spots.refreshDbn();
},
});
}
getBoxRect(): BoxRect {
@ -222,74 +226,20 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
}
startAutoscroll() {
this.lastMaxHeight = this.getWrapper().offsetHeight - this.el.offsetHeight;
// By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
setTimeout(() => {
this._toggleAutoscrollFx(true);
requestAnimationFrame(this.autoscroll);
}, 0);
}
autoscroll() {
if (this.dragging) {
const { lastClientY } = this;
const canvas = this.em.Canvas;
const win = this.getWindow();
const actualTop = win.pageYOffset;
const clientY = lastClientY || 0;
const limitTop = canvas.getConfig().autoscrollLimit!;
const limitBottom = this.getRect().height - limitTop;
let nextTop = actualTop;
if (clientY < limitTop) {
nextTop -= limitTop - clientY;
}
if (clientY > limitBottom) {
nextTop += clientY - limitBottom;
}
if (
!isUndefined(lastClientY) && // Fixes #3134
nextTop !== actualTop &&
nextTop > 0 &&
nextTop < this.lastMaxHeight
) {
const toolsEl = this.getGlobalToolsEl();
toolsEl.style.opacity = '0';
this.showGlobalTools();
win.scrollTo(0, nextTop);
canvas.spots.refreshDbn();
}
requestAnimationFrame(this.autoscroll);
}
this.autoScroller.start(this.el, this.getWindow(), {
lastMaxHeight: this.getWrapper().offsetHeight - this.el.offsetHeight,
zoom: this.em.getZoomDecimal(),
});
}
updateClientY(ev: Event) {
ev.preventDefault();
this.lastClientY = getPointerEvent(ev).clientY * this.em.getZoomDecimal();
stopAutoscroll() {
this.autoScroller.stop();
}
showGlobalTools() {
this.getGlobalToolsEl().style.opacity = '';
}
stopAutoscroll() {
this.dragging && this._toggleAutoscrollFx(false);
}
_toggleAutoscrollFx(enable: boolean) {
this.dragging = enable;
const win = this.getWindow();
const method = enable ? 'on' : 'off';
const mt = { on, off };
mt[method](win, 'mousemove dragover', this.updateClientY);
mt[method](win, 'mouseup', this.stopAutoscroll);
}
render() {
const { $el, ppfx, em } = this;
$el.attr({ class: `${ppfx}frame` });

16
packages/core/src/canvas/view/FrameWrapView.ts

@ -76,6 +76,7 @@ export default class FrameWrapView extends ModuleView<Frame> {
}
remove(opts?: any) {
this.sizeObserver?.disconnect();
this.__clear(opts);
ModuleView.prototype.remove.apply(this, opts);
//@ts-ignore
@ -161,26 +162,23 @@ export default class FrameWrapView extends ModuleView<Frame> {
const newWidth = isNumber(newW) ? `${newW}${un}` : newW;
const newHeight = isNumber(newH) ? `${newH}${un}` : newH;
style.width = newWidth;
this.sizeObserver?.disconnect();
if (model.hasAutoHeight()) {
const iframe = this.frame.el;
const { contentDocument } = iframe;
if (
iframe.contentDocument
// this doesn't work always
// && !this.sizeObserver
) {
const { contentDocument } = iframe;
if (contentDocument) {
const observer = new ResizeObserver(() => {
style.height = `${contentDocument.body.scrollHeight}px`;
const minHeight = parseFloat(model.get('minHeight')) || 0;
const heightResult = Math.max(contentDocument.body.scrollHeight, minHeight);
style.height = `${heightResult}px`;
});
observer.observe(contentDocument.body);
this.sizeObserver?.disconnect();
this.sizeObserver = observer;
}
} else {
style.height = newHeight;
this.sizeObserver?.disconnect();
delete this.sizeObserver;
}

1
packages/core/src/commands/view/SelectComponent.ts

@ -800,6 +800,7 @@ export default {
this.stopSelectComponent();
!opts.preserveSelected && em.setSelected();
this.toggleToolsEl();
this.updateAttached.cancel();
editor && editor.stopCommand('resize');
},
} as CommandObject<any, { [k: string]: any }>;

2
packages/core/src/common/index.ts

@ -24,7 +24,7 @@ export type UndoOptions = { fromUndo?: boolean };
export type WithHTMLParserOptions = { parserOptions?: HTMLParserOptions };
export type RemoveOptions = Backbone.Silenceable & UndoOptions & { dangerously?: boolean };
export type RemoveOptions = Backbone.Silenceable & UndoOptions & { dangerously?: boolean; temporary?: boolean };
export type EventHandler = Backbone.EventHandler;

5
packages/core/src/device_manager/model/Device.ts

@ -18,6 +18,11 @@ export interface DeviceProperties {
* @example '600px'
*/
height?: string;
/**
* Min height to set for the editor iframe.
* @example '600px'
*/
minHeight?: string;
/**
* The width which will be used in media queries, if empty the `width` will be used.
* @example '900px'

20
packages/core/src/editor/model/Editor.ts

@ -110,9 +110,9 @@ export default class EditorModel extends Model {
device: '',
};
}
Model = Model;
Collection = Collection;
events = EditorEvents;
__skip = false;
defaultRunning = false;
destroyed = false;
@ -532,10 +532,14 @@ export default class EditorModel extends Model {
}
}
// Hanlde multiple selection
// Handle multiple selection
if (ctrlKey && mltSel) {
return this.toggleSelected(model);
} else if (shiftKey && mltSel) {
if (this.isEditing()) {
// Fixes #6345 where a shift click while editing text should not assume a selection of a component
return;
}
this.clearSelection(this.Canvas.getWindow());
const coll = model.collection;
const index = model.index();
@ -907,10 +911,10 @@ export default class EditorModel extends Model {
* @private
*/
runDefault(opts = {}) {
const command = this.get('Commands').get(this.config.defaultCommand);
const command = this.Commands.get(this.config.defaultCommand!);
if (!command || this.defaultRunning) return;
command.stop(this, this, opts);
command.run(this, this, opts);
command.stop!(this as any, this, opts);
command.run!(this as any, this, opts);
this.defaultRunning = true;
}
@ -920,11 +924,11 @@ export default class EditorModel extends Model {
* @private
*/
stopDefault(opts = {}) {
const commands = this.get('Commands');
const commands = this.Commands;
if (!commands) return;
const command = commands.get(this.config.defaultCommand);
const command = commands.get(this.config.defaultCommand!);
if (!command || !this.defaultRunning) return;
command.stop(this, this, opts);
command.stop!(this as any, this, opts);
this.defaultRunning = false;
}

35
packages/core/src/pages/index.ts

@ -159,6 +159,41 @@ export default class PageManager extends ItemManagerModule<PageManagerConfig, Pa
return !opts.abort && rm();
}
/**
* Move a page to a specific index in the pages collection.
* If the index is out of bounds, the page will not be moved.
*
* @param {string|[Page]} page Page or page id to move.
* @param {Object} [opts] Move options
* @param {number} [opts.at] The target index where the page should be moved.
* @returns {Page | undefined} The moved page, or `undefined` if the page does not exist or the index is out of bounds.
* @example
* // Move a page to index 2
* const movedPage = pageManager.move('page-id', { at: 2 });
* if (movedPage) {
* console.log('Page moved successfully:', movedPage);
* } else {
* console.log('Page could not be moved.');
* }
*/
move(page: string | Page, opts: AddOptions = {}): Page | undefined {
const { pages } = this;
const pg = isString(page) ? this.get(page) : page;
const { at = 0, ...resOpts } = opts;
if (!pg) return;
const currIndex = pages.indexOf(pg);
const sameIndex = currIndex === at;
if (at < 0 || at >= pages.length || sameIndex) return;
this.remove(pg, { ...resOpts, temporary: true });
pages.add(pg, { ...resOpts, at });
return pg;
}
/**
* Get page by id
* @param {String} id Page id

2
packages/core/src/pages/model/Pages.ts

@ -20,7 +20,7 @@ export default class Pages extends Collection<Page> {
onRemove(removed?: Page, _p?: this, opts: RemoveOptions = {}) {
// Avoid removing frames if triggered from undo #6142
if (opts.fromUndo) return;
if (opts.fromUndo || opts.temporary) return;
removed?.onRemove();
}
}

103
packages/core/src/utils/AutoScroller.ts

@ -0,0 +1,103 @@
import { bindAll } from 'underscore';
import { getPointerEvent, off, on } from './dom';
export default class AutoScroller {
private eventEl?: HTMLElement; // Element that handles mouse events
private scrollEl?: HTMLElement | Window; // Element that will be scrolled
private dragging: boolean = false;
private lastClientY?: number;
private lastMaxHeight: number = 0;
private onScroll?: () => void;
private autoscrollLimit: number;
private zoom: number = 1;
/**
* When an element is inside an iframe, its `getBoundingClientRect()` values
* are relative to the iframe's document, not the main window's.
*/
private rectIsInScrollIframe: boolean = false;
constructor(
autoscrollLimit: number = 50,
opts?: {
lastMaxHeight?: number;
onScroll?: () => void;
rectIsInScrollIframe?: boolean;
},
) {
this.autoscrollLimit = autoscrollLimit;
this.lastMaxHeight = opts?.lastMaxHeight ?? 0;
this.onScroll = opts?.onScroll;
this.rectIsInScrollIframe = !!opts?.rectIsInScrollIframe;
bindAll(this, 'start', 'autoscroll', 'updateClientY', 'stop');
}
start(eventEl: HTMLElement, scrollEl: HTMLElement | Window, opts?: { lastMaxHeight?: number; zoom?: number }) {
this.eventEl = eventEl;
this.scrollEl = scrollEl;
this.lastMaxHeight = opts?.lastMaxHeight || Number.POSITIVE_INFINITY;
this.zoom = opts?.zoom || 1;
// By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
setTimeout(() => {
this.toggleAutoscrollFx(true);
requestAnimationFrame(this.autoscroll);
}, 0);
}
private autoscroll() {
const scrollEl = this.scrollEl;
if (this.dragging && scrollEl) {
const clientY = this.lastClientY ?? 0;
const limitTop = this.autoscrollLimit;
const eventElHeight = this.getEventElHeight();
const limitBottom = eventElHeight - limitTop;
let nextTop = 0;
if (clientY < limitTop) nextTop += clientY - limitTop;
if (clientY > limitBottom) nextTop += clientY - limitBottom;
const scrollTop = this.getElScrollTop(scrollEl);
if (this.lastClientY !== undefined && nextTop !== 0 && this.lastMaxHeight - nextTop > scrollTop) {
scrollEl.scrollBy({ top: nextTop, left: 0, behavior: 'auto' });
this.onScroll?.();
}
requestAnimationFrame(this.autoscroll);
}
}
private getEventElHeight() {
const eventEl = this.eventEl;
if (!eventEl) return 0;
const elRect = eventEl.getBoundingClientRect();
return elRect.height;
}
private updateClientY(ev: Event) {
const scrollEl = this.scrollEl;
ev.preventDefault();
const scrollTop = !this.rectIsInScrollIframe ? this.getElScrollTop(scrollEl) : 0;
this.lastClientY = getPointerEvent(ev).clientY * this.zoom - scrollTop;
}
private getElScrollTop(scrollEl: HTMLElement | Window | undefined) {
return (scrollEl instanceof HTMLElement ? scrollEl.scrollTop : scrollEl?.scrollY) || 0;
}
private toggleAutoscrollFx(enable: boolean) {
this.dragging = enable;
const eventEl = this.eventEl;
if (!eventEl) return;
const method = enable ? 'on' : 'off';
const mt = { on, off };
mt[method](eventEl, 'mousemove dragover', this.updateClientY);
mt[method](eventEl, 'mouseup', this.stop);
}
stop() {
this.toggleAutoscrollFx(false);
}
}

82
packages/core/test/specs/editor/index.ts

@ -107,4 +107,86 @@ describe('Editor', () => {
expect(umStack.length).toBe(3);
expect(keys(all).length).toBe(DEFAULT_CMPS);
});
test('One component can be selected at a time without shift', () => {
const all = editor.Components.allById();
const em = editor.em;
em.getConfig().multipleSelection = true;
const wrapper = editor.getWrapper()!;
const added = wrapper.append(`
<div>Component 1</div>
<div>Component 2</div>
`);
em.setSelected(added[0]);
em.setSelected(added[1]);
expect(editor.getSelectedAll().length).toBe(1);
});
test('Shift key should allow selecting multiple components', () => {
const all = editor.Components.allById();
const em = editor.em;
em.getConfig().multipleSelection = true;
const wrapper = editor.getWrapper()!;
const added = wrapper.append(`
<div>Component 1</div>
<div>Component 2</div>
`);
const callSelectedOptions = {
event: {
shiftKey: true,
},
};
em.setSelected(added[0], callSelectedOptions);
em.setSelected(added[1], callSelectedOptions);
expect(editor.getSelectedAll().length).toBe(2);
});
test('Shift key selecting a component that is being edited should should be ignored', () => {
const all = editor.Components.allById();
const em = editor.em;
em.getConfig().multipleSelection = true;
const wrapper = editor.getWrapper()!;
const added = wrapper.append(`
<div>Component 1</div>
<div>Component 2</div>
`);
const callSelectedOptions = {
event: {
shiftKey: true,
},
};
const firstComponent = all[keys(all)[0]];
firstComponent.em.setEditing(true);
em.setSelected(added[0], callSelectedOptions);
expect(editor.getSelectedAll().length).toBe(0);
});
test.skip('Shift key selecting a component that is being edited should not clear any text selections', () => {
const all = editor.Components.allById();
const em = editor.em;
em.getConfig().multipleSelection = true;
const wrapper = editor.getWrapper()!;
const added = wrapper.append(`
<div>Component 1</div>
<div>Component 2</div>
`);
const callSelectedOptions = {
event: {
shiftKey: true,
},
};
const firstComponent = all[keys(all)[0]];
firstComponent.em.setEditing(true);
// TODO: highlight the text of the first component
em.setSelected(added[0], callSelectedOptions);
// TODO: check if the text of the first component is still highlighted
expect(editor.getSelectedAll().length).toBe(0);
});
});

Loading…
Cancel
Save