edytor 0.1.0-next.18 → 0.1.0-next.19

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 CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
17
17
 
18
- > **Work in progress.** Edytor is a pre-release (`0.1.0-next.18`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
18
+ > **Work in progress.** Edytor is a pre-release (`0.1.0-next.19`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
19
19
 
20
20
  ## Features
21
21
 
@@ -12,4 +12,20 @@ export declare const selectionIsInside: (node: HTMLElement) => boolean;
12
12
  * already inside the host, or the view is gone.
13
13
  */
14
14
  export declare const takeKeys: (edytor: Edytor) => void;
15
+ /**
16
+ * Whether `event` is the press of a primary `mousedown` no `pointerdown`
17
+ * came before (WebKit's first press after a drag). Asked during the
18
+ * event's dispatch, after the document's capture phase saw it: by a
19
+ * listener the view's attach installs later (a plugin's `onEdytorAttached`)
20
+ * or by an element's own (bubbling, target phase), such as the chrome in the
21
+ * overlay, which starts from it as from a `pointerdown`.
22
+ */
23
+ export declare const isLonePress: (edytor: Edytor, event: MouseEvent) => boolean;
24
+ /**
25
+ * Every press on `target` (`isLonePress`): its `pointerdown`, or the lone
26
+ * `mousedown` standing for a missing one, never both for one press. For a
27
+ * listener installed after the view's attach (a plugin's press outside its
28
+ * menu). Answers the teardown.
29
+ */
30
+ export declare const onPress: (edytor: Edytor, target: EventTarget, listener: (event: MouseEvent) => void, options?: boolean | AddEventListenerOptions) => () => void;
15
31
  export declare const attachFocus: (edytor: Edytor, node: HTMLElement) => (() => void)[];
@@ -37,21 +37,54 @@ export const takeKeys = (edytor) => {
37
37
  node.focus({ preventScroll: true });
38
38
  edytor.projector.parked();
39
39
  };
40
+ /*
41
+ * The press: one per primary button press, its `pointerdown`, or a
42
+ * `mousedown` no `pointerdown` came before. WebKit fires no `pointercancel`
43
+ * at a native `dragstart` (a block handle's drag) and, believing the button
44
+ * still down, no `pointerdown` for the next press, only its `mousedown`
45
+ * (WebKit bugs 202287, 222632, 279749): that primary `mousedown` is the
46
+ * press (lone). `attachFocus` classifies each `mousedown` of its view's
47
+ * document at the capture phase, first of the view's listeners; the lone one
48
+ * of each view is kept here, read by `isLonePress` and `onPress`.
49
+ */
50
+ const lonePresses = new WeakMap();
51
+ /**
52
+ * Whether `event` is the press of a primary `mousedown` no `pointerdown`
53
+ * came before (WebKit's first press after a drag). Asked during the
54
+ * event's dispatch, after the document's capture phase saw it: by a
55
+ * listener the view's attach installs later (a plugin's `onEdytorAttached`)
56
+ * or by an element's own (bubbling, target phase), such as the chrome in the
57
+ * overlay, which starts from it as from a `pointerdown`.
58
+ */
59
+ export const isLonePress = (edytor, event) => lonePresses.get(edytor) === event;
60
+ /**
61
+ * Every press on `target` (`isLonePress`): its `pointerdown`, or the lone
62
+ * `mousedown` standing for a missing one, never both for one press. For a
63
+ * listener installed after the view's attach (a plugin's press outside its
64
+ * menu). Answers the teardown.
65
+ */
66
+ export const onPress = (edytor, target, listener, options) => {
67
+ const lone = (event) => {
68
+ if (isLonePress(edytor, event))
69
+ listener(event);
70
+ };
71
+ target.addEventListener('pointerdown', listener, options);
72
+ target.addEventListener('mousedown', lone, options);
73
+ return () => {
74
+ target.removeEventListener('pointerdown', listener, options);
75
+ target.removeEventListener('mousedown', lone, options);
76
+ };
77
+ };
40
78
  export const attachFocus = (edytor, node) => {
41
79
  let lastPointerDownInsideEditorAt = Number.NEGATIVE_INFINITY;
42
80
  /*
43
- * The press: one per primary button press, its `pointerdown`, or a
44
- * `mousedown` no `pointerdown` came before. WebKit fires no
45
- * `pointercancel` at a native `dragstart` (a block handle's drag) and,
46
- * believing the button still down, no `pointerdown` for the next press,
47
- * only its `mousedown` (WebKit bugs 202287, 222632, 279749): that
48
- * primary `mousedown` is the press (`lone`). A primary `pointerdown` arms
49
- * `compat` until the `mousedown` it is followed by (before or after its
50
- * release, a touch's comes after), so a pair counts once. Only the
51
- * browser's own `mousedown` stands for a missing press; a script's is none.
81
+ * A primary `pointerdown` arms `compat` until the `mousedown` it is
82
+ * followed by (before or after its release, a touch's comes after), so a
83
+ * pair counts once. Only the browser's own `mousedown` stands for a
84
+ * missing press; a script's is none (`lonePresses`).
52
85
  */
53
86
  let compat = false;
54
- let lone = null;
87
+ const lone = (event) => isLonePress(edytor, event);
55
88
  /** Where the press landed: the gesture serial, and whether the user went outside. */
56
89
  const gesture = (event) => {
57
90
  edytor.markUserGesture();
@@ -150,27 +183,29 @@ export const attachFocus = (edytor, node) => {
150
183
  on(node.ownerDocument, 'pointerdown', (event) => {
151
184
  if (event.isPrimary)
152
185
  compat = true;
153
- lone = null;
186
+ lonePresses.delete(edytor);
154
187
  gesture(event);
155
188
  }, { capture: true }),
156
189
  on(node.ownerDocument, 'mousedown', (event) => {
157
- lone = compat || !event.isTrusted || event.button !== 0 ? null : event;
190
+ const press = !compat && event.isTrusted && event.button === 0;
158
191
  compat = false;
159
- if (lone)
160
- gesture(event);
192
+ if (!press)
193
+ return void lonePresses.delete(edytor);
194
+ lonePresses.set(edytor, event);
195
+ gesture(event);
161
196
  }, { capture: true }),
162
197
  // A press on a block's non-editable chrome places the caret in its text.
163
198
  on(node, 'pointerdown', edytor.selection.handleNonNativeEditableBlockChromePointerDown, {
164
199
  capture: true
165
200
  }),
166
201
  on(node, 'mousedown', (event) => {
167
- if (event === lone)
202
+ if (lone(event))
168
203
  edytor.selection.handleNonNativeEditableBlockChromePointerDown(event);
169
204
  }, { capture: true }),
170
205
  // The document's capture listener already marked the gesture.
171
206
  on(node, 'pointerdown', pressInside),
172
207
  on(node, 'mousedown', (event) => {
173
- if (event === lone)
208
+ if (lone(event))
174
209
  pressInside(event);
175
210
  }),
176
211
  on(node, 'pointerup', (event) => {
@@ -225,6 +260,7 @@ export const attachFocus = (edytor, node) => {
225
260
  // Focus leaving abandons a live composition: the browser committed what it shows.
226
261
  edytor.composition.abandon();
227
262
  setTimeout(clearNativeSelectionAfterExternalFocus);
228
- })
263
+ }),
264
+ () => void lonePresses.delete(edytor)
229
265
  ];
230
266
  };
@@ -1,4 +1,5 @@
1
1
  import { BLOCK_ACTIVATE_EVENT, BLOCK_ADD_EVENT } from '../blockHandles/BlockHandleController.svelte.js';
2
+ import { onPress } from '../../events/onFocus.js';
2
3
  import { getSelectedBlocksInDocumentOrder } from '../../selection/replaceSelection.js';
3
4
  import BlockMenu from './BlockMenu.svelte';
4
5
  import { BlockMenuController } from './BlockMenuController.svelte.js';
@@ -103,12 +104,13 @@ export const createBlockMenuPlugin = (options = {}) => (edytor) => {
103
104
  const add = () => controller.close(false);
104
105
  node.addEventListener(BLOCK_ACTIVATE_EVENT, activate);
105
106
  node.addEventListener(BLOCK_ADD_EVENT, add);
106
- node.ownerDocument.addEventListener('pointerdown', outside, true);
107
+ // Every press, WebKit's lone `mousedown` after a drag too (`onPress`).
108
+ const offPress = onPress(edytor, node.ownerDocument, outside, true);
107
109
  const unmount = edytor.overlay.mount(BlockMenu, { controller, menu: options.menu }, 'edytor-block-menu-host', 70, place);
108
110
  return () => {
109
111
  node.removeEventListener(BLOCK_ACTIVATE_EVENT, activate);
110
112
  node.removeEventListener(BLOCK_ADD_EVENT, add);
111
- node.ownerDocument.removeEventListener('pointerdown', outside, true);
113
+ offPress();
112
114
  unmount();
113
115
  };
114
116
  }
@@ -33,7 +33,7 @@
33
33
  data-dragging={resize.drag ? 'true' : undefined}
34
34
  data-selecting={resize.selecting ? 'true' : undefined}
35
35
  onpointerdown={(event) => resize.start(event, strip)}
36
- onmousedown={(event) => event.preventDefault()}
36
+ onmousedown={(event) => resize.mousedown(event, strip)}
37
37
  ></div>
38
38
  {/each}
39
39
  {/if}
@@ -93,8 +93,20 @@ export declare class ColumnResize {
93
93
  private gaps;
94
94
  /** The left column's width at the drag's pointer, held to the minimum on both sides. */
95
95
  private widthAt;
96
- /** Press on `strip`: the drag starts, nothing is written until the release. */
97
- start: (event: PointerEvent, strip: Strip) => void;
96
+ /**
97
+ * A `mousedown` on `strip`: no native selection starts there; the press
98
+ * itself when no `pointerdown` came before it (`isLonePress`: WebKit's
99
+ * first press after a block drag), which starts the drag as one would.
100
+ */
101
+ mousedown: (event: MouseEvent, strip: Strip) => void;
102
+ /**
103
+ * Press on `strip`: the drag starts, nothing is written until the release.
104
+ * A `pointerdown` tracks its pointer (captured, on the document); a lone
105
+ * `mousedown` (`mousedown`) tracks the mouse on the window, its
106
+ * `mousemove`s and its `mouseup`, with the same preview, minimum, Escape
107
+ * and release.
108
+ */
109
+ start: (event: PointerEvent | MouseEvent, strip: Strip) => void;
98
110
  /**
99
111
  * The view turned readonly (or the document read-only): a drag in progress
100
112
  * ends at once, its preview dropped, nothing written (its release would be
@@ -1,4 +1,4 @@
1
- import { takeKeys } from '../../events/onFocus.js';
1
+ import { isLonePress, takeKeys } from '../../events/onFocus.js';
2
2
  import { bandOf, gapBefore } from './gaps.js';
3
3
  import { stacks } from './stacking.js';
4
4
  /** The guide line's width (the hover guide's and the drag's). */
@@ -141,7 +141,23 @@ export class ColumnResize {
141
141
  const hi = Math.max(sum - min, width);
142
142
  return Math.min(hi, Math.max(lo, width + at - from));
143
143
  }
144
- /** Press on `strip`: the drag starts, nothing is written until the release. */
144
+ /**
145
+ * A `mousedown` on `strip`: no native selection starts there; the press
146
+ * itself when no `pointerdown` came before it (`isLonePress`: WebKit's
147
+ * first press after a block drag), which starts the drag as one would.
148
+ */
149
+ mousedown = (event, strip) => {
150
+ event.preventDefault();
151
+ if (isLonePress(this.edytor, event))
152
+ this.start(event, strip);
153
+ };
154
+ /**
155
+ * Press on `strip`: the drag starts, nothing is written until the release.
156
+ * A `pointerdown` tracks its pointer (captured, on the document); a lone
157
+ * `mousedown` (`mousedown`) tracks the mouse on the window, its
158
+ * `mousemove`s and its `mouseup`, with the same preview, minimum, Escape
159
+ * and release.
160
+ */
145
161
  start = (event, strip) => {
146
162
  const { edytor } = this;
147
163
  if (event.button !== 0 || edytor.readonly || this.drag)
@@ -155,7 +171,9 @@ export class ColumnResize {
155
171
  event.preventDefault();
156
172
  event.stopPropagation();
157
173
  const target = event.currentTarget;
158
- target?.setPointerCapture?.(event.pointerId);
174
+ const pointer = event.type === 'pointerdown';
175
+ if (pointer)
176
+ target?.setPointerCapture?.(event.pointerId);
159
177
  this.drag = {
160
178
  strip,
161
179
  left,
@@ -171,6 +189,8 @@ export class ColumnResize {
171
189
  height: box.height
172
190
  };
173
191
  const document = target?.ownerDocument ?? edytor.node?.ownerDocument;
192
+ const tracked = pointer ? document : document?.defaultView;
193
+ const [moves, ups] = pointer ? ['pointermove', 'pointerup'] : ['mousemove', 'mouseup'];
174
194
  const move = (moved) => {
175
195
  if (!this.drag)
176
196
  return;
@@ -181,16 +201,16 @@ export class ColumnResize {
181
201
  edytor.overlay.invalidate();
182
202
  };
183
203
  const end = (ended) => {
184
- document?.removeEventListener('pointermove', move);
185
- document?.removeEventListener('pointerup', end);
186
- document?.removeEventListener('pointercancel', end);
204
+ tracked?.removeEventListener(moves, move);
205
+ tracked?.removeEventListener(ups, end);
206
+ tracked?.removeEventListener('pointercancel', end);
187
207
  document?.removeEventListener('keydown', escape, true);
188
208
  const drag = this.drag;
189
209
  this.drag = null;
190
210
  this.guide = null;
191
211
  this.#cancel = null;
192
212
  edytor.overlay.invalidate();
193
- if (drag && ended.type === 'pointerup') {
213
+ if (drag && ended.type === ups) {
194
214
  drag.at = ended.clientX;
195
215
  this.release(drag);
196
216
  // The editor holds the keys after the release (Notion: Mod+Z right after).
@@ -207,9 +227,10 @@ export class ColumnResize {
207
227
  end(key);
208
228
  };
209
229
  this.#cancel = () => end(new Event('cancel'));
210
- document?.addEventListener('pointermove', move);
211
- document?.addEventListener('pointerup', end);
212
- document?.addEventListener('pointercancel', end);
230
+ tracked?.addEventListener(moves, move);
231
+ tracked?.addEventListener(ups, end);
232
+ if (pointer)
233
+ tracked?.addEventListener('pointercancel', end);
213
234
  document?.addEventListener('keydown', escape, true);
214
235
  edytor.overlay.invalidate();
215
236
  };
@@ -1,4 +1,5 @@
1
1
  import { BLOCK_ACTIVATE_EVENT, BLOCK_ADD_EVENT } from '../blockHandles/BlockHandleController.svelte.js';
2
+ import { onPress } from '../../events/onFocus.js';
2
3
  import SlashMenu from './SlashMenu.svelte';
3
4
  import { SlashMenuController } from './SlashMenuController.svelte.js';
4
5
  /** The slash menu, with your own markup through `menu` or `item` snippets. */
@@ -90,12 +91,13 @@ export const createSlashMenuPlugin = (options = {}) => (edytor) => {
90
91
  };
91
92
  node.addEventListener(BLOCK_ADD_EVENT, offer);
92
93
  node.addEventListener(BLOCK_ACTIVATE_EVENT, activate);
93
- node.ownerDocument.addEventListener('pointerdown', outside, true);
94
+ // Every press, WebKit's lone `mousedown` after a drag too (`onPress`).
95
+ const offPress = onPress(edytor, node.ownerDocument, outside, true);
94
96
  const unmount = edytor.overlay.mount(SlashMenu, { controller, menu: options.menu, item: options.item }, 'edytor-slash-menu-host', 50, positionMenu);
95
97
  return () => {
96
98
  node.removeEventListener(BLOCK_ADD_EVENT, offer);
97
99
  node.removeEventListener(BLOCK_ACTIVATE_EVENT, activate);
98
- node.ownerDocument.removeEventListener('pointerdown', outside, true);
100
+ offPress();
99
101
  unmount();
100
102
  };
101
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edytor",
3
- "version": "0.1.0-next.18",
3
+ "version": "0.1.0-next.19",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/beynar/edytor.git"