autumnnote 1.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 +21 -0
- package/README.md +874 -0
- package/dist/autumnnote.css +1 -0
- package/dist/autumnnote.es.js +5888 -0
- package/dist/autumnnote.es.js.map +1 -0
- package/dist/autumnnote.umd.js +74 -0
- package/dist/autumnnote.umd.js.map +1 -0
- package/package.json +55 -0
- package/src/js/Context.js +497 -0
- package/src/js/core/dom.js +315 -0
- package/src/js/core/env.js +25 -0
- package/src/js/core/func.js +153 -0
- package/src/js/core/key.js +66 -0
- package/src/js/core/lists.js +121 -0
- package/src/js/core/markdown.js +294 -0
- package/src/js/core/range.js +194 -0
- package/src/js/core/sanitise.js +78 -0
- package/src/js/editing/History.js +205 -0
- package/src/js/editing/Style.js +329 -0
- package/src/js/editing/Table.js +59 -0
- package/src/js/editing/Typing.js +142 -0
- package/src/js/index.js +126 -0
- package/src/js/module/Buttons.js +300 -0
- package/src/js/module/Clipboard.js +460 -0
- package/src/js/module/CodeTooltip.js +428 -0
- package/src/js/module/Codeview.js +122 -0
- package/src/js/module/ContextMenu.js +470 -0
- package/src/js/module/Editor.js +528 -0
- package/src/js/module/EmojiDialog.js +726 -0
- package/src/js/module/FindReplace.js +440 -0
- package/src/js/module/Fullscreen.js +80 -0
- package/src/js/module/IconDialog.js +620 -0
- package/src/js/module/ImageDialog.js +208 -0
- package/src/js/module/ImageResizer.js +216 -0
- package/src/js/module/ImageTooltip.js +286 -0
- package/src/js/module/LinkDialog.js +204 -0
- package/src/js/module/LinkTooltip.js +242 -0
- package/src/js/module/Placeholder.js +44 -0
- package/src/js/module/ShortcutsDialog.js +141 -0
- package/src/js/module/Statusbar.js +238 -0
- package/src/js/module/TableTooltip.js +568 -0
- package/src/js/module/Toolbar.js +562 -0
- package/src/js/module/VideoDialog.js +263 -0
- package/src/js/module/VideoResizer.js +227 -0
- package/src/js/module/VideoTooltip.js +252 -0
- package/src/js/renderer.js +107 -0
- package/src/js/settings.js +134 -0
- package/src/styles/_variables.scss +48 -0
- package/src/styles/autumnnote.scss +1740 -0
- package/types/index.d.ts +324 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageDialog.js - Dialog for inserting images (by URL or file upload)
|
|
3
|
+
* Inspired by Summernote's ImageDialog — rewritten without jQuery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
7
|
+
import { withSavedRange } from '../core/range.js';
|
|
8
|
+
|
|
9
|
+
export class ImageDialog {
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('../Context.js').Context} context
|
|
12
|
+
*/
|
|
13
|
+
constructor(context) {
|
|
14
|
+
this.context = context;
|
|
15
|
+
this.options = context.options;
|
|
16
|
+
/** @type {HTMLElement|null} */
|
|
17
|
+
this._dialog = null;
|
|
18
|
+
this._disposers = [];
|
|
19
|
+
this._savedRange = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Lifecycle
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
initialize() {
|
|
27
|
+
this._dialog = this._buildDialog();
|
|
28
|
+
document.body.appendChild(this._dialog);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
destroy() {
|
|
33
|
+
this._disposers.forEach((d) => d());
|
|
34
|
+
this._disposers = [];
|
|
35
|
+
if (this._dialog && this._dialog.parentNode) {
|
|
36
|
+
this._dialog.parentNode.removeChild(this._dialog);
|
|
37
|
+
}
|
|
38
|
+
this._dialog = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Public API
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
show() {
|
|
46
|
+
withSavedRange((range) => {
|
|
47
|
+
this._savedRange = range;
|
|
48
|
+
});
|
|
49
|
+
this._urlInput.value = '';
|
|
50
|
+
this._altInput.value = '';
|
|
51
|
+
if (this._fileInput) this._fileInput.value = '';
|
|
52
|
+
this._open();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Build dialog
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
_buildDialog() {
|
|
60
|
+
const overlay = createElement('div', {
|
|
61
|
+
class: 'an-dialog-overlay',
|
|
62
|
+
role: 'dialog',
|
|
63
|
+
'aria-modal': 'true',
|
|
64
|
+
'aria-label': 'Insert image',
|
|
65
|
+
});
|
|
66
|
+
const box = createElement('div', { class: 'an-dialog-box' });
|
|
67
|
+
|
|
68
|
+
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
69
|
+
title.textContent = 'Insert Image';
|
|
70
|
+
|
|
71
|
+
// URL tab
|
|
72
|
+
const urlLabel = createElement('label', { class: 'an-label' });
|
|
73
|
+
urlLabel.textContent = 'Image URL';
|
|
74
|
+
const urlInput = createElement('input', {
|
|
75
|
+
type: 'url',
|
|
76
|
+
class: 'an-input',
|
|
77
|
+
placeholder: 'https://example.com/image.png',
|
|
78
|
+
autocomplete: 'off',
|
|
79
|
+
});
|
|
80
|
+
this._urlInput = urlInput;
|
|
81
|
+
|
|
82
|
+
// Alt text
|
|
83
|
+
const altLabel = createElement('label', { class: 'an-label' });
|
|
84
|
+
altLabel.textContent = 'Alt Text';
|
|
85
|
+
const altInput = createElement('input', {
|
|
86
|
+
type: 'text',
|
|
87
|
+
class: 'an-input',
|
|
88
|
+
placeholder: 'Describe the image',
|
|
89
|
+
autocomplete: 'off',
|
|
90
|
+
});
|
|
91
|
+
this._altInput = altInput;
|
|
92
|
+
|
|
93
|
+
box.append(title, urlLabel, urlInput, altLabel, altInput);
|
|
94
|
+
|
|
95
|
+
// Alignment
|
|
96
|
+
const alignLabel = createElement('label', { class: 'an-label' });
|
|
97
|
+
alignLabel.textContent = 'Alignment';
|
|
98
|
+
const alignRow = createElement('div', { class: 'an-align-row' });
|
|
99
|
+
const alignOptions = [
|
|
100
|
+
{ value: '', label: 'None' },
|
|
101
|
+
{ value: 'left', label: 'Left' },
|
|
102
|
+
{ value: 'center', label: 'Center' },
|
|
103
|
+
{ value: 'right', label: 'Right' },
|
|
104
|
+
];
|
|
105
|
+
alignOptions.forEach(({ value, label }) => {
|
|
106
|
+
const radioId = `an-align-${value || 'none'}`;
|
|
107
|
+
const radio = createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value });
|
|
108
|
+
if (value === '') radio.checked = true;
|
|
109
|
+
const lbl = createElement('label', { for: radioId, class: 'an-align-label' });
|
|
110
|
+
lbl.textContent = label;
|
|
111
|
+
alignRow.append(radio, lbl);
|
|
112
|
+
});
|
|
113
|
+
this._alignRow = alignRow;
|
|
114
|
+
box.append(alignLabel, alignRow);
|
|
115
|
+
if (this.options.allowImageUpload !== false) {
|
|
116
|
+
const fileLabel = createElement('label', { class: 'an-label' });
|
|
117
|
+
fileLabel.textContent = 'Or upload a file';
|
|
118
|
+
const fileInput = createElement('input', {
|
|
119
|
+
type: 'file',
|
|
120
|
+
class: 'an-input',
|
|
121
|
+
accept: 'image/*',
|
|
122
|
+
});
|
|
123
|
+
this._fileInput = fileInput;
|
|
124
|
+
const d = on(fileInput, 'change', () => this._onFileChange());
|
|
125
|
+
this._disposers.push(d);
|
|
126
|
+
box.append(fileLabel, fileInput);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Buttons
|
|
130
|
+
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
131
|
+
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
132
|
+
insertBtn.textContent = 'Insert';
|
|
133
|
+
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
134
|
+
cancelBtn.textContent = 'Cancel';
|
|
135
|
+
btnRow.appendChild(insertBtn);
|
|
136
|
+
btnRow.appendChild(cancelBtn);
|
|
137
|
+
|
|
138
|
+
box.append(btnRow);
|
|
139
|
+
overlay.appendChild(box);
|
|
140
|
+
|
|
141
|
+
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
142
|
+
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
143
|
+
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
144
|
+
const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
145
|
+
const d5 = on(altInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
146
|
+
this._disposers.push(d1, d2, d3, d4, d5);
|
|
147
|
+
|
|
148
|
+
return overlay;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Actions
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
_onFileChange() {
|
|
156
|
+
const file = this._fileInput && this._fileInput.files && this._fileInput.files[0];
|
|
157
|
+
if (!file || !file.type.startsWith('image/')) return;
|
|
158
|
+
|
|
159
|
+
// Validate file size (max 5 MB by default)
|
|
160
|
+
const maxSize = (this.options.maxImageSize || 5) * 1024 * 1024;
|
|
161
|
+
if (file.size > maxSize) {
|
|
162
|
+
const message = `Image file is too large. Maximum allowed size is ${this.options.maxImageSize || 5} MB.`;
|
|
163
|
+
console.warn('[AutumnNote] ImageDialog:', message);
|
|
164
|
+
this.context.triggerEvent('imageError', { file, message });
|
|
165
|
+
this._fileInput.value = '';
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const reader = new FileReader();
|
|
170
|
+
reader.onload = (e) => {
|
|
171
|
+
this._urlInput.value = e.target.result;
|
|
172
|
+
};
|
|
173
|
+
reader.readAsDataURL(file);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_onInsert() {
|
|
177
|
+
const src = this._urlInput.value.trim();
|
|
178
|
+
const alt = this._altInput.value.trim();
|
|
179
|
+
const alignRadio = this._alignRow && this._alignRow.querySelector('input[name="an-img-align"]:checked');
|
|
180
|
+
const align = alignRadio ? alignRadio.value : '';
|
|
181
|
+
|
|
182
|
+
if (!src) {
|
|
183
|
+
this._urlInput.focus();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this._savedRange) {
|
|
188
|
+
this._savedRange.select();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.context.invoke('editor.insertImage', src, alt, align);
|
|
192
|
+
this._close();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_open() {
|
|
196
|
+
if (this._dialog) {
|
|
197
|
+
this._dialog.style.display = 'flex';
|
|
198
|
+
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
199
|
+
setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_close() {
|
|
204
|
+
if (this._dialog) this._dialog.style.display = 'none';
|
|
205
|
+
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
206
|
+
this._savedRange = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// ImageResizer.js - Interactive resize handles for selected images in the editor
|
|
2
|
+
import { on } from '../core/dom.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Eight handle positions. 'pos' is used as CSS class suffix and
|
|
6
|
+
* to determine which axis/direction is being dragged.
|
|
7
|
+
*
|
|
8
|
+
* Pos length 2 → corner handle → maintain aspect ratio.
|
|
9
|
+
* Pos length 1 → edge handle → single-axis resize.
|
|
10
|
+
*/
|
|
11
|
+
const HANDLE_DEFS = [
|
|
12
|
+
{ pos: 'nw', cursor: 'nw-resize' },
|
|
13
|
+
{ pos: 'n', cursor: 'n-resize' },
|
|
14
|
+
{ pos: 'ne', cursor: 'ne-resize' },
|
|
15
|
+
{ pos: 'e', cursor: 'e-resize' },
|
|
16
|
+
{ pos: 'se', cursor: 'se-resize' },
|
|
17
|
+
{ pos: 's', cursor: 's-resize' },
|
|
18
|
+
{ pos: 'sw', cursor: 'sw-resize' },
|
|
19
|
+
{ pos: 'w', cursor: 'w-resize' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export class ImageResizer {
|
|
23
|
+
/** @param {import('../Context.js').Context} context */
|
|
24
|
+
constructor(context) {
|
|
25
|
+
this.context = context;
|
|
26
|
+
/** @type {HTMLImageElement|null} */
|
|
27
|
+
this._activeImg = null;
|
|
28
|
+
this._overlay = null;
|
|
29
|
+
this._disposers = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
initialize() {
|
|
33
|
+
this._overlay = this._buildOverlay();
|
|
34
|
+
document.body.appendChild(this._overlay);
|
|
35
|
+
|
|
36
|
+
const editable = this.context.layoutInfo.editable;
|
|
37
|
+
|
|
38
|
+
this._disposers.push(
|
|
39
|
+
on(editable, 'click', (e) => this._onEditorClick(e)),
|
|
40
|
+
// Also select on right-click so the highlight shows before the context menu
|
|
41
|
+
on(editable, 'contextmenu', (e) => {
|
|
42
|
+
const img = e.target.closest('img');
|
|
43
|
+
if (img) this._select(img);
|
|
44
|
+
}),
|
|
45
|
+
on(document, 'click', (e) => this._onDocClick(e)),
|
|
46
|
+
on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
47
|
+
on(window, 'resize', () => this._updateOverlayPosition()),
|
|
48
|
+
on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
destroy() {
|
|
55
|
+
this._disposers.forEach((d) => d());
|
|
56
|
+
this._disposers = [];
|
|
57
|
+
if (this._dragDisposers) {
|
|
58
|
+
this._dragDisposers.forEach((d) => d());
|
|
59
|
+
this._dragDisposers = null;
|
|
60
|
+
}
|
|
61
|
+
this._deselect();
|
|
62
|
+
if (this._overlay && this._overlay.parentNode) {
|
|
63
|
+
this._overlay.parentNode.removeChild(this._overlay);
|
|
64
|
+
}
|
|
65
|
+
this._overlay = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Public API used by other modules
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/** @returns {HTMLImageElement|null} */
|
|
73
|
+
getActiveImage() {
|
|
74
|
+
return this._activeImg;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Re-sync overlay position (call after external size changes). */
|
|
78
|
+
updateOverlay() {
|
|
79
|
+
this._updateOverlayPosition();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Programmatically clear the current image selection. */
|
|
83
|
+
deselect() {
|
|
84
|
+
this._deselect();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Internal
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
_buildOverlay() {
|
|
92
|
+
const overlay = document.createElement('div');
|
|
93
|
+
overlay.className = 'an-image-resizer';
|
|
94
|
+
overlay.style.display = 'none';
|
|
95
|
+
|
|
96
|
+
HANDLE_DEFS.forEach(({ pos }) => {
|
|
97
|
+
const h = document.createElement('div');
|
|
98
|
+
h.className = `an-resize-handle an-resize-${pos}`;
|
|
99
|
+
h.dataset.handle = pos;
|
|
100
|
+
// Attach handle listeners here so they're torn down in destroy() via _disposers
|
|
101
|
+
this._disposers.push(
|
|
102
|
+
on(h, 'mousedown', (e) => {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
this._startResize(e, pos);
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
overlay.appendChild(h);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return overlay;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_onEditorClick(e) {
|
|
115
|
+
const img = e.target.closest('img');
|
|
116
|
+
if (img) {
|
|
117
|
+
// Prevent browser from deselecting the current text selection / opening native resize
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
this._select(img);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_onDocClick(e) {
|
|
124
|
+
if (!this._activeImg) return;
|
|
125
|
+
if (e.target === this._activeImg) return;
|
|
126
|
+
if (this._overlay && this._overlay.contains(e.target)) return;
|
|
127
|
+
// Don't deselect while interacting with the context menu
|
|
128
|
+
if (e.target.closest('.an-contextmenu')) return;
|
|
129
|
+
this._deselect();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_select(img) {
|
|
133
|
+
if (this._activeImg && this._activeImg !== img) {
|
|
134
|
+
this._activeImg.classList.remove('an-image-selected');
|
|
135
|
+
}
|
|
136
|
+
this._activeImg = img;
|
|
137
|
+
img.classList.add('an-image-selected');
|
|
138
|
+
this._updateOverlayPosition();
|
|
139
|
+
this._overlay.style.display = 'block';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_deselect() {
|
|
143
|
+
if (this._activeImg) {
|
|
144
|
+
this._activeImg.classList.remove('an-image-selected');
|
|
145
|
+
this._activeImg = null;
|
|
146
|
+
}
|
|
147
|
+
if (this._overlay) this._overlay.style.display = 'none';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_updateOverlayPosition() {
|
|
151
|
+
if (!this._activeImg || !this._overlay) return;
|
|
152
|
+
const rect = this._activeImg.getBoundingClientRect();
|
|
153
|
+
this._overlay.style.left = `${rect.left}px`;
|
|
154
|
+
this._overlay.style.top = `${rect.top}px`;
|
|
155
|
+
this._overlay.style.width = `${rect.width}px`;
|
|
156
|
+
this._overlay.style.height = `${rect.height}px`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_startResize(e, pos) {
|
|
160
|
+
const img = this._activeImg;
|
|
161
|
+
if (!img) return;
|
|
162
|
+
|
|
163
|
+
const startX = e.clientX;
|
|
164
|
+
const startY = e.clientY;
|
|
165
|
+
const startW = img.offsetWidth || img.naturalWidth || 100;
|
|
166
|
+
const startH = img.offsetHeight || img.naturalHeight || 100;
|
|
167
|
+
const aspectRatio = startW / (startH || 1);
|
|
168
|
+
const isCorner = pos.length === 2; // 'nw','ne','se','sw'
|
|
169
|
+
|
|
170
|
+
const editable = this.context.layoutInfo.editable;
|
|
171
|
+
const onMove = (me) => {
|
|
172
|
+
const dx = me.clientX - startX;
|
|
173
|
+
const dy = me.clientY - startY;
|
|
174
|
+
const maxW = editable.clientWidth || Infinity;
|
|
175
|
+
let newW = startW;
|
|
176
|
+
let newH = startH;
|
|
177
|
+
|
|
178
|
+
if (pos.includes('e')) newW = Math.max(20, startW + dx);
|
|
179
|
+
if (pos.includes('w')) newW = Math.max(20, startW - dx);
|
|
180
|
+
if (pos.includes('s')) newH = Math.max(20, startH + dy);
|
|
181
|
+
if (pos.includes('n')) newH = Math.max(20, startH - dy);
|
|
182
|
+
|
|
183
|
+
// Clamp to container width
|
|
184
|
+
newW = Math.min(newW, maxW);
|
|
185
|
+
|
|
186
|
+
if (isCorner) {
|
|
187
|
+
// Lock aspect ratio: use larger absolute delta to drive both dimensions
|
|
188
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
189
|
+
newH = Math.max(20, Math.round(newW / aspectRatio));
|
|
190
|
+
} else {
|
|
191
|
+
newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
|
|
192
|
+
newH = Math.max(20, Math.round(newW / aspectRatio));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
img.style.width = `${newW}px`;
|
|
197
|
+
img.style.height = `${newH}px`;
|
|
198
|
+
this._updateOverlayPosition();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const onUp = () => {
|
|
202
|
+
document.removeEventListener('mousemove', onMove);
|
|
203
|
+
document.removeEventListener('mouseup', onUp);
|
|
204
|
+
this._dragDisposers = null;
|
|
205
|
+
this.context.invoke('editor.afterCommand');
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
document.addEventListener('mousemove', onMove);
|
|
209
|
+
document.addEventListener('mouseup', onUp);
|
|
210
|
+
// Track these so destroy() can clean them up if called during an active drag
|
|
211
|
+
this._dragDisposers = [
|
|
212
|
+
() => document.removeEventListener('mousemove', onMove),
|
|
213
|
+
() => document.removeEventListener('mouseup', onUp),
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
}
|