@batchfy/codemirror-minimap 0.1.0 → 0.1.1
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/README.md +5 -0
- package/dist/Overlay.d.ts.map +1 -1
- package/dist/geometry.d.ts +130 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +321 -166
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/Overlay.ts +151 -118
- package/src/geometry.ts +402 -0
- package/src/index.ts +39 -25
package/src/Overlay.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
|
2
|
-
import { Config
|
|
2
|
+
import { Config } from "./Config.js";
|
|
3
|
+
import {
|
|
4
|
+
readGeometry,
|
|
5
|
+
rowAtOverlayTop,
|
|
6
|
+
rowAtY,
|
|
7
|
+
scrollToRow,
|
|
8
|
+
type StableGeometry,
|
|
9
|
+
} from "./geometry.js";
|
|
3
10
|
import crelt from "crelt";
|
|
4
11
|
|
|
5
12
|
const Theme = EditorView.theme({
|
|
@@ -8,6 +15,10 @@ const Theme = EditorView.theme({
|
|
|
8
15
|
top: 0,
|
|
9
16
|
height: "100%",
|
|
10
17
|
width: "100%",
|
|
18
|
+
// The minimap is a sibling of `.cm-content` inside the scroller, so
|
|
19
|
+
// without this it inherits the caret the editor shows over text. It is
|
|
20
|
+
// not text: clicking it jumps, and the overlay is dragged.
|
|
21
|
+
cursor: "default",
|
|
11
22
|
"&.cm-minimap-overlay-mouse-over": {
|
|
12
23
|
opacity: 0,
|
|
13
24
|
transition: "visibility 0s linear 300ms, opacity 300ms",
|
|
@@ -27,6 +38,7 @@ const Theme = EditorView.theme({
|
|
|
27
38
|
top: 0,
|
|
28
39
|
width: "100%",
|
|
29
40
|
transition: "top 0s ease-in 0ms",
|
|
41
|
+
cursor: "grab",
|
|
30
42
|
"&:hover": {
|
|
31
43
|
opacity: "0.3",
|
|
32
44
|
},
|
|
@@ -37,20 +49,41 @@ const Theme = EditorView.theme({
|
|
|
37
49
|
transition: "visibility 0s linear 0ms, opacity 300ms",
|
|
38
50
|
"& .cm-minimap-overlay": {
|
|
39
51
|
opacity: "0.4",
|
|
52
|
+
cursor: "grabbing",
|
|
40
53
|
},
|
|
41
54
|
},
|
|
42
55
|
},
|
|
43
56
|
});
|
|
44
57
|
|
|
45
|
-
const SCALE = Scale.PixelMultiplier * Scale.SizeRatio;
|
|
46
|
-
|
|
47
58
|
const OverlayView = ViewPlugin.fromClass(
|
|
48
59
|
class {
|
|
49
60
|
private container: HTMLElement | undefined;
|
|
50
61
|
private dom: HTMLElement | undefined;
|
|
51
62
|
|
|
52
63
|
private _isDragging: boolean = false;
|
|
53
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Where inside the overlay the drag took hold of it, as a fraction of
|
|
66
|
+
* its height rather than a count of pixels down from its top.
|
|
67
|
+
*
|
|
68
|
+
* The overlay is sized to the lines on screen, so it shrinks as a drag
|
|
69
|
+
* carries it from a screen of code into one of wrapped prose. A pixel
|
|
70
|
+
* offset taken when it was tall goes on pointing at where its middle
|
|
71
|
+
* used to be, which by the end of the document is somewhere below its
|
|
72
|
+
* bottom edge — and the pointer would have to leave the minimap, often
|
|
73
|
+
* the window, to put the overlay where it belongs. A fraction shrinks
|
|
74
|
+
* with it and stays under the thumb.
|
|
75
|
+
*/
|
|
76
|
+
private _dragAnchor: number | undefined;
|
|
77
|
+
/** The last position the pointer reported, waiting for a frame. */
|
|
78
|
+
private _dragPointerY: number | undefined;
|
|
79
|
+
private _dragFrame: number | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Measured when the drag starts and kept for its duration. None of it
|
|
82
|
+
* moves as the document scrolls, and `update` drops it so that anything
|
|
83
|
+
* which does move it — an edit, a resize, a change of theme — is
|
|
84
|
+
* measured again.
|
|
85
|
+
*/
|
|
86
|
+
private _dragGeometry: StableGeometry | undefined;
|
|
54
87
|
|
|
55
88
|
// Bound once so that removeEventListener sees the same reference that
|
|
56
89
|
// addEventListener was given; `.bind` at call time yields a new
|
|
@@ -85,11 +118,16 @@ const OverlayView = ViewPlugin.fromClass(
|
|
|
85
118
|
|
|
86
119
|
// Initially set overlay configuration styles, height, top
|
|
87
120
|
this.computeShowOverlay();
|
|
88
|
-
this.
|
|
89
|
-
this.computeTop();
|
|
121
|
+
this.position();
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
private remove() {
|
|
125
|
+
// A minimap turned off or torn down mid-drag would otherwise leave
|
|
126
|
+
// the whole page holding the dragging cursor.
|
|
127
|
+
if (this._isDragging) {
|
|
128
|
+
this.endDrag();
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
if (this.container) {
|
|
94
132
|
this.container.removeEventListener(
|
|
95
133
|
"mousedown",
|
|
@@ -115,41 +153,38 @@ const OverlayView = ViewPlugin.fromClass(
|
|
|
115
153
|
}
|
|
116
154
|
|
|
117
155
|
if (now) {
|
|
156
|
+
// Anything that reaches an update may have changed the size of
|
|
157
|
+
// the editor, the length of the document or the height of a
|
|
158
|
+
// row, so a drag in progress measures them again.
|
|
159
|
+
this._dragGeometry = undefined;
|
|
160
|
+
|
|
118
161
|
this.computeShowOverlay();
|
|
119
162
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
163
|
+
// Unconditionally, and not just on `geometryChanged`: the rows
|
|
164
|
+
// are redrawn on every update, and an overlay that skipped one
|
|
165
|
+
// would be left marking rows that have since moved under it.
|
|
166
|
+
// The renderer runs first and measures the same update, so this
|
|
167
|
+
// reads that measurement rather than taking its own.
|
|
168
|
+
this.position(update);
|
|
124
169
|
}
|
|
125
170
|
}
|
|
126
171
|
|
|
127
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Covers the rows the editor is showing. The renderer scrolls its rows
|
|
174
|
+
* to the position the same geometry prescribes, so the overlay lands on
|
|
175
|
+
* the drawn content whatever the document does with wrapping or folds.
|
|
176
|
+
*/
|
|
177
|
+
public position(token?: object) {
|
|
128
178
|
if (!this.dom) {
|
|
129
179
|
return;
|
|
130
180
|
}
|
|
131
181
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const { clientHeight, scrollHeight, scrollTop } =
|
|
139
|
-
this.view.scrollDOM;
|
|
140
|
-
|
|
141
|
-
const maxScrollTop = scrollHeight - clientHeight;
|
|
142
|
-
const topForNonOverflowing = scrollTop / SCALE;
|
|
143
|
-
|
|
144
|
-
const height = clientHeight / SCALE;
|
|
145
|
-
const maxTop = clientHeight - height;
|
|
146
|
-
let scrollRatio = scrollTop / maxScrollTop;
|
|
147
|
-
if (isNaN(scrollRatio)) scrollRatio = 0;
|
|
148
|
-
const topForOverflowing = maxTop * scrollRatio;
|
|
149
|
-
|
|
150
|
-
const top = Math.min(topForOverflowing, topForNonOverflowing);
|
|
151
|
-
this.dom.style.top = top + "px";
|
|
152
|
-
}
|
|
182
|
+
const { overlayTop, overlayHeight } = readGeometry(
|
|
183
|
+
this.view,
|
|
184
|
+
token,
|
|
185
|
+
);
|
|
186
|
+
this.dom.style.top = overlayTop + "px";
|
|
187
|
+
this.dom.style.height = overlayHeight + "px";
|
|
153
188
|
}
|
|
154
189
|
|
|
155
190
|
public computeShowOverlay() {
|
|
@@ -185,123 +220,121 @@ const OverlayView = ViewPlugin.fromClass(
|
|
|
185
220
|
return;
|
|
186
221
|
}
|
|
187
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The minimap sits inside the scroller alongside `.cm-content`, so
|
|
225
|
+
* a mousedown on it starts a native text selection that runs on for
|
|
226
|
+
* as long as the button is held — which is what leaves the pointer
|
|
227
|
+
* showing a caret through the whole drag, and what puts a selection
|
|
228
|
+
* in the document that nobody asked for. Neither gesture here has
|
|
229
|
+
* anything to do with selecting: one jumps, one drags.
|
|
230
|
+
*/
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
|
|
188
233
|
// If target is the overlay start dragging
|
|
189
234
|
const { clientY, target } = event;
|
|
190
|
-
if (target === this.dom) {
|
|
191
|
-
|
|
235
|
+
if (target === this.dom && this.dom) {
|
|
236
|
+
const grabbed = this.dom.getBoundingClientRect();
|
|
237
|
+
|
|
238
|
+
this._dragAnchor =
|
|
239
|
+
grabbed.height > 0
|
|
240
|
+
? (clientY - grabbed.top) / grabbed.height
|
|
241
|
+
: 0;
|
|
192
242
|
this._isDragging = true;
|
|
193
243
|
this.container.classList.add("cm-minimap-overlay-active");
|
|
244
|
+
|
|
245
|
+
// The pointer spends most of a drag off the overlay, and some
|
|
246
|
+
// of it off the minimap altogether, where the rule on the
|
|
247
|
+
// overlay no longer reaches it.
|
|
248
|
+
document.body.style.cursor = "grabbing";
|
|
194
249
|
return;
|
|
195
250
|
}
|
|
196
251
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
this.view.scrollDOM;
|
|
201
|
-
const targetTop = (target as HTMLElement).getBoundingClientRect()
|
|
202
|
-
.top;
|
|
203
|
-
const deltaY = (clientY - targetTop) * SCALE;
|
|
204
|
-
|
|
205
|
-
const scrollRatio = scrollTop / (scrollHeight - clientHeight);
|
|
206
|
-
const visibleRange = clientHeight * SCALE - clientHeight;
|
|
207
|
-
const visibleTop = visibleRange * scrollRatio;
|
|
252
|
+
// A click anywhere else on the minimap centres the row underneath it
|
|
253
|
+
const geometry = readGeometry(this.view);
|
|
254
|
+
const y = clientY - this.container.getBoundingClientRect().top;
|
|
208
255
|
|
|
209
|
-
|
|
210
|
-
|
|
256
|
+
scrollToRow(
|
|
257
|
+
this.view,
|
|
258
|
+
rowAtY(geometry, y),
|
|
259
|
+
this.view.scrollDOM.clientHeight / 2,
|
|
260
|
+
);
|
|
211
261
|
}
|
|
212
262
|
|
|
213
263
|
private onMouseUp(_event: MouseEvent) {
|
|
214
264
|
// Stop dragging on mouseup
|
|
215
|
-
if (this._isDragging
|
|
216
|
-
this.
|
|
217
|
-
this._isDragging = false;
|
|
218
|
-
this.container.classList.remove("cm-minimap-overlay-active");
|
|
265
|
+
if (this._isDragging) {
|
|
266
|
+
this.endDrag();
|
|
219
267
|
}
|
|
220
268
|
}
|
|
221
269
|
|
|
222
|
-
private
|
|
223
|
-
if (
|
|
224
|
-
|
|
270
|
+
private endDrag() {
|
|
271
|
+
if (this._dragFrame !== undefined) {
|
|
272
|
+
cancelAnimationFrame(this._dragFrame);
|
|
273
|
+
this._dragFrame = undefined;
|
|
225
274
|
}
|
|
226
275
|
|
|
227
|
-
|
|
228
|
-
|
|
276
|
+
this._dragAnchor = undefined;
|
|
277
|
+
this._dragPointerY = undefined;
|
|
278
|
+
this._dragGeometry = undefined;
|
|
279
|
+
this._isDragging = false;
|
|
280
|
+
this.container?.classList.remove("cm-minimap-overlay-active");
|
|
281
|
+
document.body.style.removeProperty("cursor");
|
|
282
|
+
}
|
|
229
283
|
|
|
230
|
-
|
|
231
|
-
if (!this.
|
|
232
|
-
this._dragStartY = event.clientY;
|
|
284
|
+
private onMouseMove(event: MouseEvent) {
|
|
285
|
+
if (!this._isDragging || this._dragAnchor === undefined) {
|
|
233
286
|
return;
|
|
234
287
|
}
|
|
235
288
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const movingDown = deltaY > 0;
|
|
239
|
-
|
|
240
|
-
// Update drag position for the next tick
|
|
241
|
-
this._dragStartY = event.clientY;
|
|
242
|
-
|
|
243
|
-
const canvasHeight = this.dom.getBoundingClientRect().height;
|
|
244
|
-
const canvasAbsTop = this.dom.getBoundingClientRect().y;
|
|
245
|
-
const canvasAbsBot = canvasAbsTop + canvasHeight;
|
|
246
|
-
const canvasRelTopDouble = parseFloat(this.dom.style.top);
|
|
247
|
-
|
|
248
|
-
const scrollPosition = this.view.scrollDOM.scrollTop;
|
|
249
|
-
const editorHeight = this.view.scrollDOM.clientHeight;
|
|
250
|
-
const contentHeight = this.view.scrollDOM.scrollHeight;
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
event.stopPropagation();
|
|
251
291
|
|
|
252
|
-
|
|
253
|
-
const atBottom =
|
|
254
|
-
Math.round(scrollPosition) >=
|
|
255
|
-
Math.round(contentHeight - editorHeight);
|
|
292
|
+
this._dragPointerY = event.clientY;
|
|
256
293
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
) {
|
|
265
|
-
|
|
294
|
+
/**
|
|
295
|
+
* At most one move per frame. A trackpad reports positions faster
|
|
296
|
+
* than the screen can show them, and acting on each one means
|
|
297
|
+
* reading the layout back immediately after the last one wrote to
|
|
298
|
+
* it — the reading is what costs, and none of the extra readings
|
|
299
|
+
* can reach the screen anyway.
|
|
300
|
+
*/
|
|
301
|
+
if (this._dragFrame === undefined) {
|
|
302
|
+
this._dragFrame = requestAnimationFrame(() => {
|
|
303
|
+
this._dragFrame = undefined;
|
|
304
|
+
this.dragToPointer();
|
|
305
|
+
});
|
|
266
306
|
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private dragToPointer() {
|
|
267
310
|
if (
|
|
268
|
-
|
|
269
|
-
|
|
311
|
+
!this._isDragging ||
|
|
312
|
+
this._dragAnchor === undefined ||
|
|
313
|
+
this._dragPointerY === undefined ||
|
|
314
|
+
!this.container ||
|
|
315
|
+
!this.dom
|
|
270
316
|
) {
|
|
271
317
|
return;
|
|
272
318
|
}
|
|
273
319
|
|
|
274
|
-
|
|
275
|
-
const scrollHeight = this.view.scrollDOM.scrollHeight;
|
|
276
|
-
const clientHeight = this.view.scrollDOM.clientHeight;
|
|
277
|
-
|
|
278
|
-
const maxTopNonOverflowing = (scrollHeight - clientHeight) / SCALE;
|
|
279
|
-
const maxTopOverflowing = clientHeight - clientHeight / SCALE;
|
|
280
|
-
|
|
281
|
-
const change = canvasRelTopDouble + deltaY;
|
|
320
|
+
this._dragGeometry ??= readGeometry(this.view);
|
|
282
321
|
|
|
283
322
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
323
|
+
* The overlay's drawn height, rather than a freshly computed one:
|
|
324
|
+
* the anchor was taken against what is on the screen, the two rows
|
|
325
|
+
* a frame of lag can cost are invisible at this scale, and reading
|
|
326
|
+
* it is a single box where computing it means bisecting the height
|
|
327
|
+
* map twice.
|
|
288
328
|
*/
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
(scrollHeight - clientHeight) * relativeToMax;
|
|
292
|
-
|
|
293
|
-
const scrollPosNonOverflowing = change * SCALE;
|
|
294
|
-
this.view.scrollDOM.scrollTop = Math.max(
|
|
295
|
-
scrollPosOverflowing,
|
|
296
|
-
scrollPosNonOverflowing,
|
|
297
|
-
);
|
|
329
|
+
const overlayHeight = this.dom.getBoundingClientRect().height;
|
|
330
|
+
const containerTop = this.container.getBoundingClientRect().top;
|
|
298
331
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.
|
|
332
|
+
const top =
|
|
333
|
+
this._dragPointerY -
|
|
334
|
+
this._dragAnchor * overlayHeight -
|
|
335
|
+
containerTop;
|
|
336
|
+
|
|
337
|
+
scrollToRow(this.view, rowAtOverlayTop(this._dragGeometry, top));
|
|
305
338
|
}
|
|
306
339
|
|
|
307
340
|
public destroy() {
|
|
@@ -310,8 +343,8 @@ const OverlayView = ViewPlugin.fromClass(
|
|
|
310
343
|
},
|
|
311
344
|
{
|
|
312
345
|
eventHandlers: {
|
|
313
|
-
scroll() {
|
|
314
|
-
requestAnimationFrame(() => this.
|
|
346
|
+
scroll(event) {
|
|
347
|
+
requestAnimationFrame(() => this.position(event));
|
|
315
348
|
},
|
|
316
349
|
},
|
|
317
350
|
},
|