@haklex/rich-plugin-block-handle 0.0.65 → 0.0.67
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 +65 -0
- package/dist/BlockHandlePlugin.d.ts.map +1 -1
- package/dist/index.mjs +42 -62
- package/package.json +20 -15
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":"
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haklex/rich-plugin-block-handle",
|
|
3
|
-
"
|
|
4
|
-
"version": "0.0.65",
|
|
3
|
+
"version": "0.0.67",
|
|
5
4
|
"description": "Block handle plugin with add button and context menu",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Innei/haklex.git",
|
|
8
|
+
"directory": "packages/rich-plugin-block-handle"
|
|
9
|
+
},
|
|
6
10
|
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
7
12
|
"exports": {
|
|
8
13
|
".": {
|
|
9
14
|
"import": "./dist/index.mjs",
|
|
@@ -15,20 +20,9 @@
|
|
|
15
20
|
"files": [
|
|
16
21
|
"dist"
|
|
17
22
|
],
|
|
18
|
-
"peerDependencies": {
|
|
19
|
-
"@lexical/code": "^0.41.0",
|
|
20
|
-
"@lexical/list": "^0.41.0",
|
|
21
|
-
"@lexical/react": "^0.41.0",
|
|
22
|
-
"@lexical/rich-text": "^0.41.0",
|
|
23
|
-
"@lexical/selection": "^0.41.0",
|
|
24
|
-
"lexical": "^0.41.0",
|
|
25
|
-
"lucide-react": "^0.574.0",
|
|
26
|
-
"react": ">=19",
|
|
27
|
-
"react-dom": ">=19"
|
|
28
|
-
},
|
|
29
23
|
"dependencies": {
|
|
30
|
-
"@haklex/rich-
|
|
31
|
-
"@haklex/rich-
|
|
24
|
+
"@haklex/rich-editor-ui": "0.0.67",
|
|
25
|
+
"@haklex/rich-style-token": "0.0.67"
|
|
32
26
|
},
|
|
33
27
|
"devDependencies": {
|
|
34
28
|
"@lexical/code": "^0.41.0",
|
|
@@ -48,6 +42,17 @@
|
|
|
48
42
|
"vite": "^7.3.1",
|
|
49
43
|
"vite-plugin-dts": "^4.5.4"
|
|
50
44
|
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@lexical/code": "^0.41.0",
|
|
47
|
+
"@lexical/list": "^0.41.0",
|
|
48
|
+
"@lexical/react": "^0.41.0",
|
|
49
|
+
"@lexical/rich-text": "^0.41.0",
|
|
50
|
+
"@lexical/selection": "^0.41.0",
|
|
51
|
+
"lexical": "^0.41.0",
|
|
52
|
+
"lucide-react": "^0.574.0",
|
|
53
|
+
"react": ">=19",
|
|
54
|
+
"react-dom": ">=19"
|
|
55
|
+
},
|
|
51
56
|
"publishConfig": {
|
|
52
57
|
"access": "public"
|
|
53
58
|
},
|