dn-react-text-editor 0.1.0 → 0.1.2

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 CHANGED
@@ -1,222 +1,3 @@
1
- # React Store Input
1
+ # React Text Editor
2
2
 
3
- The goal of this package is to make state management easier when using input elements in React.
4
-
5
- It eliminates repetitive code required to implement state changes and subscriptions for input elements, and provides a simple interface.
6
-
7
- At the same time, it allows you to use all the attributes originally provided by the input tag as-is, without needing to learn this package.
8
-
9
- ## Get Started
10
-
11
- This is a simple example of how to use this package.
12
-
13
- ```tsx
14
- import { useFormStore } from "dn-react-input";
15
-
16
- export default function App() {
17
- const store = useFormStore({
18
- email: "",
19
- password: "",
20
- });
21
-
22
- const submit = async () => {
23
- const { email, password } = store.state;
24
-
25
- alert(`Email: ${email}\nPassword: ${password}`);
26
- };
27
-
28
- return (
29
- <form
30
- onSubmit={(e) => {
31
- e.preventDefault();
32
- submit();
33
- }}
34
- >
35
- <store.input name="email" type="email" />
36
- <store.input name="password" type="password" />
37
- <button type="submit">Submit</button>
38
- </form>
39
- );
40
- }
41
- ```
42
-
43
- ## How to define state?
44
-
45
- You can define any state you want as an object when calling `useStore`.
46
-
47
- ```tsx
48
- function Component() {
49
- ...
50
-
51
- const store = useStore({
52
- email: "",
53
- password: "",
54
- rememberMe: false,
55
- });
56
-
57
- ...
58
- }
59
- ```
60
-
61
- It's a single source of truth for your form state.
62
-
63
- ## How to get input values?
64
-
65
- You can access the current values of the input elements through the `state` property of the store.
66
-
67
- ```tsx
68
- function Component() {
69
- ...
70
-
71
- const submit = () => {
72
- const { email, password, rememberMe } = store.state;
73
- };
74
-
75
- ...
76
- }
77
- ```
78
-
79
- ## How to add input elements?
80
-
81
- You can add input elements using the `Input` component provided by the store. There are 'Select' and 'Textarea' components as well.
82
-
83
- ```tsx
84
- import { Input } from "dn-react-input";
85
-
86
- function Component() {
87
- ...
88
-
89
- return (
90
- <form>
91
- <Input store={store} name="email" type="email" />
92
- <Input store={store} name="password" type="password" />
93
- <Input store={store} name="rememberMe" type="checkbox" />
94
- </form>
95
- );
96
- }
97
- ```
98
-
99
- If you want to avoid passing the store to each input component, use `useStoreInput`. This hook provides input components that are already connected to the store.
100
-
101
- ```tsx
102
- import { useStoreInput } from "dn-react-input";
103
-
104
- function Component() {
105
- ...
106
- const Input = useStoreInput(store);
107
-
108
- return (
109
- <form>
110
- <Input.input name="email" type="email" />
111
- <Input.input name="password" type="password" />
112
- <Input.input name="rememberMe" type="checkbox" />
113
- </form>
114
- );
115
- }
116
- ```
117
-
118
- `useFormStore` is a facade that combines `useStore` and `useStoreInput` for convenience.
119
-
120
- ```tsx
121
- import { useFormStore } from "dn-react-input";
122
-
123
- function Component() {
124
- ...
125
- const store = useFormStore({
126
- email: "",
127
- password: "",
128
- rememberMe: false,
129
- });
130
-
131
- return (
132
- <form>
133
- <store.input name="email" type="email" />
134
- <store.input name="password" type="password" />
135
- <store.input name="rememberMe" type="checkbox" />
136
- </form>
137
- );
138
- }
139
- ```
140
-
141
- ## How to render components on state changes?
142
-
143
- If you want to render a component only when specific parts of the state change, use the `useSelector` hook.
144
-
145
- ```tsx
146
- import { useSelector } from "dn-react-input";
147
-
148
- function Component() {
149
- ...
150
- const email = useSelector(store, (state) => state.email);
151
-
152
- return <div>Your email is: {email}</div>;
153
- }
154
- ```
155
-
156
- If you want to render components in an inline manner, use the `createRender` function. By using this, you can avoid creating separate components for each part of the state you want to track.
157
-
158
- ```tsx
159
- import { createRender } from "dn-react-input";
160
-
161
- function Component() {
162
- ...
163
- return (
164
- <div>
165
- {createRender(store, (state) => <p>{state.email}</p>)}
166
- {createRender(store, (state) => <p>{state.password}</p>)}
167
- </div>
168
- );
169
- }
170
- ```
171
-
172
- ## How to subscribe to state changes?
173
-
174
- You can subscribe to state changes using the `subscribe` method of the store.
175
-
176
- ```tsx
177
- function Component() {
178
- ...
179
- useEffect(() => {
180
- const unsubscribe = store.subscribe((state) => {
181
- console.log(`State changed`, state);
182
- });
183
-
184
- return () => {
185
- unsubscribe();
186
- };
187
- }, []);
188
-
189
- ...
190
- }
191
- ```
192
-
193
- ## How to update state manually?
194
-
195
- You can update the state manually using the `dispatch` method of the store.
196
-
197
- ```tsx
198
- function Component() {
199
- ...
200
- const updateEmail = () => {
201
- store.dispatch({ email: "ohjinsu98@icloud.com" });
202
- };
203
-
204
- return <button onClick={updateEmail}>Update Email</button>;
205
- }
206
- ```
207
-
208
- The `dispatch` method uses immerjs internally to update the state, so you can also use a function to update the state based on the previous state.
209
-
210
- ```tsx
211
- function Component() {
212
- ...
213
-
214
- const updateEmail = () => {
215
- store.dispatch((state) => {
216
- state.email = "ohjinsu98@icloud.com";
217
- });
218
- };
219
-
220
- return <button onClick={updateEmail}>Update Email</button>;
221
- }
222
- ```
3
+ A rich text editor component for React built on ProseMirror.
@@ -23,6 +23,10 @@ type AttachFileOptions = {
23
23
  alt?: string;
24
24
  };
