feat: add first version of nocode editor without preview/publish/share

This commit is contained in:
Matthias Nannt
2022-06-20 20:04:23 +09:00
parent b622510dc4
commit ae5ae7b46c
20 changed files with 557 additions and 131 deletions

View File

@@ -0,0 +1,52 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { TailSpin } from "react-loader-spinner";
export default function LoadingModal({ isLoading }) {
return (
<Transition.Root show={isLoading} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 z-10 overflow-y-auto"
open={isLoading}
onClose={() => {}}
>
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-20" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-flex items-center justify-center px-4 py-20 pb-4 overflow-hidden text-left align-bottom transition-all transform rounded-lg sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
<TailSpin color="#000" height={50} width={50} />
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
}

View File

@@ -1,49 +1,104 @@
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { useNoCodeForm } from "../../lib/noCodeForm";
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
import Loading from "../Loading";
import Page from "./Page";
import UsageIntro from "./UsageIntro";
import LoadingModal from "../LoadingModal";
export default function Builder({ formId }) {
const { noCodeForm, mutateNoCodeForm, isLoadingNoCodeForm } =
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
useNoCodeForm(formId);
const [pagesDraft, setPagesDraft] = useState([]);
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const save = async () => {
setIsLoading(true);
const newNoCodeForm = JSON.parse(JSON.stringify(noCodeForm));
newNoCodeForm.pagesDraft = pagesDraft;
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
setIsLoading(false);
};
const addPage = useCallback(() => {
if (noCodeForm) {
const updatedNCF = JSON.parse(JSON.stringify(noCodeForm));
updatedNCF.pages.push({
id: uuidv4(),
elements: [],
});
mutateNoCodeForm(updatedNCF, false);
const newPagesDraft = JSON.parse(JSON.stringify(pagesDraft));
newPagesDraft.push({
id: uuidv4(),
blocks: [],
});
setPagesDraft(newPagesDraft);
}, [pagesDraft, setPagesDraft]);
const deletePage = (pageIdx) => {
const newPagesDraft = JSON.parse(JSON.stringify(pagesDraft));
newPagesDraft.splice(pageIdx, 1);
setPagesDraft(newPagesDraft);
};
const initPages = useCallback(() => {
if (!isLoadingNoCodeForm && !isInitialized) {
if (noCodeForm.pagesDraft.length === 0) {
addPage();
} else {
setPagesDraft(noCodeForm.pagesDraft);
}
setIsInitialized(true);
}
}, [mutateNoCodeForm, noCodeForm]);
}, [isLoadingNoCodeForm, noCodeForm, addPage, isInitialized]);
useEffect(() => {
if (noCodeForm && noCodeForm.pages.length === 0) addPage();
}, [noCodeForm]);
initPages();
}, [isLoadingNoCodeForm, initPages]);
if (isLoadingNoCodeForm) {
return <Loading />;
<Loading />;
}
return (
<div className="w-full bg-gray-100">
<div className="flex justify-center w-full mt-10">
<div className="w-full max-w-5xl">
<div className="grid grid-cols-1 gap-6">
{noCodeForm.pages.map((page) => (
<Page key={page.id} />
))}
</div>
<button
onClick={() => addPage()}
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 text-sm font-medium text-gray-700 border border-gray-300 border-dashed rounded-md bg-gray-50 hover:bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500"
>
+ Add Page
</button>
<>
<div className="relative z-10 flex flex-shrink-0 h-16 border-b border-gray-200 shadow-inner bg-gray-50">
<div className="flex items-center justify-center flex-1 px-4">
<nav className="flex space-x-4" aria-label="resultModes">
<button
onClick={() => save()}
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
>
Save
</button>
<button
onClick={() => addPage()}
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
>
Add Page
</button>
</nav>
</div>
</div>
</div>
<div className="w-full bg-gray-100">
<div className="flex justify-center w-full mt-10">
<div className="w-full px-4 max-w-7xl">
<div className="grid grid-cols-1 gap-6">
<div className="px-10">
<UsageIntro />
</div>
{pagesDraft.map((page, pageIdx) => (
<Page
key={page.id}
page={page}
pageIdx={pageIdx}
pagesDraft={pagesDraft}
setPagesDraft={setPagesDraft}
deletePageAction={deletePage}
/>
))}
</div>
</div>
</div>
</div>
<LoadingModal isLoading={isLoading} />
</>
);
}

