@oix1987/yjd 1.0.3 → 2.0.0
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/LICENSE +15 -0
- package/README.md +146 -142
- package/core.js +77 -0
- package/dist/core.esm.js +2 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/rich-editor.esm.js +1 -1
- package/dist/rich-editor.esm.js.map +1 -1
- package/dist/rich-editor.min.js +1 -1
- package/dist/rich-editor.min.js.map +1 -1
- package/index.d.ts +134 -103
- package/index.js +227 -0
- package/lib/core/editor.js +1806 -0
- package/lib/core/format.js +540 -0
- package/lib/core/module.js +81 -0
- package/lib/core/registry.js +158 -0
- package/lib/formats/background.js +213 -0
- package/lib/formats/bold.js +49 -0
- package/lib/formats/capitalization.js +579 -0
- package/lib/formats/color.js +183 -0
- package/lib/formats/emoji.js +282 -0
- package/lib/formats/font-family.js +548 -0
- package/lib/formats/heading.js +502 -0
- package/lib/formats/image.js +347 -0
- package/lib/formats/import.js +385 -0
- package/lib/formats/indent.js +297 -0
- package/lib/formats/italic.js +27 -0
- package/lib/formats/line-height.js +562 -0
- package/lib/formats/link.js +251 -0
- package/lib/formats/list.js +635 -0
- package/lib/formats/strike.js +31 -0
- package/lib/formats/subscript.js +40 -0
- package/lib/formats/superscript.js +39 -0
- package/lib/formats/table.js +293 -0
- package/lib/formats/tag.js +304 -0
- package/lib/formats/text-align.js +422 -0
- package/lib/formats/text-size.js +498 -0
- package/lib/formats/underline.js +30 -0
- package/lib/formats/video.js +381 -0
- package/lib/modules/block-toolbar.js +639 -0
- package/lib/modules/code-view.js +447 -0
- package/lib/modules/find-replace.js +273 -0
- package/lib/modules/history.js +425 -0
- package/lib/modules/resize-handles.js +701 -0
- package/lib/modules/slash-menu.js +183 -0
- package/lib/modules/table-toolbar.js +635 -0
- package/lib/modules/toolbar.js +607 -0
- package/lib/styles-loader.js +142 -0
- package/{dist → lib}/styles.css +1285 -35
- package/lib/styles.css.js +2 -0
- package/lib/styles.min.css +1 -0
- package/lib/ui/color-picker.js +296 -0
- package/lib/ui/customselect.js +351 -0
- package/lib/ui/emoji-picker.js +196 -0
- package/lib/ui/icons.js +145 -0
- package/lib/ui/image-popup.js +435 -0
- package/lib/ui/import-popup.js +288 -0
- package/lib/ui/link-popup.js +139 -0
- package/lib/ui/list-picker.js +307 -0
- package/lib/ui/select-button.js +68 -0
- package/lib/ui/table-popup.js +171 -0
- package/lib/ui/tag-popup.js +249 -0
- package/lib/ui/text-align-picker.js +278 -0
- package/lib/ui/video-popup.js +413 -0
- package/lib/utils/exec-command.js +72 -0
- package/lib/utils/history-helper.js +50 -0
- package/lib/utils/popup-helper.js +219 -0
- package/lib/utils/popup-positioning.js +234 -0
- package/lib/utils/sanitize.js +164 -0
- package/package.json +51 -32
- package/umd-entry.js +18 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import Module from '../core/module.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resize Handles Module - Adds resize functionality to images, videos, and tables
|
|
5
|
+
* Creates 4 corner handles for dragging to resize elements
|
|
6
|
+
*/
|
|
7
|
+
class ResizeHandles extends Module {
|
|
8
|
+
static DEFAULTS = {
|
|
9
|
+
minWidth: 50,
|
|
10
|
+
minHeight: 50,
|
|
11
|
+
maxWidth: 800,
|
|
12
|
+
maxHeight: 600,
|
|
13
|
+
maintainAspectRatio: true, // For images and videos
|
|
14
|
+
snapToGrid: false,
|
|
15
|
+
gridSize: 10
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
constructor(editor, options = {}) {
|
|
19
|
+
super(editor, options);
|
|
20
|
+
this.activeElement = null;
|
|
21
|
+
this.handles = [];
|
|
22
|
+
this.isResizing = false;
|
|
23
|
+
this.startX = 0;
|
|
24
|
+
this.startY = 0;
|
|
25
|
+
this.startWidth = 0;
|
|
26
|
+
this.startHeight = 0;
|
|
27
|
+
this.currentHandle = null;
|
|
28
|
+
this.aspectRatio = 1;
|
|
29
|
+
|
|
30
|
+
this.init();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
init() {
|
|
34
|
+
this.createHandles();
|
|
35
|
+
this.setupEventListeners();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create resize handles container
|
|
40
|
+
*/
|
|
41
|
+
createHandles() {
|
|
42
|
+
this.handlesContainer = document.createElement('div');
|
|
43
|
+
this.handlesContainer.className = 'resize-handles-container';
|
|
44
|
+
this.handlesContainer.style.position = 'absolute';
|
|
45
|
+
this.handlesContainer.style.pointerEvents = 'none';
|
|
46
|
+
this.handlesContainer.style.zIndex = '997'; // Lower than all toolbar elements
|
|
47
|
+
this.handlesContainer.style.display = 'none';
|
|
48
|
+
|
|
49
|
+
// Create 4 corner handles
|
|
50
|
+
const handlePositions = [
|
|
51
|
+
{ name: 'nw', cursor: 'nw-resize', position: { top: -4, left: -4 } },
|
|
52
|
+
{ name: 'ne', cursor: 'ne-resize', position: { top: -4, right: -4 } },
|
|
53
|
+
{ name: 'sw', cursor: 'sw-resize', position: { bottom: -4, left: -4 } },
|
|
54
|
+
{ name: 'se', cursor: 'se-resize', position: { bottom: -4, right: -4 } }
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
handlePositions.forEach(config => {
|
|
58
|
+
const handle = this.createHandle(config);
|
|
59
|
+
this.handles.push(handle);
|
|
60
|
+
this.handlesContainer.appendChild(handle);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Add to editor wrapper but ensure it's behind toolbars
|
|
64
|
+
this.editor.wrapper.appendChild(this.handlesContainer);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create individual resize handle
|
|
69
|
+
*/
|
|
70
|
+
createHandle(config) {
|
|
71
|
+
const handle = document.createElement('div');
|
|
72
|
+
handle.className = `resize-handle resize-handle-${config.name}`;
|
|
73
|
+
handle.style.position = 'absolute';
|
|
74
|
+
handle.style.width = '8px';
|
|
75
|
+
handle.style.height = '8px';
|
|
76
|
+
handle.style.backgroundColor = '#3b82f6';
|
|
77
|
+
handle.style.border = '1px solid #fff';
|
|
78
|
+
handle.style.borderRadius = '50%';
|
|
79
|
+
handle.style.cursor = config.cursor;
|
|
80
|
+
handle.style.pointerEvents = 'auto';
|
|
81
|
+
handle.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
|
|
82
|
+
handle.style.zIndex = '999'; // Lower than toolbars
|
|
83
|
+
handle.dataset.handle = config.name;
|
|
84
|
+
|
|
85
|
+
// Position handle
|
|
86
|
+
Object.entries(config.position).forEach(([key, value]) => {
|
|
87
|
+
handle.style[key] = value + 'px';
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Add event listeners
|
|
91
|
+
handle.addEventListener('mousedown', (e) => this.handleMouseDown(e, config.name));
|
|
92
|
+
|
|
93
|
+
return handle;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Setup event listeners
|
|
98
|
+
*/
|
|
99
|
+
setupEventListeners() {
|
|
100
|
+
// Keep references so listeners can be removed in destroy() (the document/
|
|
101
|
+
// window handlers would otherwise leak across editor create/destroy cycles).
|
|
102
|
+
this._onEditorClick = (e) => this.handleElementClick(e);
|
|
103
|
+
this._onDocClick = (e) => {
|
|
104
|
+
if (!this.isClickOnResizableElement(e) && !this.isClickOnHandle(e)) {
|
|
105
|
+
this.hideHandles();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
this._onDocMousemove = (e) => this.handleMouseMove(e);
|
|
109
|
+
this._onDocMouseup = (e) => this.handleMouseUp(e);
|
|
110
|
+
this._onWindowScroll = () => {
|
|
111
|
+
if (this.activeElement) this.updateHandlePosition();
|
|
112
|
+
};
|
|
113
|
+
this._onEditorScroll = () => {
|
|
114
|
+
if (this.activeElement) this.updateHandlePosition();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
this.editor.editor.addEventListener('click', this._onEditorClick);
|
|
118
|
+
document.addEventListener('click', this._onDocClick);
|
|
119
|
+
document.addEventListener('mousemove', this._onDocMousemove);
|
|
120
|
+
document.addEventListener('mouseup', this._onDocMouseup);
|
|
121
|
+
window.addEventListener('scroll', this._onWindowScroll);
|
|
122
|
+
this.editor.editor.addEventListener('scroll', this._onEditorScroll);
|
|
123
|
+
|
|
124
|
+
// Listen for DOM changes to update handles
|
|
125
|
+
this.setupMutationObserver();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Handle click on resizable elements
|
|
130
|
+
*/
|
|
131
|
+
handleElementClick(e) {
|
|
132
|
+
const target = e.target;
|
|
133
|
+
|
|
134
|
+
// Debug logging
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// Find the actual resizable element
|
|
138
|
+
let resizableElement = this.findResizableElement(target);
|
|
139
|
+
|
|
140
|
+
if (resizableElement) {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
this.showHandles(resizableElement);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find the actual resizable element from a clicked target
|
|
149
|
+
*/
|
|
150
|
+
findResizableElement(target) {
|
|
151
|
+
// If target is already resizable, return it
|
|
152
|
+
if (this.isResizableElement(target)) {
|
|
153
|
+
return target;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If target is inside a table (td, th, tr, tbody), find the table
|
|
157
|
+
if (target.tagName === 'TD' || target.tagName === 'TH' ||
|
|
158
|
+
target.tagName === 'TR' || target.tagName === 'TBODY') {
|
|
159
|
+
let parent = target.parentElement;
|
|
160
|
+
while (parent && parent.tagName !== 'TABLE') {
|
|
161
|
+
parent = parent.parentElement;
|
|
162
|
+
}
|
|
163
|
+
if (parent && this.isResizableElement(parent)) {
|
|
164
|
+
return parent;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if any parent is resizable
|
|
169
|
+
let parent = target.parentElement;
|
|
170
|
+
while (parent && parent !== this.editor.wrapper) {
|
|
171
|
+
if (this.isResizableElement(parent)) {
|
|
172
|
+
return parent;
|
|
173
|
+
}
|
|
174
|
+
parent = parent.parentElement;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if element is resizable
|
|
182
|
+
*/
|
|
183
|
+
isResizableElement(element) {
|
|
184
|
+
// Debug logging
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
const isImage = element.classList.contains('inserted-image');
|
|
188
|
+
const isVideo = element.classList.contains('inserted-video');
|
|
189
|
+
const isTable = element.classList.contains('rich-editor-table');
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
return isImage || isVideo || isTable;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if click is on resizable element
|
|
198
|
+
*/
|
|
199
|
+
isClickOnResizableElement(e) {
|
|
200
|
+
return this.isResizableElement(e.target);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if click is on resize handle
|
|
205
|
+
*/
|
|
206
|
+
isClickOnHandle(e) {
|
|
207
|
+
return e.target.classList.contains('resize-handle');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Show resize handles for element
|
|
212
|
+
*/
|
|
213
|
+
showHandles(element) {
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
this.activeElement = element;
|
|
217
|
+
this.updateHandlePosition();
|
|
218
|
+
this.handlesContainer.style.display = 'block';
|
|
219
|
+
|
|
220
|
+
// Store aspect ratio for images and videos
|
|
221
|
+
if (element.classList.contains('inserted-image') || element.classList.contains('inserted-video')) {
|
|
222
|
+
this.aspectRatio = element.offsetWidth / element.offsetHeight;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// For tables, ensure they have proper positioning and setup size monitoring
|
|
226
|
+
if (element.classList.contains('rich-editor-table')) {
|
|
227
|
+
element.style.position = 'relative';
|
|
228
|
+
element.style.display = 'table';
|
|
229
|
+
|
|
230
|
+
// Store initial dimensions for comparison
|
|
231
|
+
this.lastTableWidth = element.offsetWidth;
|
|
232
|
+
this.lastTableHeight = element.offsetHeight;
|
|
233
|
+
|
|
234
|
+
// Setup periodic size checking for tables
|
|
235
|
+
this.setupTableSizeMonitoring(element);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Hide resize handles
|
|
243
|
+
*/
|
|
244
|
+
hideHandles() {
|
|
245
|
+
// Clear table size monitoring
|
|
246
|
+
if (this.tableSizeInterval) {
|
|
247
|
+
clearInterval(this.tableSizeInterval);
|
|
248
|
+
this.tableSizeInterval = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.activeElement = null;
|
|
252
|
+
this.handlesContainer.style.display = 'none';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Update handle position based on active element
|
|
257
|
+
*/
|
|
258
|
+
updateHandlePosition() {
|
|
259
|
+
if (!this.activeElement) return;
|
|
260
|
+
|
|
261
|
+
// Check if element still exists in DOM
|
|
262
|
+
if (!document.body.contains(this.activeElement)) {
|
|
263
|
+
this.hideHandles();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const elementRect = this.activeElement.getBoundingClientRect();
|
|
268
|
+
const editorRect = this.editor.wrapper.getBoundingClientRect();
|
|
269
|
+
const scrollTop = this.editor.wrapper.scrollTop || 0;
|
|
270
|
+
const scrollLeft = this.editor.wrapper.scrollLeft || 0;
|
|
271
|
+
|
|
272
|
+
// Position handles container
|
|
273
|
+
const top = elementRect.top - editorRect.top + scrollTop;
|
|
274
|
+
const left = elementRect.left - editorRect.left + scrollLeft;
|
|
275
|
+
const width = elementRect.width;
|
|
276
|
+
const height = elementRect.height;
|
|
277
|
+
const bottom = top + height;
|
|
278
|
+
this.handlesContainer.style.top = top + 'px';
|
|
279
|
+
this.handlesContainer.style.left = left + 'px';
|
|
280
|
+
this.handlesContainer.style.width = width + 'px';
|
|
281
|
+
this.handlesContainer.style.height = height + 'px';
|
|
282
|
+
|
|
283
|
+
if(bottom < 0){
|
|
284
|
+
this.hideHandles();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if(top > editorRect.height){
|
|
288
|
+
this.hideHandles();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Handle mouse down on resize handle
|
|
295
|
+
*/
|
|
296
|
+
handleMouseDown(e, handleName) {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
e.stopPropagation();
|
|
299
|
+
|
|
300
|
+
this.isResizing = true;
|
|
301
|
+
this.currentHandle = handleName;
|
|
302
|
+
this.startX = e.clientX;
|
|
303
|
+
this.startY = e.clientY;
|
|
304
|
+
this.startWidth = this.activeElement.offsetWidth;
|
|
305
|
+
this.startHeight = this.activeElement.offsetHeight;
|
|
306
|
+
|
|
307
|
+
// Store initial position
|
|
308
|
+
const elementRect = this.activeElement.getBoundingClientRect();
|
|
309
|
+
this.startLeft = elementRect.left;
|
|
310
|
+
this.startTop = elementRect.top;
|
|
311
|
+
|
|
312
|
+
// Add resizing class for styling
|
|
313
|
+
this.activeElement.classList.add('resizing');
|
|
314
|
+
document.body.style.cursor = e.target.style.cursor;
|
|
315
|
+
document.body.style.userSelect = 'none';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Handle mouse move during resize
|
|
320
|
+
*/
|
|
321
|
+
handleMouseMove(e) {
|
|
322
|
+
if (!this.isResizing || !this.activeElement) return;
|
|
323
|
+
|
|
324
|
+
const deltaX = e.clientX - this.startX;
|
|
325
|
+
const deltaY = e.clientY - this.startY;
|
|
326
|
+
|
|
327
|
+
let newWidth = this.startWidth;
|
|
328
|
+
let newHeight = this.startHeight;
|
|
329
|
+
|
|
330
|
+
// Calculate new dimensions based on handle position
|
|
331
|
+
switch (this.currentHandle) {
|
|
332
|
+
case 'nw':
|
|
333
|
+
newWidth = this.startWidth - deltaX;
|
|
334
|
+
newHeight = this.startHeight - deltaY;
|
|
335
|
+
break;
|
|
336
|
+
case 'ne':
|
|
337
|
+
newWidth = this.startWidth + deltaX;
|
|
338
|
+
newHeight = this.startHeight - deltaY;
|
|
339
|
+
break;
|
|
340
|
+
case 'sw':
|
|
341
|
+
newWidth = this.startWidth - deltaX;
|
|
342
|
+
newHeight = this.startHeight + deltaY;
|
|
343
|
+
break;
|
|
344
|
+
case 'se':
|
|
345
|
+
newWidth = this.startWidth + deltaX;
|
|
346
|
+
newHeight = this.startHeight + deltaY;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Never let an element grow past the editor's content width (its inner
|
|
351
|
+
// width minus padding) — otherwise tables/images overflow the editor.
|
|
352
|
+
let maxW = this.options.maxWidth;
|
|
353
|
+
const area = this.editor && this.editor.editor;
|
|
354
|
+
if (area) {
|
|
355
|
+
const cs = getComputedStyle(area);
|
|
356
|
+
const avail = area.clientWidth
|
|
357
|
+
- (parseFloat(cs.paddingLeft) || 0)
|
|
358
|
+
- (parseFloat(cs.paddingRight) || 0);
|
|
359
|
+
if (avail > 0) maxW = Math.min(maxW, avail);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Apply constraints
|
|
363
|
+
newWidth = Math.max(this.options.minWidth, Math.min(maxW, newWidth));
|
|
364
|
+
newHeight = Math.max(this.options.minHeight, Math.min(this.options.maxHeight, newHeight));
|
|
365
|
+
|
|
366
|
+
// Maintain aspect ratio for images and videos
|
|
367
|
+
if ((this.activeElement.classList.contains('inserted-image') ||
|
|
368
|
+
this.activeElement.classList.contains('inserted-video')) &&
|
|
369
|
+
this.options.maintainAspectRatio) {
|
|
370
|
+
|
|
371
|
+
const ratioByWidth = newWidth / this.aspectRatio;
|
|
372
|
+
const ratioByHeight = newHeight * this.aspectRatio;
|
|
373
|
+
|
|
374
|
+
if (Math.abs(newWidth - ratioByHeight) < Math.abs(newHeight - ratioByWidth)) {
|
|
375
|
+
newWidth = ratioByHeight;
|
|
376
|
+
} else {
|
|
377
|
+
newHeight = ratioByWidth;
|
|
378
|
+
}
|
|
379
|
+
// Aspect-ratio math can push width back over the limit — re-clamp.
|
|
380
|
+
if (newWidth > maxW) {
|
|
381
|
+
newWidth = maxW;
|
|
382
|
+
newHeight = newWidth / this.aspectRatio;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Snap to grid if enabled
|
|
387
|
+
if (this.options.snapToGrid) {
|
|
388
|
+
newWidth = Math.round(newWidth / this.options.gridSize) * this.options.gridSize;
|
|
389
|
+
newHeight = Math.round(newHeight / this.options.gridSize) * this.options.gridSize;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Apply new dimensions
|
|
393
|
+
this.applyDimensions(newWidth, newHeight);
|
|
394
|
+
this.updateHandlePosition();
|
|
395
|
+
|
|
396
|
+
// Emit resize event
|
|
397
|
+
this.emit('element-resize', {
|
|
398
|
+
element: this.activeElement,
|
|
399
|
+
width: newWidth,
|
|
400
|
+
height: newHeight,
|
|
401
|
+
handle: this.currentHandle
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Handle mouse up - end resize
|
|
407
|
+
*/
|
|
408
|
+
handleMouseUp(e) {
|
|
409
|
+
if (!this.isResizing) return;
|
|
410
|
+
|
|
411
|
+
this.isResizing = false;
|
|
412
|
+
this.currentHandle = null;
|
|
413
|
+
|
|
414
|
+
// Remove resizing class
|
|
415
|
+
if (this.activeElement) {
|
|
416
|
+
this.activeElement.classList.remove('resizing');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Reset cursor
|
|
420
|
+
document.body.style.cursor = '';
|
|
421
|
+
document.body.style.userSelect = '';
|
|
422
|
+
|
|
423
|
+
// Emit resize complete event
|
|
424
|
+
this.emit('element-resize-complete', {
|
|
425
|
+
element: this.activeElement,
|
|
426
|
+
width: this.activeElement.offsetWidth,
|
|
427
|
+
height: this.activeElement.offsetHeight
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Apply dimensions to element
|
|
433
|
+
*/
|
|
434
|
+
applyDimensions(width, height) {
|
|
435
|
+
if (!this.activeElement) return;
|
|
436
|
+
|
|
437
|
+
if (this.activeElement.classList.contains('rich-editor-table')) {
|
|
438
|
+
// For tables, set both width and height
|
|
439
|
+
this.activeElement.style.width = width + 'px';
|
|
440
|
+
this.activeElement.style.minWidth = width + 'px';
|
|
441
|
+
this.activeElement.style.height = height + 'px';
|
|
442
|
+
this.activeElement.style.minHeight = height + 'px';
|
|
443
|
+
|
|
444
|
+
// Calculate cell dimensions
|
|
445
|
+
const rows = this.activeElement.querySelectorAll('tr');
|
|
446
|
+
const cols = rows.length > 0 ? rows[0].querySelectorAll('td, th').length : 0;
|
|
447
|
+
|
|
448
|
+
if (rows.length > 0 && cols > 0) {
|
|
449
|
+
const cellWidth = Math.floor(width / cols);
|
|
450
|
+
const cellHeight = Math.floor(height / rows.length);
|
|
451
|
+
|
|
452
|
+
// Apply dimensions to all cells
|
|
453
|
+
const cells = this.activeElement.querySelectorAll('td, th');
|
|
454
|
+
cells.forEach(cell => {
|
|
455
|
+
cell.style.minWidth = cellWidth + 'px';
|
|
456
|
+
cell.style.minHeight = cellHeight + 'px';
|
|
457
|
+
cell.style.height = cellHeight + 'px';
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
// For images and videos (including iframes)
|
|
462
|
+
this.activeElement.style.width = width + 'px';
|
|
463
|
+
this.activeElement.style.height = height + 'px';
|
|
464
|
+
|
|
465
|
+
// If it's an iframe, update its attributes too
|
|
466
|
+
if (this.activeElement.tagName === 'IFRAME') {
|
|
467
|
+
this.activeElement.width = width;
|
|
468
|
+
this.activeElement.height = height;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Get current active element
|
|
475
|
+
*/
|
|
476
|
+
getActiveElement() {
|
|
477
|
+
return this.activeElement;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Set active element programmatically
|
|
482
|
+
*/
|
|
483
|
+
setActiveElement(element) {
|
|
484
|
+
if (this.isResizableElement(element)) {
|
|
485
|
+
this.showHandles(element);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Check and update handles if active element has changed
|
|
491
|
+
*/
|
|
492
|
+
checkAndUpdateHandles() {
|
|
493
|
+
if (this.activeElement) {
|
|
494
|
+
// Check if element still exists and is still resizable
|
|
495
|
+
if (!document.body.contains(this.activeElement) || !this.isResizableElement(this.activeElement)) {
|
|
496
|
+
this.hideHandles();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Update position if element still exists
|
|
501
|
+
this.updateHandlePosition();
|
|
502
|
+
|
|
503
|
+
// For tables, also check if size has changed due to content
|
|
504
|
+
if (this.activeElement.classList.contains('rich-editor-table')) {
|
|
505
|
+
this.checkTableSizeChange();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Force refresh handles for current active element
|
|
512
|
+
*/
|
|
513
|
+
refreshHandles() {
|
|
514
|
+
if (this.activeElement && document.body.contains(this.activeElement)) {
|
|
515
|
+
this.updateHandlePosition();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Setup periodic monitoring for table size changes
|
|
521
|
+
*/
|
|
522
|
+
setupTableSizeMonitoring(tableElement) {
|
|
523
|
+
// Clear any existing interval
|
|
524
|
+
if (this.tableSizeInterval) {
|
|
525
|
+
clearInterval(this.tableSizeInterval);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Check table size every 100ms
|
|
529
|
+
this.tableSizeInterval = setInterval(() => {
|
|
530
|
+
if (this.activeElement && this.activeElement.classList.contains('rich-editor-table')) {
|
|
531
|
+
this.checkTableSizeChange();
|
|
532
|
+
} else {
|
|
533
|
+
// Clear interval if no longer monitoring a table
|
|
534
|
+
clearInterval(this.tableSizeInterval);
|
|
535
|
+
this.tableSizeInterval = null;
|
|
536
|
+
}
|
|
537
|
+
}, 100);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Check if table size has changed and update handles accordingly
|
|
542
|
+
*/
|
|
543
|
+
checkTableSizeChange() {
|
|
544
|
+
if (!this.activeElement || !this.activeElement.classList.contains('rich-editor-table')) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const currentWidth = this.activeElement.offsetWidth;
|
|
549
|
+
const currentHeight = this.activeElement.offsetHeight;
|
|
550
|
+
|
|
551
|
+
// Check if dimensions have changed significantly (more than 1px to avoid floating point issues)
|
|
552
|
+
if (Math.abs(currentWidth - this.lastTableWidth) > 1 ||
|
|
553
|
+
Math.abs(currentHeight - this.lastTableHeight) > 1) {
|
|
554
|
+
|
|
555
|
+
// Update stored dimensions
|
|
556
|
+
this.lastTableWidth = currentWidth;
|
|
557
|
+
this.lastTableHeight = currentHeight;
|
|
558
|
+
|
|
559
|
+
// Update handle positions
|
|
560
|
+
this.updateHandlePosition();
|
|
561
|
+
|
|
562
|
+
// Emit size change event
|
|
563
|
+
this.emit('table-size-changed', {
|
|
564
|
+
element: this.activeElement,
|
|
565
|
+
width: currentWidth,
|
|
566
|
+
height: currentHeight,
|
|
567
|
+
previousWidth: this.lastTableWidth,
|
|
568
|
+
previousHeight: this.lastTableHeight
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Setup mutation observer to watch for DOM changes
|
|
575
|
+
*/
|
|
576
|
+
setupMutationObserver() {
|
|
577
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
578
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
579
|
+
let shouldUpdate = false;
|
|
580
|
+
|
|
581
|
+
mutations.forEach((mutation) => {
|
|
582
|
+
// Check if active element was removed or modified
|
|
583
|
+
if (this.activeElement) {
|
|
584
|
+
if (mutation.type === 'childList') {
|
|
585
|
+
// Check if active element was removed
|
|
586
|
+
if (mutation.removedNodes) {
|
|
587
|
+
for (let node of mutation.removedNodes) {
|
|
588
|
+
if (node === this.activeElement || node.contains(this.activeElement)) {
|
|
589
|
+
shouldUpdate = true;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Check if active element was modified
|
|
596
|
+
if (mutation.target === this.activeElement ||
|
|
597
|
+
(mutation.target.nodeType === Node.ELEMENT_NODE &&
|
|
598
|
+
this.activeElement.contains(mutation.target))) {
|
|
599
|
+
shouldUpdate = true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Check for text content changes that might affect table size
|
|
604
|
+
if (mutation.type === 'characterData' && this.activeElement.classList.contains('rich-editor-table')) {
|
|
605
|
+
// Check if the text change is within the active table
|
|
606
|
+
let target = mutation.target;
|
|
607
|
+
while (target && target !== this.activeElement) {
|
|
608
|
+
target = target.parentNode;
|
|
609
|
+
}
|
|
610
|
+
if (target === this.activeElement) {
|
|
611
|
+
shouldUpdate = true;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Check for attribute changes that might affect size
|
|
616
|
+
if (mutation.type === 'attributes' && this.activeElement.classList.contains('rich-editor-table')) {
|
|
617
|
+
const attributeName = mutation.attributeName;
|
|
618
|
+
// Monitor changes to style attributes that affect size
|
|
619
|
+
if (attributeName === 'style' || attributeName === 'class') {
|
|
620
|
+
shouldUpdate = true;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
if (shouldUpdate) {
|
|
627
|
+
// Use setTimeout to ensure DOM is fully updated
|
|
628
|
+
setTimeout(() => {
|
|
629
|
+
this.checkAndUpdateHandles();
|
|
630
|
+
}, 0);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Start observing the editor content with more comprehensive monitoring
|
|
635
|
+
this.mutationObserver.observe(this.editor.editor, {
|
|
636
|
+
childList: true,
|
|
637
|
+
subtree: true,
|
|
638
|
+
attributes: true,
|
|
639
|
+
attributeFilter: ['style', 'class'],
|
|
640
|
+
characterData: true, // Monitor text content changes
|
|
641
|
+
characterDataOldValue: true
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Enable/disable aspect ratio maintenance
|
|
648
|
+
*/
|
|
649
|
+
setMaintainAspectRatio(maintain) {
|
|
650
|
+
this.options.maintainAspectRatio = maintain;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Set resize constraints
|
|
655
|
+
*/
|
|
656
|
+
setConstraints(minWidth, minHeight, maxWidth, maxHeight) {
|
|
657
|
+
this.options.minWidth = minWidth || this.options.minWidth;
|
|
658
|
+
this.options.minHeight = minHeight || this.options.minHeight;
|
|
659
|
+
this.options.maxWidth = maxWidth || this.options.maxWidth;
|
|
660
|
+
this.options.maxHeight = maxHeight || this.options.maxHeight;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Destroy module
|
|
665
|
+
*/
|
|
666
|
+
destroy() {
|
|
667
|
+
this.hideHandles();
|
|
668
|
+
|
|
669
|
+
// Remove global event listeners
|
|
670
|
+
if (this._onDocClick) {
|
|
671
|
+
this.editor.editor.removeEventListener('click', this._onEditorClick);
|
|
672
|
+
document.removeEventListener('click', this._onDocClick);
|
|
673
|
+
document.removeEventListener('mousemove', this._onDocMousemove);
|
|
674
|
+
document.removeEventListener('mouseup', this._onDocMouseup);
|
|
675
|
+
window.removeEventListener('scroll', this._onWindowScroll);
|
|
676
|
+
this.editor.editor.removeEventListener('scroll', this._onEditorScroll);
|
|
677
|
+
this._onEditorClick = this._onDocClick = this._onDocMousemove = null;
|
|
678
|
+
this._onDocMouseup = this._onWindowScroll = this._onEditorScroll = null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (this.handlesContainer) {
|
|
682
|
+
this.handlesContainer.remove();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Clear table size monitoring
|
|
686
|
+
if (this.tableSizeInterval) {
|
|
687
|
+
clearInterval(this.tableSizeInterval);
|
|
688
|
+
this.tableSizeInterval = null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Disconnect mutation observer
|
|
692
|
+
if (this.mutationObserver) {
|
|
693
|
+
this.mutationObserver.disconnect();
|
|
694
|
+
this.mutationObserver = null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
super.destroy();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export default ResizeHandles;
|