├── .gitignore
├── LICENSE
├── README.md
├── biome.json
├── examples
└── simple-example
│ ├── .gitignore
│ ├── favicon.svg
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ ├── App.tsx
│ ├── main.tsx
│ ├── style.css
│ └── vite-env.d.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── vite.config.js
├── images
└── solid-tiptap.png
├── lerna.json
├── package.json
├── packages
└── solid-tiptap
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── pridepack.json
│ ├── src
│ ├── Editor.tsx
│ └── index.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # parcel-bundler cache (https://parceljs.org/)
61 | .cache
62 |
63 | # next.js build output
64 | .next
65 |
66 | # nuxt.js build output
67 | .nuxt
68 |
69 | # vuepress build output
70 | .vuepress/dist
71 |
72 | # Serverless directories
73 | .serverless
74 |
75 | # FuseBox cache
76 | .fusebox/
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Alexis Munsayac
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # solid-tiptap
2 |
3 | > SolidJS bindings for [tiptap](https://www.tiptap.dev/)
4 |
5 | [](https://www.npmjs.com/package/solid-tiptap) [](https://github.com/airbnb/javascript) [](https://stackblitz.com/github/lxsmnsyc/solid-tiptap/tree/main/examples/simple-example)
6 |
7 | 
8 |
9 | ## Install
10 |
11 | ```bash
12 | npm i @tiptap/core @tiptap/pm solid-tiptap
13 | ```
14 |
15 | ```bash
16 | yarn add @tiptap/core @tiptap/pm solid-tiptap
17 | ```
18 |
19 | ```bash
20 | pnpm add @tiptap/core @tiptap/pm solid-tiptap
21 | ```
22 |
23 | ## Usage
24 |
25 | ```jsx
26 | import { createTiptapEditor } from 'solid-tiptap';
27 | import StarterKit from '@tiptap/starter-kit';
28 |
29 | function App() {
30 | let ref!: HTMLDivElement;
31 |
32 | const editor = createTiptapEditor(() => ({
33 | element: ref!,
34 | extensions: [
35 | StarterKit,
36 | ],
37 | content: `
Example Text
`,
38 | }));
39 |
40 | return
;
41 | }
42 | ```
43 |
44 | ### Transactions
45 |
46 | `createEditorTransaction` provides a way to reactively subscribe to editor changes.
47 |
48 | ```ts
49 | const isBold = createEditorTransaction(
50 | () => props.editor, // Editor instance from createTiptapEditor
51 | (editor) => editor.isActive('bold'),
52 | );
53 |
54 | createEffect(() => {
55 | if (isBold()) {
56 | // do something
57 | }
58 | });
59 | ```
60 |
61 | There are out-of-the-box utilities that wraps `createEditorTransaction` based on the [Editor API](https://www.tiptap.dev/api/editor):
62 |
63 | - `useEditorCharacterCount`: reactively subscribe to `getCharacterCount`.
64 |
65 | ```js
66 | const count = useEditorCharacterCount(() => props.editor);
67 |
68 | createEffect(() => {
69 | console.log('Character Count:', count());
70 | });
71 | ```
72 |
73 | - `useEditorHTML`: reactively subscribe to `getEditorHTML`.
74 |
75 | ```js
76 | const html = useEditorHTML(() => props.editor);
77 |
78 | createEffect(() => {
79 | updateHTML(html());
80 | });
81 | ```
82 |
83 | - `useEditorIsActive`: reactively subscribe to `isActive`.
84 |
85 | ```js
86 | const isHeading = useEditorIsActive(() => props.editor, () => 'heading', {
87 | level: 1,
88 | });
89 |
90 | createEffect(() => {
91 | if (isHeading()) {
92 | // do something
93 | }
94 | });
95 | ```
96 |
97 | - `useEditorIsEditable`: reactively subscribe to `isEditable`.
98 |
99 | ```js
100 | const isEditable = useEditorIsEditable(() => props.editor);
101 |
102 | createEffect(() => {
103 | if (isEditable()) {
104 | // do something
105 | }
106 | });
107 | ```
108 |
109 | - `useEditorIsEmpty`: reactively subscribe to `isEmpty`.
110 |
111 | ```js
112 | const isEmpty = useEditorIsEmpty(() => props.editor);
113 |
114 | createEffect(() => {
115 | if (isEmpty()) {
116 | // do something
117 | }
118 | });
119 | ```
120 |
121 | - `useEditorIsFocused`: reactively subscribe to `isFocused`.
122 |
123 | ```js
124 | const isFocused = useEditorIsFocused(() => props.editor);
125 |
126 | createEffect(() => {
127 | if (isFocused()) {
128 | // do something
129 | }
130 | });
131 | ```
132 |
133 | - `useEditorJSON`: reactively subscribe to `getJSON`.
134 |
135 | ```js
136 | const json = useEditorJSON(() => props.editor);
137 |
138 | createEffect(() => {
139 | const data = json();
140 |
141 | if (data) {
142 | uploadJSON(data);
143 | }
144 | });
145 | ```
146 |
147 | ## Tips
148 |
149 | Since `createTiptapEditor` may return `undefined` (the instanciation is scheduled), and you're planning to pass it to another component (e.g. creating `BubbleMenu` or Toolbar), it is recommended to use ternary evaluation (e.g. ``) to check if the editor is `undefined` or not.
150 |
151 | ```js
152 | const editor = createTiptapEditor(() => ({ ... }));
153 |
154 |
155 | {(instance) => }
156 |
157 | ```
158 |
159 | ## Sponsors
160 |
161 | 
162 |
163 | ## License
164 |
165 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc)
166 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@biomejs/biome/configuration_schema.json",
3 | "files": {
4 | "ignore": ["node_modules/**/*"]
5 | },
6 | "vcs": {
7 | "useIgnoreFile": true
8 | },
9 | "linter": {
10 | "enabled": true,
11 | "ignore": ["node_modules/**/*"],
12 | "rules": {
13 | "a11y": {
14 | "noAccessKey": "error",
15 | "noAriaHiddenOnFocusable": "off",
16 | "noAriaUnsupportedElements": "error",
17 | "noAutofocus": "error",
18 | "noBlankTarget": "error",
19 | "noDistractingElements": "error",
20 | "noHeaderScope": "error",
21 | "noInteractiveElementToNoninteractiveRole": "error",
22 | "noNoninteractiveElementToInteractiveRole": "error",
23 | "noNoninteractiveTabindex": "error",
24 | "noPositiveTabindex": "error",
25 | "noRedundantAlt": "error",
26 | "noRedundantRoles": "error",
27 | "noSvgWithoutTitle": "error",
28 | "useAltText": "error",
29 | "useAnchorContent": "error",
30 | "useAriaActivedescendantWithTabindex": "error",
31 | "useAriaPropsForRole": "error",
32 | "useButtonType": "error",
33 | "useHeadingContent": "error",
34 | "useHtmlLang": "error",
35 | "useIframeTitle": "warn",
36 | "useKeyWithClickEvents": "warn",
37 | "useKeyWithMouseEvents": "warn",
38 | "useMediaCaption": "error",
39 | "useValidAnchor": "error",
40 | "useValidAriaProps": "error",
41 | "useValidAriaRole": "error",
42 | "useValidAriaValues": "error",
43 | "useValidLang": "error"
44 | },
45 | "complexity": {
46 | "noBannedTypes": "error",
47 | "noExcessiveCognitiveComplexity": "error",
48 | "noExtraBooleanCast": "error",
49 | "noForEach": "error",
50 | "noMultipleSpacesInRegularExpressionLiterals": "warn",
51 | "noStaticOnlyClass": "error",
52 | "noThisInStatic": "error",
53 | "noUselessCatch": "error",
54 | "noUselessConstructor": "error",
55 | "noUselessEmptyExport": "error",
56 | "noUselessFragments": "error",
57 | "noUselessLabel": "error",
58 | "noUselessRename": "error",
59 | "noUselessSwitchCase": "error",
60 | "noUselessThisAlias": "error",
61 | "noUselessTypeConstraint": "error",
62 | "noVoid": "off",
63 | "noWith": "error",
64 | "useArrowFunction": "error",
65 | "useFlatMap": "error",
66 | "useLiteralKeys": "error",
67 | "useOptionalChain": "warn",
68 | "useRegexLiterals": "error",
69 | "useSimpleNumberKeys": "error",
70 | "useSimplifiedLogicExpression": "error"
71 | },
72 | "correctness": {
73 | "noChildrenProp": "error",
74 | "noConstantCondition": "error",
75 | "noConstAssign": "error",
76 | "noConstructorReturn": "error",
77 | "noEmptyCharacterClassInRegex": "error",
78 | "noEmptyPattern": "error",
79 | "noGlobalObjectCalls": "error",
80 | "noInnerDeclarations": "error",
81 | "noInvalidConstructorSuper": "error",
82 | "noInvalidNewBuiltin": "error",
83 | "noNewSymbol": "error",
84 | "noNonoctalDecimalEscape": "error",
85 | "noPrecisionLoss": "error",
86 | "noRenderReturnValue": "error",
87 | "noSelfAssign": "error",
88 | "noSetterReturn": "error",
89 | "noStringCaseMismatch": "error",
90 | "noSwitchDeclarations": "error",
91 | "noUndeclaredVariables": "error",
92 | "noUnnecessaryContinue": "error",
93 | "noUnreachable": "error",
94 | "noUnreachableSuper": "error",
95 | "noUnsafeFinally": "error",
96 | "noUnsafeOptionalChaining": "error",
97 | "noUnusedLabels": "error",
98 | "noUnusedVariables": "error",
99 | "noVoidElementsWithChildren": "error",
100 | "noVoidTypeReturn": "error",
101 | "useExhaustiveDependencies": "error",
102 | "useHookAtTopLevel": "error",
103 | "useIsNan": "error",
104 | "useValidForDirection": "error",
105 | "useYield": "error"
106 | },
107 | "performance": {
108 | "noAccumulatingSpread": "error",
109 | "noDelete": "off"
110 | },
111 | "security": {
112 | "noDangerouslySetInnerHtml": "error",
113 | "noDangerouslySetInnerHtmlWithChildren": "error"
114 | },
115 | "style": {
116 | "noArguments": "error",
117 | "noCommaOperator": "off",
118 | "noDefaultExport": "off",
119 | "noImplicitBoolean": "off",
120 | "noInferrableTypes": "error",
121 | "noNamespace": "error",
122 | "noNegationElse": "error",
123 | "noNonNullAssertion": "off",
124 | "noParameterAssign": "off",
125 | "noParameterProperties": "off",
126 | "noRestrictedGlobals": "error",
127 | "noShoutyConstants": "error",
128 | "noUnusedTemplateLiteral": "error",
129 | "noUselessElse": "error",
130 | "noVar": "error",
131 | "useAsConstAssertion": "error",
132 | "useBlockStatements": "error",
133 | "useCollapsedElseIf": "error",
134 | "useConst": "error",
135 | "useDefaultParameterLast": "error",
136 | "useEnumInitializers": "error",
137 | "useExponentiationOperator": "error",
138 | "useFragmentSyntax": "error",
139 | "useLiteralEnumMembers": "error",
140 | "useNamingConvention": "off",
141 | "useNumericLiterals": "error",
142 | "useSelfClosingElements": "error",
143 | "useShorthandArrayType": "error",
144 | "useShorthandAssign": "error",
145 | "useSingleCaseStatement": "error",
146 | "useSingleVarDeclarator": "error",
147 | "useTemplate": "off",
148 | "useWhile": "error"
149 | },
150 | "suspicious": {
151 | "noApproximativeNumericConstant": "error",
152 | "noArrayIndexKey": "error",
153 | "noAssignInExpressions": "error",
154 | "noAsyncPromiseExecutor": "error",
155 | "noCatchAssign": "error",
156 | "noClassAssign": "error",
157 | "noCommentText": "error",
158 | "noCompareNegZero": "error",
159 | "noConfusingLabels": "error",
160 | "noConfusingVoidType": "error",
161 | "noConsoleLog": "warn",
162 | "noConstEnum": "off",
163 | "noControlCharactersInRegex": "error",
164 | "noDebugger": "off",
165 | "noDoubleEquals": "error",
166 | "noDuplicateCase": "error",
167 | "noDuplicateClassMembers": "error",
168 | "noDuplicateJsxProps": "error",
169 | "noDuplicateObjectKeys": "error",
170 | "noDuplicateParameters": "error",
171 | "noEmptyInterface": "error",
172 | "noExplicitAny": "warn",
173 | "noExtraNonNullAssertion": "error",
174 | "noFallthroughSwitchClause": "error",
175 | "noFunctionAssign": "error",
176 | "noGlobalIsFinite": "error",
177 | "noGlobalIsNan": "error",
178 | "noImplicitAnyLet": "off",
179 | "noImportAssign": "error",
180 | "noLabelVar": "error",
181 | "noMisleadingInstantiator": "error",
182 | "noMisrefactoredShorthandAssign": "off",
183 | "noPrototypeBuiltins": "error",
184 | "noRedeclare": "error",
185 | "noRedundantUseStrict": "error",
186 | "noSelfCompare": "off",
187 | "noShadowRestrictedNames": "error",
188 | "noSparseArray": "off",
189 | "noUnsafeDeclarationMerging": "error",
190 | "noUnsafeNegation": "error",
191 | "useDefaultSwitchClauseLast": "error",
192 | "useGetterReturn": "error",
193 | "useIsArray": "error",
194 | "useNamespaceKeyword": "error",
195 | "useValidTypeof": "error"
196 | },
197 | "nursery": {
198 | "noDuplicateJsonKeys": "off",
199 | "noEmptyBlockStatements": "error",
200 | "noEmptyTypeParameters": "error",
201 | "noGlobalEval": "off",
202 | "noGlobalAssign": "error",
203 | "noInvalidUseBeforeDeclaration": "error",
204 | "noMisleadingCharacterClass": "error",
205 | "noNodejsModules": "off",
206 | "noThenProperty": "warn",
207 | "noUnusedImports": "error",
208 | "noUnusedPrivateClassMembers": "error",
209 | "noUselessLoneBlockStatements": "error",
210 | "noUselessTernary": "error",
211 | "useAwait": "error",
212 | "useConsistentArrayType": "error",
213 | "useExportType": "error",
214 | "useFilenamingConvention": "off",
215 | "useForOf": "warn",
216 | "useGroupedTypeImport": "error",
217 | "useImportRestrictions": "off",
218 | "useImportType": "error",
219 | "useNodejsImportProtocol": "warn",
220 | "useNumberNamespace": "error",
221 | "useShorthandFunctionType": "warn"
222 | }
223 | }
224 | },
225 | "formatter": {
226 | "enabled": true,
227 | "ignore": ["node_modules/**/*"],
228 | "formatWithErrors": false,
229 | "indentWidth": 2,
230 | "indentStyle": "space",
231 | "lineEnding": "lf",
232 | "lineWidth": 80
233 | },
234 | "organizeImports": {
235 | "enabled": true,
236 | "ignore": ["node_modules/**/*"]
237 | },
238 | "javascript": {
239 | "formatter": {
240 | "enabled": true,
241 | "arrowParentheses": "asNeeded",
242 | "bracketSameLine": false,
243 | "bracketSpacing": true,
244 | "indentWidth": 2,
245 | "indentStyle": "space",
246 | "jsxQuoteStyle": "double",
247 | "lineEnding": "lf",
248 | "lineWidth": 80,
249 | "quoteProperties": "asNeeded",
250 | "quoteStyle": "single",
251 | "semicolons": "always",
252 | "trailingComma": "all"
253 | },
254 | "globals": [],
255 | "parser": {
256 | "unsafeParameterDecoratorsEnabled": true
257 | }
258 | },
259 | "json": {
260 | "formatter": {
261 | "enabled": true,
262 | "indentWidth": 2,
263 | "indentStyle": "space",
264 | "lineEnding": "lf",
265 | "lineWidth": 80
266 | },
267 | "parser": {
268 | "allowComments": false,
269 | "allowTrailingCommas": false
270 | }
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/examples/simple-example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/examples/simple-example/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/simple-example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/simple-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simple-example",
3 | "type": "module",
4 | "version": "0.7.0",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "serve": "vite preview"
9 | },
10 | "devDependencies": {
11 | "autoprefixer": "^10.4.17",
12 | "postcss": "^8.4.33",
13 | "typescript": "^5.3.3",
14 | "vite": "^5.0.12",
15 | "vite-plugin-solid": "^2.9.1"
16 | },
17 | "dependencies": {
18 | "@tailwindcss/typography": "^0.5.10",
19 | "@tiptap/core": "^2.1.16",
20 | "@tiptap/extension-bubble-menu": "^2.1.16",
21 | "@tiptap/pm": "^2.1.16",
22 | "@tiptap/starter-kit": "^2.1.16",
23 | "solid-js": "^1.8.12",
24 | "solid-tiptap": "0.7.0",
25 | "tailwindcss": "^3.4.1",
26 | "terracotta": "^1.0.4"
27 | },
28 | "private": true,
29 | "publishConfig": {
30 | "access": "restricted"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/simple-example/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/examples/simple-example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import type { Editor } from '@tiptap/core';
2 | import BubbleMenu from '@tiptap/extension-bubble-menu';
3 | import StarterKit from '@tiptap/starter-kit';
4 | import type { JSX } from 'solid-js';
5 | import { Show, createSignal } from 'solid-js';
6 | import { createEditorTransaction, createTiptapEditor } from 'solid-tiptap';
7 | import { Toggle, Toolbar } from 'terracotta';
8 |
9 | function ParagraphIcon(
10 | props: JSX.IntrinsicElements['svg'] & { title: string },
11 | ): JSX.Element {
12 | return (
13 |
20 | {props.title}
21 |
22 |
23 | );
24 | }
25 |
26 | function CodeIcon(
27 | props: JSX.IntrinsicElements['svg'] & { title: string },
28 | ): JSX.Element {
29 | return (
30 |
37 | {props.title}
38 |
39 |
40 | );
41 | }
42 |
43 | function CodeBlockIcon(
44 | props: JSX.IntrinsicElements['svg'] & { title: string },
45 | ): JSX.Element {
46 | return (
47 |
54 | {props.title}
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | function OrderedListIcon(
62 | props: JSX.IntrinsicElements['svg'] & { title: string },
63 | ): JSX.Element {
64 | return (
65 |
72 | {props.title}
73 |
77 |
78 | );
79 | }
80 |
81 | function BulletListIcon(
82 | props: JSX.IntrinsicElements['svg'] & { title: string },
83 | ): JSX.Element {
84 | return (
85 |
92 | {props.title}
93 |
97 |
98 | );
99 | }
100 |
101 | function BlockquoteIcon(
102 | props: JSX.IntrinsicElements['svg'] & { title: string },
103 | ): JSX.Element {
104 | return (
105 |
112 | {props.title}
113 |
114 |
115 | );
116 | }
117 |
118 | const CONTENT = `
119 |
120 | Hi there,
121 |
122 |
123 | this is a basic example of tiptap . Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
124 |
125 |
126 |
127 | That’s a bullet list with one …
128 |
129 |
130 | … or two list items.
131 |
132 |
133 |
134 | Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
135 |
136 | body {
137 | display: none;
138 | }
139 |
140 | I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.
141 |
142 |
143 | Wow, that’s amazing. Good work, boy! 👏
144 |
145 | — Mom
146 |
147 | `;
148 |
149 | function Separator() {
150 | return (
151 |
154 | );
155 | }
156 |
157 | interface ControlProps {
158 | class: string;
159 | editor: Editor;
160 | title: string;
161 | key: string;
162 | onChange: () => void;
163 | isActive?: (editor: Editor) => boolean;
164 | children: JSX.Element;
165 | }
166 |
167 | function Control(props: ControlProps): JSX.Element {
168 | const flag = createEditorTransaction(
169 | () => props.editor,
170 | instance => {
171 | if (props.isActive) {
172 | return props.isActive(instance);
173 | }
174 | return instance.isActive(props.key);
175 | },
176 | );
177 |
178 | return (
179 |
188 | {props.children}
189 |
190 | );
191 | }
192 |
193 | interface ToolbarProps {
194 | editor: Editor;
195 | }
196 |
197 | function ToolbarContents(props: ToolbarProps): JSX.Element {
198 | return (
199 |
200 |
201 |
props.editor.chain().focus().setParagraph().run()}
206 | title="Paragraph"
207 | >
208 |
209 |
210 |
215 | props.editor.chain().focus().setHeading({ level: 1 }).run()
216 | }
217 | isActive={editor => editor.isActive('heading', { level: 1 })}
218 | title="Heading 1"
219 | >
220 | H1
221 |
222 |
227 | props.editor.chain().focus().setHeading({ level: 2 }).run()
228 | }
229 | isActive={editor => editor.isActive('heading', { level: 2 })}
230 | title="Heading 2"
231 | >
232 | H2
233 |
234 |
235 |
236 |
237 | props.editor.chain().focus().toggleBold().run()}
242 | title="Bold"
243 | >
244 | B
245 |
246 | props.editor.chain().focus().toggleItalic().run()}
251 | title="Italic"
252 | >
253 | I
254 |
255 | props.editor.chain().focus().toggleStrike().run()}
260 | title="Strike Through"
261 | >
262 | S
263 |
264 | props.editor.chain().focus().toggleCode().run()}
269 | title="Code"
270 | >
271 |
272 |
273 |
274 |
275 |
276 |
props.editor.chain().focus().toggleBulletList().run()}
281 | title="Bullet List"
282 | >
283 |
284 |
285 |
290 | props.editor.chain().focus().toggleOrderedList().run()
291 | }
292 | title="Ordered List"
293 | >
294 |
295 |
296 |
props.editor.chain().focus().toggleBlockquote().run()}
301 | title="Blockquote"
302 | >
303 |
304 |
305 |
props.editor.chain().focus().toggleCodeBlock().run()}
310 | title="Code Block"
311 | >
312 |
313 |
314 |
315 |
316 | );
317 | }
318 |
319 | export default function App(): JSX.Element {
320 | const [container, setContainer] = createSignal();
321 | const [menu, setMenu] = createSignal();
322 |
323 | const editor = createTiptapEditor(() => ({
324 | element: container()!,
325 | extensions: [
326 | StarterKit,
327 | BubbleMenu.configure({
328 | element: menu()!,
329 | }),
330 | ],
331 | editorProps: {
332 | attributes: {
333 | class: 'p-8 focus:outline-none prose max-w-full',
334 | },
335 | },
336 | content: CONTENT,
337 | }));
338 |
339 | return (
340 |
341 |
342 |
347 |
348 | {instance => }
349 |
350 |
351 |
355 |
356 |
357 | );
358 | }
359 |
--------------------------------------------------------------------------------
/examples/simple-example/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { render } from 'solid-js/web';
2 | import App from './App';
3 |
4 | import './style.css';
5 |
6 | const app = document.getElementById('app');
7 |
8 | if (app) {
9 | render(() => , app);
10 | }
11 |
--------------------------------------------------------------------------------
/examples/simple-example/src/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .dynamic-shadow {
6 | position: relative;
7 | }
8 |
9 | .dynamic-shadow:after {
10 | content: '';
11 | width: 100%;
12 | height: 100%;
13 | position: absolute;
14 | background: inherit;
15 | top: 0.5rem;
16 | filter: blur(0.4rem);
17 | opacity: 0.7;
18 | z-index: -1;
19 | }
20 |
--------------------------------------------------------------------------------
/examples/simple-example/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/simple-example/tailwind.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | mode: 'jit',
3 | content: ['./src/**/*.tsx'],
4 | darkMode: 'class', // or 'media' or 'class'
5 | variants: {},
6 | plugins: [require('@tailwindcss/typography')],
7 | };
8 |
--------------------------------------------------------------------------------
/examples/simple-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "lib": ["ESNext", "DOM"],
6 | "moduleResolution": "Bundler",
7 | "strict": true,
8 | "sourceMap": true,
9 | "resolveJsonModule": true,
10 | "esModuleInterop": true,
11 | "noEmit": true,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "noImplicitReturns": true,
15 | "jsx": "preserve",
16 | "jsxImportSource": "solid-js",
17 | },
18 | "include": ["./src"]
19 | }
20 |
--------------------------------------------------------------------------------
/examples/simple-example/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import solidPlugin from 'vite-plugin-solid';
3 |
4 | export default defineConfig({
5 | optimizeDeps: {
6 | exclude: [
7 | '@tiptap/core',
8 | '@tiptap/starter-kit',
9 | '@tiptap/extension-bubble-menu',
10 | ],
11 | },
12 | plugins: [solidPlugin()],
13 | });
14 |
--------------------------------------------------------------------------------
/images/solid-tiptap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lxsmnsyc/solid-tiptap/6b7f27a7430bba6cb5594210cd06fb0bdad30e6b/images/solid-tiptap.png
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "npmClient": "pnpm",
3 | "packages": [
4 | "packages/*",
5 | "examples/*"
6 | ],
7 | "command": {
8 | "version": {
9 | "exact": true
10 | },
11 | "publish": {
12 | "allowBranch": [
13 | "main",
14 | "vdom",
15 | "no-vdom"
16 | ],
17 | "registry": "https://registry.npmjs.org/"
18 | }
19 | },
20 | "version": "0.7.0"
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "private": true,
4 | "workspaces": ["packages/*", "examples/*"],
5 | "devDependencies": {
6 | "@biomejs/biome": "^1.5.3",
7 | "lerna": "^8.0.2",
8 | "typescript": "^5.3.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/solid-tiptap/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.production
74 | .env.development
75 |
76 | # parcel-bundler cache (https://parceljs.org/)
77 | .cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 |
86 | # Gatsby files
87 | .cache/
88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
89 | # https://nextjs.org/blog/next-9-1#public-directory-support
90 | # public
91 |
92 | # vuepress build output
93 | .vuepress/dist
94 |
95 | # Serverless directories
96 | .serverless/
97 |
98 | # FuseBox cache
99 | .fusebox/
100 |
101 | # DynamoDB Local files
102 | .dynamodb/
103 |
104 | # TernJS port file
105 | .tern-port
106 |
107 | .npmrc
108 |
--------------------------------------------------------------------------------
/packages/solid-tiptap/README.md:
--------------------------------------------------------------------------------
1 | # solid-tiptap
2 |
3 | > SolidJS bindings for [tiptap](https://www.tiptap.dev/)
4 |
5 | [](https://www.npmjs.com/package/solid-tiptap) [](https://github.com/airbnb/javascript) [](https://stackblitz.com/github/lxsmnsyc/solid-tiptap/tree/main/examples/simple-example)
6 |
7 | 
8 |
9 | ## Install
10 |
11 | ```bash
12 | npm i @tiptap/core @tiptap/pm solid-tiptap
13 | ```
14 |
15 | ```bash
16 | yarn add @tiptap/core @tiptap/pm solid-tiptap
17 | ```
18 |
19 | ```bash
20 | pnpm add @tiptap/core @tiptap/pm solid-tiptap
21 | ```
22 |
23 | ## Usage
24 |
25 | ```jsx
26 | import { createTiptapEditor } from 'solid-tiptap';
27 | import StarterKit from '@tiptap/starter-kit';
28 |
29 | function App() {
30 | let ref!: HTMLDivElement;
31 |
32 | const editor = createTiptapEditor(() => ({
33 | element: ref!,
34 | extensions: [
35 | StarterKit,
36 | ],
37 | content: `Example Text
`,
38 | }));
39 |
40 | return
;
41 | }
42 | ```
43 |
44 | ### Transactions
45 |
46 | `createEditorTransaction` provides a way to reactively subscribe to editor changes.
47 |
48 | ```ts
49 | const isBold = createEditorTransaction(
50 | () => props.editor, // Editor instance from createTiptapEditor
51 | (editor) => editor.isActive('bold'),
52 | );
53 |
54 | createEffect(() => {
55 | if (isBold()) {
56 | // do something
57 | }
58 | });
59 | ```
60 |
61 | There are out-of-the-box utilities that wraps `createEditorTransaction` based on the [Editor API](https://www.tiptap.dev/api/editor):
62 |
63 | - `useEditorCharacterCount`: reactively subscribe to `getCharacterCount`.
64 |
65 | ```js
66 | const count = useEditorCharacterCount(() => props.editor);
67 |
68 | createEffect(() => {
69 | console.log('Character Count:', count());
70 | });
71 | ```
72 |
73 | - `useEditorHTML`: reactively subscribe to `getEditorHTML`.
74 |
75 | ```js
76 | const html = useEditorHTML(() => props.editor);
77 |
78 | createEffect(() => {
79 | updateHTML(html());
80 | });
81 | ```
82 |
83 | - `useEditorIsActive`: reactively subscribe to `isActive`.
84 |
85 | ```js
86 | const isHeading = useEditorIsActive(() => props.editor, () => 'heading', {
87 | level: 1,
88 | });
89 |
90 | createEffect(() => {
91 | if (isHeading()) {
92 | // do something
93 | }
94 | });
95 | ```
96 |
97 | - `useEditorIsEditable`: reactively subscribe to `isEditable`.
98 |
99 | ```js
100 | const isEditable = useEditorIsEditable(() => props.editor);
101 |
102 | createEffect(() => {
103 | if (isEditable()) {
104 | // do something
105 | }
106 | });
107 | ```
108 |
109 | - `useEditorIsEmpty`: reactively subscribe to `isEmpty`.
110 |
111 | ```js
112 | const isEmpty = useEditorIsEmpty(() => props.editor);
113 |
114 | createEffect(() => {
115 | if (isEmpty()) {
116 | // do something
117 | }
118 | });
119 | ```
120 |
121 | - `useEditorIsFocused`: reactively subscribe to `isFocused`.
122 |
123 | ```js
124 | const isFocused = useEditorIsFocused(() => props.editor);
125 |
126 | createEffect(() => {
127 | if (isFocused()) {
128 | // do something
129 | }
130 | });
131 | ```
132 |
133 | - `useEditorJSON`: reactively subscribe to `getJSON`.
134 |
135 | ```js
136 | const json = useEditorJSON(() => props.editor);
137 |
138 | createEffect(() => {
139 | const data = json();
140 |
141 | if (data) {
142 | uploadJSON(data);
143 | }
144 | });
145 | ```
146 |
147 | ## Tips
148 |
149 | Since `createTiptapEditor` may return `undefined` (the instanciation is scheduled), and you're planning to pass it to another component (e.g. creating `BubbleMenu` or Toolbar), it is recommended to use ternary evaluation (e.g. ``) to check if the editor is `undefined` or not.
150 |
151 | ```js
152 | const editor = createTiptapEditor(() => ({ ... }));
153 |
154 |
155 | {(instance) => }
156 |
157 | ```
158 |
159 | ## Sponsors
160 |
161 | 
162 |
163 | ## License
164 |
165 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc)
166 |
--------------------------------------------------------------------------------
/packages/solid-tiptap/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.7.0",
3 | "type": "module",
4 | "types": "./dist/types/index.d.ts",
5 | "main": "./dist/cjs/production/index.cjs",
6 | "module": "./dist/esm/production/index.mjs",
7 | "exports": {
8 | ".": {
9 | "development": {
10 | "require": "./dist/cjs/development/index.cjs",
11 | "import": "./dist/esm/development/index.mjs"
12 | },
13 | "require": "./dist/cjs/production/index.cjs",
14 | "import": "./dist/esm/production/index.mjs",
15 | "types": "./dist/types/index.d.ts"
16 | }
17 | },
18 | "files": [
19 | "dist",
20 | "src"
21 | ],
22 | "engines": {
23 | "node": ">=10"
24 | },
25 | "license": "MIT",
26 | "keywords": [
27 | "pridepack"
28 | ],
29 | "name": "solid-tiptap",
30 | "devDependencies": {
31 | "@tiptap/core": "^2.1.16",
32 | "@tiptap/pm": "^2.1.16",
33 | "@types/node": "^20.11.8",
34 | "pridepack": "2.6.0",
35 | "solid-js": "^1.8.12",
36 | "tslib": "^2.6.0",
37 | "typescript": "^5.3.3"
38 | },
39 | "peerDependencies": {
40 | "@tiptap/core": "^2",
41 | "@tiptap/pm": "^2",
42 | "solid-js": "^1.7"
43 | },
44 | "scripts": {
45 | "prepublishOnly": "pridepack clean && pridepack build",
46 | "build": "pridepack build",
47 | "type-check": "pridepack check",
48 | "clean": "pridepack clean"
49 | },
50 | "description": "SolidJS bindings for Tiptap",
51 | "repository": {
52 | "url": "https://github.com/lxsmnsyc/solid-tiptap.git",
53 | "type": "git"
54 | },
55 | "homepage": "https://github.com/lxsmnsyc/solid-tiptap/tree/main/packages/solid-tiptap",
56 | "bugs": {
57 | "url": "https://github.com/lxsmnsyc/solid-tiptap/issues"
58 | },
59 | "publishConfig": {
60 | "access": "public"
61 | },
62 | "author": "Alexis Munsayac",
63 | "private": false,
64 | "typesVersions": {
65 | "*": {}
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/solid-tiptap/pridepack.json:
--------------------------------------------------------------------------------
1 | {
2 | "target": "es2018"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/solid-tiptap/src/Editor.tsx:
--------------------------------------------------------------------------------
1 | import type { EditorOptions } from '@tiptap/core';
2 | import { Editor } from '@tiptap/core';
3 | import { createEffect, createSignal, onCleanup } from 'solid-js';
4 |
5 | export type EditorRef = Editor | ((editor: Editor) => void);
6 |
7 | export type BaseEditorOptions = Omit, 'element'>;
8 |
9 | export interface UseEditorOptions
10 | extends BaseEditorOptions {
11 | element: T;
12 | }
13 |
14 | export function createEditorTransaction(
15 | instance: () => V,
16 | read: (value: V) => T,
17 | ): () => T {
18 | const [depend, update] = createSignal(undefined, { equals: false });
19 |
20 | function forceUpdate() {
21 | update();
22 | }
23 |
24 | createEffect(() => {
25 | const editor = instance();
26 | if (editor) {
27 | editor.on('transaction', forceUpdate);
28 | onCleanup(() => {
29 | editor.off('transaction', forceUpdate);
30 | });
31 | }
32 | });
33 |
34 | return () => {
35 | depend();
36 | return read(instance());
37 | };
38 | }
39 |
40 | export default function useEditor(
41 | props: () => UseEditorOptions,
42 | ): () => Editor | undefined {
43 | const [signal, setSignal] = createSignal();
44 |
45 | createEffect(() => {
46 | const instance = new Editor({
47 | ...props(),
48 | });
49 |
50 | onCleanup(() => {
51 | instance.destroy();
52 | });
53 |
54 | setSignal(instance);
55 | });
56 |
57 | return signal;
58 | }
59 |
60 | export function useEditorHTML(
61 | editor: () => V,
62 | ): () => string | undefined {
63 | return createEditorTransaction(editor, instance => instance?.getHTML());
64 | }
65 |
66 | export function useEditorJSON<
67 | V extends Editor | undefined,
68 | R extends Record,
69 | >(editor: () => V): () => R | undefined {
70 | return createEditorTransaction(editor, instance => instance?.getJSON() as R);
71 | }
72 |
73 | export function useEditorIsActive<
74 | V extends Editor | undefined,
75 | R extends Record,
76 | >(
77 | editor: () => V,
78 | ...args: [name: () => string, options?: R] | [options: R]
79 | ): () => boolean | undefined {
80 | return createEditorTransaction(editor, instance => {
81 | if (args.length === 2) {
82 | return instance?.isActive(args[0](), args[1]);
83 | }
84 | return instance?.isActive(args[0]);
85 | });
86 | }
87 |
88 | export function useEditorIsEmpty(
89 | editor: () => V,
90 | ): () => boolean | undefined {
91 | return createEditorTransaction(editor, instance => instance?.isEmpty);
92 | }
93 |
94 | export function useEditorIsEditable(
95 | editor: () => V,
96 | ): () => boolean | undefined {
97 | return createEditorTransaction(editor, instance => instance?.isEditable);
98 | }
99 |
100 | export function useEditorIsFocused(
101 | editor: () => V,
102 | ): () => boolean | undefined {
103 | return createEditorTransaction(editor, instance => instance?.isFocused);
104 | }
105 |
--------------------------------------------------------------------------------
/packages/solid-tiptap/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | default as createEditor,
3 | createEditorTransaction,
4 | default as createTiptapEditor,
5 | default as useEditor,
6 | useEditorHTML,
7 | useEditorIsActive,
8 | useEditorIsEditable,
9 | useEditorIsEmpty,
10 | useEditorIsFocused,
11 | useEditorJSON,
12 | default as useTiptapEditor,
13 | } from './Editor';
14 | export type {
15 | EditorRef,
16 | UseEditorOptions,
17 | } from './Editor';
18 |
--------------------------------------------------------------------------------
/packages/solid-tiptap/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": [
3 | "node_modules"
4 | ],
5 | "include": [
6 | "src",
7 | "types"
8 | ],
9 | "compilerOptions": {
10 | "module": "ESNext",
11 | "lib": [
12 | "ESNext",
13 | "DOM"
14 | ],
15 | "importHelpers": true,
16 | "declaration": true,
17 | "sourceMap": true,
18 | "rootDir": "./src",
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noImplicitReturns": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "moduleResolution": "Bundler",
25 | "jsx": "preserve",
26 | "jsxImportSource": "solid-js",
27 | "esModuleInterop": true,
28 | "target": "ES2017",
29 | "useDefineForClassFields": false,
30 | "declarationMap": true
31 | }
32 | }
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/**/*'
3 | - 'examples/**/*'
--------------------------------------------------------------------------------