├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── home.jpg
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.test.tsx
├── App.tsx
├── InlineStylesControls.tsx
├── Title
│ └── title.tsx
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
└── setupTests.ts
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | This project used draft.js and customize some style after typing some keys
4 |
5 | ## How to run
6 |
7 | In the project directory, you can install packages using yran:
8 | ```
9 | yarn install
10 | ```
11 |
12 | ```
13 | yarn start
14 | ```
15 |
16 |
17 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
18 |
19 |
20 | # How this works
21 |
22 | You might see this image once you run the app.
23 |
24 | 
25 |
26 | **Note: There're two ways to set style not only clicking the button shown on the image**
27 |
28 | ## - To set the unset the Bold Style
29 | ### You can set the Bold Style after typing `"* and space"` and unset typing again `"* and space"`
30 |
31 |
32 | ## - Header Style
33 | ### After typing `"# and space"` and unset typing again `"# and space"`
34 |
35 |
36 | ## - Text Color to Red
37 | ### After typing `"** and space"` and unset typing again `"** and space"`
38 |
39 |
40 | ## - UnderLine Style
41 | ### You can set the Bold Style after typing `"*** and space"` and unset typing again `"*** and space"`
42 |
43 |
44 | ## - Save to LocalStorage
45 | ### Cliking Save button will save content on editor to `localstorage` and won't disappear after you refer the page.
46 |
47 |
48 |
49 | ## Result page look like this
50 |
51 |
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-sorcerer",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^13.0.0",
8 | "@testing-library/user-event": "^13.2.1",
9 | "@types/draft-js": "^0.11.10",
10 | "@types/jest": "^27.0.1",
11 | "@types/node": "^16.7.13",
12 | "@types/react": "^18.0.0",
13 | "@types/react-dom": "^18.0.0",
14 | "draft-js": "^0.11.7",
15 | "draft-js-modifiers": "^0.2.2",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "react-scripts": "5.0.1",
19 | "typescript": "^4.4.2",
20 | "web-vitals": "^2.1.0"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "devDependencies": {
47 | "tailwindcss": "^3.3.2"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooljameswei/React-Draft.js-editor-custom/a118b008d1fed6468cfcf9d4d342a2efabe55b76/public/favicon.ico
--------------------------------------------------------------------------------
/public/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooljameswei/React-Draft.js-editor-custom/a118b008d1fed6468cfcf9d4d342a2efabe55b76/public/home.jpg
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooljameswei/React-Draft.js-editor-custom/a118b008d1fed6468cfcf9d4d342a2efabe55b76/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooljameswei/React-Draft.js-editor-custom/a118b008d1fed6468cfcf9d4d342a2efabe55b76/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from "react";
2 | import * as React from 'react'
3 | import logo from "./logo.svg";
4 | import "./App.css";
5 | import Title from "./Title/title";
6 | import { ReactDOM } from "react";
7 | import {convertToRaw, Editor, EditorState, RichUtils, DraftInlineStyle, ContentState, Modifier, convertFromRaw, getDefaultKeyBinding, KeyBindingUtil} from "draft-js";
8 | import "draft-js/dist/Draft.css";
9 | import InlineStyleControls, { inlineStyles } from "./InlineStylesControls";
10 |
11 | const { hasCommandModifier } = KeyBindingUtil;
12 | function App() {
13 | const [editorState, setEditorState] = useState(() =>
14 | EditorState.createEmpty()
15 | );
16 |
17 | useEffect(() => {
18 | // handleKeyCommand = handleKeyCommand.bind();
19 | const savedContentState = localStorage.getItem('editorState');
20 | if(savedContentState != null)
21 | {
22 | const deserializedContentState = JSON.parse(savedContentState);
23 | const updateState = EditorState.createWithContent(convertFromRaw(deserializedContentState));
24 | setEditorState(updateState);
25 | }
26 | }, [])
27 |
28 | //on Editor Change
29 | const handleEditorChange = (newState: any) => {
30 | setEditorState(newState);
31 | const rawContentState = convertToRaw(newState.getCurrentContent());
32 | // let text = rawContentState.blocks[0].text;
33 | let text= '';
34 | rawContentState.blocks.forEach(block => {
35 | text = block.text;
36 |
37 |
38 | let subString, subString2, subString3, last;
39 | last = text.substring(text.length-1, text.length);
40 | let value = last.valueOf();
41 | subString = text.substring(text.length-2, text.length);
42 | subString2 = text.substring(text.length-3, text.length);
43 | subString3 = text.substring(text.length-4, text.length);
44 | if(subString3 == "*** ")
45 | delteLastCharacters(4, text, "UNDERLINE");
46 | else if(subString2 == "** ")
47 | delteLastCharacters(3, text, "FONT_RED");
48 | else {
49 | if(subString == "* ")
50 | delteLastCharacters(2, text, "BOLD");
51 | else if(subString == "# ")
52 | delteLastCharacters(2, text, "FONT_SIZE_30");
53 | }
54 | });
55 |
56 | }
57 |
58 |
59 | //on Save button clicked
60 | const onSave = () => {
61 | const contentState = editorState.getCurrentContent();
62 | const serializedContentState = JSON.stringify(convertToRaw(contentState));
63 | localStorage.setItem('editorState', serializedContentState);
64 | }
65 |
66 | //Set Style as customize
67 | const delteLastCharacters = (len: number, text: string, style: string) => {
68 | toggleInlineStyle(style);
69 | const currentInlineStyle = editorState.getCurrentInlineStyle;
70 | text = `${text.slice(0, text.length-len)}`;
71 | console.log(text);
72 |
73 | const contentState = editorState.getCurrentContent();
74 | const selectionState = editorState.getSelection();
75 | const lastTwoChars = contentState.getBlockForKey(selectionState.getAnchorKey())
76 | .getText()
77 | .slice(len * -1 + 1);
78 |
79 | const newSelection = selectionState.merge({
80 | anchorOffset: selectionState.getFocusOffset() - len + 1,
81 | focusOffset: selectionState.getFocusOffset(),
82 | })
83 | const newContentState = Modifier.replaceText(
84 | contentState,
85 | newSelection,
86 | '',
87 | );
88 |
89 | // Create a new EditorState with the new ContentState and selection
90 | const newEditorState = EditorState.push(
91 | editorState,
92 | newContentState,
93 | 'remove-range'
94 | )
95 | let updatedEditorState = EditorState.forceSelection(
96 | newEditorState,
97 | newContentState.getSelectionAfter()
98 | );
99 |
100 | const newState = RichUtils.toggleInlineStyle(updatedEditorState, style);
101 | const newState2 = RichUtils.toggleBlockType(newState, "header-one");
102 | setEditorState(newState2);
103 | }
104 |
105 | const keyBindingFn = (e: any) => {
106 | // if(e.keyCode == 13)
107 | // return 'default-style';
108 | return getDefaultKeyBinding(e);
109 | }
110 |
111 | const handleKeyCommand = (command: string) => {
112 | if(command === 'default-style')
113 | {
114 | console.log("asdfa");
115 | // Remove all inline styles from the editor
116 |
117 | return "handled";
118 | }
119 | return 'not-handled';
120 | }
121 |
122 | const editor = useRef(null);
123 | const focusEditor = () => {
124 | editor.current?.focus();
125 | };
126 |
127 | const customStyleMap = {
128 | STRIKETHROUGH: {
129 | textDecoration: "line-through"
130 | },
131 | FONT_SIZE_30: {
132 | fontSize: "30px"
133 | },
134 | FONT_RED: {
135 | color: "red"
136 | }
137 | };
138 |
139 | const toggleInlineStyle = (inlineStyle: string) => {
140 | const newState = RichUtils.toggleInlineStyle(editorState, inlineStyle);
141 | const newState2 = RichUtils.toggleBlockType(newState, "header-one");
142 | setEditorState(newState2);
143 | };
144 |
145 | return (
146 |
147 |
148 |
156 |
160 |
161 |
170 |
171 |
172 | );
173 | }
174 |
175 | export default App;
176 |
--------------------------------------------------------------------------------
/src/InlineStylesControls.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const inlineStyles = [
4 | { label: "B", desc: "font-bold", style: "BOLD" },
5 | { label: "I", desc: "italic", style: "ITALIC" },
6 | { label: "U", desc: "underline", style: "UNDERLINE" },
7 | { label: "S", desc: "line-through", style: "STRIKETHROUGH" },
8 | { label: "H1", style: "FONT_SIZE_30", desc: "heading" },
9 | { label: "R", style: "FONT_RED", desc: "text-red-600"}
10 | ];
11 |
12 |
13 | interface InlineStyleControlsProps {
14 | currentInlineStyle: any;
15 | onToggle: (inlineStyle: string) => void;
16 | }
17 |
18 | const InlineStyleControls = ({ currentInlineStyle, onToggle}: InlineStyleControlsProps) => {
19 | return (
20 |
21 | {inlineStyles.map(type => (
22 |
35 | ))}
36 |
37 | );
38 | };
39 |
40 | export default InlineStyleControls;
41 |
--------------------------------------------------------------------------------
/src/Title/title.tsx:
--------------------------------------------------------------------------------
1 | import { convertToRaw } from "draft-js";
2 | import React from "react";
3 |
4 |
5 |
6 | interface titleProps {
7 | onSave: () => void;
8 | text: any;
9 | }
10 |
11 | const Title = (props: titleProps) => {
12 | const onSave = () => {
13 | const contentState = props.text.getCurrentContent();
14 | const serializedContentState = JSON.stringify(convertToRaw(contentState));
15 | localStorage.setItem('editorState', serializedContentState);
16 | }
17 | return (
18 | <>
19 |
20 |
Demo editor by Bryan
21 |
22 |
23 |
24 |
25 | >
26 | );
27 | }
28 |
29 | export default Title;
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | margin: 0;
7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
9 | sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | }
13 |
14 | code {
15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
16 | monospace;
17 | }
18 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById('root') as HTMLElement
9 | );
10 | root.render(
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/**/*.{js,jsx,ts,tsx}",
5 | ],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [],
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------