@haklex/rich-plugin-block-handle 0.0.65 → 0.0.66

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 ADDED
@@ -0,0 +1,65 @@
1
+ # @haklex/rich-plugin-block-handle
2
+
3
+ Block handle plugin with drag handle, add button, and context menu for the Haklex rich editor.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @haklex/rich-plugin-block-handle
9
+ ```
10
+
11
+ ## Peer Dependencies
12
+
13
+ | Package | Version |
14
+ | --- | --- |
15
+ | `@lexical/code` | `^0.41.0` |
16
+ | `@lexical/list` | `^0.41.0` |
17
+ | `@lexical/react` | `^0.41.0` |
18
+ | `@lexical/rich-text` | `^0.41.0` |
19
+ | `@lexical/selection` | `^0.41.0` |
20
+ | `lexical` | `^0.41.0` |
21
+ | `lucide-react` | `^0.574.0` |
22
+ | `react` | `>= 19` |
23
+ | `react-dom` | `>= 19` |
24
+
25
+ ## Usage
26
+
27
+ ```tsx
28
+ import { BlockHandlePlugin } from '@haklex/rich-plugin-block-handle'
29
+ import '@haklex/rich-plugin-block-handle/style.css'
30
+
31
+ function Editor() {
32
+ return (
33
+ <RichEditor>
34
+ <BlockHandlePlugin />
35
+ </RichEditor>
36
+ )
37
+ }
38
+ ```
39
+
40
+ The plugin renders a drag handle and an add button on the left side of each block. Hovering over a block reveals the handle, which supports:
41
+
42
+ - **Drag and drop** to reorder blocks
43
+ - **Add button** to insert a new block above or below
44
+ - **Context menu** with block-level actions (duplicate, delete, change type, etc.)
45
+
46
+ ## Exports
47
+
48
+ | Export | Type | Description |
49
+ | --- | --- | --- |
50
+ | `BlockHandlePlugin` | Component | Main plugin component to render inside `RichEditor` |
51
+
52
+ ## Sub-path Exports
53
+
54
+ | Path | Description |
55
+ | --- | --- |
56
+ | `@haklex/rich-plugin-block-handle` | Plugin component |
57
+ | `@haklex/rich-plugin-block-handle/style.css` | Stylesheet |
58
+
59
+ ## Part of Haklex
60
+
61
+ This package is part of the [Haklex](../../README.md) rich editor ecosystem.
62
+
63
+ ## License
64
+
65
+ MIT
@@ -1 +1 @@
1
- {"version":3,"file":"BlockHandlePlugin.d.ts","sourceRoot":"","sources":["../src/BlockHandlePlugin.tsx"],"names":[],"mappings":"AAkDA,OAAO,KAAK,EAGV,YAAY,EACb,MAAM,OAAO,CAAA;AAmqBd,wBAAgB,iBAAiB,IAAI,YAAY,CAGhD"}
1
+ {"version":3,"file":"BlockHandlePlugin.d.ts","sourceRoot":"","sources":["../src/BlockHandlePlugin.tsx"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAgD,YAAY,EAAE,MAAM,OAAO,CAAC;AAgpBxF,wBAAgB,iBAAiB,IAAI,YAAY,CAGhD"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { jsx, jsxs, Fragment } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator } from "@haklex/rich-editor-ui";
3
+ import { usePortalTheme } from "@haklex/rich-style-token";
3
4
  import { $createCodeNode } from "@lexical/code";
4
5
  import { INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list";
5
6
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
@@ -43,7 +44,7 @@ function getBlockElement(editor, target) {
43
44
  return null;
44
45
  }
45
46
  function getNearestBlockByY(rootElement, clientY) {
46
- const blocks = Array.from(rootElement.children).filter(
47
+ const blocks = [...rootElement.children].filter(
47
48
  (child) => child instanceof HTMLElement
48
49
  );
49
50
  if (!blocks.length) return null;
@@ -67,13 +68,8 @@ function getDropTargetBlock(editor, rootElement, event) {
67
68
  if (event.clientY < rootRect.top || event.clientY > rootRect.bottom) {
68
69
  return null;
69
70
  }
70
- const points = [
71
- { x: event.clientX, y: event.clientY }
72
- ];
73
- const clampedX = Math.min(
74
- rootRect.right - 1,
75
- Math.max(rootRect.left + 1, event.clientX)
76
- );
71
+ const points = [{ x: event.clientX, y: event.clientY }];
72
+ const clampedX = Math.min(rootRect.right - 1, Math.max(rootRect.left + 1, event.clientX));
77
73
  if (clampedX !== event.clientX) {
78
74
  points.unshift({ x: clampedX, y: event.clientY });
79
75
  }
@@ -107,9 +103,8 @@ function $cloneNode(node) {
107
103
  }
108
104
  return clone;
109
105
  }
110
- function BlockHandleInner({
111
- editor
112
- }) {
106
+ function BlockHandleInner({ editor }) {
107
+ const { className: portalClassName, theme } = usePortalTheme();
113
108
  const [handle, setHandle] = useState({
114
109
  visible: false,
115
110
  top: 0,
@@ -165,9 +160,7 @@ function BlockHandleInner({
165
160
  const el = activeBlockRef.current;
166
161
  if (!el || !el.isConnected) {
167
162
  activeBlockRef.current = null;
168
- setHandle(
169
- (s) => s.visible ? { ...s, visible: false, nodeKey: null } : s
170
- );
163
+ setHandle((s) => s.visible ? { ...s, visible: false, nodeKey: null } : s);
171
164
  return;
172
165
  }
173
166
  const rootElement = editor.getRootElement();
@@ -349,21 +342,25 @@ function BlockHandleInner({
349
342
  const preview = block.cloneNode(true);
350
343
  preview.classList.add(dragPreview);
351
344
  preview.style.width = `${rect.width}px`;
352
- document.body.append(preview);
345
+ if (portalClassName) {
346
+ const wrapper = document.createElement("div");
347
+ wrapper.className = portalClassName;
348
+ wrapper.setAttribute("data-theme", theme);
349
+ wrapper.style.cssText = "position:fixed;top:-10000px;left:-10000px;pointer-events:none";
350
+ wrapper.appendChild(preview);
351
+ document.body.append(wrapper);
352
+ dragPreviewRef.current = wrapper;
353
+ } else {
354
+ document.body.append(preview);
355
+ dragPreviewRef.current = preview;
356
+ }
353
357
  draggingBlockRef.current = block;
354
- dragPreviewRef.current = preview;
355
358
  block.classList.add(draggingBlock);
356
- const offsetX = Math.max(
357
- 12,
358
- Math.min(rect.width - 12, e.clientX - rect.left)
359
- );
360
- const offsetY = Math.max(
361
- 8,
362
- Math.min(rect.height - 8, e.clientY - rect.top)
363
- );
359
+ const offsetX = Math.max(12, Math.min(rect.width - 12, e.clientX - rect.left));
360
+ const offsetY = Math.max(8, Math.min(rect.height - 8, e.clientY - rect.top));
364
361
  e.dataTransfer.setDragImage(preview, offsetX, offsetY);
365
362
  },
366
- [clearDragVisualState, handle.nodeKey]
363
+ [clearDragVisualState, handle.nodeKey, portalClassName, theme]
367
364
  );
368
365
  const onGripOpenChange = useCallback(
369
366
  (open) => {
@@ -480,7 +477,12 @@ function BlockHandleInner({
480
477
  clearDragState();
481
478
  };
482
479
  }, [clearDragVisualState, editor]);
483
- return /* @__PURE__ */ jsxs(Fragment, { children: [
480
+ const themeWrapperProps = portalClassName ? {
481
+ "className": portalClassName,
482
+ "data-theme": theme,
483
+ "style": { display: "contents" }
484
+ } : {};
485
+ return /* @__PURE__ */ jsxs("div", { ...themeWrapperProps, children: [
484
486
  /* @__PURE__ */ jsxs(
485
487
  "div",
486
488
  {
@@ -489,42 +491,27 @@ function BlockHandleInner({
489
491
  onMouseEnter: onHandleEnter,
490
492
  onMouseLeave: onHandleLeave,
491
493
  children: [
492
- /* @__PURE__ */ jsx(
493
- "button",
494
- {
495
- className: handleBtn,
496
- "aria-label": "Add block",
497
- onClick: handleAddBlock,
498
- children: /* @__PURE__ */ jsx(Plus, { size: 14 })
499
- }
500
- ),
494
+ /* @__PURE__ */ jsx("button", { "aria-label": "Add block", className: handleBtn, onClick: handleAddBlock, children: /* @__PURE__ */ jsx(Plus, { size: 14 }) }),
501
495
  /* @__PURE__ */ jsxs(DropdownMenu, { open: gripMenuOpen, onOpenChange: onGripOpenChange, children: [
502
496
  /* @__PURE__ */ jsx(
503
497
  DropdownMenuTrigger,
504
498
  {
505
- className: handleBtn,
506
- "aria-label": "Block actions",
507
499
  draggable: true,
500
+ "aria-label": "Block actions",
501
+ className: handleBtn,
502
+ onClick: onGripClick,
508
503
  onDragStart: onGripDragStart,
509
504
  onMouseDownCapture: onGripMouseDownCapture,
510
- onClick: onGripClick,
511
505
  children: /* @__PURE__ */ jsx(GripVertical, { size: 14 })
512
506
  }
513
507
  ),
514
- /* @__PURE__ */ jsxs(DropdownMenuContent, { side: "bottom", align: "start", sideOffset: 4, children: [
508
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", side: "bottom", sideOffset: 4, children: [
515
509
  /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
516
510
  /* @__PURE__ */ jsx(DropdownMenuLabel, { children: "TURN INTO" }),
517
- TURN_INTO_ITEMS.map((item) => /* @__PURE__ */ jsxs(
518
- DropdownMenuItem,
519
- {
520
- onClick: () => handleTurnInto(item.key),
521
- children: [
522
- /* @__PURE__ */ jsx(item.icon, {}),
523
- item.label
524
- ]
525
- },
526
- item.key
527
- ))
511
+ TURN_INTO_ITEMS.map((item) => /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => handleTurnInto(item.key), children: [
512
+ /* @__PURE__ */ jsx(item.icon, {}),
513
+ item.label
514
+ ] }, item.key))
528
515
  ] }),
529
516
  /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
530
517
  /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
@@ -543,17 +530,10 @@ function BlockHandleInner({
543
530
  ] })
544
531
  ] }),
545
532
  /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
546
- /* @__PURE__ */ jsxs(
547
- DropdownMenuItem,
548
- {
549
- className: menuItemDestructive,
550
- onClick: handleDelete,
551
- children: [
552
- /* @__PURE__ */ jsx(Trash2, {}),
553
- "Delete"
554
- ]
555
- }
556
- )
533
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { className: menuItemDestructive, onClick: handleDelete, children: [
534
+ /* @__PURE__ */ jsx(Trash2, {}),
535
+ "Delete"
536
+ ] })
557
537
  ] })
558
538
  ] })
559
539
  ]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@haklex/rich-plugin-block-handle",
3
3
  "type": "module",
4
- "version": "0.0.65",
4
+ "version": "0.0.66",
5
5
  "description": "Block handle plugin with add button and context menu",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -27,8 +27,8 @@
27
27
  "react-dom": ">=19"
28
28
  },
29
29
  "dependencies": {
30
- "@haklex/rich-style-token": "0.0.65",
31
- "@haklex/rich-editor-ui": "0.0.65"
30
+ "@haklex/rich-editor-ui": "0.0.66",
31
+ "@haklex/rich-style-token": "0.0.66"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@lexical/code": "^0.41.0",
@@ -51,6 +51,11 @@
51
51
  "publishConfig": {
52
52
  "access": "public"
53
53
  },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/Innei/haklex.git",
57
+ "directory": "packages/rich-plugin-block-handle"
58
+ },
54
59
  "scripts": {
55
60
  "build": "vite build",
56
61
  "dev:build": "vite build --watch"