@bigbinary/neeto-editor 0.2.0 → 0.2.1

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.
Files changed (41) hide show
  1. package/index.js +27 -6
  2. package/package.json +7 -4
  3. package/src/Common/Button.js +95 -0
  4. package/src/Common/Description.js +1 -5
  5. package/src/Common/Dropdown/index.js +6 -2
  6. package/src/Common/Input.js +70 -0
  7. package/src/Common/Label.js +45 -0
  8. package/src/Common/Modal.js +91 -0
  9. package/src/Common/Tab.js +79 -0
  10. package/src/Common/ToolTip.js +37 -0
  11. package/src/Editor/CustomExtensions/BubbleMenu/index.js +2 -2
  12. package/src/Editor/CustomExtensions/FixedMenu/FontSizeOption.js +3 -3
  13. package/src/Editor/CustomExtensions/FixedMenu/TextColorOption.js +1 -1
  14. package/src/Editor/CustomExtensions/FixedMenu/index.js +7 -10
  15. package/src/Editor/CustomExtensions/Image/LinkUploader/URLForm.js +39 -0
  16. package/src/Editor/CustomExtensions/Image/LocalUploader.js +21 -0
  17. package/src/Editor/CustomExtensions/Image/ProgressBar.js +34 -0
  18. package/src/Editor/CustomExtensions/Image/Uploader.js +72 -31
  19. package/src/Editor/CustomExtensions/Image/constants.js +5 -0
  20. package/src/Editor/CustomExtensions/Mention/ExtensionConfig.js +0 -1
  21. package/src/Editor/CustomExtensions/Mention/MentionList.js +1 -1
  22. package/src/Editor/CustomExtensions/SlashCommands/ExtensionConfig.js +180 -176
  23. package/src/Editor/CustomExtensions/Variable/index.js +3 -3
  24. package/src/Editor/CustomExtensions/useCustomExtensions.js +2 -1
  25. package/src/Editor/index.js +17 -5
  26. package/src/constants/regexp.js +1 -0
  27. package/src/examples/constants.js +1 -1
  28. package/src/examples/index.js +25 -25
  29. package/src/hooks/useTabBar.js +9 -0
  30. package/src/index.scss +5 -0
  31. package/src/styles/abstracts/_neeto-ui-variables.scss +80 -9
  32. package/src/styles/abstracts/_variables.scss +4 -1
  33. package/src/styles/components/_button.scss +161 -0
  34. package/src/styles/components/_editor.scss +4 -0
  35. package/src/styles/components/_fixed-menu.scss +4 -0
  36. package/src/styles/components/_image-uploader.scss +109 -0
  37. package/src/styles/components/_input.scss +165 -0
  38. package/src/styles/components/_tab.scss +74 -0
  39. package/src/styles/components/_tooltip.scss +152 -0
  40. package/webpack.config.js +7 -0
  41. package/webpack.dev.config.js +7 -0