View File

@@ -1,38 +0,0 @@
import { useCallback, useRef } from "react";
import { createReactEditorJS } from "react-editor-js";
const ReactEditorJS = createReactEditorJS();
const Editor = ({}) => {
const editorCore = useRef(null);
const handleInitialize = useCallback((instance) => {
editorCore.current = instance;
}, []);
/* const handleSave = useCallback(async () => {
const savedData = await editorCore.current.save();
console.log(savedData);
}, []);
setTimeout(() => {
// save every ten seconds
handleSave();
}, 10000); */
const EDITOR_JS_TOOLS = {};
// Editor.js This will show block editor in component
// pass EDITOR_JS_TOOLS in tools props to configure tools with editor.js
return (
<ReactEditorJS
onInitialize={handleInitialize}
tools={EDITOR_JS_TOOLS}
minHeight={0}
/>
);
};
// Return the CustomEditor to use by other components.
export default Editor;

View File

@@ -1,12 +1,56 @@
import { TrashIcon } from "@heroicons/react/outline";
import dynamic from "next/dynamic";
let Editor = dynamic(() => import("./Editor"), {
let Editor = dynamic(() => import("../editorjs/Editor"), {
ssr: false,
});
export default function Page({}) {
export default function Page({
page,
pageIdx,
pagesDraft,
setPagesDraft,
deletePageAction,
}) {
const updatePage = (blocks) => {
const newPagesDraft = JSON.parse(JSON.stringify(pagesDraft));
if (pageIdx < newPagesDraft.length) {
newPagesDraft[pageIdx].blocks = blocks;
setPagesDraft(newPagesDraft);
} else {
throw Error(
`updatePage error: Page at position ${pageIdx} not found in pagesDraft`
);
}
};
return (
<div className="w-full p-10 bg-white rounded-lg">
{Editor && <Editor />}
<div className="flex w-full">
<div className="flex w-8">
{pageIdx !== 0 && (
<button
className="flex items-center h-full text-gray-400"
onClick={() => {
if (confirm("Do you really want to delete this page?")) {
deletePageAction(pageIdx);
}
}}
>
<TrashIcon className="w-5 h-5" />
</button>
)}
</div>
<div className="relative w-full p-10 bg-white rounded-lg">
<div className="relative">
{Editor && (
<Editor
id={`${page.id}-editor`}
autofocus={pageIdx === 0}
onChange={(blocks) => updatePage(blocks)}
value={pagesDraft[pageIdx]}
/>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
/* This example requires Tailwind CSS v2.0+ */
import { InformationCircleIcon } from "@heroicons/react/solid";
import { useState } from "react";
export default function UsageIntro() {
const [dismissed, setDismissed] = useState(false);
return (
!dismissed && (
<div className="p-4 bg-gray-100 border border-gray-700 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="w-5 h-5 text-blue-400"
aria-hidden="true"
/>
</div>
<div className="flex-1 ml-3 md:flex md:justify-between">
<p className="text-sm text-gray-700">
Welcome to the snoopForms No-Code Editor. Use &apos;tab&apos; to
add new blocks or change their options. You can also drag &apos;n
drop blocks to reorder them.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<a
onClick={() => setDismissed(true)}
className="font-medium text-gray-700 whitespace-nowrap hover:text-gray-600"
>
Dismiss
</a>
</p>
</div>
</div>
</div>
)
);
}

View File

@@ -0,0 +1,57 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Fragment, useEffect, useRef, useState } from "react";
import EditorJS from "@editorjs/editorjs";
import DragDrop from "editorjs-drag-drop";
import Undo from "editorjs-undo";
import TextQuestion from "./tools/TextQuestion";
const Editor = ({ id, autofocus = false, onChange, value }) => {
const [blocks, setBlocks] = useState([]);
const ejInstance = useRef<EditorJS | null>();
useEffect(() => {
onChange(blocks);
}, [blocks]);
// This will run only once
useEffect(() => {
if (!ejInstance.current) {
initEditor();
}
return () => {
destroyEditor();
};
async function destroyEditor() {
await ejInstance.current.isReady;
ejInstance.current.destroy();
ejInstance.current = null;
}
}, []);
const initEditor = () => {
const editor = new EditorJS({
minHeight: 0,
holder: id,
data: value,
onReady: () => {
ejInstance.current = editor;
new DragDrop(editor);
new Undo({ editor });
},
onChange: async () => {
let content = await editor.saver.save();
setBlocks(content.blocks);
},
autofocus: autofocus,
tools: { textQuestion: TextQuestion },
});
};
return (
<Fragment>
<div id={id} />
</Fragment>
);
};
export default Editor;

View File

@@ -0,0 +1,77 @@
import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs";
import ReactDOM from "react-dom";
//styles imports in angular.json
interface TextQuestionData extends BlockToolData {
latexString: string;
}
export default class TextQuestion implements BlockTool {
label: string;
placeholder: string;
api: API;
static get toolbox(): { icon: string; title?: string } {
return {
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-justify"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>`,
title: "Text Question",
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: TextQuestionData;
}) {
this.label = data.label;
this.placeholder = data.placeholder;
}
save(block: HTMLDivElement) {
// console.log(block)
// ;(window as any).x = block
return {
label: (block.firstElementChild.firstElementChild as HTMLInputElement)
.value,
placeholder: (
block.firstElementChild.lastElementChild as HTMLInputElement
).value,
};
}
/* renderSettings(): HTMLElement {
return document.createElement("div");
} */
render(): HTMLElement {
/* this.wrapperDiv.innerHTML = "";
this.latexDiv.innerHTML = "";
this.renderLatex();
this.wrapperDiv.append(this.latexDiv);
this.wrapperDiv.append(this.editTextfield);
return this.wrapperDiv; */
const container = document.createElement("div");
const toolView = (
<div className="pb-3">
<input
type="text"
id="label"
defaultValue={this.label}
className="block p-0 text-sm font-medium text-gray-700 border-0 border-transparent ring-0 focus:ring-0"
placeholder="Your Question"
/>
<input
type="text"
className="block w-full mt-1 text-gray-400 border-gray-300 rounded-md shadow-sm sm:text-sm placeholder:text-gray-300"
placeholder="optional placeholder"
defaultValue={this.placeholder}
/>
</div>
);
ReactDOM.render(toolView, container);
return container;
}
}

View File

@@ -35,7 +35,7 @@ const libs = [
},
];
export default function FormCode() {
export default function FormCode({ formId }) {
const [selectedLib, setSelectedLib] = useState(null);
return (
@@ -54,6 +54,32 @@ export default function FormCode() {
programming language or framework.
</p>
</div>
<hr className="my-5 text-gray-600" />
<div>
<label htmlFor="formId" className="block text-base text-gray-800">
Your form ID
</label>
<div className="mt-5 sm:flex sm:items-center">
<div className="w-full sm:max-w-xs">
<input
id="formId"
type="text"
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-snoopred-500 focus:border-snoopred-500 sm:text-sm disabled:bg-gray-100"
value={formId}
disabled
/>
</div>
<button
onClick={() => {
navigator.clipboard.writeText(formId);
}}
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 font-medium text-white bg-gray-800 border border-transparent rounded-md shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Copy
</button>
</div>
</div>
<hr className="my-5 text-gray-600 " />
<div className="mt-5">
<RadioGroup value={selectedLib} onChange={setSelectedLib}>
<RadioGroup.Label className="text-xs font-medium tracking-wide text-gray-500 uppercase">

View File

@@ -40,18 +40,6 @@ export default function LayoutFormResults({
</div>
</div>
</div>
<div className="relative z-10 flex flex-shrink-0 h-16 border-b border-gray-200 shadow-inner bg-gray-50">
<div className="flex items-center justify-center flex-1 px-4">
<nav className="flex space-x-4" aria-label="resultModes">
<button
onClick={() => {}}
className="px-3 py-2 text-sm font-medium text-gray-600 border border-gray-800 rounded-md hover:text-gray-600"
>
Save
</button>
</nav>
</div>
</div>
</header>
{/* Main content */}