Skip to content

Commit 0fea6bb

Browse files
committed
Add smart bracket features to Tiptap
1 parent 00b1bdc commit 0fea6bb

File tree

2 files changed

+87
-0
lines changed

2 files changed

+87
-0
lines changed

resources/js/tiptap.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Table from '@tiptap/extension-table';
66
import TableRow from '@tiptap/extension-table-row';
77
import { common, createLowlight } from 'lowlight';
88

9+
import { SmartBracket } from './tiptap/extension-smart-bracket';
910
import { CustomCodeBlockLowlight } from './tiptap/extension-custom-code-block-low-light';
1011
import { CustomImage } from './tiptap/extension-custom-image';
1112
import { CustomLink } from './tiptap/extension-custom-link';
@@ -82,6 +83,7 @@ window.setupEditor = function (options) {
8283
},
8384
codeBlock: false,
8485
}),
86+
SmartBracket,
8587
CustomCodeBlockLowlight.configure({
8688
defaultLanguage: 'plaintext',
8789
lowlight: createLowlight(common),
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Plugin, TextSelection } from '@tiptap/pm/state';
2+
import { Extension } from '@tiptap/core';
3+
4+
const bracketPairs = {
5+
'<': '>',
6+
'«': '»',
7+
'(': ')',
8+
'[': ']',
9+
'{': '}',
10+
'"': '"',
11+
"'": "'",
12+
};
13+
14+
export const SmartBracket = Extension.create({
15+
name: 'smartBracket',
16+
17+
addProseMirrorPlugins() {
18+
return [
19+
new Plugin({
20+
props: {
21+
handleTextInput: (view, from, to, text) => {
22+
if (!(text in bracketPairs)) {
23+
return false;
24+
}
25+
26+
const { state, dispatch } = view;
27+
const { tr, selection } = state;
28+
const closing = bracketPairs[text];
29+
30+
if (selection.empty) {
31+
tr.insertText(text + closing, from, to);
32+
tr.setSelection(
33+
state.selection.constructor.near(tr.doc.resolve(from + 1))
34+
);
35+
} else {
36+
const { from: selFrom, to: selTo } = selection;
37+
tr.insertText(text, selFrom, selFrom);
38+
tr.insertText(closing, selTo + 1, selTo + 1);
39+
tr.setSelection(state.selection.constructor.create(tr.doc, selFrom + 1, selTo + 1));
40+
}
41+
42+
dispatch(tr);
43+
return true;
44+
},
45+
handleKeyDown(view, event) {
46+
const { state, dispatch } = view;
47+
const { selection } = state;
48+
const { $from } = selection;
49+
50+
// Skip over closing chars
51+
if (selection.empty && Object.values(bracketPairs).includes(event.key)) {
52+
const nextChar = $from.nodeAfter?.text?.[0];
53+
54+
if (nextChar === event.key) {
55+
dispatch(
56+
state.tr.setSelection(
57+
TextSelection.create(state.doc, $from.pos + 1)
58+
)
59+
);
60+
61+
return true;
62+
}
63+
}
64+
65+
// Delete bracket pair when cursor between opening and closing chars
66+
if (event.key === 'Backspace' && selection.empty) {
67+
const prevChar = $from.nodeBefore?.text?.slice(-1);
68+
const nextChar = $from.nodeAfter?.text?.[0];
69+
70+
if (prevChar && bracketPairs[prevChar] === nextChar) {
71+
dispatch(
72+
state.tr.delete($from.pos - 1, $from.pos + 1)
73+
);
74+
75+
return true;
76+
}
77+
}
78+
79+
return false;
80+
}
81+
},
82+
}),
83+
];
84+
},
85+
});

0 commit comments

Comments
 (0)