@@ -0,0 +1,34 @@
1
+ import React, { useState, useEffect } from "react";
2
+
3
+ import { Close } from "@bigbinary/neeto-icons";
4
+
5
+ const ProgressBar = ({ uppy }) => {
6
+ const [progress, setProgress] = useState(0);
7
+
8
+ useEffect(() => {
9
+ uppy.on("progress", setProgress);
10
+ }, [uppy]);
11
+
12
+ const progressPercentage = `${progress}%`;
13
+
14
+ return (
15
+ <div className="progress-bar__root">
16
+ <div className="flex items-center justify-between">
17
+ <span className="progress-bar__percent-text">{progressPercentage}</span>
18
+ <button onClick={uppy.cancelAll}>
19
+ <Close size={16} />
20
+ </button>
21
+ </div>
22
+ <div className="progress-bar__indicator">
23
+ <div className="flex h-full">
24
+ <div
25
+ style={{ width: progressPercentage }}
26
+ className="flex flex-col justify-center text-center text-white shadow-none whitespace-nowrap progress-bar__indicator-inner"
27
+ ></div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export default ProgressBar;
@@ -1,40 +1,81 @@
1
- import React from "react";
1
+ import React, { useState, useMemo } from "react";
2
2
  import { view } from "@risingstack/react-easy-state";
3
3
  import Uppy from "@uppy/core";
4
- import { DashboardModal } from "@uppy/react";
5
4
  import XHRUpload from "@uppy/xhr-upload";
6
- import "@uppy/core/dist/style.css";
7
- import "@uppy/dashboard/dist/style.css";
8
- import sharedState from "../../sharedState";
9
-
10
- const ImageUpload = ({ editor, imageUploadUrl }) => {
11
- const uppy = new Uppy({
12
- allowMultipleUploads: false,
13
- autoProceed: true,
14
- debug: true,
15
- });
16
-
17
- uppy.use(XHRUpload, {
18
- endpoint: imageUploadUrl || "/api/v1/direct_uploads",
19
- formData: true,
20
- fieldName: "blob",
21
- });
22
-
23
- uppy.on("upload-success", (file, response) => {
24
- const url = response.body.imageURL;
5
+ import Modal from "common/Modal";
6
+ import Tab from "common/Tab";
7
+ import useTabBar from "hooks/useTabBar";
8
+
9
+ import LocalUploader from "./LocalUploader";
10
+ import ProgressBar from "./ProgressBar";
11
+
12
+ import URLForm from "./LinkUploader/URLForm";
13
+
14
+ import { IMAGE_UPLOAD_OPTIONS } from "./constants";
15
+
16
+ const ImageUpload = ({ editor, imageUploadUrl, isVisible, setIsVisible }) => {
17
+ const [isUploading, setIsUploading] = useState(false);
18
+ const [activeTab, setActiveTab] = useTabBar(IMAGE_UPLOAD_OPTIONS);
19
+
20
+ const uppy = useMemo(
21
+ () =>
22
+ new Uppy({
23
+ allowMultipleUploads: false,
24
+ autoProceed: true,
25
+ debug: true,
26
+ })
27
+ .use(XHRUpload, {
28
+ endpoint: imageUploadUrl || "/api/v1/direct_uploads",
29
+ formData: true,
30
+ fieldName: "blob",
31
+ })
32
+ .on("upload", () => setIsUploading(true))
33
+ .on("upload-success", (file, response) => {
34
+ const url = response.body.imageURL;
35
+ editor.chain().focus().setImage({ src: url }).run();
36
+ setIsVisible(false);
37
+ })
38
+ .on("cancel-all", () => setIsUploading(false))
39
+ .on("complete", () => setIsUploading(false)),
40
+ [editor]
41
+ );
42
+
43
+ const handleUrlFormSubmit = (url) => {
25
44
  editor.chain().focus().setImage({ src: url }).run();
26
- });
45
+ setIsVisible(false);
46
+ };
27
47
 
28
48
  return (
29
- <div>
30
- <DashboardModal
31
- uppy={uppy}
32
- proudlyDisplayPoweredByUppy={false}
33
- closeModalOnClickOutside
34
- open={sharedState.showImageUpload}
35
- onRequestClose={() => (sharedState.showImageUpload = false)}
36
- />
37
- </div>
49
+ <Modal isVisible={isVisible} onClose={() => setIsVisible(false)}>
50
+ <div className="image-uploader__root">
51
+ <Tab>
52
+ {IMAGE_UPLOAD_OPTIONS.map((option) => (
53
+ <Tab.Item
54
+ active={activeTab === option.key}
55
+ onClick={() => setActiveTab(option)}
56
+ >
57
+ {option.title}
58
+ </Tab.Item>
59
+ ))}
60
+ </Tab>
61
+
62
+ <div className="image-uploader__content">
63
+ {isUploading ? (
64
+ <div className="flex flex-col items-center justify-center flex-1 text-center">
65
+ <span className="label--primary">Uploading...</span>
66
+ <span className="label--secondary">
67
+ {uppy.getFiles()[0]?.name}
68
+ </span>
69
+ <ProgressBar uppy={uppy} />
70
+ </div>
71
+ ) : activeTab === "local" ? (
72
+ <LocalUploader uppy={uppy} />
73
+ ) : activeTab === "link" ? (
74
+ <URLForm onSubmit={handleUrlFormSubmit} />
75
+ ) : null}
76
+ </div>
77
+ </div>
78
+ </Modal>
38
79
  );
39
80
  };
40
81
 
@@ -0,0 +1,5 @@
1
+ export const IMAGE_UPLOAD_OPTIONS = [
2
+ { title: "Upload", key: "local" },
3
+ { title: "Link", key: "link" },
4
+ { title: "Unsplash", key: "unsplash" },
5
+ ];
@@ -13,7 +13,6 @@ const suggestion = {
13
13
 
14
14
  return {
15
15
  onStart: (props) => {
16
- console.log({ props });
17
16
  reactRenderer = new ReactRenderer(MentionList, {
18
17
  props,
19
18
  editor: props.editor,
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import classNames from "classnames";
3
3
 
4
- import Avatar from "../../../Common/Avatar";
4
+ import Avatar from "common/Avatar";
5
5
 
6
6
  export class MentionList extends React.Component {
7
7
  state = { selectedIndex: 0 };
@@ -1,4 +1,3 @@
1
- import sharedState from "../../sharedState";
2
1
  import { Extension } from "@tiptap/core";
3
2
  import CommandsList from "./CommandsList";
4
3
  import { PluginKey } from "prosemirror-state";
@@ -19,187 +18,192 @@ import {
19
18
 
20
19
  export const CommandsPluginKey = new PluginKey("commands");
21
20
 
22
- export default Extension.create({
23
- addOptions() {
24
- return {
25
- HTMLAttributes: {
26
- class: "commands",
27
- },
28
- suggestion: {
29
- char: "/",
30
- startOfLine: false,
31
- pluginKey: CommandsPluginKey,
32
- command: ({ editor, range, props }) => {
33
- props.command({ editor, range });
34
- },
35
-
36
- items: () => {
37
- return [
38
- {
39
- title: "Paragraph",
40
- description: "Add a plain text block",
41
- Icon: Paragraph,
42
- command: ({ editor, range }) => {
43
- editor
44
- .chain()
45
- .focus()
46
- .deleteRange(range)
47
- .setNode("paragraph")
48
- .run();
49
- },
50
- },
51
- {
52
- title: "H1",
53
- description: "Add a big heading",
54
- Icon: TextH1,
55
- command: ({ editor, range }) => {
56
- editor
57
- .chain()
58
- .focus()
59
- .deleteRange(range)
60
- .setNode("heading", { level: 1 })
61
- .run();
62
- },
63
- },
64
- {
65
- title: "H2",
66
- description: "Add a sub-heading",
67
- Icon: TextH2,
68
- command: ({ editor, range }) => {
69
- editor
70
- .chain()
71
- .focus()
72
- .deleteRange(range)
73
- .setNode("heading", { level: 2 })
74
- .run();
75
- },
76
- },
77
- {
78
- title: "Numbered list",
79
- description: "Add a list with numbering",
80
- Icon: ListNumber,
81
- command: ({ editor, range }) => {
82
- editor
83
- .chain()
84
- .focus()
85
- .deleteRange(range)
86
- .toggleOrderedList()
87
- .run();
88
- },
89
- },
90
- {
91
- title: "Bulleted list",
92
- description: "Add an list bullets",
93
- Icon: ListDot,
94
- command: ({ editor, range }) => {
95
- editor
96
- .chain()
97
- .focus()
98
- .deleteRange(range)
99
- .toggleBulletList()
100
- .run();
101
- },
102
- },
103
- {
104
- title: "Blockquote",
105
- description: "Add a quote",
106
- Icon: Blockquote,
107
- command: ({ editor, range }) => {
108
- editor
109
- .chain()
110
- .focus()
111
- .deleteRange(range)
112
- .toggleBlockquote()
113
- .run();
114
- },
21
+ export default {
22
+ configure: ({ setImageUploadVisible }) =>
23
+ Extension.create({
24
+ addOptions() {
25
+ return {
26
+ HTMLAttributes: {
27
+ class: "commands",
28
+ },
29
+ suggestion: {
30
+ char: "/",
31
+ startOfLine: false,
32
+ pluginKey: CommandsPluginKey,
33
+ command: ({ editor, range, props }) => {
34
+ props.command({ editor, range });
115
35
  },
116
- {
117
- title: "Image",
118
- description: "Add an image",
119
- Icon: Image,
120
- command: ({ editor, range }) => {
121
- sharedState.showImageUpload = true;
122
- sharedState.range = range;
123
- editor.chain().focus().deleteRange(range).run();
124
- },
125
- },
126
- {
127
- title: "Youtube/Vimeo",
128
- description: "Embed a video from major services",
129
- Icon: Video,
130
- command: ({ editor, range }) => {
131
- const embedURL = prompt("Please enter Youtube/Vimeo embed URL");
132
- editor
133
- .chain()
134
- .focus()
135
- .deleteRange(range)
136
- .setExternalVideo({ src: embedURL })
137
- .run();
138
- },
139
- },
140
- {
141
- title: "Code block",
142
- description: "Add a code block with syntax highlighting",
143
- Icon: Code,
144
- command: ({ editor, range }) => {
145
- editor
146
- .chain()
147
- .focus()
148
- .deleteRange(range)
149
- .toggleCodeBlock()
150
- .run();
151
- },
36
+
37
+ items: () => {
38
+ return [
39
+ {
40
+ title: "Paragraph",
41
+ description: "Add a plain text block",
42
+ Icon: Paragraph,
43
+ command: ({ editor, range }) => {
44
+ editor
45
+ .chain()
46
+ .focus()
47
+ .deleteRange(range)
48
+ .setNode("paragraph")
49
+ .run();
50
+ },
51
+ },
52
+ {
53
+ title: "H1",
54
+ description: "Add a big heading",
55
+ Icon: TextH1,
56
+ command: ({ editor, range }) => {
57
+ editor
58
+ .chain()
59
+ .focus()
60
+ .deleteRange(range)
61
+ .setNode("heading", { level: 1 })
62
+ .run();
63
+ },
64
+ },
65
+ {
66
+ title: "H2",
67
+ description: "Add a sub-heading",
68
+ Icon: TextH2,
69
+ command: ({ editor, range }) => {
70
+ editor
71
+ .chain()
72
+ .focus()
73
+ .deleteRange(range)
74
+ .setNode("heading", { level: 2 })
75
+ .run();
76
+ },
77
+ },
78
+ {
79
+ title: "Numbered list",
80
+ description: "Add a list with numbering",
81
+ Icon: ListNumber,
82
+ command: ({ editor, range }) => {
83
+ editor
84
+ .chain()
85
+ .focus()
86
+ .deleteRange(range)
87
+ .toggleOrderedList()
88
+ .run();
89
+ },
90
+ },
91
+ {
92
+ title: "Bulleted list",
93
+ description: "Add an list bullets",
94
+ Icon: ListDot,
95
+ command: ({ editor, range }) => {
96
+ editor
97
+ .chain()
98
+ .focus()
99
+ .deleteRange(range)
100
+ .toggleBulletList()
101
+ .run();
102
+ },
103
+ },
104
+ {
105
+ title: "Blockquote",
106
+ description: "Add a quote",
107
+ Icon: Blockquote,
108
+ command: ({ editor, range }) => {
109
+ editor
110
+ .chain()
111
+ .focus()
112
+ .deleteRange(range)
113
+ .toggleBlockquote()
114
+ .run();
115
+ },
116
+ },
117
+ {
118
+ title: "Image",
119
+ description: "Add an image",
120
+ Icon: Image,
121
+ command: ({ editor, range }) => {
122
+ setImageUploadVisible(true);
123
+ editor.chain().focus().deleteRange(range).run();
124
+ },
125
+ },
126
+ {
127
+ title: "Youtube/Vimeo",
128
+ description: "Embed a video from major services",
129
+ Icon: Video,
130
+ command: ({ editor, range }) => {
131
+ const embedURL = prompt(
132
+ "Please enter Youtube/Vimeo embed URL"
133
+ );
134
+ editor
135
+ .chain()
136
+ .focus()
137
+ .deleteRange(range)
138
+ .setExternalVideo({ src: embedURL })
139
+ .run();
140
+ },
141
+ },
142
+ {
143
+ title: "Code block",
144
+ description: "Add a code block with syntax highlighting",
145
+ Icon: Code,
146
+ command: ({ editor, range }) => {
147
+ editor
148
+ .chain()
149
+ .focus()
150
+ .deleteRange(range)
151
+ .toggleCodeBlock()
152
+ .run();
153
+ },
154
+ },
155
+ ];
152
156
  },
153
- ];
154
- },
155
157
 
156
- render: () => {
157
- let reactRenderer;
158
- let popup;
158
+ render: () => {
159
+ let reactRenderer;
160
+ let popup;
159
161
 
160
- return {
161
- onStart: (props) => {
162
- reactRenderer = new ReactRenderer(CommandsList, {
163
- props,
164
- editor: props.editor,
165
- });
162
+ return {
163
+ onStart: (props) => {
164
+ reactRenderer = new ReactRenderer(CommandsList, {
165
+ props,
166
+ editor: props.editor,
167
+ });
166
168
 
167
- popup = tippy("body", {
168
- getReferenceClientRect: props.clientRect,
169
- appendTo: () => document.body,
170
- content: reactRenderer.element,
171
- showOnCreate: true,
172
- interactive: true,
173
- trigger: "manual",
174
- placement: "bottom-start",
175
- });
176
- },
177
- onUpdate(props) {
178
- reactRenderer.updateProps(props);
169
+ popup = tippy("body", {
170
+ getReferenceClientRect: props.clientRect,
171
+ appendTo: () => document.body,
172
+ content: reactRenderer.element,
173
+ showOnCreate: true,
174
+ interactive: true,
175
+ trigger: "manual",
176
+ placement: "bottom-start",
177
+ arrow: false,
178
+ });
179
+ },
180
+ onUpdate(props) {
181
+ reactRenderer.updateProps(props);
179
182
 
180
- popup[0].setProps({
181
- getReferenceClientRect: props.clientRect,
182
- });
183
+ popup[0].setProps({
184
+ getReferenceClientRect: props.clientRect,
185
+ });
186
+ },
187
+ onKeyDown(props) {
188
+ return reactRenderer.ref?.onKeyDown(props);
189
+ },
190
+ onExit() {
191
+ popup[0].destroy();
192
+ reactRenderer.destroy();
193
+ },
194
+ };
183
195
  },
184
- onKeyDown(props) {
185
- return reactRenderer.ref?.onKeyDown(props);
186
- },
187
- onExit() {
188
- popup[0].destroy();
189
- reactRenderer.destroy();
190
- },
191
- };
192
- },
196
+ },
197
+ };
193
198
  },
194
- };
195
- },
196
199
 
197
- addProseMirrorPlugins() {
198
- return [
199
- Suggestion({
200
- editor: this.editor,
201
- ...this.options.suggestion,
202
- }),
203
- ];
204
- },
205
- });
200
+ addProseMirrorPlugins() {
201
+ return [
202
+ Suggestion({
203
+ editor: this.editor,
204
+ ...this.options.suggestion,
205
+ }),
206
+ ];
207
+ },
208
+ }),
209
+ };
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
 
3
3
  import VariableList from "./VariableList";
4
- import Dropdown from "../../../Common/Dropdown";
5
- import { HashtagFilled } from "../../../Common/Icons";
4
+ import Dropdown from "common/Dropdown";
5
+ import { HashtagFilled } from "common/Icons";
6
6
 
7
7
  import { MENU_ICON_SIZE } from "../FixedMenu/constants";
8
8
 
@@ -20,7 +20,7 @@ const Variables = ({ editor, variables }) => {
20
20
  return (
21
21
  <Dropdown
22
22
  customTarget={() => (
23
- <button className="relative p-3 editor-fixed-menu--item variable-selection-popup">
23
+ <button className="relative h-full p-3 editor-fixed-menu--item variable-selection-popup">
24
24
  <HashtagFilled size={MENU_ICON_SIZE} />
25
25
  </button>
26
26
  )}
@@ -27,6 +27,7 @@ export default function useCustomExtensions({
27
27
  variables,
28
28
  isSlashCommandsActive,
29
29
  showImageInMention,
30
+ setImageUploadVisible,
30
31
  }) {
31
32
  let customExtensions;
32
33
 
@@ -62,7 +63,7 @@ export default function useCustomExtensions({
62
63
  }
63
64
 
64
65
  if (isSlashCommandsActive) {
65
- customExtensions.push(SlashCommands);
66
+ customExtensions.push(SlashCommands.configure({ setImageUploadVisible }));
66
67
  }
67
68
 
68
69
  if (!isEmpty(mentions)) {
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useState } from "react";
2
2
  import classNames from "classnames";
3
3
  import { useEditor, EditorContent } from "@tiptap/react";
4
4
 
@@ -34,6 +34,8 @@ const Tiptap = (
34
34
  },
35
35
  ref
36
36
  ) => {
37
+ const [isImageUploadVisible, setImageUploadVisible] = useState(false);
38
+
37
39
  const isFixedMenuActive = menuType === "fixed";
38
40
  const isBubbleMenuActive = menuType === "bubble";
39
41
  const isSlashCommandsActive = !hideSlashCommands;
@@ -47,6 +49,7 @@ const Tiptap = (
47
49
  variables,
48
50
  isSlashCommandsActive,
49
51
  showImageInMention,
52
+ setImageUploadVisible,
50
53
  });
51
54
 
52
55
  const editor = useEditor({
@@ -76,16 +79,25 @@ const Tiptap = (
76
79
  }));
77
80
 
78
81
  return (
79
- <>
82
+ <div>
80
83
  {isFixedMenuActive ? (
81
- <FixedMenu editor={editor} variables={variables} />
84
+ <FixedMenu
85
+ editor={editor}
86
+ variables={variables}
87
+ setImageUploadVisible={setImageUploadVisible}
88
+ />
82
89
  ) : null}
83
90
  {isBubbleMenuActive ? (
84
91
  <BubbleMenu editor={editor} formatterOptions={formatterOptions} />
85
92
  ) : null}
86
- <ImageUploader editor={editor} imageUploadUrl={uploadEndpoint} />
93
+ <ImageUploader
94
+ isVisible={isImageUploadVisible}
95
+ setIsVisible={setImageUploadVisible}
96
+ editor={editor}
97
+ imageUploadUrl={uploadEndpoint}
98
+ />
87
99
  <EditorContent editor={editor} {...otherProps} />
88
- </>
100
+ </div>
89
101
  );
90
102
  };
91
103
 
@@ -0,0 +1 @@
1
+ export const UrlRegExp = /(http)?s?:?(\/\/[^"']*)/;
@@ -89,7 +89,7 @@ export const STRINGS = {
89
89
  <Editor placeholder="Input text here" />`,
90
90
 
91
91
  forceTitleSampleCode: `
92
- const placeholder = { header: 'Input title here' };
92
+ const placeholder = { heading: 'Input title here' };
93
93
 
94
94
  <Editor placeholder={placeholder} forceTitle />`,
95
95
  };