mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +01:00
ui: implement full editor
This commit is contained in:
@@ -4,9 +4,14 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"emotion-theming": "^10.0.19",
|
||||
"quill-blot-formatter": "^1.0.5",
|
||||
"quill-emoji": "^0.1.7",
|
||||
"quill-magic-url": "^1.0.3",
|
||||
"quill-markdown-shortcuts": "^0.0.10",
|
||||
"react": "^16.11.0",
|
||||
"react-dom": "^16.11.0",
|
||||
"react-feather": "^2.0.3",
|
||||
"react-quill": "^1.3.3",
|
||||
"react-scripts": "3.2.0",
|
||||
"rebass": "^4.0.7"
|
||||
},
|
||||
|
||||
45
apps/web/src/Components/Editor/editor.css
Normal file
45
apps/web/src/Components/Editor/editor.css
Normal file
@@ -0,0 +1,45 @@
|
||||
.quill {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.ql-container.ql-snow {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ql-container.ql-snow {
|
||||
overflow-y: none;
|
||||
}
|
||||
|
||||
/*Quill Toolbar*/
|
||||
|
||||
.ql-toolbar.ql-snow svg {
|
||||
height: 22px !important;
|
||||
width: 22px;
|
||||
}
|
||||
.ql-toolbar.ql-snow span {
|
||||
padding-top: 1px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.ql-formats {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.ql-header.ql-picker {
|
||||
width: 120px !important;
|
||||
}
|
||||
.ql-toolbar.ql-snow {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #f0f0f0 !important;
|
||||
border-top: 1px solid #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
border: none;
|
||||
font-family: Quicksand, sans-serif;
|
||||
font-weight: 500;
|
||||
padding: 12px 15px;
|
||||
font-size: 42px;
|
||||
}
|
||||
.editor-title:focus {
|
||||
outline: none;
|
||||
}
|
||||
56
apps/web/src/Components/Editor/index.js
Normal file
56
apps/web/src/Components/Editor/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useEffect } from "react";
|
||||
import "./editor.css";
|
||||
import ReactQuill, { Quill } from "react-quill";
|
||||
import "react-quill/dist/quill.bubble.css";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
import { Box } from "rebass";
|
||||
import MarkdownShortcuts from "./modules/markdown";
|
||||
import MagicUrl from "quill-magic-url";
|
||||
|
||||
Quill.register("modules/markdownShortcuts", MarkdownShortcuts);
|
||||
Quill.register("modules/magicUrl", MagicUrl);
|
||||
|
||||
const modules = {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, 4, 5, 6] }],
|
||||
["bold", "italic", "underline", "strike", "blockquote", { align: [] }],
|
||||
[
|
||||
{ list: "ordered" },
|
||||
{ list: "bullet" },
|
||||
{ indent: "-1" },
|
||||
{ indent: "+1" }
|
||||
],
|
||||
[{ size: ["small", false, "large", "huge"] }],
|
||||
["code-block", { script: "sub" }, { script: "super" }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
["link", "image", "video"],
|
||||
[{ direction: "rtl" }, "clean"]
|
||||
],
|
||||
markdownShortcuts: {},
|
||||
magicUrl: true
|
||||
};
|
||||
|
||||
const Editor = props => {
|
||||
useEffect(() => {
|
||||
// move the toolbar outside (easiest way)
|
||||
const toolbar = document.querySelector(".ql-toolbar.ql-snow");
|
||||
const quill = document.querySelector(".quill");
|
||||
const editor = document.querySelector(".editor");
|
||||
if (toolbar && quill && editor) {
|
||||
editor.appendChild(toolbar);
|
||||
editor.appendChild(quill);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Box
|
||||
className="editor"
|
||||
display="flex"
|
||||
flex="1 1 auto"
|
||||
flexDirection="column"
|
||||
>
|
||||
<input className="editor-title" placeholder="Untitled" />
|
||||
<ReactQuill modules={modules} theme="snow" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default Editor;
|
||||
9
apps/web/src/Components/Editor/modules/markdown/hr.js
Normal file
9
apps/web/src/Components/Editor/modules/markdown/hr.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Quill from "quill";
|
||||
|
||||
let BlockEmbed = Quill.import("blots/block/embed");
|
||||
|
||||
class HorizontalRule extends BlockEmbed {}
|
||||
HorizontalRule.blotName = "hr";
|
||||
HorizontalRule.tagName = "hr";
|
||||
|
||||
export default HorizontalRule;
|
||||
328
apps/web/src/Components/Editor/modules/markdown/index.js
Normal file
328
apps/web/src/Components/Editor/modules/markdown/index.js
Normal file
@@ -0,0 +1,328 @@
|
||||
// Quill.js Plugin - Markdown Shortcuts
|
||||
// This is a module for the Quill.js WYSIWYG editor (https://quilljs.com/)
|
||||
// which converts text entered as markdown to rich text.
|
||||
//
|
||||
// v0.0.5
|
||||
//
|
||||
// Author: Patrick Lee (me@patricklee.nyc)
|
||||
//
|
||||
// (c) Copyright 2017 Patrick Lee (me@patricklee.nyc).
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Quill from "quill";
|
||||
import HorizontalRule from "./hr";
|
||||
|
||||
Quill.register("formats/horizontal", HorizontalRule);
|
||||
|
||||
class MarkdownShortcuts {
|
||||
constructor(quill, options) {
|
||||
this.quill = quill;
|
||||
this.options = options;
|
||||
|
||||
this.ignoreTags = ["PRE"];
|
||||
this.matches = [
|
||||
{
|
||||
name: "header",
|
||||
pattern: /^(#){1,6}\s/g,
|
||||
action: (text, selection, pattern) => {
|
||||
var match = pattern.exec(text);
|
||||
if (!match) return;
|
||||
const size = match[0].length;
|
||||
// Need to defer this action https://github.com/quilljs/quill/issues/1134
|
||||
setTimeout(() => {
|
||||
this.quill.formatLine(selection.index, 0, "header", size - 1);
|
||||
this.quill.deleteText(selection.index - size, size);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "blockquote",
|
||||
pattern: /^(>)\s/g,
|
||||
action: (text, selection) => {
|
||||
// Need to defer this action https://github.com/quilljs/quill/issues/1134
|
||||
setTimeout(() => {
|
||||
this.quill.formatLine(selection.index, 1, "blockquote", true);
|
||||
this.quill.deleteText(selection.index - 2, 2);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "code-block",
|
||||
pattern: /^`{3}(?:\s|\n)/g,
|
||||
action: (text, selection) => {
|
||||
// Need to defer this action https://github.com/quilljs/quill/issues/1134
|
||||
setTimeout(() => {
|
||||
this.quill.formatLine(selection.index, 1, "code-block", true);
|
||||
this.quill.deleteText(selection.index - 4, 4);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "bolditalic",
|
||||
pattern: /(?:\*|_){3}(.+?)(?:\*|_){3}/g,
|
||||
action: (text, selection, pattern, lineStart) => {
|
||||
let match = pattern.exec(text);
|
||||
|
||||
const annotatedText = match[0];
|
||||
const matchedText = match[1];
|
||||
const startIndex = lineStart + match.index;
|
||||
|
||||
if (text.match(/^([*_ \n]+)$/g)) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.quill.deleteText(startIndex, annotatedText.length);
|
||||
this.quill.insertText(startIndex, matchedText, {
|
||||
bold: true,
|
||||
italic: true
|
||||
});
|
||||
this.quill.format("bold", false);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "bold",
|
||||
pattern: /(?:\*|_){2}(.+?)(?:\*|_){2}/g,
|
||||
action: (text, selection, pattern, lineStart) => {
|
||||
let match = pattern.exec(text);
|
||||
|
||||
const annotatedText = match[0];
|
||||
const matchedText = match[1];
|
||||
const startIndex = lineStart + match.index;
|
||||
|
||||
if (text.match(/^([*_ \n]+)$/g)) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.quill.deleteText(startIndex, annotatedText.length);
|
||||
this.quill.insertText(startIndex, matchedText, { bold: true });
|
||||
this.quill.format("bold", false);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
pattern: /(?:\*|_){1}(.+?)(?:\*|_){1}/g,
|
||||
action: (text, selection, pattern, lineStart) => {
|
||||
let match = pattern.exec(text);
|
||||
|
||||
const annotatedText = match[0];
|
||||
const matchedText = match[1];
|
||||
const startIndex = lineStart + match.index;
|
||||
|
||||
if (text.match(/^([*_ \n]+)$/g)) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.quill.deleteText(startIndex, annotatedText.length);
|
||||
this.quill.insertText(startIndex, matchedText, { italic: true });
|
||||
this.quill.format("italic", false);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "strikethrough",
|
||||
pattern: /(?:~~)(.+?)(?:~~)/g,
|
||||
action: (text, selection, pattern, lineStart) => {
|
||||
let match = pattern.exec(text);
|
||||
|
||||
const annotatedText = match[0];
|
||||
const matchedText = match[1];
|
||||
const startIndex = lineStart + match.index;
|
||||
|
||||
if (text.match(/^([*_ \n]+)$/g)) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.quill.deleteText(startIndex, annotatedText.length);
|
||||
this.quill.insertText(startIndex, matchedText, { strike: true });
|
||||
this.quill.format("strike", false);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
pattern: /(?:`)(.+?)(?:`)/g,
|
||||
action: (text, selection, pattern, lineStart) => {
|
||||
let match = pattern.exec(text);
|
||||
|
||||
const annotatedText = match[0];
|
||||
const matchedText = match[1];
|
||||
const startIndex = lineStart + match.index;
|
||||
|
||||
if (text.match(/^([*_ \n]+)$/g)) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.quill.deleteText(startIndex, annotatedText.length);
|
||||
this.quill.insertText(startIndex, matchedText, { code: true });
|
||||
this.quill.format("code", false);
|
||||
this.quill.insertText(this.quill.getSelection(), " ");
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "hr",
|
||||
pattern: /^([-*]\s?){3}/g,
|
||||
action: (text, selection) => {
|
||||
const startIndex = selection.index - text.length;
|
||||
setTimeout(() => {
|
||||
this.quill.deleteText(startIndex, text.length);
|
||||
|
||||
this.quill.insertEmbed(
|
||||
startIndex + 1,
|
||||
"hr",
|
||||
true,
|
||||
Quill.sources.USER
|
||||
);
|
||||
this.quill.insertText(startIndex + 2, "\n", Quill.sources.SILENT);
|
||||
this.quill.setSelection(startIndex + 2, Quill.sources.SILENT);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "asterisk-ul",
|
||||
pattern: /^(\*|\+)\s$/g,
|
||||
action: (text, selection, pattern) => {
|
||||
setTimeout(() => {
|
||||
let index = selection.index;
|
||||
this.quill.formatLine(index, 1, "list", "unordered");
|
||||
console.log(text, selection, pattern);
|
||||
if (text.trim() === "*") {
|
||||
this.quill.deleteText(index, 1);
|
||||
} else if (text.trim() === "+") {
|
||||
this.quill.deleteText(index - 2, 2);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
pattern: /(?:!\[(.+?)\])(?:\((.+?)\))/g,
|
||||
action: (text, selection, pattern) => {
|
||||
const startIndex = text.search(pattern);
|
||||
const matchedText = text.match(pattern)[0];
|
||||
// const hrefText = text.match(/(?:!\[(.*?)\])/g)[0]
|
||||
const hrefLink = text.match(/(?:\((.*?)\))/g)[0];
|
||||
const start = selection.index - matchedText.length - 1;
|
||||
if (startIndex !== -1) {
|
||||
setTimeout(() => {
|
||||
this.quill.deleteText(start, matchedText.length);
|
||||
this.quill.insertEmbed(
|
||||
start,
|
||||
"image",
|
||||
hrefLink.slice(1, hrefLink.length - 1)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "link",
|
||||
pattern: /(?:\[(.+?)\])(?:\((.+?)\))/g,
|
||||
action: (text, selection, pattern) => {
|
||||
const startIndex = text.search(pattern);
|
||||
const matchedText = text.match(pattern)[0];
|
||||
const hrefText = text.match(/(?:\[(.*?)\])/g)[0];
|
||||
const hrefLink = text.match(/(?:\((.*?)\))/g)[0];
|
||||
const start = selection.index - matchedText.length - 1;
|
||||
if (startIndex !== -1) {
|
||||
setTimeout(() => {
|
||||
this.quill.deleteText(start, matchedText.length);
|
||||
this.quill.insertText(
|
||||
start,
|
||||
hrefText.slice(1, hrefText.length - 1),
|
||||
"link",
|
||||
hrefLink.slice(1, hrefLink.length - 1)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Handler that looks for insert deltas that match specific characters
|
||||
this.quill.on("text-change", (delta, oldContents, source) => {
|
||||
// remove formatting when there's no text left
|
||||
if (
|
||||
delta.ops[0].delete !== undefined &&
|
||||
source == "user" &&
|
||||
this.quill.getLength() <= 2
|
||||
) {
|
||||
this.quill.removeFormat(0, 1);
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < delta.ops.length; i++) {
|
||||
if (delta.ops[i].hasOwnProperty("insert")) {
|
||||
if (delta.ops[i].insert === " ") {
|
||||
this.onSpace();
|
||||
} else if (delta.ops[i].insert === "\n") {
|
||||
this.onEnter();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isValid(text, tagName) {
|
||||
return (
|
||||
typeof text !== "undefined" &&
|
||||
text &&
|
||||
this.ignoreTags.indexOf(tagName) === -1
|
||||
);
|
||||
}
|
||||
|
||||
onSpace() {
|
||||
const selection = this.quill.getSelection();
|
||||
if (!selection) return;
|
||||
const [line, offset] = this.quill.getLine(selection.index);
|
||||
const text = line.domNode.textContent;
|
||||
const lineStart = selection.index - offset;
|
||||
if (this.isValid(text, line.domNode.tagName)) {
|
||||
for (let match of this.matches) {
|
||||
const matchedText = text.match(match.pattern);
|
||||
if (matchedText) {
|
||||
// We need to replace only matched text not the whole line
|
||||
match.action(text, selection, match.pattern, lineStart);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEnter() {
|
||||
let selection = this.quill.getSelection();
|
||||
if (!selection) return;
|
||||
const [line, offset] = this.quill.getLine(selection.index);
|
||||
const text = line.domNode.textContent + " ";
|
||||
const lineStart = selection.index - offset;
|
||||
selection.length = selection.index++;
|
||||
if (this.isValid(text, line.domNode.tagName)) {
|
||||
for (let match of this.matches) {
|
||||
const matchedText = text.match(match.pattern);
|
||||
if (matchedText) {
|
||||
match.action(text, selection, match.pattern, lineStart);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (window.Quill) {
|
||||
window.Quill.register("modules/markdownShortcuts", MarkdownShortcuts);
|
||||
}
|
||||
|
||||
export default MarkdownShortcuts;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import "./app.css";
|
||||
import SideBar from "./Components/SideBar";
|
||||
import Editor from "./Components/Editor";
|
||||
import { ThemeProvider } from "emotion-theming";
|
||||
import { Flex, Box, Text, Button, Card } from "rebass";
|
||||
import { Flex, Box, Text, Button, Card, Heading } from "rebass";
|
||||
import * as Icon from "react-feather";
|
||||
import theme from "./theme";
|
||||
|
||||
@@ -29,27 +29,29 @@ const NavMenuItem = props => (
|
||||
}}
|
||||
/>
|
||||
<props.item.icon
|
||||
size={15}
|
||||
style={{ marginTop: 2 /* correction with Quicksand font */ }}
|
||||
size={25}
|
||||
stroke-width={1.3}
|
||||
style={{ marginRight: 3 }}
|
||||
/>
|
||||
<Text sx={{ fontSize: 15, marginLeft: 1 }}>{props.item.title}</Text>
|
||||
{/* <Text sx={{ fontSize: 15, marginLeft: 1 }}>{props.item.title}</Text> */}
|
||||
</Flex>
|
||||
</Button>
|
||||
);
|
||||
|
||||
function App() {
|
||||
const navItems = [
|
||||
{ title: "Arkane", icon: Icon.User },
|
||||
{ title: "Home", icon: Icon.Home },
|
||||
{ title: "Notebooks", icon: Icon.Book },
|
||||
{ title: "Folders", icon: Icon.Folder },
|
||||
{ title: "Lists", icon: Icon.List },
|
||||
{ title: "Get Pro", icon: Icon.Star }
|
||||
];
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [selectedIndex, setSelectedIndex] = useState(1);
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Flex height="100%" alignContent="stretch">
|
||||
<Box width="13%" bg="navbg" px={0}>
|
||||
<Box width="4%" bg="navbg" px={0}>
|
||||
{navItems.map((item, index) => (
|
||||
<NavMenuItem
|
||||
onSelected={() => setSelectedIndex(index)}
|
||||
@@ -59,9 +61,10 @@ function App() {
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box px={2} py={2}>
|
||||
<Card />
|
||||
</Box>
|
||||
<Flex flex="1 1 auto" flexDirection="row" alignContent="stretch" px={0}>
|
||||
<Box bg="#fbfbfb" flex="1 1 auto" px={0}></Box>
|
||||
<Editor />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,12 @@ export default {
|
||||
breakpoints: ["40em", "52em", "64em"],
|
||||
colors: {
|
||||
primary: "white",
|
||||
fontPrimary: "black",
|
||||
accent: "#1790F3",
|
||||
navbg: "#f0f0f0",
|
||||
transparent: "#00000000"
|
||||
transparent: "#00000000",
|
||||
//font related
|
||||
fontPrimary: "black",
|
||||
fontSecondary: "white"
|
||||
},
|
||||
fontSizes: [12, 14, 16, 20, 24, 32, 48, 64],
|
||||
spaces: [0, 5, 10, 15, 20, 25],
|
||||
@@ -26,7 +28,7 @@ export default {
|
||||
},
|
||||
radii: {
|
||||
none: 0,
|
||||
default: 5
|
||||
default: 10
|
||||
},
|
||||
buttons: {
|
||||
nav: {
|
||||
|
||||
2052
apps/web/yarn.lock
2052
apps/web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user