25
25
  };
26
+ declare const base64ImageUploader: (file: File) => Promise<{
27
+ src: string;
28
+ alt: string;
29
+ }>;
26
30
  declare function createAttachFile({ schema, generateMetadata, uploadFile, }: AttachFileOptions): AttachFile;
27
31
 
28
- export { type AttachFile, createAttachFile };
32
+ export { type AttachFile, base64ImageUploader, createAttachFile };
@@ -23,6 +23,10 @@ type AttachFileOptions = {
23
23
  alt?: string;
24
24
  };
25
25
  };
26
+ declare const base64ImageUploader: (file: File) => Promise<{
27
+ src: string;
28
+ alt: string;
29
+ }>;
26
30
  declare function createAttachFile({ schema, generateMetadata, uploadFile, }: AttachFileOptions): AttachFile;
27
31
 
28
- export { type AttachFile, createAttachFile };
32
+ export { type AttachFile, base64ImageUploader, createAttachFile };
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/attach_file.tsx
21
21
  var attach_file_exports = {};
22
22
  __export(attach_file_exports, {
23
+ base64ImageUploader: () => base64ImageUploader,
23
24
  createAttachFile: () => createAttachFile
24
25
  });
25
26
  module.exports = __toCommonJS(attach_file_exports);
@@ -91,15 +92,24 @@ var findPlaceholder = (state, id) => {
91
92
  };
92
93
 
93
94
  // src/attach_file.tsx
