@prosekit/web 0.5.3 → 0.5.4

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.
@@ -333,7 +333,7 @@ declare const blockHandlePopoverEvents: EventDeclarations<BlockHandlePopoverEven
333
333
  export { blockHandlePopoverEvents }
334
334
  export { blockHandlePopoverEvents as blockHandlePopoverEvents_alias_1 }
335
335
 
336
- declare interface BlockHandlePopoverProps extends Omit<OverlayPositionerProps, 'placement'> {
336
+ declare interface BlockHandlePopoverProps extends Omit<OverlayPositionerProps, 'placement' | 'hoist'> {
337
337
  /**
338
338
  * The ProseKit editor instance.
339
339
  *
@@ -344,9 +344,16 @@ declare interface BlockHandlePopoverProps extends Omit<OverlayPositionerProps, '
344
344
  /**
345
345
  * The placement of the popover, relative to the hovered block.
346
346
  *
347
- * @default "left-start"
347
+ * @default "left"
348
348
  */
349
349
  placement: Placement;
350
+ /**
351
+ * Whether to use the browser [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
352
+ * to place the floating element on top of other page content.
353
+ *
354
+ * @default false
355
+ */
356
+ hoist: boolean;
350
357
  }
351
358
  export { BlockHandlePopoverProps }
352
359
  export { BlockHandlePopoverProps as BlockHandlePopoverProps_alias_1 }
@@ -376,6 +383,16 @@ export declare function defineElementHoverHandler(handler: ElementHoverHandler):
376
383
 
377
384
  export declare type ElementHoverHandler = (reference: VirtualElement | null, hoverState: HoverState | null) => void;
378
385
 
386
+ /**
387
+ * Similar to `element.getBoundingClientRect`, but handles `display: contents` elements.
388
+ */
389
+ export declare function getClientRect(element: Element): {
390
+ top: number;
391
+ right: number;
392
+ bottom: number;
393
+ left: number;
394
+ };
395
+
379
396
  export declare function getHoveringCell(view: EditorView, event: MouseEvent): HoveringCellInfo | undefined;
380
397
 
381
398
  export declare function getStateWithDefaults<Props extends Record<string, any> = Record<string, any>>(state: Partial<SignalState<Props>>, props: PropDeclarations<Props>): SignalState<Props>;
@@ -597,6 +614,13 @@ export { popoverTriggerProps as popoverTriggerProps_alias_1 }
597
614
 
598
615
  export declare const queryContext: Context<string>;
599
616
 
617
+ export declare interface Rect {
618
+ top: number;
619
+ right: number;
620
+ bottom: number;
621
+ left: number;
622
+ }
623
+
600
624
  declare class ResizableHandleElement extends ResizableHandleElement_base {
601
625
  }
602
626
  export { ResizableHandleElement }
@@ -155,6 +155,30 @@ import {
155
155
  union
156
156
  } from "@prosekit/core";
157
157
 
158
+ // src/utils/get-client-rect.ts
159
+ function getClientRect(element) {
160
+ const rect = element.getBoundingClientRect();
161
+ if (rect.width === 0 && rect.height === 0 && rect.x === 0 && rect.y === 0) {
162
+ if (element.getClientRects().length === 0) {
163
+ const children = Array.from(element.children);
164
+ const rects = children.map((child) => getClientRect(child)).filter((x) => !!x);
165
+ if (rects.length === 0) {
166
+ return rect;
167
+ }
168
+ if (rects.length === 1) {
169
+ return rects[0];
170
+ }
171
+ return {
172
+ top: Math.min(...rects.map((rect2) => rect2.top)),
173
+ right: Math.max(...rects.map((rect2) => rect2.right)),
174
+ bottom: Math.max(...rects.map((rect2) => rect2.bottom)),
175
+ left: Math.min(...rects.map((rect2) => rect2.left))
176
+ };
177
+ }
178
+ }
179
+ return rect;
180
+ }
181
+
158
182
  // src/utils/throttle.ts
159
183
  function throttle(callback, wait) {
160
184
  let lastTime = 0;
@@ -168,56 +192,39 @@ function throttle(callback, wait) {
168
192
  }
169
193
 
170
194
  // src/components/block-handle/block-handle-popover/pointer-move.ts
195
+ var TEXT_NODE = 3;
196
+ var ELEMENT_NODE = 1;
171
197
  function defineElementHoverHandler(handler) {
198
+ const handleElement = (element, node, pos, parentElement) => {
199
+ const reference = {
200
+ contextElement: element,
201
+ getBoundingClientRect: () => {
202
+ const rect = findFirstLineRect(parentElement, element);
203
+ return rect ? fulfillRect(rect) : fallbackRect;
204
+ }
205
+ };
206
+ handler(reference, { element, node, pos });
207
+ };
172
208
  const handlePointerEvent = (view, event) => {
173
- const rect = view.dom.getBoundingClientRect();
174
- const pos = view.posAtCoords({
175
- top: event.clientY,
176
- // Use the center of the editor
177
- left: rect.left + rect.width / 2
178
- })?.inside;
179
- if (pos == null || pos < 0) {
209
+ const { x, y } = event;
210
+ const block = findBlockByCoordinate(view, x, y);
211
+ if (!block) {
180
212
  handler(null, null);
181
213
  return;
182
214
  }
183
- const $pos = view.state.doc.resolve(pos);
184
- const node = view.state.doc.nodeAt(pos);
185
- const element = view.nodeDOM(pos);
186
- if ($pos.depth >= 1 && $pos.index($pos.depth) === 0) {
187
- const ancestorPos = $pos.before($pos.depth);
188
- const node2 = view.state.doc.nodeAt(ancestorPos);
189
- const element2 = view.nodeDOM(ancestorPos);
190
- if (!element2 || !node2) {
191
- handler(null, null);
192
- return;
193
- }
194
- const reference = {
195
- contextElement: element2,
196
- // Get the bounding client rect of the parent node, including its
197
- // margins.
198
- getBoundingClientRect: () => {
199
- const rect2 = element2.getBoundingClientRect();
200
- const style = window.getComputedStyle(element2);
201
- const marginTop = Number.parseInt(style.marginTop, 10) || 0;
202
- const marginRight = Number.parseInt(style.marginRight, 10) || 0;
203
- const marginBottom = Number.parseInt(style.marginBottom, 10) || 0;
204
- const marginLeft = Number.parseInt(style.marginLeft, 10) || 0;
205
- return {
206
- top: rect2.top - marginTop,
207
- right: rect2.right + marginRight,
208
- bottom: rect2.bottom + marginBottom,
209
- left: rect2.left - marginLeft,
210
- width: rect2.width + marginLeft + marginRight,
211
- height: rect2.height + marginTop + marginBottom,
212
- x: rect2.x - marginLeft,
213
- y: rect2.y - marginTop
214
- };
215
- }
216
- };
217
- handler(reference, { element: element2, node: node2, pos: ancestorPos });
215
+ const { node, pos } = block;
216
+ const element = getElementAtPos(view, pos);
217
+ if (!element) {
218
+ handler(null, null);
218
219
  return;
219
220
  }
220
- handler(element, element && node && { element, node, pos });
221
+ const $pos = view.state.doc.resolve(pos);
222
+ let parentElement;
223
+ if ($pos.depth > 0 && $pos.index($pos.depth) === 0) {
224
+ const parentPos = $pos.before($pos.depth);
225
+ parentElement = view.nodeDOM(parentPos);
226
+ }
227
+ handleElement(element, node, pos, parentElement);
221
228
  };
222
229
  return union(
223
230
  defineDOMEventHandler("pointermove", throttle(handlePointerEvent, 200)),
@@ -225,6 +232,154 @@ function defineElementHoverHandler(handler) {
225
232
  defineDOMEventHandler("keypress", () => handler(null, null))
226
233
  );
227
234
  }
235
+ function getElementAtPos(view, pos) {
236
+ const node = view.nodeDOM(pos);
237
+ if (node && node.nodeType === ELEMENT_NODE) {
238
+ return node;
239
+ }
240
+ }
241
+ function findBlockByCoordinate(view, x, y) {
242
+ const dom = view.dom;
243
+ const rect = getClientRect(dom);
244
+ if (!isWithinRect(rect, x, y)) {
245
+ return;
246
+ }
247
+ let parent = view.state.doc;
248
+ let pos = -1;
249
+ while (parent) {
250
+ if (parent.isBlock && (parent.isTextblock || parent.isAtom)) {
251
+ return { node: parent, pos };
252
+ }
253
+ const children = [];
254
+ const positions = [];
255
+ parent.forEach((child, offset) => {
256
+ children.push(child);
257
+ positions.push(offset + pos + 1);
258
+ });
259
+ let lo = 0;
260
+ let hi = children.length - 1;
261
+ while (lo <= hi) {
262
+ const i = hi - (hi - lo >> 1);
263
+ const childDOM = view.nodeDOM(positions[i]);
264
+ const childRect = getNodeRect(childDOM);
265
+ if (!childRect) {
266
+ console.warn("[prosekit] Unable to get rect at position", positions[i]);
267
+ return;
268
+ }
269
+ if (childRect.top > y) {
270
+ hi = i - 1;
271
+ } else if (childRect.bottom < y) {
272
+ lo = i + 1;
273
+ } else {
274
+ lo = i;
275
+ break;
276
+ }
277
+ }
278
+ if (lo > hi) {
279
+ return;
280
+ }
281
+ parent = children[lo];
282
+ pos = positions[lo];
283
+ }
284
+ }
285
+ function getNodeRect(node) {
286
+ if (!node || node.nodeType !== ELEMENT_NODE) {
287
+ return;
288
+ }
289
+ const element = node;
290
+ if (!element.isConnected) {
291
+ return;
292
+ }
293
+ return getClientRect(element);
294
+ }
295
+ function isWithinRect(rect, x, y) {
296
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
297
+ }
298
+ function findFirstLineRect(outer, inner) {
299
+ if (outer && !outer.isConnected) {
300
+ return;
301
+ }
302
+ if (inner && !inner.isConnected) {
303
+ return;
304
+ }
305
+ if (outer && inner) {
306
+ const outerRect = findOuterRect(outer);
307
+ const innerRect = findFirstLineRectInNode(inner);
308
+ if (outerRect && innerRect) {
309
+ const { top, bottom } = innerRect;
310
+ const { left, right } = outerRect;
311
+ return { top, bottom, left, right };
312
+ } else {
313
+ return outerRect || innerRect;
314
+ }
315
+ } else if (outer) {
316
+ return findFirstLineRectInNode(outer);
317
+ } else if (inner) {
318
+ return findFirstLineRectInNode(inner);
319
+ }
320
+ }
321
+ function findOuterRect(node) {
322
+ if (node.nodeType !== ELEMENT_NODE) {
323
+ return;
324
+ }
325
+ const element = node;
326
+ const rect = getClientRect(element);
327
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
328
+ const marginLeft = style && Number.parseInt(style.marginLeft, 10) || 0;
329
+ const marginRight = style && Number.parseInt(style.marginRight, 10) || 0;
330
+ const left = rect.left - marginLeft;
331
+ const right = rect.right + marginRight;
332
+ return { top: rect.top, bottom: rect.bottom, left, right };
333
+ }
334
+ function findFirstLineRectInNode(node) {
335
+ switch (node.nodeType) {
336
+ case TEXT_NODE:
337
+ return findFirstLineRectInTextNode(node);
338
+ case ELEMENT_NODE:
339
+ return findFirstLineRectInElement(node);
340
+ }
341
+ }
342
+ function findFirstLineRectInTextNode(node) {
343
+ const ownerDocument = node.ownerDocument;
344
+ if (!ownerDocument) {
345
+ return;
346
+ }
347
+ const range = ownerDocument.createRange();
348
+ range.setStart(node, 0);
349
+ range.setEnd(node, 0);
350
+ const rects = range.getClientRects();
351
+ return rects[0];
352
+ }
353
+ function findFirstLineRectInElement(element) {
354
+ if (element.nodeName === "BR") {
355
+ return element.getBoundingClientRect();
356
+ }
357
+ const rect = getClientRect(element);
358
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
359
+ const marginLeft = style && Number.parseInt(style.marginLeft, 10) || 0;
360
+ const marginRight = style && Number.parseInt(style.marginRight, 10) || 0;
361
+ const left = rect.left - marginLeft;
362
+ const right = rect.right + marginRight;
363
+ const lineHeight = style && Number.parseInt(style.lineHeight, 10) || 24;
364
+ const paddingTop = style && Number.parseInt(style.paddingTop, 10) || 0;
365
+ const borderTop = style && Number.parseInt(style.borderTopWidth, 10) || 0;
366
+ const top = rect.top + paddingTop + borderTop;
367
+ const bottom = top + lineHeight;
368
+ return { top, bottom, left, right };
369
+ }
370
+ function fulfillRect({ top, right, bottom, left }) {
371
+ return { top, right, bottom, left, width: right - left, height: bottom - top, x: left, y: top };
372
+ }
373
+ var fallbackRect = Object.freeze({
374
+ top: -9999,
375
+ right: -9999,
376
+ bottom: -9999,
377
+ left: -9999,
378
+ width: 0,
379
+ height: 0,
380
+ x: -9999,
381
+ y: -9999
382
+ });
228
383
 
229
384
  // src/components/block-handle/block-handle-popover/setup.ts
230
385
  function useBlockHandlePopover(host, { state }) {
@@ -268,7 +423,10 @@ import {
268
423
  var blockHandlePopoverProps = {
269
424
  ...overlayPositionerProps,
270
425
  editor: { default: null },
271
- placement: { default: "left-start" }
426
+ placement: { default: "left" },
427
+ // Enabling `hoist` will cause the block handle to have a small delay when
428
+ // scrolling the page.
429
+ hoist: { default: false }
272
430
  };
273
431
  var blockHandlePopoverEvents = {};
274
432
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@prosekit/web",
3
3
  "type": "module",
4
- "version": "0.5.3",
4
+ "version": "0.5.4",
5
5
  "private": false,
6
6
  "author": {
7
7
  "name": "ocavue",
@@ -72,20 +72,20 @@
72
72
  "dependencies": {
73
73
  "@aria-ui/collection": "^0.0.4",
74
74
  "@aria-ui/core": "^0.0.20",
75
- "@aria-ui/listbox": "^0.0.22",
76
- "@aria-ui/menu": "^0.0.17",
75
+ "@aria-ui/listbox": "^0.0.23",
76
+ "@aria-ui/menu": "^0.0.18",
77
77
  "@aria-ui/overlay": "^0.0.23",
78
- "@aria-ui/popover": "^0.0.25",
79
- "@aria-ui/presence": "^0.0.17",
80
- "@aria-ui/tooltip": "^0.0.27",
81
- "@floating-ui/dom": "^1.6.12",
82
- "@zag-js/dom-query": "^0.79.3",
78
+ "@aria-ui/popover": "^0.0.26",
79
+ "@aria-ui/presence": "^0.0.18",
80
+ "@aria-ui/tooltip": "^0.0.28",
81
+ "@floating-ui/dom": "^1.6.13",
82
+ "@zag-js/dom-query": "^0.81.2",
83
83
  "just-map-values": "^3.2.0",
84
84
  "just-omit": "^2.2.0",
85
85
  "prosemirror-tables": "^1.6.2",
86
86
  "@prosekit/core": "^0.7.14",
87
- "@prosekit/pm": "^0.1.9",
88
- "@prosekit/extensions": "^0.7.23"
87
+ "@prosekit/extensions": "^0.7.23",
88
+ "@prosekit/pm": "^0.1.9"
89
89
  },
90
90
  "devDependencies": {
91
91
  "tsup": "^8.3.5",