ui: implement full editor

This commit is contained in:
thecodrr
2019-11-25 19:12:24 +05:00
parent 481dc7adc7
commit 43489a6532
8 changed files with 1677 additions and 849 deletions

View File

@@ -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"
},

View 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;
}

View 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;

View 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;

View 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;

View File

@@ -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>
);

View File

@@ -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: {

File diff suppressed because it is too large Load Diff