@bigbinary/neeto-editor 0.1.15 → 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 (72) hide show
  1. package/index.js +37 -14
  2. package/package.json +18 -4
  3. package/src/App.js +5 -28
  4. package/src/Common/Avatar.js +168 -0
  5. package/src/Common/Button.js +95 -0
  6. package/src/Common/CodeBlock.js +11 -0
  7. package/src/Common/Description.js +8 -0
  8. package/src/Common/Dropdown/index.js +122 -0
  9. package/src/Common/Heading.js +13 -0
  10. package/src/Common/HighlightText.js +7 -0
  11. package/src/Common/Icons/HashtagFilled.js +59 -0
  12. package/src/Common/Icons/TextColor.js +35 -0
  13. package/src/Common/Icons/index.js +2 -0
  14. package/src/Common/Input.js +70 -0
  15. package/src/Common/Label.js +45 -0
  16. package/src/Common/ListItems.js +17 -0
  17. package/src/Common/Modal.js +91 -0
  18. package/src/Common/Tab.js +79 -0
  19. package/src/Common/ToolTip.js +37 -0
  20. package/src/Editor/CustomExtensions/BubbleMenu/index.js +33 -26
  21. package/src/Editor/CustomExtensions/Embeds.js +5 -3
  22. package/src/Editor/CustomExtensions/FixedMenu/FontSizeOption.js +32 -0
  23. package/src/Editor/CustomExtensions/FixedMenu/TextColorOption.js +29 -0
  24. package/src/Editor/CustomExtensions/FixedMenu/constants.js +3 -0
  25. package/src/Editor/CustomExtensions/FixedMenu/index.js +183 -0
  26. package/src/Editor/CustomExtensions/Image/LinkUploader/URLForm.js +39 -0
  27. package/src/Editor/CustomExtensions/Image/LocalUploader.js +21 -0
  28. package/src/Editor/CustomExtensions/Image/ProgressBar.js +34 -0
  29. package/src/Editor/CustomExtensions/Image/Uploader.js +72 -31
  30. package/src/Editor/CustomExtensions/Image/constants.js +5 -0
  31. package/src/Editor/CustomExtensions/Mention/ExtensionConfig.js +66 -0
  32. package/src/Editor/CustomExtensions/Mention/MentionList.js +96 -0
  33. package/src/Editor/CustomExtensions/Mention/helpers.js +23 -0
  34. package/src/Editor/CustomExtensions/Placeholder/ExtensionConfig.js +81 -0
  35. package/src/Editor/CustomExtensions/Placeholder/helpers.js +18 -0
  36. package/src/Editor/CustomExtensions/SlashCommands/Commands.js +5 -10
  37. package/src/Editor/CustomExtensions/SlashCommands/CommandsList.js +15 -16
  38. package/src/Editor/CustomExtensions/SlashCommands/ExtensionConfig.js +200 -155
  39. package/src/Editor/CustomExtensions/Variable/ExtensionConfig.js +208 -0
  40. package/src/Editor/CustomExtensions/Variable/VariableList.js +45 -0
  41. package/src/Editor/CustomExtensions/Variable/VariableSuggestion.js +20 -0
  42. package/src/Editor/CustomExtensions/Variable/helpers.js +31 -0
  43. package/src/Editor/CustomExtensions/Variable/index.js +35 -0
  44. package/src/Editor/CustomExtensions/useCustomExtensions.js +88 -0
  45. package/src/Editor/index.js +59 -41
  46. package/src/constants/regexp.js +1 -0
  47. package/src/examples/constants.js +95 -0
  48. package/src/examples/index.js +186 -0
  49. package/src/hooks/useOutsideClick.js +19 -0
  50. package/src/hooks/useTabBar.js +9 -0
  51. package/src/index.scss +32 -12
  52. package/src/styles/abstracts/_mixins.scss +20 -0
  53. package/src/styles/abstracts/_neeto-ui-variables.scss +107 -0
  54. package/src/styles/abstracts/_variables.scss +13 -0
  55. package/src/styles/components/_avatar.scss +105 -0
  56. package/src/styles/components/_button.scss +161 -0
  57. package/src/styles/components/_codeblock.scss +16 -0
  58. package/src/{Editor/styles/CommandsList.scss → styles/components/_command-list.scss} +12 -1
  59. package/src/styles/components/_dropdown.scss +69 -0
  60. package/src/styles/components/_editor-variables.scss +12 -0
  61. package/src/{Editor/styles/EditorStyles.scss → styles/components/_editor.scss} +33 -7
  62. package/src/styles/components/_fixed-menu.scss +17 -0
  63. package/src/styles/components/_image-uploader.scss +109 -0
  64. package/src/styles/components/_input.scss +165 -0
  65. package/src/styles/components/_tab.scss +74 -0
  66. package/src/styles/components/_tooltip.scss +152 -0
  67. package/src/utils/common.js +13 -0
  68. package/webpack.config.js +7 -0
  69. package/webpack.dev.config.js +7 -0
  70. package/public/logo192.png +0 -0
  71. package/public/logo512.png +0 -0
  72. package/src/logo.svg +0 -1
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+
3
+ import { DragDrop } from "@uppy/react";
4
+
5
+ const LocalUploader = ({ uppy }) => {
6
+ return (
7
+ <DragDrop
8
+ note="Max. File Size: 5MB"
9
+ uppy={uppy}
10
+ locale={{
11
+ strings: {
12
+ dropHereOr: "Drop your file(s) here or %{browse}",
13
+ browse: "Browse",
14
+ },
15
+ }}
16
+ className="local-upload__root"
17
+ />
18
+ );
19
+ };
20
+
21
+ export default LocalUploader;
@@ -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 }) => {
11
- const uppy = new Uppy({
12
- allowMultipleUploads: false,
13
- autoProceed: true,
14
- debug: true,
15
- });
16
-
17
- uppy.use(XHRUpload, {
18
- endpoint: "/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
+ ];
@@ -0,0 +1,66 @@
1
+ import Mention from "@tiptap/extension-mention";
2
+ import tippy from "tippy.js";
3
+ import { ReactRenderer } from "@tiptap/react";
4
+
5
+ import { MentionList } from "./MentionList";
6
+
7
+ import { createMentionSuggestions } from "./helpers";
8
+
9
+ const suggestion = {
10
+ render: () => {
11
+ let reactRenderer;
12
+ let popup;
13
+
14
+ return {
15
+ onStart: (props) => {
16
+ reactRenderer = new ReactRenderer(MentionList, {
17
+ props,
18
+ editor: props.editor,
19
+ });
20
+
21
+ popup = tippy("body", {
22
+ getReferenceClientRect: props.clientRect,
23
+ appendTo: () => document.body,
24
+ content: reactRenderer.element,
25
+ showOnCreate: true,
26
+ interactive: true,
27
+ trigger: "manual",
28
+ placement: "bottom-start",
29
+ });
30
+ },
31
+
32
+ onUpdate(props) {
33
+ reactRenderer.updateProps(props);
34
+
35
+ popup[0].setProps({
36
+ getReferenceClientRect: props.clientRect,
37
+ });
38
+ },
39
+
40
+ onKeyDown(props) {
41
+ if (props.event.key === "Escape") {
42
+ popup[0].hide();
43
+
44
+ return true;
45
+ }
46
+
47
+ return reactRenderer.ref?.onKeyDown(props);
48
+ },
49
+
50
+ onExit() {
51
+ popup[0].destroy();
52
+ reactRenderer.destroy();
53
+ },
54
+ };
55
+ },
56
+ };
57
+
58
+ export default {
59
+ configure: ({ suggestion: suggestionConfig = {}, ...otherConfig }) =>
60
+ Mention.configure({
61
+ ...otherConfig,
62
+ suggestion: { ...suggestion, ...suggestionConfig },
63
+ }),
64
+ };
65
+
66
+ export { createMentionSuggestions };
@@ -0,0 +1,96 @@
1
+ import React from "react";
2
+ import classNames from "classnames";
3
+
4
+ import Avatar from "common/Avatar";
5
+
6
+ export class MentionList extends React.Component {
7
+ state = { selectedIndex: 0 };
8
+
9
+ componentDidUpdate(prevProps) {
10
+ const { items } = this.props;
11
+ if (items !== prevProps.items) {
12
+ this.setState({ selectedIndex: 0 });
13
+ }
14
+ }
15
+
16
+ selectItem = (index) => {
17
+ const { items, command } = this.props;
18
+ const item = items[index];
19
+
20
+ if (item) {
21
+ command({ label: item.label, id: item.key });
22
+ }
23
+ };
24
+
25
+ upHandler = () => {
26
+ const { items } = this.props;
27
+ this.setState((prevState) => {
28
+ const { selectedIndex } = prevState;
29
+ const nextSelectedIndex =
30
+ (selectedIndex + items.length - 1) % items.length;
31
+ return {
32
+ selectedIndex: nextSelectedIndex,
33
+ };
34
+ });
35
+ };
36
+
37
+ downHandler = () => {
38
+ const { items } = this.props;
39
+ this.setState((prevState) => {
40
+ const { selectedIndex } = prevState;
41
+ const nextSelectedIndex = (selectedIndex + 1) % items.length;
42
+ return {
43
+ selectedIndex: nextSelectedIndex,
44
+ };
45
+ });
46
+ };
47
+
48
+ enterHandler = () => {
49
+ const { selectedIndex } = this.state;
50
+ this.selectItem(selectedIndex);
51
+ };
52
+
53
+ onKeyDown = ({ event }) => {
54
+ const keyDownHandlers = {
55
+ ArrowUp: this.upHandler,
56
+ ArrowDown: this.downHandler,
57
+ Enter: this.enterHandler,
58
+ };
59
+
60
+ if (keyDownHandlers.hasOwnProperty(event.key)) {
61
+ keyDownHandlers[event.key]();
62
+ return true;
63
+ }
64
+
65
+ return false;
66
+ };
67
+
68
+ render() {
69
+ const { selectedIndex } = this.state;
70
+ const { items } = this.props;
71
+
72
+ const containerClassName =
73
+ "relative p-2 space-y-1 overflow-hidden rounded shadow editor-command-list--root";
74
+ const itemClassName =
75
+ "flex items-center w-full px-4 py-2 transition-all duration-100 ease-in-out cursor-pointer text-xs text-white rounded editor-command-list--item";
76
+
77
+ return (
78
+ <div className={containerClassName}>
79
+ {items.map(({ label, imageUrl, showImage }, index) => (
80
+ <button
81
+ className={classNames(itemClassName, {
82
+ selected_item: index === selectedIndex,
83
+ })}
84
+ key={label}
85
+ onClick={() => this.selectItem(index)}
86
+ >
87
+ {showImage ? (
88
+ <Avatar user={{ name: label, imageUrl }} className="mr-2" />
89
+ ) : null}
90
+ <span>{label}</span>
91
+ </button>
92
+ ))}
93
+ </div>
94
+ );
95
+ }
96
+ }
@@ -0,0 +1,23 @@
1
+ export const createMentionSuggestions = (
2
+ items = [],
3
+ { limit = 5, showImage = false } = {}
4
+ ) => {
5
+ const allSuggestions = items.map((item) => {
6
+ let suggestionObj;
7
+ if (typeof item === "string") {
8
+ suggestionObj = { key: item, label: item };
9
+ } else if (typeof item === "object") {
10
+ suggestionObj = { ...item };
11
+ }
12
+ suggestionObj.showImage = showImage;
13
+
14
+ return suggestionObj;
15
+ });
16
+
17
+ return ({ query }) =>
18
+ allSuggestions
19
+ .filter((suggestion) =>
20
+ suggestion.label.toLowerCase().startsWith(query.toLowerCase())
21
+ )
22
+ .slice(0, limit);
23
+ };
@@ -0,0 +1,81 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { Decoration, DecorationSet } from "prosemirror-view";
3
+ import { Plugin } from "prosemirror-state";
4
+
5
+ import { placeholderGenerator } from "./helpers";
6
+
7
+ export default Extension.create({
8
+ name: "placeholder",
9
+
10
+ addOptions() {
11
+ return {
12
+ excludeNodeTypes: ["variable"],
13
+ emptyEditorClass: "is-editor-empty",
14
+ emptyNodeClass: "is-empty",
15
+ placeholder: "Write something …",
16
+ showOnlyWhenEditable: true,
17
+ showOnlyCurrent: true,
18
+ includeChildren: false,
19
+ };
20
+ },
21
+
22
+ addProseMirrorPlugins() {
23
+ return [
24
+ new Plugin({
25
+ props: {
26
+ decorations: ({ doc, selection }) => {
27
+ const active =
28
+ this.editor.isEditable || !this.options.showOnlyWhenEditable;
29
+ const { anchor } = selection;
30
+ const decorations = [];
31
+
32
+ if (!active) {
33
+ return;
34
+ }
35
+
36
+ doc.descendants((node, pos) => {
37
+ const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
38
+ const isEmpty = !node.isLeaf && !node.childCount;
39
+
40
+ const isExcluded = this.options.excludeNodeTypes.includes(
41
+ node.type.name
42
+ );
43
+
44
+ if (
45
+ (hasAnchor || !this.options.showOnlyCurrent) &&
46
+ !isExcluded &&
47
+ isEmpty
48
+ ) {
49
+ const classes = [this.options.emptyNodeClass];
50
+
51
+ if (this.editor.isEmpty) {
52
+ classes.push(this.options.emptyEditorClass);
53
+ }
54
+
55
+ const decoration = Decoration.node(pos, pos + node.nodeSize, {
56
+ class: classes.join(" "),
57
+ "data-placeholder":
58
+ typeof this.options.placeholder === "function"
59
+ ? this.options.placeholder({
60
+ editor: this.editor,
61
+ node,
62
+ pos,
63
+ })
64
+ : this.options.placeholder,
65
+ });
66
+
67
+ decorations.push(decoration);
68
+ }
69
+
70
+ return this.options.includeChildren;
71
+ });
72
+
73
+ return DecorationSet.create(doc, decorations);
74
+ },
75
+ },
76
+ }),
77
+ ];
78
+ },
79
+ });
80
+
81
+ export { placeholderGenerator };
@@ -0,0 +1,18 @@
1
+ import isPlainObject from "lodash.isplainobject";
2
+
3
+ export const placeholderGenerator = (placeholder) => {
4
+ const type = typeof placeholder;
5
+
6
+ if (type === "string" || type === "funtion") {
7
+ return placeholder;
8
+ }
9
+
10
+ if (isPlainObject(placeholder)) {
11
+ return ({ node }) => {
12
+ const { name } = node.type;
13
+ return placeholder[name];
14
+ };
15
+ }
16
+
17
+ return "";
18
+ };
@@ -1,18 +1,13 @@
1
1
  import { Extension } from "@tiptap/core";
2
+ import { PluginKey } from "prosemirror-state";
2
3
  import Suggestion from "@tiptap/suggestion";
3
4
 
5
+ export const CommandsPluginKey = new PluginKey("commands");
6
+
4
7
  export default Extension.create({
5
- name: "mention",
8
+ name: "commands",
6
9
 
7
- defaultOptions: {
8
- suggestion: {
9
- char: "/",
10
- startOfLine: false,
11
- command: ({ editor, range, props }) => {
12
- props.command({ editor, range });
13
- },
14
- },
15
- },
10
+ defaultOptions: {},
16
11
 
17
12
  addProseMirrorPlugins() {
18
13
  return [
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import classnames from "classnames";
3
- import "../../styles/CommandsList.scss";
4
3
 
5
4
  class CommandsList extends React.Component {
6
5
  constructor(props) {
@@ -19,7 +18,7 @@ class CommandsList extends React.Component {
19
18
  }
20
19
  }
21
20
 
22
- onKeyDown({ event }) {
21
+ onKeyDown = ({ event }) => {
23
22
  if (event.key === "ArrowUp") {
24
23
  this.upHandler();
25
24
  return true;
@@ -36,45 +35,45 @@ class CommandsList extends React.Component {
36
35
  }
37
36
 
38
37
  return false;
39
- }
38
+ };
40
39
 
41
- upHandler() {
40
+ upHandler = () => {
42
41
  this.setState({
43
42
  selectedIndex:
44
43
  (this.state.selectedIndex + this.props.items.length - 1) %
45
44
  this.props.items.length,
46
45
  });
47
- }
46
+ };
48
47
 
49
- downHandler() {
48
+ downHandler = () => {
50
49
  this.setState({
51
50
  selectedIndex: (this.state.selectedIndex + 1) % this.props.items.length,
52
51
  });
53
- }
52
+ };
54
53
 
55
- enterHandler() {
54
+ enterHandler = () => {
56
55
  this.selectItem(this.state.selectedIndex);
57
- }
56
+ };
58
57
 
59
- selectItem(index) {
58
+ selectItem = (index) => {
60
59
  const item = this.props.items[index];
61
60
 
62
61
  if (item) {
63
62
  const { editor, range } = this.props;
64
63
  item.command({ editor, range });
65
64
  }
66
- }
65
+ };
67
66
 
68
67
  render() {
69
68
  return (
70
- <div className="relative py-2 overflow-hidden bg-gray-900 rounded shadow">
69
+ <div className="relative p-3 space-y-2 overflow-hidden rounded shadow editor-command-list--root">
71
70
  {this.props.items.map((item, index) => (
72
71
  <Item
73
72
  key={item.title}
74
73
  item={item}
75
74
  index={index}
76
75
  selectedIndex={this.state.selectedIndex}
77
- selectItem={this.selectItem}
76
+ selectItem={() => this.selectItem(index)}
78
77
  />
79
78
  ))}
80
79
  </div>
@@ -87,15 +86,15 @@ const Item = ({ item, selectedIndex, index, selectItem }) => {
87
86
  return (
88
87
  <div
89
88
  className={classnames(
90
- "flex items-center w-full px-4 py-2 space-x-4 transition-all duration-100",
89
+ "flex items-center w-full px-4 py-2 space-x-4 transition-all duration-100 ease-in-out editor-command-list--item cursor-pointer rounded",
91
90
  {
92
- "bg-gray-800": index === selectedIndex,
91
+ selected_item: index === selectedIndex,
93
92
  }
94
93
  )}
95
94
  onClick={() => selectItem(index)}
96
95
  >
97
96
  {Icon && (
98
- <div className="p-1 text-gray-100 bg-gray-800 rounded-sm">
97
+ <div className="p-1 text-gray-100 rounded-sm">
99
98
  <Icon size={18} />
100
99
  </div>
101
100
  )}