@tmagic/stage 1.0.0-beta.6 → 1.0.0-beta.9

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/src/StageMask.ts CHANGED
@@ -16,17 +16,16 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { EventEmitter } from 'events';
19
+ import { throttle } from 'lodash-es';
20
20
 
21
- import Guides from '@scena/guides';
22
- import { isNumber } from 'lodash';
23
-
24
- import { GHOST_EL_ID_PREFIX } from './const';
21
+ import { Mode, MouseButton, ZIndex } from './const';
22
+ import Rule from './Rule';
25
23
  import type StageCore from './StageCore';
26
- import type { CanSelect, StageMaskConfig } from './types';
27
- import { MouseButton, ZIndex } from './types';
24
+ import type { StageMaskConfig } from './types';
25
+ import { createDiv, getScrollParent, isFixedParent } from './util';
28
26
 
29
27
  const wrapperClassName = 'editor-mask-wrapper';
28
+ const throttleTime = 100;
30
29
 
31
30
  const hideScrollbar = () => {
32
31
  const style = globalThis.document.createElement('style');
@@ -36,30 +35,30 @@ const hideScrollbar = () => {
36
35
  globalThis.document.head.appendChild(style);
37
36
  };
38
37
 
39
- const createContent = (): HTMLDivElement => {
40
- const el = globalThis.document.createElement('div');
41
- el.className = 'editor-mask';
42
- el.style.cssText = `
38
+ const createContent = (): HTMLDivElement =>
39
+ createDiv({
40
+ className: 'editor-mask',
41
+ cssText: `
43
42
  position: absolute;
44
43
  top: 0;
45
44
  left: 0;
46
45
  transform: translate3d(0, 0, 0);
47
- `;
48
- return el;
49
- };
46
+ `,
47
+ });
50
48
 
