@tmagic/editor 1.0.0-beta.1 → 1.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/style.css +7 -7
- package/dist/tmagic-editor.es.js +403 -167
- package/dist/tmagic-editor.es.js.map +1 -1
- package/dist/tmagic-editor.umd.js +424 -169
- package/dist/tmagic-editor.umd.js.map +1 -1
- package/dist/types/src/Editor.vue.d.ts +5 -5
- package/dist/types/src/components/ScrollViewer.vue.d.ts +23 -0
- package/dist/types/src/layouts/CodeEditor.vue.d.ts +5 -4
- package/dist/types/src/layouts/workspace/Stage.vue.d.ts +76 -37
- package/dist/types/src/layouts/workspace/Workspace.vue.d.ts +1 -5
- package/dist/types/src/services/ui.d.ts +10 -3
- package/dist/types/src/type.d.ts +15 -6
- package/dist/types/src/utils/scroll-viewer.d.ts +35 -0
- package/package.json +12 -10
- package/src/Editor.vue +5 -5
- package/src/components/ScrollViewer.vue +65 -0
- package/src/layouts/CodeEditor.vue +35 -59
- package/src/layouts/workspace/Stage.vue +62 -50
- package/src/layouts/workspace/Workspace.vue +2 -21
- package/src/services/editor.ts +3 -2
- package/src/services/ui.ts +36 -2
- package/src/theme/stage.scss +7 -7
- package/src/type.ts +17 -6
- package/src/utils/scroll-viewer.ts +213 -0
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<scroll-viewer
|
|
3
|
+
class="m-editor-stage"
|
|
4
|
+
ref="stageWrap"
|
|
5
|
+
:width="stageRect?.width"
|
|
6
|
+
:height="stageRect?.height"
|
|
7
|
+
:zoom="zoom"
|
|
8
|
+
>
|
|
3
9
|
<div
|
|
4
10
|
class="m-editor-stage-container"
|
|
5
11
|
ref="stageContainer"
|
|
6
|
-
:style="stageStyle"
|
|
7
12
|
@contextmenu="contextmenuHandler"
|
|
13
|
+
:style="`transform: scale(${zoom})`"
|
|
8
14
|
></div>
|
|
9
15
|
<teleport to="body">
|
|
10
16
|
<viewer-menu ref="menu" :style="menuStyle"></viewer-menu>
|
|
11
17
|
</teleport>
|
|
12
|
-
</
|
|
18
|
+
</scroll-viewer>
|
|
13
19
|
</template>
|
|
14
20
|
|
|
15
21
|
<script lang="ts">
|
|
@@ -17,6 +23,7 @@ import {
|
|
|
17
23
|
computed,
|
|
18
24
|
defineComponent,
|
|
19
25
|
inject,
|
|
26
|
+
nextTick,
|
|
20
27
|
onMounted,
|
|
21
28
|
onUnmounted,
|
|
22
29
|
PropType,
|
|
@@ -31,7 +38,8 @@ import type { MApp, MNode, MPage } from '@tmagic/schema';
|
|
|
31
38
|
import type { MoveableOptions, Runtime, SortEventData, UpdateEventData } from '@tmagic/stage';
|
|
32
39
|
import StageCore from '@tmagic/stage';
|
|
33
40
|
|
|
34
|
-
import
|
|
41
|
+
import ScrollViewer from '@editor/components/ScrollViewer.vue';
|
|
42
|
+
import type { Services, StageRect } from '@editor/type';
|
|
35
43
|
|
|
36
44
|
import ViewerMenu from './ViewerMenu.vue';
|
|
37
45
|
|
|
@@ -79,6 +87,7 @@ export default defineComponent({
|
|
|
79
87
|
|
|
80
88
|
components: {
|
|
81
89
|
ViewerMenu,
|
|
90
|
+
ScrollViewer,
|
|
82
91
|
},
|
|
83
92
|
|
|
84
93
|
props: {
|
|
@@ -88,26 +97,6 @@ export default defineComponent({
|
|
|
88
97
|
|
|
89
98
|
runtimeUrl: String,
|
|
90
99
|
|
|
91
|
-
root: {
|
|
92
|
-
type: Object as PropType<MApp>,
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
page: {
|
|
96
|
-
type: Object as PropType<MPage>,
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
node: {
|
|
100
|
-
type: Object as PropType<MNode>,
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
uiSelectMode: {
|
|
104
|
-
type: Boolean,
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
zoom: {
|
|
108
|
-
type: Number,
|
|
109
|
-
},
|
|
110
|
-
|
|
111
100
|
canSelect: {
|
|
112
101
|
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
|
|
113
102
|
default: (el: HTMLElement) => Boolean(el.id),
|
|
@@ -125,12 +114,16 @@ export default defineComponent({
|
|
|
125
114
|
|
|
126
115
|
setup(props, { emit }) {
|
|
127
116
|
const services = inject<Services>('services');
|
|
117
|
+
|
|
118
|
+
const stageWrap = ref<InstanceType<typeof ScrollViewer>>();
|
|
128
119
|
const stageContainer = ref<HTMLDivElement>();
|
|
129
120
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
|
|
122
|
+
const uiSelectMode = computed(() => services?.uiService.get<boolean>('uiSelectMode'));
|
|
123
|
+
const root = computed(() => services?.editorService.get<MApp>('root'));
|
|
124
|
+
const page = computed(() => services?.editorService.get<MPage>('page'));
|
|
125
|
+
const zoom = computed(() => services?.uiService.get<number>('zoom'));
|
|
126
|
+
const node = computed(() => services?.editorService.get<MNode>('node'));
|
|
134
127
|
|
|
135
128
|
let stage: StageCore | null = null;
|
|
136
129
|
let runtime: Runtime | null = null;
|
|
@@ -139,16 +132,16 @@ export default defineComponent({
|
|
|
139
132
|
if (stage) return;
|
|
140
133
|
|
|
141
134
|
if (!stageContainer.value) return;
|
|
142
|
-
if (!(props.runtimeUrl || props.render) || !
|
|
135
|
+
if (!(props.runtimeUrl || props.render) || !root.value) return;
|
|
143
136
|
|
|
144
137
|
stage = new StageCore({
|
|
145
138
|
render: props.render,
|
|
146
139
|
runtimeUrl: props.runtimeUrl,
|
|
147
|
-
zoom:
|
|
140
|
+
zoom: zoom.value,
|
|
148
141
|
canSelect: (el, stop) => {
|
|
149
142
|
const elCanSelect = props.canSelect(el);
|
|
150
143
|
// 在组件联动过程中不能再往下选择,返回并触发 ui-select
|
|
151
|
-
if (
|
|
144
|
+
if (uiSelectMode.value && elCanSelect) {
|
|
152
145
|
document.dispatchEvent(new CustomEvent('ui-select', { detail: el }));
|
|
153
146
|
return stop();
|
|
154
147
|
}
|
|
@@ -176,45 +169,64 @@ export default defineComponent({
|
|
|
176
169
|
services?.uiService.set('showGuides', true);
|
|
177
170
|
});
|
|
178
171
|
|
|
179
|
-
if (!
|
|
172
|
+
if (!node.value?.id) return;
|
|
180
173
|
stage?.on('runtime-ready', (rt) => {
|
|
181
174
|
runtime = rt;
|
|
182
175
|
// toRaw返回的值是一个引用而非快照,需要cloneDeep
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
root.value && runtime?.updateRootConfig(cloneDeep(toRaw(root.value)));
|
|
177
|
+
page.value?.id && runtime?.updatePageId?.(page.value.id);
|
|
185
178
|
setTimeout(() => {
|
|
186
|
-
|
|
179
|
+
node.value && stage?.select(toRaw(node.value.id));
|
|
187
180
|
});
|
|
188
181
|
});
|
|
189
182
|
});
|
|
190
183
|
|
|
191
|
-
watch(
|
|
192
|
-
(
|
|
193
|
-
(zoom)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
watch(zoom, (zoom) => {
|
|
185
|
+
if (!stage || !zoom) return;
|
|
186
|
+
stage.setZoom(zoom);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
watch(root, (root) => {
|
|
190
|
+
if (runtime && root) {
|
|
191
|
+
runtime.updateRootConfig(cloneDeep(toRaw(root)));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
198
194
|
|
|
199
195
|
watch(
|
|
200
|
-
() =>
|
|
201
|
-
(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
196
|
+
() => node.value?.id,
|
|
197
|
+
(id) => {
|
|
198
|
+
nextTick(() => {
|
|
199
|
+
// 等待相关dom变更完成后,再select,适用大多数场景
|
|
200
|
+
id && stage?.select(id);
|
|
201
|
+
});
|
|
205
202
|
},
|
|
206
203
|
);
|
|
207
204
|
|
|
205
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
206
|
+
for (const { contentRect } of entries) {
|
|
207
|
+
services?.uiService.set('stageContainerRect', {
|
|
208
|
+
width: contentRect.width,
|
|
209
|
+
height: contentRect.height,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
onMounted(() => {
|
|
215
|
+
stageWrap.value?.container && resizeObserver.observe(stageWrap.value.container);
|
|
216
|
+
});
|
|
217
|
+
|
|
208
218
|
onUnmounted(() => {
|
|
209
219
|
stage?.destroy();
|
|
220
|
+
resizeObserver.disconnect();
|
|
210
221
|
services?.editorService.set('stage', null);
|
|
211
222
|
});
|
|
212
223
|
|
|
213
224
|
return {
|
|
214
|
-
|
|
215
|
-
...useMenu(),
|
|
216
|
-
|
|
225
|
+
stageWrap,
|
|
217
226
|
stageContainer,
|
|
227
|
+
stageRect,
|
|
228
|
+
zoom,
|
|
229
|
+
...useMenu(),
|
|
218
230
|
};
|
|
219
231
|
},
|
|
220
232
|
});
|
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
:key="page?.id"
|
|
5
5
|
:runtime-url="runtimeUrl"
|
|
6
6
|
:render="render"
|
|
7
|
-
:ui-select-mode="uiSelectMode"
|
|
8
|
-
:root="root"
|
|
9
|
-
:page="page"
|
|
10
|
-
:node="node"
|
|
11
|
-
:zoom="zoom"
|
|
12
7
|
:moveable-options="moveableOptions"
|
|
13
8
|
:can-select="canSelect"
|
|
14
9
|
@select="selectHandler"
|
|
@@ -26,9 +21,9 @@
|
|
|
26
21
|
</template>
|
|
27
22
|
|
|
28
23
|
<script lang="ts">
|
|
29
|
-
import { computed, defineComponent, inject,
|
|
24
|
+
import { computed, defineComponent, inject, PropType } from 'vue';
|
|
30
25
|
|
|
31
|
-
import type {
|
|
26
|
+
import type { MComponent, MContainer, MPage } from '@tmagic/schema';
|
|
32
27
|
import type { MoveableOptions, SortEventData } from '@tmagic/stage';
|
|
33
28
|
import StageCore from '@tmagic/stage';
|
|
34
29
|
|
|
@@ -63,23 +58,9 @@ export default defineComponent({
|
|
|
63
58
|
|
|
64
59
|
setup() {
|
|
65
60
|
const services = inject<Services>('services');
|
|
66
|
-
const node = computed(() => services?.editorService.get<MNode>('node'));
|
|
67
|
-
const stage = computed(() => services?.editorService.get<StageCore>('stage'));
|
|
68
|
-
|
|
69
|
-
watch([() => node.value?.id, stage], ([id, stage]) => {
|
|
70
|
-
nextTick(() => {
|
|
71
|
-
// 等待相关dom变更完成后,再select,适用大多数场景
|
|
72
|
-
id && stage?.select(id);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
61
|
|
|
76
62
|
return {
|
|
77
|
-
uiSelectMode: computed(() => services?.uiService.get<boolean>('uiSelectMode')),
|
|
78
|
-
root: computed(() => services?.editorService.get<MApp>('root')),
|
|
79
63
|
page: computed(() => services?.editorService.get<MPage>('page')),
|
|
80
|
-
zoom: computed(() => services?.uiService.get<number>('zoom')),
|
|
81
|
-
|
|
82
|
-
node,
|
|
83
64
|
|
|
84
65
|
selectHandler(el: HTMLElement) {
|
|
85
66
|
services?.editorService.select(el.id);
|
package/src/services/editor.ts
CHANGED
|
@@ -289,8 +289,6 @@ class Editor extends BaseService {
|
|
|
289
289
|
if (!info.node) throw new Error(`获取不到id为${config.id}的节点`);
|
|
290
290
|
|
|
291
291
|
const node = cloneDeep(toRaw(info.node));
|
|
292
|
-
const { parent } = info;
|
|
293
|
-
if (!parent) throw new Error('获取不到父级节点');
|
|
294
292
|
|
|
295
293
|
let newConfig = await this.toggleFixedPosition(toRaw(config), node, this.get<MApp>('root'));
|
|
296
294
|
|
|
@@ -303,6 +301,9 @@ class Editor extends BaseService {
|
|
|
303
301
|
return newConfig;
|
|
304
302
|
}
|
|
305
303
|
|
|
304
|
+
const { parent } = info;
|
|
305
|
+
if (!parent) throw new Error('获取不到父级节点');
|
|
306
|
+
|
|
306
307
|
const parentNodeItems = parent.items;
|
|
307
308
|
const index = getNodeIndex(newConfig, parent);
|
|
308
309
|
|
package/src/services/ui.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { reactive, toRaw } from 'vue';
|
|
|
21
21
|
import type StageCore from '@tmagic/stage';
|
|
22
22
|
|
|
23
23
|
import editorService from '@editor/services/editor';
|
|
24
|
-
import { GetColumnWidth, SetColumnWidth, UiState } from '@editor/type';
|
|
24
|
+
import type { GetColumnWidth, SetColumnWidth, StageRect, UiState } from '@editor/type';
|
|
25
25
|
|
|
26
26
|
import BaseService from './BaseService';
|
|
27
27
|
|
|
@@ -29,7 +29,14 @@ const state = reactive<UiState>({
|
|
|
29
29
|
uiSelectMode: false,
|
|
30
30
|
showSrc: false,
|
|
31
31
|
zoom: 1,
|
|
32
|
-
|
|
32
|
+
stageContainerRect: {
|
|
33
|
+
width: 0,
|
|
34
|
+
height: 0,
|
|
35
|
+
},
|
|
36
|
+
stageRect: {
|
|
37
|
+
width: 375,
|
|
38
|
+
height: 817,
|
|
39
|
+
},
|
|
33
40
|
columnWidth: {
|
|
34
41
|
left: 310,
|
|
35
42
|
center: globalThis.document.body.clientWidth - 310 - 400,
|
|
@@ -57,6 +64,11 @@ class Ui extends BaseService {
|
|
|
57
64
|
return;
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
if (name === 'stageRect') {
|
|
68
|
+
this.setStageRect(value as unknown as StageRect);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
if (name === 'showGuides') {
|
|
61
73
|
mask?.showGuides(value as unknown as boolean);
|
|
62
74
|
}
|
|
@@ -66,6 +78,10 @@ class Ui extends BaseService {
|
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
(state as any)[name] = value;
|
|
81
|
+
|
|
82
|
+
if (name === 'stageContainerRect') {
|
|
83
|
+
state.zoom = this.calcZoom();
|
|
84
|
+
}
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
public get<T>(name: keyof typeof state): T {
|
|
@@ -94,6 +110,24 @@ class Ui extends BaseService {
|
|
|
94
110
|
|
|
95
111
|
state.columnWidth = columnWidth;
|
|
96
112
|
}
|
|
113
|
+
|
|
114
|
+
private setStageRect(value: StageRect) {
|
|
115
|
+
state.stageRect = {
|
|
116
|
+
...state.stageRect,
|
|
117
|
+
...value,
|
|
118
|
+
};
|
|
119
|
+
state.zoom = this.calcZoom();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private calcZoom() {
|
|
123
|
+
const { stageRect, stageContainerRect } = state;
|
|
124
|
+
const { height, width } = stageContainerRect;
|
|
125
|
+
if (!width || !height) return 1;
|
|
126
|
+
if (width > stageRect.width && height > stageRect.height) {
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
return Math.min((width - 100) / stageRect.width || 1, (height - 100) / stageRect.height || 1);
|
|
130
|
+
}
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
export type UiService = Ui;
|
package/src/theme/stage.scss
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
position: relative;
|
|
3
3
|
width: 100%;
|
|
4
4
|
height: calc(100% - $--page-bar-height);
|
|
5
|
-
overflow:
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
display: flex;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
align-items: center;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
.m-editor-stage-container {
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
11
14
|
z-index: 0;
|
|
12
|
-
top: 50%;
|
|
13
|
-
margin: 0 auto;
|
|
14
15
|
position: relative;
|
|
15
|
-
width: 375px;
|
|
16
|
-
height: 80%;
|
|
17
16
|
border: 1px solid $--border-color;
|
|
17
|
+
transition: transform 0.3s;
|
|
18
18
|
|
|
19
19
|
&::-webkit-scrollbar {
|
|
20
20
|
width: 0 !important;
|
package/src/type.ts
CHANGED
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { Component } from 'vue';
|
|
19
|
+
import type { Component } from 'vue';
|
|
20
20
|
|
|
21
|
-
import { FormConfig } from '@tmagic/form';
|
|
22
|
-
import { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
|
|
23
|
-
import StageCore from '@tmagic/stage';
|
|
21
|
+
import type { FormConfig } from '@tmagic/form';
|
|
22
|
+
import type { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
|
|
23
|
+
import type StageCore from '@tmagic/stage';
|
|
24
24
|
|
|
25
25
|
import type { ComponentListService } from '@editor/services/componentList';
|
|
26
26
|
import type { EditorService } from '@editor/services/editor';
|
|
@@ -75,6 +75,11 @@ export interface GetColumnWidth {
|
|
|
75
75
|
right: number;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export interface StageRect {
|
|
79
|
+
width: number;
|
|
80
|
+
height: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
export interface UiState {
|
|
79
84
|
/** 当前点击画布是否触发选中,true: 不触发,false: 触发,默认为false */
|
|
80
85
|
uiSelectMode: boolean;
|
|
@@ -82,8 +87,10 @@ export interface UiState {
|
|
|
82
87
|
showSrc: boolean;
|
|
83
88
|
/** 画布显示放大倍数,默认为 1 */
|
|
84
89
|
zoom: number;
|
|
85
|
-
/**
|
|
86
|
-
|
|
90
|
+
/** 画布容器的宽高 */
|
|
91
|
+
stageContainerRect: StageRect;
|
|
92
|
+
/** 画布顶层div的宽高,可用于改变画布的大小 */
|
|
93
|
+
stageRect: StageRect;
|
|
87
94
|
/** 编辑器列布局每一列的宽度,分为左中右三列 */
|
|
88
95
|
columnWidth: GetColumnWidth;
|
|
89
96
|
/** 是否显示画布参考线,true: 显示,false: 不显示,默认为true */
|
|
@@ -234,3 +241,7 @@ export enum Layout {
|
|
|
234
241
|
RELATIVE = 'relative',
|
|
235
242
|
ABSOLUTE = 'absolute',
|
|
236
243
|
}
|
|
244
|
+
|
|
245
|
+
export enum Keys {
|
|
246
|
+
ESCAPE = 'Space',
|
|
247
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Keys } from '@editor/type';
|
|
2
|
+
|
|
3
|
+
interface ScrollViewerOptions {
|
|
4
|
+
container: HTMLDivElement;
|
|
5
|
+
target: HTMLDivElement;
|
|
6
|
+
zoom: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ScrollViewer {
|
|
10
|
+
private enter = false;
|
|
11
|
+
private targetEnter = false;
|
|
12
|
+
private keydown = false;
|
|
13
|
+
private container: HTMLDivElement;
|
|
14
|
+
private target: HTMLDivElement;
|
|
15
|
+
private zoom = 1;
|
|
16
|
+
|
|
17
|
+
private scrollLeft = 0;
|
|
18
|
+
private scrollTop = 0;
|
|
19
|
+
|
|
20
|
+
private x = 0;
|
|
21
|
+
private y = 0;
|
|
22
|
+
|
|
23
|
+
private resizeObserver = new ResizeObserver((entries) => {
|
|
24
|
+
for (const { contentRect } of entries) {
|
|
25
|
+
const { width, height } = contentRect;
|
|
26
|
+
const targetRect = this.target.getBoundingClientRect();
|
|
27
|
+
const targetWidth = targetRect.width * this.zoom;
|
|
28
|
+
const targetHeight = targetRect.height * this.zoom;
|
|
29
|
+
|
|
30
|
+
if (targetWidth < width) {
|
|
31
|
+
(this.target as any)._left = 0;
|
|
32
|
+
}
|
|
33
|
+
if (targetHeight < height) {
|
|
34
|
+
(this.target as any)._top = 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.scroll();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
constructor(options: ScrollViewerOptions) {
|
|
42
|
+
this.container = options.container;
|
|
43
|
+
this.target = options.target;
|
|
44
|
+
this.zoom = options.zoom;
|
|
45
|
+
|
|
46
|
+
globalThis.addEventListener('keydown', this.keydownHandler);
|
|
47
|
+
globalThis.addEventListener('keyup', this.keyupHandler);
|
|
48
|
+
|
|
49
|
+
this.container.addEventListener('mouseenter', this.mouseEnterHandler);
|
|
50
|
+
this.container.addEventListener('mouseleave', this.mouseLeaveHandler);
|
|
51
|
+
this.target.addEventListener('mouseenter', this.targetMouseEnterHandler);
|
|
52
|
+
this.target.addEventListener('mouseleave', this.targetMouseLeaveHandler);
|
|
53
|
+
|
|
54
|
+
this.container.addEventListener('wheel', this.wheelHandler);
|
|
55
|
+
|
|
56
|
+
this.resizeObserver.observe(this.container);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public destroy() {
|
|
60
|
+
this.resizeObserver.disconnect();
|
|
61
|
+
|
|
62
|
+
this.container.removeEventListener('mouseenter', this.mouseEnterHandler);
|
|
63
|
+
this.container.removeEventListener('mouseleave', this.mouseLeaveHandler);
|
|
64
|
+
this.target.removeEventListener('mouseenter', this.targetMouseEnterHandler);
|
|
65
|
+
this.target.removeEventListener('mouseleave', this.targetMouseLeaveHandler);
|
|
66
|
+
globalThis.removeEventListener('keydown', this.keydownHandler);
|
|
67
|
+
globalThis.removeEventListener('keyup', this.keyupHandler);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public setZoom(zoom: number) {
|
|
71
|
+
this.zoom = zoom;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private scroll() {
|
|
75
|
+
const scrollLeft = (this.target as any)._left;
|
|
76
|
+
const scrollTop = (this.target as any)._top;
|
|
77
|
+
|
|
78
|
+
this.target.style.transform = `translate(${scrollLeft}px, ${scrollTop}px)`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private removeHandler() {
|
|
82
|
+
this.target.style.cursor = '';
|
|
83
|
+
this.target.removeEventListener('mousedown', this.mousedownHandler);
|
|
84
|
+
document.removeEventListener('mousemove', this.mousemoveHandler);
|
|
85
|
+
document.removeEventListener('mouseup', this.mouseupHandler);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private wheelHandler = (event: WheelEvent) => {
|
|
89
|
+
if (this.targetEnter) return;
|
|
90
|
+
|
|
91
|
+
const { deltaX, deltaY, currentTarget } = event;
|
|
92
|
+
|
|
93
|
+
if (currentTarget !== this.container) return;
|
|
94
|
+
|
|
95
|
+
this.setScrollOffset(deltaX, deltaY);
|
|
96
|
+
this.scroll();
|
|
97
|
+
this.scrollLeft = (this.target as any)._left;
|
|
98
|
+
this.scrollTop = (this.target as any)._top;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
private mouseEnterHandler = () => {
|
|
102
|
+
this.enter = true;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
private mouseLeaveHandler = () => {
|
|
106
|
+
this.enter = false;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
private targetMouseEnterHandler = () => {
|
|
110
|
+
this.targetEnter = true;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
private targetMouseLeaveHandler = () => {
|
|
114
|
+
this.targetEnter = false;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
private mousedownHandler = (event: MouseEvent) => {
|
|
118
|
+
if (!this.keydown) return;
|
|
119
|
+
|
|
120
|
+
event.stopImmediatePropagation();
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
|
|
123
|
+
this.target.style.cursor = 'grabbing';
|
|
124
|
+
|
|
125
|
+
this.x = event.clientX;
|
|
126
|
+
this.y = event.clientY;
|
|
127
|
+
|
|
128
|
+
document.addEventListener('mousemove', this.mousemoveHandler);
|
|
129
|
+
document.addEventListener('mouseup', this.mouseupHandler);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
private mouseupHandler = () => {
|
|
133
|
+
this.x = 0;
|
|
134
|
+
this.y = 0;
|
|
135
|
+
|
|
136
|
+
this.scrollLeft = (this.target as any)._left;
|
|
137
|
+
this.scrollTop = (this.target as any)._top;
|
|
138
|
+
this.removeHandler();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
private mousemoveHandler = (event: MouseEvent) => {
|
|
142
|
+
event.stopImmediatePropagation();
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
|
|
145
|
+
const deltaX = event.clientX - this.x;
|
|
146
|
+
const deltaY = event.clientY - this.y;
|
|
147
|
+
|
|
148
|
+
this.setScrollOffset(deltaX, deltaY);
|
|
149
|
+
this.scroll();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
private keydownHandler = (event: KeyboardEvent) => {
|
|
153
|
+
if (event.code === Keys.ESCAPE && this.enter) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
event.stopImmediatePropagation();
|
|
156
|
+
event.stopPropagation();
|
|
157
|
+
} else if (this.keydown) return;
|
|
158
|
+
|
|
159
|
+
this.keydown = true;
|
|
160
|
+
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
|
|
163
|
+
this.target.style.cursor = 'grab';
|
|
164
|
+
this.container.addEventListener('mousedown', this.mousedownHandler);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
private keyupHandler = (event: KeyboardEvent) => {
|
|
168
|
+
if (event.code !== Keys.ESCAPE || !this.keydown) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
event.stopImmediatePropagation();
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
|
|
176
|
+
this.keydown = false;
|
|
177
|
+
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
|
|
180
|
+
this.removeHandler();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
private setScrollOffset(deltaX: number, deltaY: number) {
|
|
184
|
+
const { width, height } = this.container.getBoundingClientRect();
|
|
185
|
+
const targetRect = this.target.getBoundingClientRect();
|
|
186
|
+
|
|
187
|
+
const targetWidth = targetRect.width * this.zoom;
|
|
188
|
+
const targetHeight = targetRect.height * this.zoom;
|
|
189
|
+
|
|
190
|
+
let y = 0;
|
|
191
|
+
|
|
192
|
+
if (targetHeight > height) {
|
|
193
|
+
if (deltaY > 0) {
|
|
194
|
+
y = this.scrollTop + Math.min(targetHeight - height - this.scrollTop, deltaY);
|
|
195
|
+
} else {
|
|
196
|
+
y = this.scrollTop + Math.max(-(targetHeight - height + this.scrollTop), deltaY);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let x = 0;
|
|
201
|
+
|
|
202
|
+
if (targetWidth > width) {
|
|
203
|
+
if (deltaX > 0) {
|
|
204
|
+
x = this.scrollLeft + Math.min(targetWidth - width - this.scrollLeft, deltaX);
|
|
205
|
+
} else {
|
|
206
|
+
x = this.scrollLeft + Math.max(-(targetWidth - width + this.scrollLeft), deltaX);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
(this.target as any)._left = x;
|
|
211
|
+
(this.target as any)._top = y;
|
|
212
|
+
}
|
|
213
|
+
}
|