mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
refactor: 重新设计stage不同布局下的实现
This commit is contained in:
parent
9252eb2ae0
commit
2ade3d03f7
150
packages/stage/src/Rule.ts
Normal file
150
packages/stage/src/Rule.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import Guides, { GuidesEvents } from '@scena/guides';
|
||||
|
||||
import { GuidesType } from './const';
|
||||
|
||||
export default class Rule extends EventEmitter {
|
||||
public hGuides: Guides;
|
||||
public vGuides: Guides;
|
||||
public horizontalGuidelines: number[] = [];
|
||||
public verticalGuidelines: number[] = [];
|
||||
|
||||
private container: HTMLDivElement;
|
||||
private containerResizeObserver: ResizeObserver;
|
||||
|
||||
constructor(container: HTMLDivElement) {
|
||||
super();
|
||||
|
||||
this.container = container;
|
||||
this.hGuides = this.createGuides(GuidesType.HORIZONTAL);
|
||||
this.vGuides = this.createGuides(GuidesType.VERTICAL);
|
||||
|
||||
this.hGuides.on('changeGuides', this.hGuidesChangeGuidesHandler);
|
||||
this.vGuides.on('changeGuides', this.vGuidesChangeGuidesHandler);
|
||||
|
||||
this.containerResizeObserver = new ResizeObserver(() => {
|
||||
this.vGuides.resize();
|
||||
this.hGuides.resize();
|
||||
});
|
||||
this.containerResizeObserver.observe(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示标尺
|
||||
* @param show 是否显示
|
||||
*/
|
||||
public showGuides(show = true) {
|
||||
this.hGuides.setState({
|
||||
showGuides: show,
|
||||
});
|
||||
|
||||
this.vGuides.setState({
|
||||
showGuides: show,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有参考线
|
||||
*/
|
||||
public clearGuides() {
|
||||
this.horizontalGuidelines = [];
|
||||
this.verticalGuidelines = [];
|
||||
|
||||
this.vGuides.setState({
|
||||
defaultGuides: [],
|
||||
});
|
||||
|
||||
this.hGuides.setState({
|
||||
defaultGuides: [],
|
||||
});
|
||||
|
||||
this.emit('changeGuides', {
|
||||
type: GuidesType.VERTICAL,
|
||||
guides: [],
|
||||
});
|
||||
|
||||
this.emit('changeGuides', {
|
||||
type: GuidesType.HORIZONTAL,
|
||||
guides: [],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示标尺
|
||||
* @param show 是否显示
|
||||
*/
|
||||
public showRule(show = true) {
|
||||
// 当尺子隐藏时发现大小变化,显示后会变形,所以这里做重新初始化处理
|
||||
if (show) {
|
||||
this.hGuides.destroy();
|
||||
this.hGuides = this.createGuides(GuidesType.HORIZONTAL, this.horizontalGuidelines);
|
||||
|
||||
this.vGuides.destroy();
|
||||
this.vGuides = this.createGuides(GuidesType.VERTICAL, this.verticalGuidelines);
|
||||
} else {
|
||||
this.hGuides.setState({
|
||||
rulerStyle: {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
this.vGuides.setState({
|
||||
rulerStyle: {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scrollRule(scrollTop: number) {
|
||||
this.hGuides.scrollGuides(scrollTop);
|
||||
this.hGuides.scroll(0);
|
||||
|
||||
this.vGuides.scrollGuides(0);
|
||||
this.vGuides.scroll(scrollTop);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.hGuides.off('changeGuides', this.hGuidesChangeGuidesHandler);
|
||||
this.vGuides.off('changeGuides', this.vGuidesChangeGuidesHandler);
|
||||
this.containerResizeObserver.disconnect();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
private getGuidesStyle = (type: GuidesType) => ({
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
left: type === GuidesType.HORIZONTAL ? 0 : '-30px',
|
||||
top: type === GuidesType.HORIZONTAL ? '-30px' : 0,
|
||||
width: type === GuidesType.HORIZONTAL ? '100%' : '30px',
|
||||
height: type === GuidesType.HORIZONTAL ? '30px' : '100%',
|
||||
});
|
||||
|
||||
private createGuides = (type: GuidesType, defaultGuides: number[] = []): Guides =>
|
||||
new Guides(this.container, {
|
||||
type,
|
||||
defaultGuides,
|
||||
displayDragPos: true,
|
||||
backgroundColor: '#fff',
|
||||
lineColor: '#000',
|
||||
textColor: '#000',
|
||||
style: this.getGuidesStyle(type),
|
||||
});
|
||||
|
||||
private hGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
|
||||
this.horizontalGuidelines = e.guides;
|
||||
this.emit('changeGuides', {
|
||||
type: GuidesType.HORIZONTAL,
|
||||
guides: this.horizontalGuidelines,
|
||||
});
|
||||
};
|
||||
|
||||
private vGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
|
||||
this.verticalGuidelines = e.guides;
|
||||
this.emit('changeGuides', {
|
||||
type: GuidesType.VERTICAL,
|
||||
guides: this.verticalGuidelines,
|
||||
});
|
||||
};
|
||||
}
|
@ -18,16 +18,22 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { GuidesEvents } from '@scena/guides';
|
||||
|
||||
import { Id } from '@tmagic/schema';
|
||||
|
||||
import { DEFAULT_ZOOM } from './const';
|
||||
import { DEFAULT_ZOOM, GHOST_EL_ID_PREFIX } from './const';
|
||||
import StageDragResize from './StageDragResize';
|
||||
import StageMask from './StageMask';
|
||||
import StageRender from './StageRender';
|
||||
import { RemoveData, Runtime, SortEventData, StageCoreConfig, UpdateData, UpdateEventData } from './types';
|
||||
import { isFixed } from './util';
|
||||
import {
|
||||
CanSelect,
|
||||
GuidesEventData,
|
||||
RemoveData,
|
||||
Runtime,
|
||||
SortEventData,
|
||||
StageCoreConfig,
|
||||
UpdateData,
|
||||
UpdateEventData,
|
||||
} from './types';
|
||||
|
||||
export default class StageCore extends EventEmitter {
|
||||
public selectedDom: Element | undefined;
|
||||
@ -37,6 +43,7 @@ export default class StageCore extends EventEmitter {
|
||||
public dr: StageDragResize;
|
||||
public config: StageCoreConfig;
|
||||
public zoom = DEFAULT_ZOOM;
|
||||
private canSelect: CanSelect;
|
||||
|
||||
constructor(config: StageCoreConfig) {
|
||||
super();
|
||||
@ -44,6 +51,7 @@ export default class StageCore extends EventEmitter {
|
||||
this.config = config;
|
||||
|
||||
this.setZoom(config.zoom);
|
||||
this.canSelect = config.canSelect || ((el: HTMLElement) => !!el.id);
|
||||
|
||||
this.renderer = new StageRender({ core: this });
|
||||
this.mask = new StageMask({ core: this });
|
||||
@ -52,23 +60,61 @@ export default class StageCore extends EventEmitter {
|
||||
this.renderer.on('runtime-ready', (runtime: Runtime) => this.emit('runtime-ready', runtime));
|
||||
this.renderer.on('page-el-update', (el: HTMLElement) => this.mask?.observe(el));
|
||||
|
||||
this.mask.on('select', async (el: Element) => {
|
||||
await this.dr?.select(el as HTMLElement);
|
||||
});
|
||||
this.mask.on('selected', (el: Element) => {
|
||||
this.select(el as HTMLElement);
|
||||
this.emit('select', el);
|
||||
});
|
||||
this.mask
|
||||
.on('beforeSelect', (event: MouseEvent) => {
|
||||
this.setElementFromPoint(event);
|
||||
})
|
||||
.on('select', () => {
|
||||
this.emit('select', this.selectedDom);
|
||||
})
|
||||
.on('changeGuides', (data: GuidesEventData) => {
|
||||
this.dr.setGuidelines(data.type, data.guides);
|
||||
this.emit('changeGuides', data);
|
||||
});
|
||||
|
||||
this.dr.on('update', (data: UpdateEventData) => this.emit('update', data));
|
||||
this.dr.on('sort', (data: UpdateEventData) => this.emit('sort', data));
|
||||
// 要先触发select,在触发update
|
||||
this.dr
|
||||
.on('update', (data: UpdateEventData) => {
|
||||
setTimeout(() => this.emit('update', data));
|
||||
})
|
||||
.on('sort', (data: UpdateEventData) => {
|
||||
setTimeout(() => this.emit('sort', data));
|
||||
});
|
||||
}
|
||||
|
||||
public async setElementFromPoint(event: MouseEvent) {
|
||||
const { renderer, zoom } = this;
|
||||
|
||||
const doc = renderer.contentWindow?.document;
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
|
||||
if (renderer.iframe) {
|
||||
const rect = renderer.iframe.getClientRects()[0];
|
||||
if (rect) {
|
||||
x = x - rect.left;
|
||||
y = y - rect.top;
|
||||
}
|
||||
}
|
||||
|
||||
const els = doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
|
||||
|
||||
let stopped = false;
|
||||
const stop = () => (stopped = true);
|
||||
for (const el of els) {
|
||||
if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.canSelect(el, stop))) {
|
||||
if (stopped) break;
|
||||
this.select(el, event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中组件
|
||||
* @param idOrEl 组件Dom节点的id属性,或者Dom节点
|
||||
*/
|
||||
public async select(idOrEl: Id | HTMLElement): Promise<void> {
|
||||
public async select(idOrEl: Id | HTMLElement, event?: MouseEvent): Promise<void> {
|
||||
let el;
|
||||
if (typeof idOrEl === 'string' || typeof idOrEl === 'number') {
|
||||
const runtime = await this.renderer?.getRuntime();
|
||||
@ -78,15 +124,22 @@ export default class StageCore extends EventEmitter {
|
||||
el = idOrEl;
|
||||
}
|
||||
|
||||
if (this.selectedDom === el) return;
|
||||
if (el === this.selectedDom) return;
|
||||
|
||||
await this.beforeSelect(el);
|
||||
const runtime = await this.renderer?.getRuntime();
|
||||
if (runtime?.beforeSelect) {
|
||||
await runtime.beforeSelect(el);
|
||||
}
|
||||
|
||||
this.mask.setLayout(el);
|
||||
this.dr?.select(el, event);
|
||||
this.selectedDom = el;
|
||||
this.setMaskLayout(el);
|
||||
this.dr?.select(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选中的节点
|
||||
* @param data 更新的数据
|
||||
*/
|
||||
public update(data: UpdateData): void {
|
||||
const { config } = data;
|
||||
|
||||
@ -97,9 +150,9 @@ export default class StageCore extends EventEmitter {
|
||||
const el = this.renderer.contentWindow?.document.getElementById(`${config.id}`);
|
||||
if (el) {
|
||||
// 更新了组件的布局,需要重新设置mask是否可以滚动
|
||||
this.setMaskLayout(el);
|
||||
this.mask.setLayout(el);
|
||||
this.dr?.select(el);
|
||||
}
|
||||
this.dr?.refresh();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
@ -130,12 +183,6 @@ export default class StageCore extends EventEmitter {
|
||||
renderer.mount(el);
|
||||
mask.mount(el);
|
||||
|
||||
const { wrapper: maskWrapper, hGuides, vGuides } = mask;
|
||||
|
||||
maskWrapper.addEventListener('scroll', this.maskScrollHandler);
|
||||
hGuides.on('changeGuides', this.hGuidesChangeGuidesHandler);
|
||||
vGuides.on('changeGuides', this.vGuidesChangeGuidesHandler);
|
||||
|
||||
this.emit('mounted');
|
||||
}
|
||||
|
||||
@ -144,8 +191,7 @@ export default class StageCore extends EventEmitter {
|
||||
*/
|
||||
public clearGuides() {
|
||||
this.mask.clearGuides();
|
||||
this.dr.setHGuidelines([]);
|
||||
this.dr.setVGuidelines([]);
|
||||
this.dr.clearGuides();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,69 +199,11 @@ export default class StageCore extends EventEmitter {
|
||||
*/
|
||||
public destroy(): void {
|
||||
const { mask, renderer, dr } = this;
|
||||
const { wrapper: maskWrapper, hGuides, vGuides } = mask;
|
||||
|
||||
renderer.destroy();
|
||||
mask.destroy();
|
||||
dr.destroy();
|
||||
maskWrapper.removeEventListener('scroll', this.maskScrollHandler);
|
||||
hGuides.off('changeGuides', this.hGuidesChangeGuidesHandler);
|
||||
vGuides.off('changeGuides', this.vGuidesChangeGuidesHandler);
|
||||
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
private maskScrollHandler = (event: Event) => {
|
||||
const { mask, renderer } = this;
|
||||
const { wrapper: maskWrapper, hGuides, vGuides } = mask;
|
||||
const { scrollTop } = maskWrapper;
|
||||
|
||||
renderer?.contentWindow?.document.documentElement.scrollTo({ top: scrollTop });
|
||||
|
||||
hGuides.scrollGuides(scrollTop);
|
||||
hGuides.scroll(0);
|
||||
|
||||
vGuides.scrollGuides(0);
|
||||
vGuides.scroll(scrollTop);
|
||||
|
||||
this.emit('scroll', event);
|
||||
};
|
||||
|
||||
private hGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
|
||||
this.dr.setHGuidelines(e.guides);
|
||||
this.emit('changeGuides');
|
||||
};
|
||||
|
||||
private vGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
|
||||
this.dr.setVGuidelines(e.guides);
|
||||
this.emit('changeGuides');
|
||||
};
|
||||
|
||||
private setMaskLayout(el: HTMLElement): void {
|
||||
let fixed = false;
|
||||
let dom = el;
|
||||
while (dom) {
|
||||
fixed = isFixed(dom);
|
||||
if (fixed) {
|
||||
break;
|
||||
}
|
||||
const { parentElement } = dom;
|
||||
if (!parentElement || parentElement.tagName === 'BODY') {
|
||||
break;
|
||||
}
|
||||
dom = parentElement;
|
||||
}
|
||||
|
||||
if (fixed) {
|
||||
this.mask?.setFixed();
|
||||
} else {
|
||||
this.mask?.setAbsolute();
|
||||
}
|
||||
}
|
||||
|
||||
private async beforeSelect(el: HTMLElement): Promise<void> {
|
||||
const runtime = await this.renderer?.getRuntime();
|
||||
if (runtime?.beforeSelect) {
|
||||
await runtime.beforeSelect(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ import { EventEmitter } from 'events';
|
||||
import type { MoveableOptions } from 'moveable';
|
||||
import Moveable from 'moveable';
|
||||
|
||||
import { GHOST_EL_ID_PREFIX } from './const';
|
||||
import { GHOST_EL_ID_PREFIX, GuidesType, Mode } from './const';
|
||||
import StageCore from './StageCore';
|
||||
import type { SortEventData, StageDragResizeConfig } from './types';
|
||||
import { getAbsolutePosition, getMode, getOffset, Mode } from './util';
|
||||
import { getAbsolutePosition, getMode, getOffset } from './util';
|
||||
|
||||
enum ActionStatus {
|
||||
START = 'start',
|
||||
@ -40,12 +40,13 @@ export default class StageDragResize extends EventEmitter {
|
||||
public core: StageCore;
|
||||
public container: HTMLElement;
|
||||
public target?: HTMLElement;
|
||||
public dragEl?: HTMLElement;
|
||||
public moveable?: Moveable;
|
||||
public horizontalGuidelines: number[] = [];
|
||||
public verticalGuidelines: number[] = [];
|
||||
|
||||
private moveableOptions: MoveableOptions = {};
|
||||
private dragStatus: ActionStatus = ActionStatus.END;
|
||||
private elObserver?: ResizeObserver;
|
||||
private ghostEl: HTMLElement | undefined;
|
||||
private mode: Mode = Mode.ABSOLUTE;
|
||||
|
||||
@ -54,81 +55,86 @@ export default class StageDragResize extends EventEmitter {
|
||||
|
||||
this.core = config.core;
|
||||
this.container = config.container;
|
||||
this.initObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将选中框渲染并覆盖到选中的组件Dom节点上方
|
||||
* 当选中的节点是不是absolute时,会创建一个新的节点出来作为拖拽目标
|
||||
* @param el 选中组件的Dom节点元素
|
||||
* @param event 鼠标事件
|
||||
*/
|
||||
public async select(el: HTMLElement): Promise<void> {
|
||||
if (this.target === el) {
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveable?.destroy();
|
||||
|
||||
public async select(el: HTMLElement, event?: MouseEvent): Promise<void> {
|
||||
this.target = el;
|
||||
this.mode = getMode(el);
|
||||
this.destroyDragEl();
|
||||
this.destroyGhostEl();
|
||||
|
||||
const options = await this.getOptions();
|
||||
if (this.mode !== Mode.ABSOLUTE) {
|
||||
this.dragEl = this.generateDragEl(el);
|
||||
}
|
||||
|
||||
this.moveable = new Moveable(this.container, options);
|
||||
this.bindResizeEvent();
|
||||
this.bindDragEvent();
|
||||
this.moveableOptions = await this.getOptions({
|
||||
target: this.dragEl || this.target,
|
||||
});
|
||||
|
||||
this.syncRect(el);
|
||||
this.initMoveable();
|
||||
|
||||
if (event) {
|
||||
this.moveable?.dragStart(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化选中框并渲染出来
|
||||
* @param param0
|
||||
*/
|
||||
|
||||
public async refresh() {
|
||||
if (!this.moveable) throw new Error('未初始化moveable');
|
||||
|
||||
const options = await this.getOptions();
|
||||
Object.entries(options).forEach(([key, value]) => {
|
||||
(this.moveable as any)[key] = value;
|
||||
});
|
||||
this.updateMoveableTarget();
|
||||
}
|
||||
|
||||
public async setVGuidelines(verticalGuidelines: number[]): Promise<void> {
|
||||
this.verticalGuidelines = verticalGuidelines;
|
||||
this.target && (await this.select(this.target));
|
||||
}
|
||||
|
||||
public async setHGuidelines(horizontalGuidelines: number[]): Promise<void> {
|
||||
this.horizontalGuidelines = horizontalGuidelines;
|
||||
this.target && (await this.select(this.target));
|
||||
}
|
||||
|
||||
public updateMoveableTarget(target?: HTMLElement): void {
|
||||
if (!this.moveable) throw new Error('为初始化目标');
|
||||
|
||||
if (target) {
|
||||
this.moveable.target = target;
|
||||
}
|
||||
|
||||
if (this.target) {
|
||||
this.mode = getMode(this.target);
|
||||
}
|
||||
|
||||
this.moveable.updateTarget();
|
||||
}
|
||||
|
||||
public setGuidelines(type: GuidesType, guidelines: number[]): void {
|
||||
if (type === GuidesType.HORIZONTAL) {
|
||||
this.horizontalGuidelines = guidelines;
|
||||
} else if (type === GuidesType.VERTICAL) {
|
||||
this.verticalGuidelines = guidelines;
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public clearGuides() {
|
||||
this.verticalGuidelines = [];
|
||||
this.horizontalGuidelines = [];
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁实例
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.destroyGhostEl();
|
||||
this.moveable?.destroy();
|
||||
this.destroyGhostEl();
|
||||
this.destroyDragEl();
|
||||
this.dragStatus = ActionStatus.END;
|
||||
this.elObserver?.disconnect();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
private initMoveable() {
|
||||
this.moveable?.destroy();
|
||||
|
||||
this.moveable = new Moveable(this.container, {
|
||||
...this.moveableOptions,
|
||||
});
|
||||
|
||||
this.bindResizeEvent();
|
||||
this.bindDragEvent();
|
||||
}
|
||||
|
||||
private bindResizeEvent(): void {
|
||||
if (!this.moveable) throw new Error('moveable 为初始化');
|
||||
|
||||
@ -142,50 +148,38 @@ export default class StageDragResize extends EventEmitter {
|
||||
const rect = this.moveable!.getRect();
|
||||
const offset = getAbsolutePosition(e.target as HTMLElement, rect);
|
||||
e.dragStart.set([offset.left, offset.top]);
|
||||
|
||||
if (this.ghostEl) {
|
||||
this.destroyGhostEl();
|
||||
this.updateMoveableTarget(this.target);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('resize', ({ target, width, height, drag }) => {
|
||||
if (!this.moveable) return;
|
||||
if (!this.target) return;
|
||||
.on('resize', ({ width, height, drag }) => {
|
||||
if (!this.moveable || !this.target) return;
|
||||
const { beforeTranslate } = drag;
|
||||
frame.translate = beforeTranslate;
|
||||
this.dragStatus = ActionStatus.ING;
|
||||
|
||||
target.style.width = `${width}px`;
|
||||
target.style.height = `${height}px`;
|
||||
this.target.style.width = `${width}px`;
|
||||
this.target.style.height = `${height}px`;
|
||||
|
||||
if ([Mode.ABSOLUTE, Mode.FIXED].includes(this.mode)) {
|
||||
target.style.left = `${beforeTranslate[0]}px`;
|
||||
target.style.top = `${beforeTranslate[1]}px`;
|
||||
// 流式布局
|
||||
if (this.mode === Mode.SORTABLE && this.ghostEl) {
|
||||
this.target.style.top = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
this.target.style.left = `${beforeTranslate[0]}px`;
|
||||
this.target.style.top = `${beforeTranslate[1]}px`;
|
||||
|
||||
if (this.dragEl) {
|
||||
this.dragEl.style.width = `${width}px`;
|
||||
this.dragEl.style.height = `${height}px`;
|
||||
|
||||
const offset = getAbsolutePosition(this.target, { left: beforeTranslate[0], top: beforeTranslate[1] });
|
||||
this.dragEl.style.left = `${offset.left}px`;
|
||||
this.dragEl.style.top = `${offset.top}px`;
|
||||
}
|
||||
})
|
||||
.on('resizeEnd', ({ target }) => {
|
||||
.on('resizeEnd', () => {
|
||||
this.dragStatus = ActionStatus.END;
|
||||
|
||||
const rect = this.moveable!.getRect();
|
||||
const offset = getAbsolutePosition(target as HTMLElement, rect);
|
||||
|
||||
this.updateMoveableTarget(this.target);
|
||||
|
||||
this.emit('update', {
|
||||
el: this.target,
|
||||
style: {
|
||||
width: this.calcValueByFontsize(rect.width),
|
||||
height: this.calcValueByFontsize(rect.height),
|
||||
position: this.target?.style.position,
|
||||
...(this.mode === Mode.SORTABLE
|
||||
? {}
|
||||
: {
|
||||
left: this.calcValueByFontsize(offset.left),
|
||||
top: this.calcValueByFontsize(offset.top),
|
||||
}),
|
||||
},
|
||||
});
|
||||
this.drag();
|
||||
});
|
||||
}
|
||||
|
||||
@ -200,42 +194,43 @@ export default class StageDragResize extends EventEmitter {
|
||||
|
||||
if (this.mode === Mode.SORTABLE) {
|
||||
this.ghostEl = this.generateGhostEl(this.target);
|
||||
this.updateMoveableTarget(this.ghostEl);
|
||||
}
|
||||
})
|
||||
.on('drag', ({ target, left, top }) => {
|
||||
.on('drag', ({ left, top }) => {
|
||||
if (!this.target) return;
|
||||
this.dragStatus = ActionStatus.ING;
|
||||
if (this.mode === Mode.SORTABLE && (!this.ghostEl || target !== this.ghostEl)) {
|
||||
|
||||
const offset = getAbsolutePosition(this.target, { left, top });
|
||||
|
||||
// 流式布局
|
||||
if (this.ghostEl && this.dragEl) {
|
||||
this.dragEl.style.top = `${top}px`;
|
||||
this.ghostEl.style.top = `${offset.top}px`;
|
||||
return;
|
||||
}
|
||||
|
||||
if ([Mode.ABSOLUTE, Mode.FIXED].includes(this.mode)) {
|
||||
target.style.left = `${left}px`;
|
||||
target.style.top = `${top}px`;
|
||||
} else if (this.target) {
|
||||
const offset = getAbsolutePosition(this.target, getOffset(this.target));
|
||||
target.style.top = `${offset.top + top}px`;
|
||||
// 固定布局
|
||||
if (this.dragEl) {
|
||||
this.dragEl.style.left = `${left}px`;
|
||||
this.dragEl.style.top = `${top}px`;
|
||||
}
|
||||
|
||||
this.target.style.left = `${left}px`;
|
||||
this.target.style.top = `${top}px`;
|
||||
})
|
||||
.on('dragEnd', () => {
|
||||
// 点击不拖动时会触发dragStart和dragEnd,但是不会有drag事件
|
||||
if (this.dragStatus !== ActionStatus.ING) {
|
||||
return;
|
||||
if (this.dragStatus === ActionStatus.ING) {
|
||||
switch (this.mode) {
|
||||
case Mode.SORTABLE:
|
||||
this.sort();
|
||||
break;
|
||||
default:
|
||||
this.drag();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.target) return;
|
||||
|
||||
this.dragStatus = ActionStatus.END;
|
||||
this.updateMoveableTarget(this.target);
|
||||
|
||||
switch (this.mode) {
|
||||
case Mode.SORTABLE:
|
||||
this.sort();
|
||||
break;
|
||||
default:
|
||||
this.drag();
|
||||
}
|
||||
|
||||
this.destroyGhostEl();
|
||||
});
|
||||
}
|
||||
@ -246,12 +241,13 @@ export default class StageDragResize extends EventEmitter {
|
||||
(await renderer.getRuntime())?.getSnapElements ||
|
||||
(() => {
|
||||
const doc = renderer.contentWindow?.document;
|
||||
const elementGuidelines = (doc ? Array.from(doc.querySelectorAll('[id]')) : [])
|
||||
// 排除掉当前组件本身
|
||||
.filter((element) => element !== this.target && !this.target?.contains(element));
|
||||
return elementGuidelines as HTMLElement[];
|
||||
return doc ? Array.from(doc.querySelectorAll('[id]')) : [];
|
||||
});
|
||||
return getSnapElements(el);
|
||||
return (
|
||||
getSnapElements(el)
|
||||
// 排除掉当前组件本身
|
||||
.filter((element) => element !== this.target && !this.target?.contains(element))
|
||||
);
|
||||
}
|
||||
|
||||
private sort(): void {
|
||||
@ -275,13 +271,14 @@ export default class StageDragResize extends EventEmitter {
|
||||
|
||||
private drag(): void {
|
||||
const rect = this.moveable!.getRect();
|
||||
const offset = getAbsolutePosition(this.target as HTMLElement, rect);
|
||||
const offset =
|
||||
this.mode === Mode.SORTABLE ? { left: 0, top: 0 } : getAbsolutePosition(this.target as HTMLElement, rect);
|
||||
|
||||
this.emit('update', {
|
||||
el: this.target,
|
||||
style: {
|
||||
left: this.calcValueByFontsize(this.mode === Mode.FIXED ? rect.left : offset.left),
|
||||
top: this.calcValueByFontsize(this.mode === Mode.FIXED ? rect.top : offset.top),
|
||||
left: this.calcValueByFontsize(offset.left),
|
||||
top: this.calcValueByFontsize(offset.top),
|
||||
width: this.calcValueByFontsize(rect.width),
|
||||
height: this.calcValueByFontsize(rect.height),
|
||||
},
|
||||
@ -310,14 +307,35 @@ export default class StageDragResize extends EventEmitter {
|
||||
this.ghostEl = undefined;
|
||||
}
|
||||
|
||||
private generateDragEl(el: HTMLElement): HTMLElement {
|
||||
if (this.dragEl) {
|
||||
this.destroyDragEl();
|
||||
}
|
||||
|
||||
const { left, top, width, height } = el.getBoundingClientRect();
|
||||
const dragEl = globalThis.document.createElement('div');
|
||||
dragEl.style.cssText = `
|
||||
position: absolute;
|
||||
left: ${left}px;
|
||||
top: ${top}px;
|
||||
width: ${width}px;
|
||||
height: ${height}px;
|
||||
`;
|
||||
this.container.append(dragEl);
|
||||
return dragEl;
|
||||
}
|
||||
|
||||
private destroyDragEl(): void {
|
||||
this.dragEl?.remove();
|
||||
this.dragEl = undefined;
|
||||
}
|
||||
|
||||
private async getOptions(options: MoveableOptions = {}): Promise<MoveableOptions> {
|
||||
if (!this.target) return {};
|
||||
|
||||
const isSortable = this.mode === Mode.SORTABLE;
|
||||
const { config, renderer, mask } = this.core;
|
||||
const { iframe } = renderer;
|
||||
const isAbsolute = this.mode === Mode.ABSOLUTE;
|
||||
|
||||
let { moveableOptions = {} } = config;
|
||||
let { moveableOptions = {} } = this.core.config;
|
||||
|
||||
if (typeof moveableOptions === 'function') {
|
||||
moveableOptions = moveableOptions(this.core);
|
||||
@ -326,25 +344,26 @@ export default class StageDragResize extends EventEmitter {
|
||||
const boundsOptions = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: iframe?.clientWidth,
|
||||
bottom: this.mode === Mode.FIXED ? iframe?.clientHeight : mask.page?.clientHeight,
|
||||
right: this.container.clientWidth,
|
||||
bottom: this.container.clientHeight,
|
||||
...(moveableOptions.bounds || {}),
|
||||
};
|
||||
|
||||
return {
|
||||
target: this.target,
|
||||
scrollable: true,
|
||||
origin: true,
|
||||
zoom: 1,
|
||||
dragArea: true,
|
||||
draggable: true,
|
||||
resizable: true,
|
||||
snappable: !isSortable,
|
||||
snapGap: !isSortable,
|
||||
snapCenter: !isSortable,
|
||||
container: renderer.contentWindow?.document.body,
|
||||
snappable: isAbsolute,
|
||||
snapGap: isAbsolute,
|
||||
snapElement: isAbsolute,
|
||||
snapVertical: isAbsolute,
|
||||
snapHorizontal: isAbsolute,
|
||||
snapCenter: isAbsolute,
|
||||
|
||||
elementGuidelines: isSortable ? [] : await this.getSnapElements(this.target),
|
||||
elementGuidelines: !isAbsolute ? [] : await this.getSnapElements(this.target),
|
||||
horizontalGuidelines: this.horizontalGuidelines,
|
||||
verticalGuidelines: this.verticalGuidelines,
|
||||
|
||||
@ -354,36 +373,6 @@ export default class StageDragResize extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
private initObserver(): void {
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.elObserver = new ResizeObserver(() => {
|
||||
const doc = this.core.renderer.contentWindow?.document;
|
||||
if (!doc || !this.target || !this.moveable) return;
|
||||
|
||||
/** 组件可能已经重新渲染了,所以需要重新获取新的dom */
|
||||
const target = doc.getElementById(this.target.id);
|
||||
|
||||
if (this.ghostEl) {
|
||||
this.destroyGhostEl();
|
||||
}
|
||||
|
||||
if (target && target !== this.target) {
|
||||
this.syncRect(target);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
this.updateMoveableTarget(this.target);
|
||||
});
|
||||
}
|
||||
|
||||
private syncRect(el: HTMLElement): void {
|
||||
this.elObserver?.disconnect();
|
||||
this.elObserver?.observe(el);
|
||||
}
|
||||
|
||||
private calcValueByFontsize(value: number) {
|
||||
const { contentWindow } = this.core.renderer;
|
||||
const fontSize = contentWindow?.document.documentElement.style.fontSize;
|
||||
|
@ -16,15 +16,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import Guides from '@scena/guides';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
import { GHOST_EL_ID_PREFIX } from './const';
|
||||
import { Mode, MouseButton, ZIndex } from './const';
|
||||
import Rule from './Rule';
|
||||
import type StageCore from './StageCore';
|
||||
import type { CanSelect, StageMaskConfig } from './types';
|
||||
import { MouseButton, ZIndex } from './types';
|
||||
import type { StageMaskConfig } from './types';
|
||||
import { createDiv, getScrollParent, isFixed } from './util';
|
||||
|
||||
const wrapperClassName = 'editor-mask-wrapper';
|
||||
|
||||
@ -36,30 +32,30 @@ const hideScrollbar = () => {
|
||||
globalThis.document.head.appendChild(style);
|
||||
};
|
||||
|
||||
const createContent = (): HTMLDivElement => {
|
||||
const el = globalThis.document.createElement('div');
|
||||
el.className = 'editor-mask';
|
||||
el.style.cssText = `
|
||||
const createContent = (): HTMLDivElement =>
|
||||
createDiv({
|
||||
className: 'editor-mask',
|
||||
cssText: `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate3d(0, 0, 0);
|
||||
`;
|
||||
return el;
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
const createWrapper = (): HTMLDivElement => {
|
||||
const el = globalThis.document.createElement('div');
|
||||
el.className = wrapperClassName;
|
||||
el.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
z-index: ${ZIndex.MASK};
|
||||
`;
|
||||
const el = createDiv({
|
||||
className: wrapperClassName,
|
||||
cssText: `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
z-index: ${ZIndex.MASK};
|
||||
`,
|
||||
});
|
||||
|
||||
hideScrollbar();
|
||||
|
||||
@ -70,54 +66,45 @@ const createWrapper = (): HTMLDivElement => {
|
||||
* 蒙层
|
||||
* @description 用于拦截页面的点击动作,避免点击时触发组件自身动作;在编辑器中点击组件应当是选中组件;
|
||||
*/
|
||||
export default class StageMask extends EventEmitter {
|
||||
export default class StageMask extends Rule {
|
||||
public content: HTMLDivElement = createContent();
|
||||
public wrapper: HTMLDivElement = createWrapper();
|
||||
public wrapper: HTMLDivElement;
|
||||
public core: StageCore;
|
||||
public page: HTMLElement | null = null;
|
||||
public pageScrollParent: HTMLElement | null = null;
|
||||
public scrollTop = 0;
|
||||
public scrollLeft = 0;
|
||||
public width = 0;
|
||||
public height = 0;
|
||||
public wrapperHeight = 0;
|
||||
public wrapperWidth = 0;
|
||||
|
||||
public hGuides: Guides;
|
||||
public vGuides: Guides;
|
||||
|
||||
private target: Element | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private parentResizeObserver: ResizeObserver | null = null;
|
||||
private canSelect: CanSelect;
|
||||
private mode: Mode = Mode.ABSOLUTE;
|
||||
private pageResizeObserver: ResizeObserver | null = null;
|
||||
private wrapperResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
constructor(config: StageMaskConfig) {
|
||||
super();
|
||||
const wrapper = createWrapper();
|
||||
super(wrapper);
|
||||
|
||||
this.wrapper = wrapper;
|
||||
this.core = config.core;
|
||||
const { config: coreConfig } = config.core;
|
||||
|
||||
this.canSelect = coreConfig.canSelect || ((el: HTMLElement) => !!el.id);
|
||||
this.content.addEventListener('mousedown', this.mouseDownHandler);
|
||||
this.content.addEventListener('contextmenu', this.contextmenuHandler);
|
||||
this.wrapper.appendChild(this.content);
|
||||
|
||||
this.hGuides = this.createGuides('horizontal');
|
||||
this.vGuides = this.createGuides('vertical');
|
||||
this.content.addEventListener('wheel', this.mouseWheelHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置成固定定位模式
|
||||
*/
|
||||
public setFixed(): void {
|
||||
this.wrapper.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
this.wrapper.style.overflow = 'hidden';
|
||||
// 要等滚动条滚上去,才刷新选中框
|
||||
setTimeout(() => {
|
||||
this.core.dr.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置成绝对定位模式
|
||||
*/
|
||||
public setAbsolute(): void {
|
||||
this.wrapper.style.overflow = 'auto';
|
||||
public setMode(mode: Mode) {
|
||||
this.mode = mode;
|
||||
this.scroll();
|
||||
if (mode === Mode.FIXED) {
|
||||
this.content.style.width = `${this.wrapperWidth}px`;
|
||||
this.content.style.height = `${this.wrapperHeight}px`;
|
||||
} else {
|
||||
this.content.style.width = `${this.width}px`;
|
||||
this.content.style.height = `${this.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,17 +116,26 @@ export default class StageMask extends EventEmitter {
|
||||
if (!page) return;
|
||||
|
||||
this.page = page;
|
||||
this.resizeObserver?.disconnect();
|
||||
this.pageScrollParent = getScrollParent(page);
|
||||
this.pageResizeObserver?.disconnect();
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
this.pageResizeObserver = new ResizeObserver((entries) => {
|
||||
const [entry] = entries;
|
||||
const { clientHeight, clientWidth } = entry.target;
|
||||
this.setHeight(clientHeight);
|
||||
this.setWidth(clientWidth);
|
||||
});
|
||||
|
||||
this.resizeObserver.observe(page);
|
||||
this.pageResizeObserver.observe(page);
|
||||
|
||||
this.wrapperResizeObserver = new ResizeObserver((entries) => {
|
||||
const [entry] = entries;
|
||||
const { clientHeight, clientWidth } = entry.target;
|
||||
this.wrapperHeight = clientHeight;
|
||||
this.wrapperWidth = clientWidth;
|
||||
});
|
||||
this.wrapperResizeObserver.observe(this.wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,12 +147,24 @@ export default class StageMask extends EventEmitter {
|
||||
if (!this.content) throw new Error('content 不存在');
|
||||
|
||||
el.appendChild(this.wrapper);
|
||||
}
|
||||
|
||||
this.parentResizeObserver = new ResizeObserver(() => {
|
||||
this.vGuides.resize();
|
||||
this.hGuides.resize();
|
||||
});
|
||||
this.parentResizeObserver.observe(el);
|
||||
public setLayout(el: HTMLElement): void {
|
||||
let fixed = false;
|
||||
let dom = el;
|
||||
while (dom) {
|
||||
fixed = isFixed(dom);
|
||||
if (fixed) {
|
||||
break;
|
||||
}
|
||||
const { parentElement } = dom;
|
||||
if (!parentElement || parentElement.tagName === 'BODY') {
|
||||
break;
|
||||
}
|
||||
dom = parentElement;
|
||||
}
|
||||
|
||||
this.setMode(fixed ? Mode.FIXED : Mode.ABSOLUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -165,78 +173,44 @@ export default class StageMask extends EventEmitter {
|
||||
public destroy(): void {
|
||||
this.content?.remove();
|
||||
this.page = null;
|
||||
this.resizeObserver?.disconnect();
|
||||
this.parentResizeObserver?.disconnect();
|
||||
this.removeAllListeners();
|
||||
this.pageScrollParent = null;
|
||||
this.pageResizeObserver?.disconnect();
|
||||
this.wrapperResizeObserver?.disconnect();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示标尺
|
||||
* @param show 是否显示
|
||||
*/
|
||||
public showGuides(show = true) {
|
||||
this.hGuides.setState({
|
||||
showGuides: show,
|
||||
});
|
||||
private scroll() {
|
||||
let { scrollLeft, scrollTop } = this;
|
||||
|
||||
this.vGuides.setState({
|
||||
showGuides: show,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示标尺
|
||||
* @param show 是否显示
|
||||
*/
|
||||
public showRule(show = true) {
|
||||
// 当尺子隐藏时发现大小变化,显示后会变形,所以这里做重新初始化处理
|
||||
if (show) {
|
||||
this.hGuides.destroy();
|
||||
this.hGuides = this.createGuides('horizontal', this.core.dr.horizontalGuidelines);
|
||||
|
||||
this.vGuides.destroy();
|
||||
this.vGuides = this.createGuides('vertical', this.core.dr.verticalGuidelines);
|
||||
} else {
|
||||
this.hGuides.setState({
|
||||
rulerStyle: {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
this.vGuides.setState({
|
||||
rulerStyle: {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
});
|
||||
if (this.mode === Mode.FIXED) {
|
||||
scrollLeft = 0;
|
||||
scrollTop = 0;
|
||||
}
|
||||
|
||||
this.scrollRule(scrollTop);
|
||||
this.scrollTo(scrollLeft, scrollTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有参考线
|
||||
*/
|
||||
public clearGuides() {
|
||||
this.vGuides.setState({
|
||||
defaultGuides: [],
|
||||
});
|
||||
this.hGuides.setState({
|
||||
defaultGuides: [],
|
||||
});
|
||||
private scrollTo(scrollLeft: number, scrollTop: number): void {
|
||||
this.content.style.transform = `translate3d(${-scrollLeft}px, ${-scrollTop}px, 0)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置蒙层高度
|
||||
* @param height 高度
|
||||
*/
|
||||
private setHeight(height: number | string): void {
|
||||
this.content.style.height = isNumber(height) ? `${height}px` : height;
|
||||
private setHeight(height: number): void {
|
||||
this.height = height;
|
||||
this.content.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置蒙层宽度
|
||||
* @param width 宽度
|
||||
*/
|
||||
private setWidth(width: number | string): void {
|
||||
this.content.style.width = isNumber(width) ? `${width}px` : width;
|
||||
private setWidth(width: number): void {
|
||||
this.width = width;
|
||||
this.content.style.width = `${width}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -244,86 +218,64 @@ export default class StageMask extends EventEmitter {
|
||||
* @param event 事件对象
|
||||
*/
|
||||
private mouseDownHandler = async (event: MouseEvent): Promise<void> => {
|
||||
if (event.button !== MouseButton.LEFT) return;
|
||||
event.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.button !== MouseButton.LEFT && event.button !== MouseButton.RIGHT) return;
|
||||
|
||||
// 点击的对象如果是选中框,则不需要再触发选中了,而可能是拖动行为
|
||||
if ((event.target as HTMLDivElement).className.indexOf('moveable-control') !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.content.addEventListener('mousemove', this.mouseMoveHandler);
|
||||
this.emit('beforeSelect', event);
|
||||
|
||||
this.select(event);
|
||||
// 如果是右键点击,这里的mouseup事件监听没有效果
|
||||
globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
|
||||
};
|
||||
|
||||
private mouseUpHandler = (): void => {
|
||||
this.content.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||
this.content.removeEventListener('mouseup', this.mouseUpHandler);
|
||||
this.emit('selected', this.target);
|
||||
this.target = null;
|
||||
globalThis.document.removeEventListener('mouseup', this.mouseUpHandler);
|
||||
this.emit('select');
|
||||
};
|
||||
|
||||
private mouseMoveHandler = (event: MouseEvent): void => {
|
||||
// 避免触摸板轻触移动拖动组件
|
||||
if (event.buttons) {
|
||||
this.core.dr.moveable?.dragStart(event);
|
||||
}
|
||||
this.content.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||
};
|
||||
private mouseWheelHandler = (event: WheelEvent) => {
|
||||
if (!this.page) throw new Error('page 未初始化');
|
||||
|
||||
private contextmenuHandler = async (event: MouseEvent): Promise<void> => {
|
||||
await this.select(event);
|
||||
this.mouseUpHandler();
|
||||
};
|
||||
const { deltaY, deltaX } = event;
|
||||
const { height, wrapperHeight, width, wrapperWidth } = this;
|
||||
|
||||
private async select(event: MouseEvent) {
|
||||
const { renderer, zoom } = this.core;
|
||||
const maxScrollTop = height - wrapperHeight;
|
||||
const maxScrollLeft = width - wrapperWidth;
|
||||
|
||||
const doc = renderer.contentWindow?.document;
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
|
||||
if (renderer.iframe) {
|
||||
const rect = renderer.iframe.getClientRects()[0];
|
||||
if (rect) {
|
||||
x = x - rect.left;
|
||||
y = y - rect.top;
|
||||
if (maxScrollTop > 0) {
|
||||
if (deltaY > 0) {
|
||||
this.scrollTop = this.scrollTop + Math.min(maxScrollTop - this.scrollTop, deltaY);
|
||||
} else {
|
||||
this.scrollTop = Math.max(this.scrollTop + deltaY, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const els = doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
|
||||
|
||||
let stopped = false;
|
||||
const stop = () => (stopped = true);
|
||||
for (const el of els) {
|
||||
if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.canSelect(el, stop))) {
|
||||
if (stopped) break;
|
||||
|
||||
this.emit('select', el, event);
|
||||
this.target = el;
|
||||
// 如果是右键点击,这里的mouseup事件监听没有效果
|
||||
this.content.addEventListener('mouseup', this.mouseUpHandler);
|
||||
break;
|
||||
if (width > wrapperWidth) {
|
||||
if (deltaX > 0) {
|
||||
this.scrollLeft = this.scrollLeft + Math.min(maxScrollLeft - this.scrollLeft, deltaX);
|
||||
} else {
|
||||
this.scrollLeft = Math.max(this.scrollLeft + deltaX, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getGuidesStyle = (type: 'horizontal' | 'vertical') => ({
|
||||
position: 'fixed',
|
||||
left: type === 'horizontal' ? 0 : '-30px',
|
||||
top: type === 'horizontal' ? '-30px' : 0,
|
||||
width: type === 'horizontal' ? '100%' : '30px',
|
||||
height: type === 'horizontal' ? '30px' : '100%',
|
||||
});
|
||||
if (this.mode !== Mode.FIXED) {
|
||||
this.scrollTo(this.scrollLeft, this.scrollTop);
|
||||
}
|
||||
|
||||
private createGuides = (type: 'horizontal' | 'vertical', defaultGuides: number[] = []): Guides =>
|
||||
new Guides(this.wrapper, {
|
||||
type,
|
||||
defaultGuides,
|
||||
displayDragPos: true,
|
||||
backgroundColor: '#fff',
|
||||
lineColor: '#000',
|
||||
textColor: '#000',
|
||||
style: this.getGuidesStyle(type),
|
||||
});
|
||||
if (this.pageScrollParent) {
|
||||
this.pageScrollParent.scrollTo({
|
||||
top: this.scrollTop,
|
||||
left: this.scrollLeft,
|
||||
});
|
||||
}
|
||||
this.scroll();
|
||||
|
||||
this.emit('scroll', event);
|
||||
};
|
||||
}
|
||||
|
@ -21,3 +21,24 @@ export const GHOST_EL_ID_PREFIX = 'ghost_el_';
|
||||
|
||||
// 默认放到缩小倍数
|
||||
export const DEFAULT_ZOOM = 1;
|
||||
|
||||
export enum GuidesType {
|
||||
HORIZONTAL = 'horizontal',
|
||||
VERTICAL = 'vertical',
|
||||
}
|
||||
|
||||
export enum ZIndex {
|
||||
MASK = '99999',
|
||||
}
|
||||
|
||||
export enum MouseButton {
|
||||
LEFT = 0,
|
||||
MIDDLE = 1,
|
||||
RIGHT = 2,
|
||||
}
|
||||
|
||||
export enum Mode {
|
||||
ABSOLUTE = 'absolute',
|
||||
FIXED = 'fixed',
|
||||
SORTABLE = 'sortable',
|
||||
}
|
||||
|
@ -1,7 +1,26 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MoveableOptions } from 'react-moveable/declaration/types';
|
||||
|
||||
import { Id, MApp, MNode } from '@tmagic/schema';
|
||||
|
||||
import { GuidesType } from './const';
|
||||
import StageCore from './StageCore';
|
||||
|
||||
export type CanSelect = (el: HTMLElement, stop: () => boolean) => boolean | Promise<boolean>;
|
||||
@ -41,23 +60,10 @@ export interface Offset {
|
||||
top: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface GuidesEventData {
|
||||
type: GuidesType;
|
||||
guides: number[];
|
||||
}
|
||||
|
||||
export interface UpdateEventData {
|
||||
el: HTMLElement;
|
||||
@ -108,14 +114,3 @@ export interface Magic {
|
||||
export interface RuntimeWindow extends Window {
|
||||
magic: Magic;
|
||||
}
|
||||
|
||||
export enum ZIndex {
|
||||
MASK = '99999',
|
||||
GHOST_EL = '99998',
|
||||
}
|
||||
|
||||
export enum MouseButton {
|
||||
LEFT = 0,
|
||||
MIDDLE = 1,
|
||||
RIGHT = 2,
|
||||
}
|
||||
|
@ -16,14 +16,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Mode } from './const';
|
||||
import type { Offset } from './types';
|
||||
|
||||
export enum Mode {
|
||||
ABSOLUTE = 'absolute',
|
||||
FIXED = 'fixed',
|
||||
SORTABLE = 'sortable',
|
||||
}
|
||||
|
||||
export const getOffset = (el: HTMLElement): Offset => {
|
||||
const { transform } = getComputedStyle(el);
|
||||
const { offsetParent } = el;
|
||||
@ -118,3 +113,27 @@ export const getMode = (el: HTMLElement): Mode => {
|
||||
if (isStatic(el) || isRelative(el)) return Mode.SORTABLE;
|
||||
return Mode.ABSOLUTE;
|
||||
};
|
||||
|
||||
export const getScrollParent = (element: HTMLElement, includeHidden = false): HTMLElement | null => {
|
||||
let style = getComputedStyle(element);
|
||||
const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
|
||||
|
||||
if (isFixed(element)) return null;
|
||||
for (let parent = element; parent.parentElement; ) {
|
||||
parent = parent.parentElement;
|
||||
style = getComputedStyle(parent);
|
||||
if (isAbsolute(element) && isStatic(element)) {
|
||||
continue;
|
||||
}
|
||||
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createDiv = ({ className, cssText }: { className: string; cssText: string }) => {
|
||||
const el = globalThis.document.createElement('div');
|
||||
el.className = className;
|
||||
el.style.cssText = cssText;
|
||||
return el;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user