@peers-app/peers-ui 0.15.4 → 0.15.5
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/.github/workflows/publish.yml +3 -3
- package/dist/components/markdown-editor/editor.js +2 -1
- package/dist/components/markdown-editor/move-line-plugin.d.ts +8 -0
- package/dist/components/markdown-editor/move-line-plugin.js +96 -0
- package/package.json +3 -3
- package/src/components/markdown-editor/editor.tsx +2 -0
- package/src/components/markdown-editor/move-line-plugin.tsx +116 -0
|
@@ -13,10 +13,10 @@ jobs:
|
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
15
|
- name: Checkout code
|
|
16
|
-
uses: actions/checkout@
|
|
16
|
+
uses: actions/checkout@v6
|
|
17
17
|
|
|
18
18
|
- name: Setup Node.js
|
|
19
|
-
uses: actions/setup-node@
|
|
19
|
+
uses: actions/setup-node@v6
|
|
20
20
|
with:
|
|
21
21
|
node-version: '22'
|
|
22
22
|
registry-url: 'https://registry.npmjs.org'
|
|
@@ -40,7 +40,7 @@ jobs:
|
|
|
40
40
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
41
41
|
|
|
42
42
|
- name: Create GitHub Release
|
|
43
|
-
uses: softprops/action-gh-release@
|
|
43
|
+
uses: softprops/action-gh-release@v3
|
|
44
44
|
if: startsWith(github.ref, 'refs/tags/')
|
|
45
45
|
with:
|
|
46
46
|
generate_release_notes: true
|
|
@@ -61,6 +61,7 @@ const autolink_plugin_1 = require("./autolink-plugin");
|
|
|
61
61
|
const markdown_plugin_1 = require("./markdown-plugin");
|
|
62
62
|
const mention_node_1 = require("./mention-node");
|
|
63
63
|
const mentions_plugin_1 = require("./mentions-plugin");
|
|
64
|
+
const move_line_plugin_1 = require("./move-line-plugin");
|
|
64
65
|
const theme_1 = __importDefault(require("./theme"));
|
|
65
66
|
const toolbar_1 = require("./toolbar");
|
|
66
67
|
const editorConfig = {
|
|
@@ -110,7 +111,7 @@ function MarkdownEditor(props) {
|
|
|
110
111
|
};
|
|
111
112
|
}
|
|
112
113
|
const _mentionConfigs = props.mentionConfigs ?? mention_configs_1.mentionConfigs;
|
|
113
|
-
return ((0, jsx_runtime_1.jsx)(LexicalComposer_1.LexicalComposer, { initialConfig: { ...editorConfig }, children: (0, jsx_runtime_1.jsxs)("div", { className: "editor-container", children: [props.hideToolbar ? null : (0, jsx_runtime_1.jsx)(toolbar_1.ToolbarPlugin, { topRightControls: props.topRightControls }), (0, jsx_runtime_1.jsxs)("div", { className: "editor-inner", ref: editorRef, style: { maxHeight: props.maxHeight, overflowY: "auto" }, children: [(0, jsx_runtime_1.jsx)(LexicalRichTextPlugin_1.RichTextPlugin, { contentEditable: (0, jsx_runtime_1.jsx)(LexicalContentEditable_1.ContentEditable, { className: "editor-input p-2" }), placeholder: null, ErrorBoundary: LexicalErrorBoundary_1.LexicalErrorBoundary }), (0, jsx_runtime_1.jsx)(LexicalHistoryPlugin_1.HistoryPlugin, {}), props.autoFocus && (0, jsx_runtime_1.jsx)(LexicalAutoFocusPlugin_1.AutoFocusPlugin, { defaultSelection: "rootEnd" }), (0, jsx_runtime_1.jsx)(LexicalListPlugin_1.ListPlugin, {}), _mentionConfigs.length > 0 && ((0, jsx_runtime_1.jsx)(mentions_plugin_1.MentionsPlugin, { mentionConfigs: _mentionConfigs, mentionsOpen: mentionsOpen })), (0, jsx_runtime_1.jsx)(autolink_plugin_1.LexicalAutoLinkPlugin, {}), (0, jsx_runtime_1.jsx)(markdown_plugin_1.MarkdownPlugin, { markdownObs: props.value }), (0, jsx_runtime_1.jsx)(LexicalMarkdownShortcutPlugin_1.MarkdownShortcutPlugin, { transformers: markdown_plugin_1.customMarkdownTransformers }), (0, jsx_runtime_1.jsx)(LexicalCheckListPlugin_1.CheckListPlugin, {}), (0, jsx_runtime_1.jsx)(LexicalTabIndentationPlugin_1.TabIndentationPlugin, {}), (0, jsx_runtime_1.jsx)(OnKeyDownPlugin, { effects: props.effects, mentionsOpen: mentionsOpen })] })] }) }));
|
|
114
|
+
return ((0, jsx_runtime_1.jsx)(LexicalComposer_1.LexicalComposer, { initialConfig: { ...editorConfig }, children: (0, jsx_runtime_1.jsxs)("div", { className: "editor-container", children: [props.hideToolbar ? null : (0, jsx_runtime_1.jsx)(toolbar_1.ToolbarPlugin, { topRightControls: props.topRightControls }), (0, jsx_runtime_1.jsxs)("div", { className: "editor-inner", ref: editorRef, style: { maxHeight: props.maxHeight, overflowY: "auto" }, children: [(0, jsx_runtime_1.jsx)(LexicalRichTextPlugin_1.RichTextPlugin, { contentEditable: (0, jsx_runtime_1.jsx)(LexicalContentEditable_1.ContentEditable, { className: "editor-input p-2" }), placeholder: null, ErrorBoundary: LexicalErrorBoundary_1.LexicalErrorBoundary }), (0, jsx_runtime_1.jsx)(LexicalHistoryPlugin_1.HistoryPlugin, {}), props.autoFocus && (0, jsx_runtime_1.jsx)(LexicalAutoFocusPlugin_1.AutoFocusPlugin, { defaultSelection: "rootEnd" }), (0, jsx_runtime_1.jsx)(LexicalListPlugin_1.ListPlugin, {}), _mentionConfigs.length > 0 && ((0, jsx_runtime_1.jsx)(mentions_plugin_1.MentionsPlugin, { mentionConfigs: _mentionConfigs, mentionsOpen: mentionsOpen })), (0, jsx_runtime_1.jsx)(autolink_plugin_1.LexicalAutoLinkPlugin, {}), (0, jsx_runtime_1.jsx)(markdown_plugin_1.MarkdownPlugin, { markdownObs: props.value }), (0, jsx_runtime_1.jsx)(LexicalMarkdownShortcutPlugin_1.MarkdownShortcutPlugin, { transformers: markdown_plugin_1.customMarkdownTransformers }), (0, jsx_runtime_1.jsx)(LexicalCheckListPlugin_1.CheckListPlugin, {}), (0, jsx_runtime_1.jsx)(LexicalTabIndentationPlugin_1.TabIndentationPlugin, {}), (0, jsx_runtime_1.jsx)(move_line_plugin_1.MoveLineWithAltArrowsPlugin, { mentionsOpen: mentionsOpen }), (0, jsx_runtime_1.jsx)(OnKeyDownPlugin, { effects: props.effects, mentionsOpen: mentionsOpen })] })] }) }));
|
|
114
115
|
}
|
|
115
116
|
const OnKeyDownPlugin = (props) => {
|
|
116
117
|
const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Observable } from "@peers-app/peers-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Registers Alt/Option + ArrowUp/ArrowDown to swap the caret's block (or list item) with an adjacent sibling.
|
|
4
|
+
* @param props.mentionsOpen — when true, the plugin defers so mention typeahead keeps keyboard focus.
|
|
5
|
+
*/
|
|
6
|
+
export declare function MoveLineWithAltArrowsPlugin(props: {
|
|
7
|
+
mentionsOpen: Observable<boolean>;
|
|
8
|
+
}): null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MoveLineWithAltArrowsPlugin = MoveLineWithAltArrowsPlugin;
|
|
4
|
+
const list_1 = require("@lexical/list");
|
|
5
|
+
const LexicalComposerContext_1 = require("@lexical/react/LexicalComposerContext");
|
|
6
|
+
const lexical_1 = require("lexical");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
/**
|
|
9
|
+
* Returns the node to reorder for Alt/Option + arrow "move line":
|
|
10
|
+
* a list item (among siblings in its list), or else the top-level block under the root.
|
|
11
|
+
*/
|
|
12
|
+
function $getMovableBlock(node) {
|
|
13
|
+
let n = node;
|
|
14
|
+
while (n !== null) {
|
|
15
|
+
const parent = n.getParent();
|
|
16
|
+
if (parent === null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if ((0, list_1.$isListNode)(parent) && (0, list_1.$isListItemNode)(n)) {
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
if ((0, lexical_1.$isRootOrShadowRoot)(parent)) {
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
n = parent;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Registers Alt/Option + ArrowUp/ArrowDown to swap the caret's block (or list item) with an adjacent sibling.
|
|
31
|
+
* @param props.mentionsOpen — when true, the plugin defers so mention typeahead keeps keyboard focus.
|
|
32
|
+
*/
|
|
33
|
+
function MoveLineWithAltArrowsPlugin(props) {
|
|
34
|
+
const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
|
|
35
|
+
(0, react_1.useEffect)(() => {
|
|
36
|
+
return editor.registerCommand(lexical_1.KEY_DOWN_COMMAND, (event) => {
|
|
37
|
+
if (props.mentionsOpen()) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (!event.altKey || event.metaKey || event.ctrlKey) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const code = event.code;
|
|
44
|
+
if (code !== "ArrowUp" && code !== "ArrowDown") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// KEY_DOWN_COMMAND runs inside Lexical's updateEditorSync; do not call
|
|
48
|
+
// editor.update() here — it would queue and run after this handler returns,
|
|
49
|
+
// so handled would stay false, preventDefault would never run, and the
|
|
50
|
+
// built-in arrow handler + browser would move the caret on top of our swap.
|
|
51
|
+
const selection = (0, lexical_1.$getSelection)();
|
|
52
|
+
if (!(0, lexical_1.$isRangeSelection)(selection) || !selection.isCollapsed()) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const anchorNode = selection.anchor.getNode();
|
|
56
|
+
const block = $getMovableBlock(anchorNode);
|
|
57
|
+
if (block === null) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const anchorKey = selection.anchor.key;
|
|
61
|
+
const anchorOffset = selection.anchor.offset;
|
|
62
|
+
const anchorType = selection.anchor.type;
|
|
63
|
+
const focusKey = selection.focus.key;
|
|
64
|
+
const focusOffset = selection.focus.offset;
|
|
65
|
+
const focusType = selection.focus.type;
|
|
66
|
+
if (code === "ArrowUp") {
|
|
67
|
+
const prev = block.getPreviousSibling();
|
|
68
|
+
if (prev === null) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
prev.insertBefore(block, false);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const next = block.getNextSibling();
|
|
75
|
+
if (next === null) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
next.insertAfter(block, false);
|
|
79
|
+
}
|
|
80
|
+
if ((0, lexical_1.$getNodeByKey)(anchorKey) !== null) {
|
|
81
|
+
const restored = (0, lexical_1.$createRangeSelection)();
|
|
82
|
+
restored.format = selection.format;
|
|
83
|
+
restored.style = selection.style;
|
|
84
|
+
restored.anchor.set(anchorKey, anchorOffset, anchorType);
|
|
85
|
+
restored.focus.set(focusKey, focusOffset, focusType);
|
|
86
|
+
(0, lexical_1.$setSelection)(restored);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
block.selectEnd();
|
|
90
|
+
}
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
return true;
|
|
93
|
+
}, lexical_1.COMMAND_PRIORITY_HIGH);
|
|
94
|
+
}, [editor]);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lint:fix": "biome check --write ."
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@peers-app/peers-sdk": "^0.15.
|
|
31
|
+
"@peers-app/peers-sdk": "^0.15.5",
|
|
32
32
|
"bootstrap": "^5.3.3",
|
|
33
33
|
"react": "^18.0.0",
|
|
34
34
|
"react-dom": "^18.0.0"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@babel/preset-env": "^7.24.5",
|
|
40
40
|
"@babel/preset-react": "^7.24.1",
|
|
41
41
|
"@babel/preset-typescript": "^7.27.1",
|
|
42
|
-
"@peers-app/peers-sdk": "0.15.
|
|
42
|
+
"@peers-app/peers-sdk": "0.15.5",
|
|
43
43
|
"@testing-library/dom": "^10.4.0",
|
|
44
44
|
"@testing-library/jest-dom": "^6.6.3",
|
|
45
45
|
"@testing-library/react": "^16.3.0",
|
|
@@ -21,6 +21,7 @@ import { LexicalAutoLinkPlugin } from "./autolink-plugin";
|
|
|
21
21
|
import { customMarkdownTransformers, MarkdownPlugin } from "./markdown-plugin";
|
|
22
22
|
import { MentionNode } from "./mention-node";
|
|
23
23
|
import { MentionsPlugin } from "./mentions-plugin";
|
|
24
|
+
import { MoveLineWithAltArrowsPlugin } from "./move-line-plugin";
|
|
24
25
|
import theme from "./theme";
|
|
25
26
|
import { type IToolbarControl, ToolbarPlugin } from "./toolbar";
|
|
26
27
|
|
|
@@ -119,6 +120,7 @@ export function MarkdownEditor(props: IMarkdownEditorProps) {
|
|
|
119
120
|
<MarkdownShortcutPlugin transformers={customMarkdownTransformers} />
|
|
120
121
|
<CheckListPlugin />
|
|
121
122
|
<TabIndentationPlugin />
|
|
123
|
+
<MoveLineWithAltArrowsPlugin mentionsOpen={mentionsOpen} />
|
|
122
124
|
<OnKeyDownPlugin effects={props.effects} mentionsOpen={mentionsOpen} />
|
|
123
125
|
</div>
|
|
124
126
|
</div>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { $isListItemNode, $isListNode } from "@lexical/list";
|
|
2
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
3
|
+
import type { Observable } from "@peers-app/peers-sdk";
|
|
4
|
+
import {
|
|
5
|
+
$createRangeSelection,
|
|
6
|
+
$getNodeByKey,
|
|
7
|
+
$getSelection,
|
|
8
|
+
$isRangeSelection,
|
|
9
|
+
$isRootOrShadowRoot,
|
|
10
|
+
$setSelection,
|
|
11
|
+
COMMAND_PRIORITY_HIGH,
|
|
12
|
+
KEY_DOWN_COMMAND,
|
|
13
|
+
type LexicalNode,
|
|
14
|
+
} from "lexical";
|
|
15
|
+
import { useEffect } from "react";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns the node to reorder for Alt/Option + arrow "move line":
|
|
19
|
+
* a list item (among siblings in its list), or else the top-level block under the root.
|
|
20
|
+
*/
|
|
21
|
+
function $getMovableBlock(node: LexicalNode): LexicalNode | null {
|
|
22
|
+
let n: LexicalNode | null = node;
|
|
23
|
+
while (n !== null) {
|
|
24
|
+
const parent: LexicalNode | null = n.getParent();
|
|
25
|
+
if (parent === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if ($isListNode(parent) && $isListItemNode(n)) {
|
|
29
|
+
return n;
|
|
30
|
+
}
|
|
31
|
+
if ($isRootOrShadowRoot(parent)) {
|
|
32
|
+
return n;
|
|
33
|
+
}
|
|
34
|
+
n = parent;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Registers Alt/Option + ArrowUp/ArrowDown to swap the caret's block (or list item) with an adjacent sibling.
|
|
41
|
+
* @param props.mentionsOpen — when true, the plugin defers so mention typeahead keeps keyboard focus.
|
|
42
|
+
*/
|
|
43
|
+
export function MoveLineWithAltArrowsPlugin(props: { mentionsOpen: Observable<boolean> }) {
|
|
44
|
+
const [editor] = useLexicalComposerContext();
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
return editor.registerCommand(
|
|
48
|
+
KEY_DOWN_COMMAND,
|
|
49
|
+
(event: KeyboardEvent) => {
|
|
50
|
+
if (props.mentionsOpen()) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (!event.altKey || event.metaKey || event.ctrlKey) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const code = event.code;
|
|
57
|
+
if (code !== "ArrowUp" && code !== "ArrowDown") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// KEY_DOWN_COMMAND runs inside Lexical's updateEditorSync; do not call
|
|
62
|
+
// editor.update() here — it would queue and run after this handler returns,
|
|
63
|
+
// so handled would stay false, preventDefault would never run, and the
|
|
64
|
+
// built-in arrow handler + browser would move the caret on top of our swap.
|
|
65
|
+
|
|
66
|
+
const selection = $getSelection();
|
|
67
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const anchorNode = selection.anchor.getNode();
|
|
71
|
+
const block = $getMovableBlock(anchorNode);
|
|
72
|
+
if (block === null) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const anchorKey = selection.anchor.key;
|
|
77
|
+
const anchorOffset = selection.anchor.offset;
|
|
78
|
+
const anchorType = selection.anchor.type;
|
|
79
|
+
const focusKey = selection.focus.key;
|
|
80
|
+
const focusOffset = selection.focus.offset;
|
|
81
|
+
const focusType = selection.focus.type;
|
|
82
|
+
|
|
83
|
+
if (code === "ArrowUp") {
|
|
84
|
+
const prev = block.getPreviousSibling();
|
|
85
|
+
if (prev === null) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
prev.insertBefore(block, false);
|
|
89
|
+
} else {
|
|
90
|
+
const next = block.getNextSibling();
|
|
91
|
+
if (next === null) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
next.insertAfter(block, false);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ($getNodeByKey(anchorKey) !== null) {
|
|
98
|
+
const restored = $createRangeSelection();
|
|
99
|
+
restored.format = selection.format;
|
|
100
|
+
restored.style = selection.style;
|
|
101
|
+
restored.anchor.set(anchorKey, anchorOffset, anchorType);
|
|
102
|
+
restored.focus.set(focusKey, focusOffset, focusType);
|
|
103
|
+
$setSelection(restored);
|
|
104
|
+
} else {
|
|
105
|
+
block.selectEnd();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
return true;
|
|
110
|
+
},
|
|
111
|
+
COMMAND_PRIORITY_HIGH,
|
|
112
|
+
);
|
|
113
|
+
}, [editor]);
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|