51
49
  const createWrapper = (): HTMLDivElement => {
52
- const el = globalThis.document.createElement('div');
53
- el.className = wrapperClassName;
54
- el.style.cssText = `
55
- position: absolute;
56
- top: 0;
57
- left: 0;
58
- height: 100%;
59
- width: 100%;
60
- overflow: auto;
61
- z-index: ${ZIndex.MASK};
62
- `;
50
+ const el = createDiv({
51
+ className: wrapperClassName,
52
+ cssText: `
53
+ position: absolute;
54
+ top: 0;
55
+ left: 0;
56
+ height: 100%;
57
+ width: 100%;
58
+ overflow: hidden;
59
+ z-index: ${ZIndex.MASK};
60
+ `,
61
+ });
63
62
 
64
63
  hideScrollbar();
65
64
 
@@ -70,54 +69,53 @@ const createWrapper = (): HTMLDivElement => {
70
69
  * 蒙层
71
70
  * @description 用于拦截页面的点击动作,避免点击时触发组件自身动作;在编辑器中点击组件应当是选中组件;
72
71
  */
73
- export default class StageMask extends EventEmitter {
72
+ export default class StageMask extends Rule {
74
73
  public content: HTMLDivElement = createContent();
75
- public wrapper: HTMLDivElement = createWrapper();
74
+ public wrapper: HTMLDivElement;
76
75
  public core: StageCore;
77
76
  public page: HTMLElement | null = null;
78
-
79
- public hGuides: Guides;
80
- public vGuides: Guides;
81
-
82
- private target: Element | null = null;
83
- private resizeObserver: ResizeObserver | null = null;
84
- private parentResizeObserver: ResizeObserver | null = null;
85
- private canSelect: CanSelect;
77
+ public pageScrollParent: HTMLElement | null = null;
78
+ public scrollTop = 0;
79
+ public scrollLeft = 0;
80
+ public width = 0;
81
+ public height = 0;
82
+ public wrapperHeight = 0;
83
+ public wrapperWidth = 0;
84
+
85
+ private mode: Mode = Mode.ABSOLUTE;
86
+ private pageResizeObserver: ResizeObserver | null = null;
87
+ private wrapperResizeObserver: ResizeObserver | null = null;
88
+ /**
89
+ * 高亮事件处理函数
90
+ * @param event 事件对象
91
+ */
92
+ private highlightHandler = throttle((event: MouseEvent): void => {
93
+ this.emit('highlight', event);
94
+ }, throttleTime);
86
95
 
87
96
  constructor(config: StageMaskConfig) {
88
- super();
97
+ const wrapper = createWrapper();
98
+ super(wrapper);
89
99
 
100
+ this.wrapper = wrapper;
90
101
  this.core = config.core;
91
- const { config: coreConfig } = config.core;
92
102
 
93
- this.canSelect = coreConfig.canSelect || ((el: HTMLElement) => !!el.id);
94
103
  this.content.addEventListener('mousedown', this.mouseDownHandler);
95
- this.content.addEventListener('contextmenu', this.contextmenuHandler);
96
104
  this.wrapper.appendChild(this.content);
97
-
98
- this.hGuides = this.createGuides('horizontal');
99
- this.vGuides = this.createGuides('vertical');
105
+ this.content.addEventListener('wheel', this.mouseWheelHandler);
106
+ this.content.addEventListener('mousemove', this.highlightHandler);
100
107
  }
101
108
 
102
- /**
103
- * 设置成固定定位模式
104
- */
105
- public setFixed(): void {
106
- this.wrapper.scrollTo({
107
- top: 0,
108
- });
109
- this.wrapper.style.overflow = 'hidden';
110
- // 要等滚动条滚上去,才刷新选中框
111
- setTimeout(() => {
112
- this.core.dr.refresh();
113
- });
114
- }
115
-
116
- /**
117
- * 设置成绝对定位模式
118
- */
119
- public setAbsolute(): void {
120
- this.wrapper.style.overflow = 'auto';
109
+ public setMode(mode: Mode) {
110
+ this.mode = mode;
111
+ this.scroll();
112
+ if (mode === Mode.FIXED) {
113
+ this.content.style.width = `${this.wrapperWidth}px`;
114
+ this.content.style.height = `${this.wrapperHeight}px`;
115
+ } else {
116
+ this.content.style.width = `${this.width}px`;
117
+ this.content.style.height = `${this.height}px`;
118
+ }
121
119
  }
122
120
 
123
121
  /**
@@ -129,17 +127,26 @@ export default class StageMask extends EventEmitter {
129
127
  if (!page) return;
130
128
 
131
129
  this.page = page;
132
- this.resizeObserver?.disconnect();
130
+ this.pageScrollParent = getScrollParent(page) || this.core.renderer.contentWindow?.document.documentElement || null;
131
+ this.pageResizeObserver?.disconnect();
133
132
 
134
133
  if (typeof ResizeObserver !== 'undefined') {
135
- this.resizeObserver = new ResizeObserver((entries) => {
134
+ this.pageResizeObserver = new ResizeObserver((entries) => {
136
135
  const [entry] = entries;
137
136
  const { clientHeight, clientWidth } = entry.target;
138
137
  this.setHeight(clientHeight);
139
138
  this.setWidth(clientWidth);
140
139
  });
141
140
 
142
- this.resizeObserver.observe(page);
141
+ this.pageResizeObserver.observe(page);
142
+
143
+ this.wrapperResizeObserver = new ResizeObserver((entries) => {
144
+ const [entry] = entries;
145
+ const { clientHeight, clientWidth } = entry.target;
146
+ this.wrapperHeight = clientHeight;
147
+ this.wrapperWidth = clientWidth;
148
+ });
149
+ this.wrapperResizeObserver.observe(this.wrapper);
143
150
  }
144
151
  }
145
152
 
@@ -151,12 +158,10 @@ export default class StageMask extends EventEmitter {
151
158
  if (!this.content) throw new Error('content 不存在');
152
159
 
153
160
  el.appendChild(this.wrapper);
161
+ }
154
162
 
155
- this.parentResizeObserver = new ResizeObserver(() => {
156
- this.vGuides.resize();
157
- this.hGuides.resize();
158
- });
159
- this.parentResizeObserver.observe(el);
163
+ public setLayout(el: HTMLElement): void {
164
+ this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
160
165
  }
161
166
 
162
167
  /**
@@ -165,165 +170,112 @@ export default class StageMask extends EventEmitter {
165
170
  public destroy(): void {
166
171
  this.content?.remove();
167
172
  this.page = null;
168
- this.resizeObserver?.disconnect();
169
- this.parentResizeObserver?.disconnect();
170
- this.removeAllListeners();
173
+ this.pageScrollParent = null;
174
+ this.pageResizeObserver?.disconnect();
175
+ this.wrapperResizeObserver?.disconnect();
176
+ super.destroy();
171
177
  }
172
178
 
173
- /**
174
- * 是否显示标尺
175
- * @param show 是否显示
176
- */
177
- public showGuides(show = true) {
178
- this.hGuides.setState({
179
- showGuides: show,
180
- });
181
-
182
- this.vGuides.setState({
183
- showGuides: show,
184
- });
185
- }
186
-
187
- /**
188
- * 是否显示标尺
189
- * @param show 是否显示
190
- */
191
- public showRule(show = true) {
192
- // 当尺子隐藏时发现大小变化,显示后会变形,所以这里做重新初始化处理
193
- if (show) {
194
- this.hGuides.destroy();
195
- this.hGuides = this.createGuides('horizontal', this.core.dr.horizontalGuidelines);
196
-
197
- this.vGuides.destroy();
198
- this.vGuides = this.createGuides('vertical', this.core.dr.verticalGuidelines);
199
- } else {
200
- this.hGuides.setState({
201
- rulerStyle: {
202
- visibility: 'hidden',
203
- },
204
- });
179
+ private scroll() {
180
+ let { scrollLeft, scrollTop } = this;
205
181
 
206
- this.vGuides.setState({
207
- rulerStyle: {
208
- visibility: 'hidden',
209
- },
210
- });
182
+ if (this.mode === Mode.FIXED) {
183
+ scrollLeft = 0;
184
+ scrollTop = 0;
211
185
  }
186
+
187
+ this.scrollRule(scrollTop);
188
+ this.scrollTo(scrollLeft, scrollTop);
212
189
  }
213
190
 
214
- /**
215
- * 清空所有参考线
216
- */
217
- public clearGuides() {
218
- this.vGuides.setState({
219
- defaultGuides: [],
220
- });
221
- this.hGuides.setState({
222
- defaultGuides: [],
223
- });
191
+ private scrollTo(scrollLeft: number, scrollTop: number): void {
192
+ this.content.style.transform = `translate3d(${-scrollLeft}px, ${-scrollTop}px, 0)`;
224
193
  }
225
194
 
226
195
  /**
227
196
  * 设置蒙层高度
228
197
  * @param height 高度
229
198
  */
230
- private setHeight(height: number | string): void {
231
- this.content.style.height = isNumber(height) ? `${height}px` : height;
199
+ private setHeight(height: number): void {
200
+ this.height = height;
201
+ this.content.style.height = `${height}px`;
232
202
  }
233
203
 
234
204
  /**
235
205
  * 设置蒙层宽度
236
206
  * @param width 宽度
237
207
  */
238
- private setWidth(width: number | string): void {
239
- this.content.style.width = isNumber(width) ? `${width}px` : width;
208
+ private setWidth(width: number): void {
209
+ this.width = width;
210
+ this.content.style.width = `${width}px`;
240
211
  }
241
212
 
242
213
  /**
243
214
  * 点击事件处理函数
244
215
  * @param event 事件对象
245
216
  */
246
- private mouseDownHandler = async (event: MouseEvent): Promise<void> => {
247
- if (event.button !== MouseButton.LEFT) return;
217
+ private mouseDownHandler = (event: MouseEvent): void => {
218
+ event.stopImmediatePropagation();
219
+ event.stopPropagation();
220
+
221
+ if (event.button !== MouseButton.LEFT && event.button !== MouseButton.RIGHT) return;
248
222
 
249
223
  // 点击的对象如果是选中框,则不需要再触发选中了,而可能是拖动行为
250
224
  if ((event.target as HTMLDivElement).className.indexOf('moveable-control') !== -1) {
251
225
  return;
252
226
  }
253
227
 
254
- this.content.addEventListener('mousemove', this.mouseMoveHandler);
228
+ this.emit('beforeSelect', event);
255
229
 
256
- this.select(event);
230
+ // 如果是右键点击,这里的mouseup事件监听没有效果
231
+ globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
232
+ this.content.removeEventListener('mousemove', this.highlightHandler);
233
+ this.emit('clearHighlight');
257
234
  };
258
235
 
259
236
  private mouseUpHandler = (): void => {
260
- this.content.removeEventListener('mousemove', this.mouseMoveHandler);
261
- this.content.removeEventListener('mouseup', this.mouseUpHandler);
262
- this.emit('selected', this.target);
263
- this.target = null;
237
+ globalThis.document.removeEventListener('mouseup', this.mouseUpHandler);
238
+ this.content.addEventListener('mousemove', this.highlightHandler);
239
+ this.emit('select');
264
240
  };
265
241
 
266
- private mouseMoveHandler = (event: MouseEvent): void => {
267
- // 避免触摸板轻触移动拖动组件
268
- if (event.buttons) {
269
- this.core.dr.moveable?.dragStart(event);
270
- }
271
- this.content.removeEventListener('mousemove', this.mouseMoveHandler);
272
- };
273
-
274
- private contextmenuHandler = async (event: MouseEvent): Promise<void> => {
275
- await this.select(event);
276
- this.mouseUpHandler();
277
- };
242
+ private mouseWheelHandler = (event: WheelEvent) => {
243
+ if (!this.page) throw new Error('page 未初始化');
278
244
 
279
- private async select(event: MouseEvent) {
280
- const { renderer, zoom } = this.core;
245
+ const { deltaY, deltaX } = event;
246
+ const { height, wrapperHeight, width, wrapperWidth } = this;
281
247
 
282
- const doc = renderer.contentWindow?.document;
283
- let x = event.clientX;
284
- let y = event.clientY;
248
+ const maxScrollTop = height - wrapperHeight;
249
+ const maxScrollLeft = width - wrapperWidth;
285
250
 
286
- if (renderer.iframe) {
287
- const rect = renderer.iframe.getClientRects()[0];
288
- if (rect) {
289
- x = x - rect.left;
290
- y = y - rect.top;
251
+ if (maxScrollTop > 0) {
252
+ if (deltaY > 0) {
253
+ this.scrollTop = this.scrollTop + Math.min(maxScrollTop - this.scrollTop, deltaY);
254
+ } else {
255
+ this.scrollTop = Math.max(this.scrollTop + deltaY, 0);
291
256
  }
292
257
  }
293
258
 
294
- const els = doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
295
-
296
- let stopped = false;
297
- const stop = () => (stopped = true);
298
- for (const el of els) {
299
- if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.canSelect(el, stop))) {
300
- if (stopped) break;
301
-
302
- this.emit('select', el, event);
303
- this.target = el;
304
- // 如果是右键点击,这里的mouseup事件监听没有效果
305
- this.content.addEventListener('mouseup', this.mouseUpHandler);
306
- break;
259
+ if (width > wrapperWidth) {
260
+ if (deltaX > 0) {
261
+ this.scrollLeft = this.scrollLeft + Math.min(maxScrollLeft - this.scrollLeft, deltaX);
262
+ } else {
263
+ this.scrollLeft = Math.max(this.scrollLeft + deltaX, 0);
307
264
  }
308
265
  }
309
- }
310
266
 
311
- private getGuidesStyle = (type: 'horizontal' | 'vertical') => ({
312
- position: 'fixed',
313
- left: type === 'horizontal' ? 0 : '-30px',
314
- top: type === 'horizontal' ? '-30px' : 0,
315
- width: type === 'horizontal' ? '100%' : '30px',
316
- height: type === 'horizontal' ? '30px' : '100%',
317
- });
267
+ if (this.mode !== Mode.FIXED) {
268
+ this.scrollTo(this.scrollLeft, this.scrollTop);
269
+ }
270
+
271
+ if (this.pageScrollParent) {
272
+ this.pageScrollParent.scrollTo({
273
+ top: this.scrollTop,
274
+ left: this.scrollLeft,
275
+ });
276
+ }
277
+ this.scroll();
318
278
 
319
- private createGuides = (type: 'horizontal' | 'vertical', defaultGuides: number[] = []): Guides =>
320
- new Guides(this.wrapper, {
321
- type,
322
- defaultGuides,
323
- displayDragPos: true,
324
- backgroundColor: '#fff',
325
- lineColor: '#000',
326
- textColor: '#000',
327
- style: this.getGuidesStyle(type),
328
- });
279
+ this.emit('scroll', event);
280
+ };
329
281
  }
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { EventEmitter } from 'events';
20
20
 
21
+ import { SELECTED_CLASS, ZIndex } from './const';
21
22
  import StageCore from './StageCore';
22
23
  import type { Runtime, RuntimeWindow, StageRenderConfig } from './types';
23
24
  import { getHost, isSameDomain } from './util';
@@ -52,15 +53,7 @@ export default class StageRender extends EventEmitter {
52
53
  height: 100%;
53
54
  `;
54
55
 
55
- this.iframe.onload = async () => {
56
- this.emit('onload');
57
- if (this.render) {
58
- const el = await this.render(this.core);
59
- if (el) {
60
- this.iframe?.contentDocument?.body?.appendChild(el);
61
- }
62
- }
63
- };
56
+ this.iframe.addEventListener('load', this.loadHandler);
64
57
  }
65
58
 
66
59
  public getMagicApi = () => ({
@@ -113,9 +106,32 @@ export default class StageRender extends EventEmitter {
113
106
  * 销毁实例
114
107
  */
115
108
  public destroy(): void {
109
+ this.iframe?.removeEventListener('load', this.loadHandler);
116
110
  this.contentWindow = null;
117
111
  this.iframe?.remove();
118
112
  this.iframe = undefined;
119
113
  this.removeAllListeners();
120
114
  }
115
+
116
+ private loadHandler = async () => {
117
+ this.emit('onload');
118
+
119
+ if (this.render) {
120
+ const el = await this.render(this.core);
121
+ if (el) {
122
+ this.iframe?.contentDocument?.body?.appendChild(el);
123
+ }
124
+ }
125
+
126
+ if (this.contentWindow) {
127
+ const style = this.contentWindow.document.createElement('style');
128
+ style.id = 'tmagic-stage-render';
129
+ style.innerHTML = `
130
+ .${SELECTED_CLASS}, .${SELECTED_CLASS}-parent {
131
+ z-index: ${ZIndex.SELECTED_EL};
132
+ }
133
+ `;
134
+ this.contentWindow.document.head.appendChild(style);
135
+ }
136
+ };
121
137
  }
package/src/const.ts CHANGED
@@ -21,3 +21,27 @@ export const GHOST_EL_ID_PREFIX = 'ghost_el_';
21
21
 
22
22
  // 默认放到缩小倍数
23
23
  export const DEFAULT_ZOOM = 1;
24
+
25
+ export enum GuidesType {
26
+ HORIZONTAL = 'horizontal',
27
+ VERTICAL = 'vertical',
28
+ }
29
+
30
+ export enum ZIndex {
31
+ MASK = '99999',
32
+ SELECTED_EL = '666',
33
+ }
34
+
35
+ export enum MouseButton {
36
+ LEFT = 0,
37
+ MIDDLE = 1,
38
+ RIGHT = 2,
39
+ }
40
+
41
+ export enum Mode {
42
+ ABSOLUTE = 'absolute',
43
+ FIXED = 'fixed',
44
+ SORTABLE = 'sortable',
45
+ }
46
+
47
+ export const SELECTED_CLASS = 'tmagic-stage-selected-area';
package/src/types.ts CHANGED
@@ -1,7 +1,26 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
1
19
  import { MoveableOptions } from 'react-moveable/declaration/types';
2
20
 
3
21
  import { Id, MApp, MNode } from '@tmagic/schema';
4
22
 
23
+ import { GuidesType } from './const';
5
24
  import StageCore from './StageCore';
6
25
 
7
26
  export type CanSelect = (el: HTMLElement, stop: () => boolean) => boolean | Promise<boolean>;
@@ -41,23 +60,10 @@ export interface Offset {
41
60
  top: number;
42
61
  }
43
62
 
44
- /*
45
- * Tencent is pleased to support the open source community by making TMagicEditor available.
46
- *
47
- * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
48
- *
49
- * Licensed under the Apache License, Version 2.0 (the "License");
50
- * you may not use this file except in compliance with the License.
51
- * You may obtain a copy of the License at
52
- *
53
- * http://www.apache.org/licenses/LICENSE-2.0
54
- *
55
- * Unless required by applicable law or agreed to in writing, software
56
- * distributed under the License is distributed on an "AS IS" BASIS,
57
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
58
- * See the License for the specific language governing permissions and
59
- * limitations under the License.
60
- */
63
+ export interface GuidesEventData {
64
+ type: GuidesType;
65
+ guides: number[];
66
+ }
61
67
 
62
68
  export interface UpdateEventData {
63
69
  el: HTMLElement;
@@ -109,13 +115,7 @@ export interface RuntimeWindow extends Window {
109
115
  magic: Magic;
110
116
  }
111
117
 
112
- export enum ZIndex {
113
- MASK = '99999',
114
- GHOST_EL = '99998',
115
- }
116
-
117
- export enum MouseButton {
118
- LEFT = 0,
119
- MIDDLE = 1,
120
- RIGHT = 2,
118
+ export interface StageHighlightConfig {
119
+ core: StageCore;
120
+ container: HTMLElement;
121
121
  }
package/src/util.ts CHANGED
@@ -16,14 +16,9 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
+ import { Mode, SELECTED_CLASS } from './const';
19
20
  import type { Offset } from './types';
20
21
 
21
- export enum Mode {
22
- ABSOLUTE = 'absolute',
23
- FIXED = 'fixed',
24
- SORTABLE = 'sortable',
25
- }
26
-
27
22
  export const getOffset = (el: HTMLElement): Offset => {
28
23
  const { transform } = getComputedStyle(el);
29
24
  const { offsetParent } = el;
@@ -113,8 +108,63 @@ export const isFixed = (el?: HTMLElement): boolean => {
113
108
  return getComputedStyle(el).position === 'fixed';
114
109
  };
115
110
 
111
+ export const isFixedParent = (el: HTMLElement) => {
112
+ let fixed = false;
113
+ let dom = el;
114
+ while (dom) {
115
+ fixed = isFixed(dom);
116
+ if (fixed) {
117
+ break;
118
+ }
119
+ const { parentElement } = dom;
120
+ if (!parentElement || parentElement.tagName === 'BODY') {
121
+ break;
122
+ }
123
+ dom = parentElement;
124
+ }
125
+ return fixed;
126
+ };
127
+
116
128
  export const getMode = (el: HTMLElement): Mode => {
117
- if (isFixed(el)) return Mode.FIXED;
129
+ if (isFixedParent(el)) return Mode.FIXED;
118
130
  if (isStatic(el) || isRelative(el)) return Mode.SORTABLE;
119
131
  return Mode.ABSOLUTE;
120
132
  };
133
+
134
+ export const getScrollParent = (element: HTMLElement, includeHidden = false): HTMLElement | null => {
135
+ let style = getComputedStyle(element);
136
+ const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
137
+
138
+ if (isFixed(element)) return null;
139
+ for (let parent = element; parent.parentElement; ) {
140
+ parent = parent.parentElement;
141
+ style = getComputedStyle(parent);
142
+ if (isAbsolute(element) && isStatic(element)) {
143
+ continue;
144
+ }
145
+ if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
146
+ }
147
+
148
+ return null;
149
+ };
150
+
151
+ export const createDiv = ({ className, cssText }: { className: string; cssText: string }) => {
152
+ const el = globalThis.document.createElement('div');
153
+ el.className = className;
154
+ el.style.cssText = cssText;
155
+ return el;
156
+ };
157
+
158
+ export const removeSelectedClassName = (doc: Document) => {
159
+ const oldEl = doc.querySelector(`.${SELECTED_CLASS}`);
160
+
161
+ if (oldEl) {
162
+ oldEl.classList.remove(SELECTED_CLASS);
163
+ (oldEl.parentNode as HTMLDivElement)?.classList.remove(`${SELECTED_CLASS}-parent`);
164
+ }
165
+ };
166
+
167
+ export const addSelectedClassName = (el: Element) => {
168
+ el.classList.add(SELECTED_CLASS);
169
+ (el.parentNode as Element)?.classList.add(`${SELECTED_CLASS}-parent`);
170
+ };