95
+ var base64ImageUploader = async (file) => {
96
+ const base64 = await new Promise((resolve, reject) => {
97
+ const reader = new FileReader();
98
+ reader.onload = () => {
99
+ resolve(reader.result);
100
+ };
101
+ reader.onerror = reject;
102
+ reader.readAsDataURL(file);
103
+ });
104
+ return {
105
+ src: base64,
106
+ alt: file.name
107
+ };
108
+ };
94
109
  function createAttachFile({
95
110
  schema,
96
111
  generateMetadata,
97
- uploadFile = (file) => {
98
- return {
99
- src: URL.createObjectURL(file),
100
- alt: file.name
101
- };
102
- }
112
+ uploadFile = base64ImageUploader
103
113
  }) {
104
114
  const attachEachFile = async (view, file, pos) => {
105
115
  const metadata = generateMetadata ? await generateMetadata(file) : {};
@@ -151,9 +161,7 @@ function createAttachFile({
151
161
  }
152
162
  view.dispatch(tr2.replaceWith($pos, $pos, node));
153
163
  } catch (e) {
154
- view.dispatch(
155
- tr.setMeta(uploadPlaceholderPlugin, { remove: { id } })
156
- );
164
+ view.dispatch(tr.setMeta(uploadPlaceholderPlugin, { remove: { id } }));
157
165
  }
158
166
  };
159
167
  return async (view, files, pos) => {
@@ -165,5 +173,6 @@ function createAttachFile({
165
173
  }
166
174
  // Annotate the CommonJS export names for ESM import in node:
167
175
  0 && (module.exports = {
176
+ base64ImageUploader,
168
177
  createAttachFile
169
178
  });
@@ -67,15 +67,24 @@ var findPlaceholder = (state, id) => {
67
67
  };
68
68
 
69
69
  // src/attach_file.tsx
70
+ var base64ImageUploader = async (file) => {
71
+ const base64 = await new Promise((resolve, reject) => {
72
+ const reader = new FileReader();
73
+ reader.onload = () => {
74
+ resolve(reader.result);
75
+ };
76
+ reader.onerror = reject;
77
+ reader.readAsDataURL(file);
78
+ });
79
+ return {
80
+ src: base64,
81
+ alt: file.name
82
+ };
83
+ };
70
84
  function createAttachFile({
71
85
  schema,
72
86
  generateMetadata,
73
- uploadFile = (file) => {
74
- return {
75
- src: URL.createObjectURL(file),
76
- alt: file.name
77
- };
78
- }
87
+ uploadFile = base64ImageUploader
79
88
  }) {
80
89
  const attachEachFile = async (view, file, pos) => {
81
90
  const metadata = generateMetadata ? await generateMetadata(file) : {};
@@ -127,9 +136,7 @@ function createAttachFile({
127
136
  }
128
137
  view.dispatch(tr2.replaceWith($pos, $pos, node));
129
138
  } catch (e) {
130
- view.dispatch(
131
- tr.setMeta(uploadPlaceholderPlugin, { remove: { id } })
132
- );
139
+ view.dispatch(tr.setMeta(uploadPlaceholderPlugin, { remove: { id } }));
133
140
  }
134
141
  };
135
142
  return async (view, files, pos) => {
@@ -140,5 +147,6 @@ function createAttachFile({
140
147
  };
141
148
  }
142
149
  export {
150
+ base64ImageUploader,
143
151
  createAttachFile
144
152
  };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { TextEditorController, TextEditorProps, createTextEditor } from './text_editor.mjs';
2
2
  export { createSchema } from './schema.mjs';
3
- export { AttachFile, createAttachFile } from './attach_file.mjs';
3
+ export { AttachFile, base64ImageUploader, createAttachFile } from './attach_file.mjs';
4
4
  import 'orderedmap';
5
5
  import 'prosemirror-model';
6
6
  import 'react';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { TextEditorController, TextEditorProps, createTextEditor } from './text_editor.js';
2
2
  export { createSchema } from './schema.js';
3
- export { AttachFile, createAttachFile } from './attach_file.js';
3
+ export { AttachFile, base64ImageUploader, createAttachFile } from './attach_file.js';
4
4
  import 'orderedmap';
5
5
  import 'prosemirror-model';
6
6
  import 'react';
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ base64ImageUploader: () => base64ImageUploader,
33
34
  createAttachFile: () => createAttachFile,
34
35
  createSchema: () => createSchema,
35
36
  createTextEditor: () => createTextEditor
@@ -472,15 +473,24 @@ function cn(...classes) {
472
473
 
473
474
  // src/attach_file.tsx
474
475
  var import_prosemirror_view3 = require("prosemirror-view");
476
+ var base64ImageUploader = async (file) => {
477
+ const base64 = await new Promise((resolve, reject) => {
478
+ const reader = new FileReader();
479
+ reader.onload = () => {
480
+ resolve(reader.result);
481
+ };
482
+ reader.onerror = reject;
483
+ reader.readAsDataURL(file);
484
+ });
485
+ return {
486
+ src: base64,
487
+ alt: file.name
488
+ };
489
+ };
475
490
  function createAttachFile({
476
491
  schema,
477
492
  generateMetadata,
478
- uploadFile = (file) => {
479
- return {
480
- src: URL.createObjectURL(file),
481
- alt: file.name
482
- };
483
- }
493
+ uploadFile = base64ImageUploader
484
494
  }) {
485
495
  const attachEachFile = async (view, file, pos) => {
486
496
  const metadata = generateMetadata ? await generateMetadata(file) : {};
@@ -532,9 +542,7 @@ function createAttachFile({
532
542
  }
533
543
  view.dispatch(tr2.replaceWith($pos, $pos, node));
534
544
  } catch (e) {
535
- view.dispatch(
536
- tr.setMeta(uploadPlaceholderPlugin, { remove: { id } })
537
- );
545
+ view.dispatch(tr.setMeta(uploadPlaceholderPlugin, { remove: { id } }));
538
546
  }
539
547
  };
540
548
  return async (view, files, pos) => {
@@ -573,12 +581,9 @@ function createTextEditor(options = {}) {
573
581
  } = {}) {
574
582
  const containerRef = (0, import_react2.useRef)(null);
575
583
  const inputRef = (0, import_react2.useRef)(null);
576
- (0, import_react2.useEffect)(() => {
577
- const element = containerRef.current;
578
- if (!element) {
579
- return;
580
- }
581
- const subject = new import_rxjs.Subject();
584
+ const controllerRef = (0, import_react2.useRef)(null);
585
+ const subject = (0, import_react.useMemo)(() => new import_rxjs.Subject(), []);
586
+ (0, import_react.useImperativeHandle)(ref, () => {
582
587
  const wrapper = document.createElement("div");
583
588
  const toInnerHTML = (value) => {
584
589
  if (mode === "html") {
@@ -586,10 +591,8 @@ function createTextEditor(options = {}) {
586
591
  }
587
592
  return value.split("\n").map((line) => `<p>${line}</p>`).join("");
588
593
  };
589
- wrapper.innerHTML = toInnerHTML(
590
- defaultValue ? String(defaultValue) : ""
591
- );
592
- const view = new import_prosemirror_view4.EditorView(element, {
594
+ wrapper.innerHTML = toInnerHTML(defaultValue ? String(defaultValue) : "");
595
+ const view = new import_prosemirror_view4.EditorView(containerRef.current, {
593
596
  ...editor,
594
597
  attributes: (state2) => {
595
598
  const propsAttributes = (() => {
@@ -664,27 +667,11 @@ function createTextEditor(options = {}) {
664
667
  const state2 = view.state;
665
668
  return state2.doc.textBetween(0, state2.doc.content.size, "\n");
666
669
  }
667
- const sub = subject.pipe(
668
- (0, import_rxjs.filter)((tr) => tr.docChanged),
669
- (0, import_rxjs.debounceTime)(updateDelay)
670
- ).subscribe(() => {
671
- if (inputRef.current) {
672
- switch (mode) {
673
- case "text":
674
- inputRef.current.value = toTextContent();
675
- break;
676
- default:
677
- inputRef.current.value = toHTML();
678
- break;
679
- }
680
- const event = new Event("input", { bubbles: true });
681
- inputRef.current.dispatchEvent(event);
682
- }
683
- });
684
670
  if (autoFocus) {
685
671
  view.focus();
686
672
  }
687
673
  const textEditorController = {
674
+ schema,
688
675
  view,
689
676
  subject,
690
677
  set value(value) {
@@ -700,26 +687,33 @@ function createTextEditor(options = {}) {
700
687
  },
701
688
  clear
702
689
  };
703
- if (typeof ref === "function") {
704
- ref(textEditorController);
705
- } else if (ref) {
706
- ref.current = textEditorController;
690
+ controllerRef.current = textEditorController;
691
+ return textEditorController;
692
+ });
693
+ (0, import_react2.useEffect)(() => {
694
+ const controller = controllerRef.current;
695
+ if (!controller) {
696
+ return;
697
+ }
698
+ const sub = controller.subject.pipe(
699
+ (0, import_rxjs.filter)((tr) => tr.docChanged),
700
+ (0, import_rxjs.debounceTime)(updateDelay)
701
+ ).subscribe(() => {
702
+ if (inputRef.current) {
703
+ inputRef.current.value = controller.value;
704
+ const event = new Event("input", { bubbles: true });
705
+ inputRef.current.dispatchEvent(event);
706
+ }
707
+ });
708
+ if (autoFocus) {
709
+ controller.view.focus();
707
710
  }
708
711
  return () => {
709
712
  sub.unsubscribe();
710
- view.destroy();
711
- element.innerHTML = "";
713
+ controller.view.destroy();
712
714
  };
713
715
  }, []);
714
- return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("div", { ref: containerRef, className: container, ...props }), /* @__PURE__ */ import_react.default.createElement(
715
- "input",
716
- {
717
- ref: inputRef,
718
- type: "hidden",
719
- name,
720
- onInput: onChange
721
- }
722
- ));
716
+ return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("div", { ref: containerRef, className: container, ...props }), /* @__PURE__ */ import_react.default.createElement("input", { ref: inputRef, type: "hidden", name, onInput: onChange }));
723
717
  }
724
718
  return {
725
719
  schema,
@@ -729,6 +723,7 @@ function createTextEditor(options = {}) {
729
723
  }
730
724
  // Annotate the CommonJS export names for ESM import in node:
731
725
  0 && (module.exports = {
726
+ base64ImageUploader,
732
727
  createAttachFile,
733
728
  createSchema,
734
729
  createTextEditor
package/dist/index.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  // src/text_editor.tsx
2
- import React from "react";
2
+ import React, {
3
+ useImperativeHandle,
4
+ useMemo
5
+ } from "react";
3
6
  import {
4
7
  EditorState as EditorState2
5
8
  } from "prosemirror-state";
@@ -436,15 +439,24 @@ function cn(...classes) {
436
439
 
437
440
  // src/attach_file.tsx
438
441
  import "prosemirror-view";
442
+ var base64ImageUploader = async (file) => {
443
+ const base64 = await new Promise((resolve, reject) => {
444
+ const reader = new FileReader();
445
+ reader.onload = () => {
446
+ resolve(reader.result);
447
+ };
448
+ reader.onerror = reject;
449
+ reader.readAsDataURL(file);
450
+ });
451
+ return {
452
+ src: base64,
453
+ alt: file.name
454
+ };
455
+ };
439
456
  function createAttachFile({
440
457
  schema,
441
458
  generateMetadata,
442
- uploadFile = (file) => {
443
- return {
444
- src: URL.createObjectURL(file),
445
- alt: file.name
446
- };
447
- }
459
+ uploadFile = base64ImageUploader
448
460
  }) {
449
461
  const attachEachFile = async (view, file, pos) => {
450
462
  const metadata = generateMetadata ? await generateMetadata(file) : {};
@@ -496,9 +508,7 @@ function createAttachFile({
496
508
  }
497
509
  view.dispatch(tr2.replaceWith($pos, $pos, node));
498
510
  } catch (e) {
499
- view.dispatch(
500
- tr.setMeta(uploadPlaceholderPlugin, { remove: { id } })
501
- );
511
+ view.dispatch(tr.setMeta(uploadPlaceholderPlugin, { remove: { id } }));
502
512
  }
503
513
  };
504
514
  return async (view, files, pos) => {
@@ -537,12 +547,9 @@ function createTextEditor(options = {}) {
537
547
  } = {}) {
538
548
  const containerRef = useRef(null);
539
549
  const inputRef = useRef(null);
540
- useEffect(() => {
541
- const element = containerRef.current;
542
- if (!element) {
543
- return;
544
- }
545
- const subject = new Subject();
550
+ const controllerRef = useRef(null);
551
+ const subject = useMemo(() => new Subject(), []);
552
+ useImperativeHandle(ref, () => {
546
553
  const wrapper = document.createElement("div");
547
554
  const toInnerHTML = (value) => {
548
555
  if (mode === "html") {
@@ -550,10 +557,8 @@ function createTextEditor(options = {}) {
550
557
  }
551
558
  return value.split("\n").map((line) => `<p>${line}</p>`).join("");
552
559
  };
553
- wrapper.innerHTML = toInnerHTML(
554
- defaultValue ? String(defaultValue) : ""
555
- );
556
- const view = new EditorView3(element, {
560
+ wrapper.innerHTML = toInnerHTML(defaultValue ? String(defaultValue) : "");
561
+ const view = new EditorView3(containerRef.current, {
557
562
  ...editor,
558
563
  attributes: (state2) => {
559
564
  const propsAttributes = (() => {
@@ -628,27 +633,11 @@ function createTextEditor(options = {}) {
628
633
  const state2 = view.state;
629
634
  return state2.doc.textBetween(0, state2.doc.content.size, "\n");
630
635
  }
631
- const sub = subject.pipe(
632
- filter((tr) => tr.docChanged),
633
- debounceTime(updateDelay)
634
- ).subscribe(() => {
635
- if (inputRef.current) {
636
- switch (mode) {
637
- case "text":
638
- inputRef.current.value = toTextContent();
639
- break;
640
- default:
641
- inputRef.current.value = toHTML();
642
- break;
643
- }
644
- const event = new Event("input", { bubbles: true });
645
- inputRef.current.dispatchEvent(event);
646
- }
647
- });
648
636
  if (autoFocus) {
649
637
  view.focus();
650
638
  }
651
639
  const textEditorController = {
640
+ schema,
652
641
  view,
653
642
  subject,
654
643
  set value(value) {
@@ -664,26 +653,33 @@ function createTextEditor(options = {}) {
664
653
  },
665
654
  clear
666
655
  };
667
- if (typeof ref === "function") {
668
- ref(textEditorController);
669
- } else if (ref) {
670
- ref.current = textEditorController;
656
+ controllerRef.current = textEditorController;
657
+ return textEditorController;
658
+ });
659
+ useEffect(() => {
660
+ const controller = controllerRef.current;
661
+ if (!controller) {
662
+ return;
663
+ }
664
+ const sub = controller.subject.pipe(
665
+ filter((tr) => tr.docChanged),
666
+ debounceTime(updateDelay)
667
+ ).subscribe(() => {
668
+ if (inputRef.current) {
669
+ inputRef.current.value = controller.value;
670
+ const event = new Event("input", { bubbles: true });
671
+ inputRef.current.dispatchEvent(event);
672
+ }
673
+ });
674
+ if (autoFocus) {
675
+ controller.view.focus();
671
676
  }
672
677
  return () => {
673
678
  sub.unsubscribe();
674
- view.destroy();
675
- element.innerHTML = "";
679
+ controller.view.destroy();
676
680
  };
677
681
  }, []);
678
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { ref: containerRef, className: container, ...props }), /* @__PURE__ */ React.createElement(
679
- "input",
680
- {
681
- ref: inputRef,
682
- type: "hidden",
683
- name,
684
- onInput: onChange
685
- }
686
- ));
682
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { ref: containerRef, className: container, ...props }), /* @__PURE__ */ React.createElement("input", { ref: inputRef, type: "hidden", name, onInput: onChange }));
687
683
  }
688
684
  return {
689
685
  schema,
@@ -692,6 +688,7 @@ function createTextEditor(options = {}) {
692
688
  };
693
689
  }
694
690
  export {
691
+ base64ImageUploader,
695
692
  createAttachFile,
696
693
  createSchema,
697
694
  createTextEditor
@@ -4,10 +4,11 @@ import * as prosemirror_model from 'prosemirror-model';
4
4
  import React, { DetailedHTMLProps, InputHTMLAttributes, Ref } from 'react';
5
5
  import { Transaction, EditorStateConfig } from 'prosemirror-state';
6
6
  import { EditorView, DirectEditorProps } from 'prosemirror-view';
7
+ import { createSchema } from './schema.mjs';
7
8
  import { Subject } from 'rxjs';
8
- import './schema.mjs';
9
9
 
10
10
  type TextEditorController = {
11
+ schema: ReturnType<typeof createSchema>;
11
12
  view: EditorView;
12
13
  subject: Subject<Transaction>;
13
14
  set value(value: string);
@@ -4,10 +4,11 @@ import * as prosemirror_model from 'prosemirror-model';
4
4
  import React, { DetailedHTMLProps, InputHTMLAttributes, Ref } from 'react';
5
5
  import { Transaction, EditorStateConfig } from 'prosemirror-state';
6
6
  import { EditorView, DirectEditorProps } from 'prosemirror-view';
7
+ import { createSchema } from './schema.js';
7
8
  import { Subject } from 'rxjs';
8
- import './schema.js';
9
9
 
10
10
  type TextEditorController = {
11
+ schema: ReturnType<typeof createSchema>;
11
12
  view: EditorView;
12
13
  subject: Subject<Transaction>;
13
14
  set value(value: string);
@@ -468,15 +468,24 @@ function cn(...classes) {
468
468
 
469
469
  // src/attach_file.tsx
470
470
  var import_prosemirror_view3 = require("prosemirror-view");
471
+ var base64ImageUploader = async (file) => {
472
+ const base64 = await new Promise((resolve, reject) => {
473
+ const reader = new FileReader();
474
+ reader.onload = () => {
475
+ resolve(reader.result);
476
+ };
477
+ reader.onerror = reject;
478
+ reader.readAsDataURL(file);
479
+ });
480
+ return {
481
+ src: base64,
482
+ alt: file.name
483
+ };
484
+ };
471
485
  function createAttachFile({
472
486
  schema,
473
487
  generateMetadata,
474
- uploadFile = (file) => {
475
- return {
476
- src: URL.createObjectURL(file),
477
- alt: file.name
478
- };
479
- }
488
+ uploadFile = base64ImageUploader
480
489
  }) {
481
490
  const attachEachFile = async (view, file, pos) => {
482
491
  const metadata = generateMetadata ? await generateMetadata(file) : {};
@@ -528,9 +537,7 @@ function createAttachFile({
528
537
  }
529
538
  view.dispatch(tr2.replaceWith($pos, $pos, node));
530
539
  } catch (e) {
531
- view.dispatch(
532
- tr.setMeta(uploadPlaceholderPlugin, { remove: { id } })
533
- );
540
+ view.dispatch(tr.setMeta(uploadPlaceholderPlugin, { remove: { id } }));
534
541
  }
535
542
  };
536
543
  return async (view, files, pos) => {
@@ -569,12 +576,9 @@ function createTextEditor(options = {}) {
569
576
  } = {}) {
570
577
  const containerRef = (0, import_react2.useRef)(null);
571
578
  const inputRef = (0, import_react2.useRef)(null);
572
- (0, import_react2.useEffect)(() => {
573
- const element = containerRef.current;
574
- if (!element) {
575
- return;
576
- }
577
- const subject = new import_rxjs.Subject();
579
+ const controllerRef = (0, import_react2.useRef)(null);
580
+ const subject = (0, import_react.useMemo)(() => new import_rxjs.Subject(), []);
581
+ (0, import_react.useImperativeHandle)(ref, () => {
578
582
  const wrapper = document.createElement("div");
579
583
  const toInnerHTML = (value) => {
580
584
  if (mode === "html") {
@@ -582,10 +586,8 @@ function createTextEditor(options = {}) {
582
586
  }
583
587
  return value.split("\n").map((line) => `<p>${line}</p>`).join("");
584
588
  };
585
- wrapper.innerHTML = toInnerHTML(
586
- defaultValue ? String(defaultValue) : ""
587
- );
588
- const view = new import_prosemirror_view4.EditorView(element, {
589
+ wrapper.innerHTML = toInnerHTML(defaultValue ? String(defaultValue) : "");
590
+ const view = new import_prosemirror_view4.EditorView(containerRef.current, {
589
591
  ...editor,
590
592
  attributes: (state2) => {
591
593
  const propsAttributes = (() => {
@@ -660,27 +662,11 @@ function createTextEditor(options = {}) {
660
662
  const state2 = view.state;
661
663
  return state2.doc.textBetween(0, state2.doc.content.size, "\n");
662
664
  }
663
- const sub = subject.pipe(
664
- (0, import_rxjs.filter)((tr) => tr.docChanged),
665
- (0, import_rxjs.debounceTime)(updateDelay)
666
- ).subscribe(() => {
667
- if (inputRef.current) {
668
- switch (mode) {
669
- case "text":
670
- inputRef.current.value = toTextContent();
671
- break;
672
- default:
673
- inputRef.current.value = toHTML();
674
- break;
675
- }
676
- const event = new Event("input", { bubbles: true });
677
- inputRef.current.dispatchEvent(event);
678
- }
679
- });
680
665
  if (autoFocus) {
681
666
  view.focus();
682
667
  }
683
668
  const textEditorController = {
669
+ schema,
684
670
  view,
685
671
  subject,
686
672
  set value(value) {
@@ -696,26 +682,33 @@ function createTextEditor(options = {}) {
696
682
  },
697
683
  clear
698
684
  };
699
- if (typeof ref === "function") {
700
- ref(textEditorController);
701
- } else if (ref) {
702
- ref.current = textEditorController;
685
+ controllerRef.current = textEditorController;
686
+ return textEditorController;
687
+ });
688
+ (0, import_react2.useEffect)(() => {
689
+ const controller = controllerRef.current;
690
+ if (!controller) {
691
+ return;
692
+ }
693
+ const sub = controller.subject.pipe(
694
+ (0, import_rxjs.filter)((tr) => tr.docChanged),
695
+ (0, import_rxjs.debounceTime)(updateDelay)
696
+ ).subscribe(() => {
697
+ if (inputRef.current) {
698
+ inputRef.current.value = controller.value;
699
+ const event = new Event("input", { bubbles: true });
700
+ inputRef.current.dispatchEvent(event);
701
+ }
702
+ });
703
+ if (autoFocus) {
704
+ controller.view.focus();
703
705
  }
704
706
  return () => {
705
707
  sub.unsubscribe();
706
- view.destroy();
707
- element.innerHTML = "";
708
+ controller.view.destroy();
708
709
  };
709
710
  }, []);
710
- return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("div", { ref: containerRef, className: container, ...props }), /* @__PURE__ */ import_react.default.createElement(
711
- "input",
712
- {
713
- ref: inputRef,
714
- type: "hidden",
715
- name,
716
- onInput: onChange
717
- }
718
- ));
711
+ return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("div", { ref: containerRef, className: container, ...props }), /* @__PURE__ */ import_react.default.createElement("input", { ref: inputRef, type: "hidden", name, onInput: onChange }));
719
712
  }
720
713
  return {
721
714
  schema,
@@ -1,5 +1,8 @@
1
1
  // src/text_editor.tsx
2
- import React from "react";
2
+ import React, {
3
+ useImperativeHandle,
4
+ useMemo
5
+ } from "react";
3
6
  import {
4
7
  EditorState as EditorState2
5
8
  } from "prosemirror-state";
@@ -436,15 +439,24 @@ function cn(...classes) {
436
439
 
437
440
  // src/attach_file.tsx
438
441
  import "prosemirror-view";
442
+ var base64ImageUploader = async (file) => {
443
+ const base64 = await new Promise((resolve, reject) => {
444
+ const reader = new FileReader();
445
+ reader.onload = () => {
446
+ resolve(reader.result);
447
+ };
448
+ reader.onerror = reject;
449
+ reader.readAsDataURL(file);
450
+ });
451
+ return {
452
+ src: base64,
453
+ alt: file.name
454
+ };
455
+ };
439
456
  function createAttachFile({
440
457
  schema,
441
458
  generateMetadata,
442
- uploadFile = (file) => {
443
- return {
444
- src: URL.createObjectURL(file),
445
- alt: file.name
446
- };
447
- }
459
+ uploadFile = base64ImageUploader
448
460
  }) {
449
461
  const attachEachFile = async (view, file, pos) => {
450
462
  const metadata = generateMetadata ? await generateMetadata(file) : {};
@@ -496,9 +508,7 @@ function createAttachFile({
496
508
  }
497
509
  view.dispatch(tr2.replaceWith($pos, $pos, node));
498
510
  } catch (e) {
499
- view.dispatch(
500
- tr.setMeta(uploadPlaceholderPlugin, { remove: { id } })
501
- );
511
+ view.dispatch(tr.setMeta(uploadPlaceholderPlugin, { remove: { id } }));
502
512
  }
503
513
  };
504
514
  return async (view, files, pos) => {
@@ -537,12 +547,9 @@ function createTextEditor(options = {}) {
537
547
  } = {}) {
538
548
  const containerRef = useRef(null);
539
549
  const inputRef = useRef(null);
540
- useEffect(() => {
541
- const element = containerRef.current;
542
- if (!element) {
543
- return;
544
- }
545
- const subject = new Subject();
550
+ const controllerRef = useRef(null);
551
+ const subject = useMemo(() => new Subject(), []);
552
+ useImperativeHandle(ref, () => {
546
553
  const wrapper = document.createElement("div");
547
554
  const toInnerHTML = (value) => {
548
555
  if (mode === "html") {
@@ -550,10 +557,8 @@ function createTextEditor(options = {}) {
550
557
  }
551
558
  return value.split("\n").map((line) => `<p>${line}</p>`).join("");
552
559
  };
553
- wrapper.innerHTML = toInnerHTML(
554
- defaultValue ? String(defaultValue) : ""
555
- );
556
- const view = new EditorView3(element, {
560
+ wrapper.innerHTML = toInnerHTML(defaultValue ? String(defaultValue) : "");
561
+ const view = new EditorView3(containerRef.current, {
557
562
  ...editor,
558
563
  attributes: (state2) => {
559
564
  const propsAttributes = (() => {
@@ -628,27 +633,11 @@ function createTextEditor(options = {}) {
628
633
  const state2 = view.state;
629
634
  return state2.doc.textBetween(0, state2.doc.content.size, "\n");
630
635
  }
631
- const sub = subject.pipe(
632
- filter((tr) => tr.docChanged),
633
- debounceTime(updateDelay)
634
- ).subscribe(() => {
635
- if (inputRef.current) {
636
- switch (mode) {
637
- case "text":
638
- inputRef.current.value = toTextContent();
639
- break;
640
- default:
641
- inputRef.current.value = toHTML();
642
- break;
643
- }
644
- const event = new Event("input", { bubbles: true });
645
- inputRef.current.dispatchEvent(event);
646
- }
647
- });
648
636
  if (autoFocus) {
649
637
  view.focus();
650
638
  }
651
639
  const textEditorController = {
640
+ schema,
652
641
  view,
653
642
  subject,
654
643
  set value(value) {
@@ -664,26 +653,33 @@ function createTextEditor(options = {}) {
664
653
  },
665
654
  clear
666
655
  };
667
- if (typeof ref === "function") {
668
- ref(textEditorController);
669
- } else if (ref) {
670
- ref.current = textEditorController;
656
+ controllerRef.current = textEditorController;
657
+ return textEditorController;
658
+ });
659
+ useEffect(() => {
660
+ const controller = controllerRef.current;
661
+ if (!controller) {
662
+ return;
663
+ }
664
+ const sub = controller.subject.pipe(
665
+ filter((tr) => tr.docChanged),
666
+ debounceTime(updateDelay)
667
+ ).subscribe(() => {
668
+ if (inputRef.current) {
669
+ inputRef.current.value = controller.value;
670
+ const event = new Event("input", { bubbles: true });
671
+ inputRef.current.dispatchEvent(event);
672
+ }
673
+ });
674
+ if (autoFocus) {
675
+ controller.view.focus();
671
676
  }
672
677
  return () => {
673
678
  sub.unsubscribe();
674
- view.destroy();
675
- element.innerHTML = "";
679
+ controller.view.destroy();
676
680
  };
677
681
  }, []);
678
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { ref: containerRef, className: container, ...props }), /* @__PURE__ */ React.createElement(
679
- "input",
680
- {
681
- ref: inputRef,
682
- type: "hidden",
683
- name,
684
- onInput: onChange
685
- }
686
- ));
682
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { ref: containerRef, className: container, ...props }), /* @__PURE__ */ React.createElement("input", { ref: inputRef, type: "hidden", name, onInput: onChange }));
687
683
  }
688
684
  return {
689
685
  schema,
package/package.json CHANGED
@@ -1,56 +1,56 @@
1
1
  {
2
- "name": "dn-react-text-editor",
3
- "version": "0.1.0",
4
- "types": "./dist/index.d.ts",
5
- "main": "./dist/index.mjs",
6
- "module": "./dist/index.js",
7
- "sideEffects": false,
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
13
- },
14
- "./prosemirror": {
15
- "types": "./dist/prosemirror/index.d.ts",
16
- "import": "./dist/prosemirror/index.mjs",
17
- "require": "./dist/prosemirror/index.js"
18
- }
2
+ "name": "dn-react-text-editor",
3
+ "version": "0.1.2",
4
+ "types": "./dist/index.d.ts",
5
+ "main": "./dist/index.mjs",
6
+ "module": "./dist/index.js",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
19
13
  },
20
- "scripts": {
21
- "build": "NODE_OPTIONS='--max-old-space-size=16384' tsup",
22
- "dev": "tsup --watch"
23
- },
24
- "repository": {
25
- "type": "git",
26
- "url": "git+https://github.com/dndnsoft/dn-react-text-editor.git"
27
- },
28
- "author": "",
29
- "license": "MIT",
30
- "bugs": {
31
- "url": "https://github.com/dndnsoft/dn-react-text-editor/issues"
32
- },
33
- "homepage": "https://github.com/dndnsoft/dn-react-text-editor#readme",
34
- "description": "",
35
- "devDependencies": {
36
- "@types/node": "^24.10.1",
37
- "@types/react": "^19",
38
- "@types/react-dom": "^19",
39
- "tsup": "^8.5.1",
40
- "typescript": "^5.7.3"
41
- },
42
- "peerDependencies": {
43
- "react": "^19",
44
- "react-dom": "^19"
45
- },
46
- "dependencies": {
47
- "prosemirror-commands": "^1.7.1",
48
- "prosemirror-history": "^1.5.0",
49
- "prosemirror-keymap": "^1.2.3",
50
- "prosemirror-model": "^1.25.4",
51
- "prosemirror-schema-list": "^1.5.1",
52
- "prosemirror-state": "^1.4.4",
53
- "prosemirror-view": "^1.41.3",
54
- "rxjs": "^7.8.2"
14
+ "./prosemirror": {
15
+ "types": "./dist/prosemirror/index.d.ts",
16
+ "import": "./dist/prosemirror/index.mjs",
17
+ "require": "./dist/prosemirror/index.js"
55
18
  }
56
- }
19
+ },
20
+ "scripts": {
21
+ "build": "NODE_OPTIONS='--max-old-space-size=16384' tsup",
22
+ "dev": "tsup --watch"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/dndnsoft/dn-react-text-editor.git"
27
+ },
28
+ "author": "",
29
+ "license": "MIT",
30
+ "bugs": {
31
+ "url": "https://github.com/dndnsoft/dn-react-text-editor/issues"
32
+ },
33
+ "homepage": "https://github.com/dndnsoft/dn-react-text-editor#readme",
34
+ "description": "",
35
+ "devDependencies": {
36
+ "@types/node": "^24.10.1",
37
+ "@types/react": "^19",
38
+ "@types/react-dom": "^19",
39
+ "tsup": "^8.5.1",
40
+ "typescript": "^5.7.3"
41
+ },
42
+ "peerDependencies": {
43
+ "react": "^19",
44
+ "react-dom": "^19"
45
+ },
46
+ "dependencies": {
47
+ "prosemirror-commands": "^1.7.1",
48
+ "prosemirror-history": "^1.5.0",
49
+ "prosemirror-keymap": "^1.2.3",
50
+ "prosemirror-model": "^1.25.4",
51
+ "prosemirror-schema-list": "^1.5.1",
52
+ "prosemirror-state": "^1.4.4",
53
+ "prosemirror-view": "^1.41.3",
54
+ "rxjs": "^7.8.2"
55
+ }
56
+ }