diff --git a/packages/next.js/.env.example b/packages/next.js/.env.example
new file mode 100644
index 000000000..e3754a217
--- /dev/null
+++ b/packages/next.js/.env.example
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_PROVIDER_URL='ws://localhost:2300/collaboration'
+NEXT_PUBLIC_RESTAPI_URL='http://localhost:2300/api'
diff --git a/packages/next.js/.gitignore b/packages/next.js/.gitignore
new file mode 100644
index 000000000..d435dcd9e
--- /dev/null
+++ b/packages/next.js/.gitignore
@@ -0,0 +1,48 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+.env*.production
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# next-pwa
+/public/precache.*.*.js
+/public/sw.js
+/public/workbox-*.js
+/public/worker-*.js
+/public/fallback-*.js
+/public/precache.*.*.js.map
+/public/sw.js.map
+/public/workbox-*.js.map
+/public/worker-*.js.map
+/public/fallback-*.js
diff --git a/packages/next.js/README.md b/packages/next.js/README.md
new file mode 100644
index 000000000..c2c18f499
--- /dev/null
+++ b/packages/next.js/README.md
@@ -0,0 +1,23 @@
+# Progressive Web App Example
+
+This example uses [`next-pwa`](https://github.com/shadowwalker/next-pwa) to create a progressive web app (PWA) powered by [Workbox](https://developers.google.com/web/tools/workbox/).
+
+## Deploy your own
+
+Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
+
+[](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/progressive-web-app&project-name=progressive-web-app&repository-name=progressive-web-app)
+
+## How to use
+
+Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
+
+```bash
+npx create-next-app --example progressive-web-app progressive-web-app
+# or
+yarn create next-app --example progressive-web-app progressive-web-app
+# or
+pnpm create next-app --example progressive-web-app progressive-web-app
+```
+
+Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
diff --git a/packages/next.js/components/Button.jsx b/packages/next.js/components/Button.jsx
new file mode 100644
index 000000000..a5a5cd915
--- /dev/null
+++ b/packages/next.js/components/Button.jsx
@@ -0,0 +1,21 @@
+const Button = ({ children, style, onClick: click, loading, className }) => {
+ return (
+
+ {loading
+ ? <>
+
+
+
+
+ Processing...
+ >
+ : children}
+
+ )
+}
+
+export default Button
diff --git a/packages/next.js/components/Counter.jsx b/packages/next.js/components/Counter.jsx
new file mode 100644
index 000000000..8ae47ae5e
--- /dev/null
+++ b/packages/next.js/components/Counter.jsx
@@ -0,0 +1,37 @@
+
+import { useEffect, useRef, useState } from 'react'
+
+const Counter = ({ seconds, callback, stopTimer }) => {
+ const [timeLeft, setTimeLeft] = useState(+seconds)
+ const intervalRef = useRef()
+
+ useEffect(() => {
+ intervalRef.current = setInterval(() => {
+ setTimeLeft((t) => t - 1)
+ }, 1000)
+
+ return () => {
+ console.log('aklsdlaks;dlk close')
+ clearInterval(intervalRef.current)
+ }
+ }, [])
+
+ // useEffect(() => {
+ // console.log("stopTimer changed", stopTimer)
+ // }, [stopTimer])
+
+ useEffect(() => {
+ if (timeLeft <= 0) {
+ clearInterval(intervalRef.current)
+ if (callback) callback()
+ }
+ }, [timeLeft])
+
+ return (
+ <>
+
{timeLeft}s
+ >
+ )
+}
+
+export default Counter
diff --git a/packages/next.js/components/ReloadPrompt.jsx b/packages/next.js/components/ReloadPrompt.jsx
new file mode 100644
index 000000000..3bc7563c8
--- /dev/null
+++ b/packages/next.js/components/ReloadPrompt.jsx
@@ -0,0 +1,98 @@
+import React, { useState, useEffect } from 'react'
+import Counter from './Counter'
+const intervalMS = 1000 * 60 // 1min
+
+const PwaUpdater = () => {
+ const wb = window?.workbox
+
+ const [isOpen, setIsOpen] = useState(false);
+ const onConfirmActivate = () => wb.messageSkipWaiting();
+
+
+ if (typeof window !== 'undefined' && 'serviceWorker' in navigator && window.workbox !== undefined) {
+
+ useEffect(() => {
+
+ console.log("ReloadPrompt", wb)
+ // add event listeners to handle any of PWA lifecycle event
+ // https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-window.Workbox#events
+ wb.addEventListener('installed', event => {
+ console.log(`Event ${event.type} is triggered.`)
+ console.log(event)
+ })
+
+ wb.addEventListener('controlling', event => {
+ console.log(`Event ${event.type} is triggered.`)
+ console.log(event)
+ window.location.reload();
+ })
+
+ wb.addEventListener('activated', event => {
+ console.log(`Event ${event.type} is triggered.`)
+ console.log(event)
+ })
+
+ // A common UX pattern for progressive web apps is to show a banner when a service worker has updated and waiting to install.
+ // NOTE: MUST set skipWaiting to false in next.config.js pwa object
+ // https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users
+ const promptNewVersionAvailable = event => {
+ console.log("promptNewVersionAvailable", event)
+ // `event.wasWaitingBeforeRegister` will be false if this is the first time the updated service worker is waiting.
+ // When `event.wasWaitingBeforeRegister` is true, a previously updated service worker is still waiting.
+ // You may want to customize the UI prompt accordingly.
+ if (confirm('A newer version of this web app is available, reload to update?')) {
+ wb.addEventListener('controlling', event => {
+ // window.location.reload()
+ console.log("new version is available")
+ })
+
+ // Send a message to the waiting service worker, instructing it to activate.
+ // wb.messageSkipWaiting()
+ } else {
+ console.log(
+ 'User rejected to reload the web app, keep using old version. New version will be automatically load when user open the app next time.'
+ )
+ }
+ }
+
+ // wb.addEventListener('waiting', promptNewVersionAvailable)
+ wb.addEventListener('waiting', () => setIsOpen(true));
+ wb.register();
+
+ // ISSUE - this is not working as expected, why?
+ // I could only make message event listenser work when I manually add this listenser into sw.js file
+ wb.addEventListener('message', event => {
+ console.log(`Event ${event.type} is triggered.`)
+ console.log(event)
+ })
+
+ }, []);
+
+ }
+
+
+ return (
+
+
+ Hey, a new version is available! Please click below to update.
+
+
+
+ Reload and update
+
+ setIsOpen(false)}
+ >
+ Cancel
+
+
+
+
+ );
+ }
+
+ export default PwaUpdater;
diff --git a/packages/next.js/components/TipTap/OnlineIndicator.jsx b/packages/next.js/components/TipTap/OnlineIndicator.jsx
new file mode 100644
index 000000000..174fbb64e
--- /dev/null
+++ b/packages/next.js/components/TipTap/OnlineIndicator.jsx
@@ -0,0 +1,56 @@
+import React, { useState, useEffect } from 'react'
+
+import { OfflineCloud, OnlineCloud } from '../icons/Icons'
+
+const OnlineIndicator = () => {
+ const [isOnline, setIsOnline] = useState(false)
+ const [showStatus, setShowStatus] = useState(true)
+
+
+ useEffect(() => {
+ const handleOnlineStatus = () => {
+ setIsOnline(true)
+ setShowStatus(true)
+ }
+ const handleOfflineStatus = () => {
+ setIsOnline(false)
+ setShowStatus(true)
+ }
+
+ window.addEventListener('online', handleOnlineStatus)
+ window.addEventListener('offline', handleOfflineStatus)
+
+ return () => {
+ window.removeEventListener('online', handleOnlineStatus)
+ window.removeEventListener('offline', handleOfflineStatus)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (showStatus) {
+ const timer = setTimeout(() => {
+ setShowStatus(false)
+ }, 5000)
+
+ return () => clearTimeout(timer)
+ }
+ }, [showStatus])
+
+ return (
+
+ {showStatus && (
+
+ {isOnline
+ ?
+ Saved to docsplus
+
+ :
+ Working offline
+ }
+
+ )}
+
+ )
+}
+
+export default OnlineIndicator
diff --git a/packages/next.js/components/TipTap/PadTitle.jsx b/packages/next.js/components/TipTap/PadTitle.jsx
new file mode 100644
index 000000000..2ff17377f
--- /dev/null
+++ b/packages/next.js/components/TipTap/PadTitle.jsx
@@ -0,0 +1,72 @@
+import Link from 'next/link'
+
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import {
+ DocsPlus
+} from '../icons/Icons'
+
+import OnlineIndicator from './OnlineIndicator'
+
+const PadTitle = ({ docTitle, docId,docSlug, provider }) => {
+ const queryClient = useQueryClient()
+
+ const { isLoading, isSuccess, mutate } = useMutation({
+ mutationKey: ['updateDocumentMetadata'],
+ mutationFn: ({ title, docId }) => {
+ // NOTE: This is a hack to get the correct URL in the build time
+ const url = `${process.env.NEXT_PUBLIC_RESTAPI_URL}/documents/${docId.split('.').at(-1)}`
+
+
+ return fetch(url, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ title })
+ })
+ .then(res => res.json())
+ },
+ onSuccess: (data) => {
+ queryClient.setQueryData(['getDocumentMetadataByDocName'], data)
+ }
+ })
+
+ const saveData = (e) => {
+ if (e.target.innerText === docTitle) return
+ mutate({
+ title: e.target.innerText,
+ docId: docId.split('.').at(-1)
+ })
+ }
+
+
+ return (
+
+
+
+
+
+
+
+ {isLoading
+ ? 'Loading...'
+ :
{
+ if (event.key === 'Enter') {
+ e.preventDefault()
+ e.target.blur()
+ }
+ }}
+ >
+
}
+
+
+
+ )
+}
+
+export default PadTitle
diff --git a/packages/next.js/components/TipTap/Placeholder.jsx b/packages/next.js/components/TipTap/Placeholder.jsx
new file mode 100644
index 000000000..a451a8850
--- /dev/null
+++ b/packages/next.js/components/TipTap/Placeholder.jsx
@@ -0,0 +1,66 @@
+import { Extension } from '@tiptap/core'
+import { Plugin } from 'prosemirror-state'
+import { Decoration, DecorationSet } from 'prosemirror-view'
+
+const Placeholder = Extension.create({
+ name: 'placeholder',
+ addOptions () {
+ return {
+ emptyEditorClass: 'is-editor-empty',
+ emptyNodeClass: 'is-empty',
+ placeholder: 'Write something …',
+ showOnlyWhenEditable: true,
+ showOnlyCurrent: true,
+ includeChildren: false
+ }
+ },
+ addProseMirrorPlugins () {
+ return [
+ new Plugin({
+ props: {
+ decorations: ({ doc, selection }) => {
+ const active = this.editor.isEditable || !this.options.showOnlyWhenEditable
+ const { anchor } = selection
+ const decorations = []
+
+ if (!active) {
+ return null
+ }
+ doc.descendants((node, pos) => {
+ const hasAnchor = anchor >= pos && anchor <= (pos + node.nodeSize)
+ const isEmpty = !node.isLeaf && !node.childCount
+
+ if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) {
+ const classes = [this.options.emptyNodeClass]
+
+ if (this.editor.isEmpty) {
+ classes.push(this.options.emptyEditorClass)
+ }
+ const decoration = Decoration.node(pos, pos + node.nodeSize, {
+ class: classes.join(' '),
+ 'data-placeholder': typeof this.options.placeholder === 'function'
+ ? this.options.placeholder({
+ editor: this.editor,
+ node,
+ pos,
+ hasAnchor
+ })
+ : this.options.placeholder
+ })
+
+ decorations.push(decoration)
+ }
+
+ return this.options.includeChildren
+ })
+
+ return DecorationSet.create(doc, decorations)
+ }
+ }
+ })
+ ]
+ }
+})
+
+export { Placeholder, Placeholder as default }
+// # sourceMappingURL=tiptap-extension-placeholder.esm.js.map
diff --git a/packages/next.js/components/TipTap/TableOfContents.jsx b/packages/next.js/components/TipTap/TableOfContents.jsx
new file mode 100644
index 000000000..1677b3a28
--- /dev/null
+++ b/packages/next.js/components/TipTap/TableOfContents.jsx
@@ -0,0 +1,126 @@
+import React, { useCallback, useEffect, useState } from 'react'
+import PubSub from 'pubsub-js'
+
+const TableOfcontent = ({ editor, className }) => {
+ const [items, setItems] = useState([])
+
+ const handleUpdate = useCallback((data) => {
+ const headings = []
+
+ // TODO: check the object id performance
+ // TODO: heading must be url frindly, so I have to map id with SLUGs
+ editor?.state?.doc?.descendants((node, _pos, _parent, _index) => {
+ if (node.type.name === 'contentHeading') {
+ // https://stackoverflow.com/questions/59829232/offsettop-return-0
+ function getOffsetTop (element) {
+ return element ? (element.offsetTop + getOffsetTop(element.offsetParent)) : 0
+ }
+
+ const headingId = _parent.attrs?.id || node?.attrs.id || '1'
+ const headingSection = document.querySelector(`.ProseMirror .heading[data-id="${headingId}"]`)
+
+ headings.push({
+ level: node.attrs?.level,
+ text: node?.textContent,
+ id: headingId,
+ open: data && headingId === data.headingId ? data.crinkleOpen : headingSection?.classList.contains('opend'),
+ offsetTop: getOffsetTop(headingSection)
+ })
+ }
+ })
+ setItems(headings)
+ }, [editor])
+
+ useEffect(handleUpdate, [])
+
+ useEffect(() => {
+ if (!editor) {
+ return null
+ }
+ editor?.on('update', handleUpdate)
+
+ return () => {
+ editor?.off('update', handleUpdate)
+ }
+ }, [editor])
+
+ useEffect(() => {
+ PubSub.subscribe('toggleHeadingsContent', function (messag, data) {
+ handleUpdate(data)
+ })
+
+ return () => PubSub.unsubscribe('toggleHeadingsContent')
+ }, [])
+
+ const scroll2Header = (e) => {
+ e.preventDefault()
+ let id = e.target.getAttribute('data-id')
+ const offsetParent = e.target.closest('.toc__item').getAttribute('data-offsettop')
+
+ if (offsetParent === '0') id = '1'
+
+ document.querySelector(`.heading[data-id="${id}"]`)?.scrollIntoView()
+ }
+
+ // console.log(newItems)
+
+ const toggleSection = (item) => {
+ document
+ .querySelector(`.ProseMirror .heading[data-id="${item.id}"] .buttonWrapper .btnFold`)?.click()
+
+ setItems(x => items.map((i) => {
+ if (i.id === item.id) {
+ return {
+ ...i,
+ open: !i.open
+ }
+ }
+
+ return i
+ }))
+ }
+
+ function renderToc (items) {
+ const renderedItems = []
+ let i = 0
+
+ while (i < items.length) {
+ const item = items[i]
+ const children = []
+ let j = i + 1
+
+ while (j < items.length && items[j].level > item.level) {
+ children.push(items[j])
+ j++
+ }
+ renderedItems.push(
+
+
+ toggleSection(item)}>
+
+ {item.text}
+
+
+
+ {children.length > 0 &&
{renderToc(children)}
}
+
+ )
+ i = j
+ }
+
+ return renderedItems
+ }
+
+ return (
+
+ {renderToc(items)}
+
+
)
+}
+
+export default TableOfcontent
diff --git a/packages/next.js/components/TipTap/TableOfContentss.js b/packages/next.js/components/TipTap/TableOfContentss.js
new file mode 100644
index 000000000..7a1cb7fc2
--- /dev/null
+++ b/packages/next.js/components/TipTap/TableOfContentss.js
@@ -0,0 +1,90 @@
+import { mergeAttributes, Node } from '@tiptap/core'
+
+// import Component from './TableOfContents'
+
+export default Node.create({
+ name: 'tableOfContents',
+
+ group: 'block',
+
+ atom: true,
+
+ parseHTML() {
+ return [
+ {
+ tag: 'toc'
+ }
+ ]
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['toc', mergeAttributes(HTMLAttributes)]
+ },
+
+ onCreate() {
+ onBeforeCreate
+ const { view, state } = this.editor
+ const { tr, doc } = state
+ const { types, attributeName, generateID } = this.options
+
+ if (this.editor.extensionManager.extensions.find(extension => extension.name === 'collaboration')) {
+ return
+ }
+ console.log({ view, state, tr, doc, types, attributeName, generateID })
+ },
+
+ addNodeView() {
+ return ''
+ }
+ // addGlobalAttributes() {
+ // return [
+ // {
+ // types: this.options.types,
+ // attributes: {
+ // [this.options.attributeName]: {
+ // default: null,
+ // parseHTML: element => element.getAttribute(`data-${ this.options.attributeName }`),
+ // renderHTML: attributes => {
+ // if (!attributes[this.options.attributeName]) {
+ // return {};
+ // }
+ // return {
+ // [`data-${ this.options.attributeName }`]: attributes[this.options.attributeName],
+ // };
+ // },
+ // },
+ // },
+ // },
+ // ];
+ // },
+
+ // addGlobalAttributes() {
+ // return [
+ // {
+ // types: ['heading', 'paragraph'],
+ // parseHTML: element => element.getAttribute(`parent`),
+ // renderHTML: attributes => {
+ // // if (!attributes.level) {
+ // // return {};
+ // // }
+ // return {
+ // parent: attributes.parent,
+ // style: `color: red`,
+ // };
+ // },
+ // attributes: {
+ // // id: {
+ // // default: "id",
+ // // },
+ // style: { default: "" },
+ // parent: {
+ // default: "default"
+ // },
+ // level: {
+ // default: "default"
+ // }
+ // },
+ // },
+ // ]
+ // },
+})
diff --git a/packages/next.js/components/TipTap/TipTap.jsx b/packages/next.js/components/TipTap/TipTap.jsx
new file mode 100644
index 000000000..5d5812438
--- /dev/null
+++ b/packages/next.js/components/TipTap/TipTap.jsx
@@ -0,0 +1,283 @@
+import React, { useEffect, useState } from 'react'
+import { useEditor, EditorContent } from '@tiptap/react'
+import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration'
+import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
+import Link from '@tiptap/extension-link'
+import Image from '@tiptap/extension-image'
+import TaskItem from '@tiptap/extension-task-item'
+import TaskList from '@tiptap/extension-task-list'
+import Highlight from '@tiptap/extension-highlight'
+import Typography from '@tiptap/extension-typography'
+import randomColor from 'randomcolor'
+import Underline from '@tiptap/extension-underline'
+
+import Placeholder from '@tiptap/extension-placeholder'
+import Gapcursor from '@tiptap/extension-gapcursor'
+import { Node } from '@tiptap/core'
+
+import ListItem from '@tiptap/extension-list-item'
+import OrderedList from '@tiptap/extension-ordered-list'
+import HardBreak from '@tiptap/extension-hard-break'
+
+import Bold from '@tiptap/extension-bold'
+import Italic from '@tiptap/extension-italic'
+import BulletList from '@tiptap/extension-bullet-list'
+import Strike from '@tiptap/extension-strike'
+import Superscript from '@tiptap/extension-superscript'
+import Subscript from '@tiptap/extension-subscript'
+import Blockquote from '@tiptap/extension-blockquote'
+import TextAlign from '@tiptap/extension-text-align'
+import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
+import css from 'highlight.js/lib/languages/css'
+import js from 'highlight.js/lib/languages/javascript'
+import ts from 'highlight.js/lib/languages/typescript'
+import html from 'highlight.js/lib/languages/xml'
+import md from 'highlight.js/lib/languages/markdown'
+import yaml from 'highlight.js/lib/languages/yaml'
+import python from 'highlight.js/lib/languages/python'
+import json from 'highlight.js/lib/languages/json'
+import bash from 'highlight.js/lib/languages/bash'
+// load all highlight.js languages
+import { lowlight } from 'lowlight'
+import ShortUniqueId from 'short-unique-id'
+// Table Node Block
+import Table from '@tiptap/extension-table'
+import TableCell from '@tiptap/extension-table-cell'
+import TableHeader from '@tiptap/extension-table-header'
+import TableRow from '@tiptap/extension-table-row'
+
+import Heading from './extentions/Heading'
+import ContentHeading from './extentions/ContentHeading'
+import UniqueID from './extentions/UniqueId'
+import ContentWrapper from './extentions/ContentWrapper'
+
+lowlight.registerLanguage('html', html)
+lowlight.registerLanguage('css', css)
+lowlight.registerLanguage('js', js)
+lowlight.registerLanguage('ts', ts)
+lowlight.registerLanguage('markdown', md)
+lowlight.registerLanguage('python', python)
+lowlight.registerLanguage('yaml', yaml)
+lowlight.registerLanguage('json', json)
+lowlight.registerLanguage('bash', bash)
+
+const Document = Node.create({
+ name: 'doc',
+ topNode: true,
+ content: 'heading+'
+})
+
+const Paragraph = Node.create({
+ name: 'paragraph',
+ group: 'block',
+ content: 'inline*',
+ parseHTML () {
+ return [
+ { tag: 'p' }
+ ]
+ },
+ renderHTML ({ HTMLAttributes }) {
+ return ['p', HTMLAttributes, 0]
+ }
+})
+
+const Text = Node.create({
+ name: 'text',
+ group: 'inline'
+})
+
+const scrollDown = () => {
+ const url = new URL(window.location)
+ const id = url.searchParams.get('id')
+
+ if (!id) return
+ setTimeout(() => {
+ console.log({
+ do: document.querySelector('.tipta__editor'),
+ param: url,
+ id: url.searchParams.get('id'),
+ nodeTarget: document.querySelector(`[data-id="${url.searchParams.get('id')}"]`)
+ })
+ document.querySelector(`[data-id="${url.searchParams.get('id')}"]`)?.scrollIntoView()
+ }, 200)
+}
+
+const Editor = ({ padName, provider, ydoc, defualtContent = '', spellcheck = false, children }) => {
+ if (!provider) {
+ return {
+ extensions: [
+ Document,
+ Bold,
+ Italic,
+ BulletList,
+ Strike,
+ HardBreak,
+ Gapcursor,
+ Paragraph,
+ Text,
+ ListItem,
+ OrderedList,
+ Heading.configure(),
+ ContentHeading,
+ ContentWrapper
+ ]
+ }
+ }
+
+ return {
+ onCreate: (editor) => {
+ // console.log("onCreate", editor)
+ scrollDown()
+ },
+ onUpdate: (editor) => {
+ // console.log("onUpdate", editor)
+ },
+ editorProps: {
+ attributes: {
+ spellcheck
+ }
+ },
+ extensions: [
+ UniqueID.configure({
+ types: ['heading', 'link'],
+ filterTransaction: transaction => !isChangeOrigin(transaction),
+ generateID: () => {
+ const uid = new ShortUniqueId()
+
+ return uid.stamp(16)
+ }
+ }),
+ Document,
+ Bold,
+ Italic,
+ BulletList,
+ Strike,
+ HardBreak,
+ Gapcursor,
+ Paragraph,
+ Text,
+ ListItem,
+ OrderedList,
+ Heading.configure(),
+ CodeBlockLowlight.configure({
+ lowlight
+ }),
+ ContentHeading,
+ ContentWrapper,
+ Superscript,
+ Subscript,
+ Blockquote,
+ TextAlign,
+ Underline,
+ Link.configure({
+ protocols: ['ftp', 'mailto']
+ }),
+ Image.configure({
+ inline: true,
+ allowBase64: true,
+ HTMLAttributes: {
+ class: 'image-class'
+ }
+ }),
+ TaskList,
+ TaskItem.configure({
+ nested: true,
+ HTMLAttributes: {
+ class: 'tasks-class'
+ }
+ }),
+ Highlight,
+ Typography,
+ Table.configure({
+ resizable: true
+ }),
+ TableRow,
+ TableHeader,
+ TableCell,
+ Collaboration.configure({
+ document: provider.document
+ }),
+ CollaborationCursor.configure({
+ provider,
+ user: { name: 'Adam Doe', color: randomColor() }
+ }),
+ Placeholder.configure({
+ includeChildren: true,
+ placeholder: ({ node }) => {
+ const nodeType = node.type.name
+
+ if (nodeType === 'contentHeading') {
+ const level = node.attrs.level
+
+ return level - 1 === 0 ? 'Title' : `Heading ${level - 1}`
+ } else if (nodeType === 'heading') {
+ const level = node.attrs.level
+
+ return level - 1 === 0 ? 'Title' : `Heading ${level - 1}`
+ } else if (nodeType === 'paragraph') {
+ const msg = [
+ 'Type your thoughts here ...',
+ 'Start your writing here ...',
+ "What's on your mind? ...",
+ 'Let your words flow ...',
+ 'Unleash your creativity ...',
+ 'Your text here ...',
+ 'Express yourself ...',
+ 'Write your ideas down ...',
+ 'Share your thoughts ...',
+ 'Start typing ...',
+ 'Begin typing here ...',
+ 'Jot down your ideas here ...',
+ 'Let your imagination run wild ...',
+ 'Capture your thoughts ...',
+ 'Brainstorm your next masterpiece ...',
+ 'Put your ideas into words ...',
+ 'Express your creativity ...',
+ 'Create something amazing ...',
+ 'Write down your musings ...',
+ 'Craft your unique story ...',
+ 'Unleash your inner writer ...',
+ 'Share your innovative ideas ...',
+ 'Pen your next big idea ...',
+ 'Write from the heart ...',
+ 'Manifest your creativity here ...',
+ 'Share your brilliant insights ...',
+ 'Craft your vision ...',
+ 'Distill your thoughts into words ...',
+ 'Let your inspiration flow ...',
+ 'Pen your creative masterpiece ...',
+ 'Draft your imaginative ideas here ...',
+ 'Bring your imagination to life ...',
+ 'Write your way to clarity ...',
+ 'Craft your next big breakthrough ...',
+ 'Let your ideas take shape ...',
+ 'Create your own narrative ...',
+ 'Put your originality on paper ...',
+ 'Express your unique perspective ...',
+ 'Inscribe your original thoughts ...',
+ 'Unfold your creative potential ...',
+ 'Give voice to your ideas ...',
+ 'Paint a picture with your words ...',
+ 'Sculpt your ideas into sentences ...',
+ 'Design your masterpiece with words ...',
+ 'Shape your vision with language ...',
+ 'Craft your story with care ...',
+ 'Compose your narrative with passion ...',
+ 'Build your message from scratch ...',
+ 'Develop your concepts with clarity ...',
+ 'Forge your ideas into a cohesive whole ...',
+ 'Carve out your ideas with precision ...'
+ ]
+
+ return msg[Math.floor(Math.random() * msg.length + 1)]
+ }
+
+ return null
+ }
+ })
+ ],
+ defualtContent: ''
+ }
+}
+
+export default Editor
diff --git a/packages/next.js/components/TipTap/Toolbar.jsx b/packages/next.js/components/TipTap/Toolbar.jsx
new file mode 100644
index 000000000..799e1f320
--- /dev/null
+++ b/packages/next.js/components/TipTap/Toolbar.jsx
@@ -0,0 +1,345 @@
+import React, { useEffect, useState, useCallback } from 'react'
+import Select from 'react-select'
+
+import {
+ Bold,
+ Italic,
+ Underline,
+ OrderList,
+ BulletList,
+ Link,
+ CheckList,
+ Image,
+ Gear,
+ ClearMark,
+ Stric,
+ HighlightMarker,
+ Undo,
+ Redo,
+ Printer
+} from '../../components/icons/Icons'
+
+const GearModal = (props) => {
+ return (
+
+ {props.children}
+
+ )
+}
+
+const Toolbar = ({ editor }) => {
+ if (!editor) {
+ return null
+ }
+
+ const setLink = useCallback(() => {
+ const previousUrl = editor.getAttributes('link').href
+ const url = window.prompt('URL', previousUrl)
+
+ if (editor.isActive('link')) {
+ return editor.chain().focus().unsetLink().run()
+ }
+
+ // cancelled
+ if (url === null) {
+ return
+ }
+
+ // empty
+ if (url === '') {
+ editor.chain().focus().extendMarkRange('link').unsetLink().run()
+
+ return
+ }
+
+ // update link
+ editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
+ }, [editor])
+
+ const addImage = useCallback(() => {
+ const url = window.prompt('URL')
+
+ if (url) {
+ editor.chain().focus().setImage({ src: url }).run()
+ }
+ }, [editor])
+
+ const options = [
+ { value: 0, label: 'Normal Text' },
+ { value: 1, label: 'Heading 1' },
+ { value: 2, label: 'Heading 2' },
+ { value: 3, label: 'Heading 3' },
+ { value: 4, label: 'Heading 4' },
+ { value: 5, label: 'Heading 5' },
+ { value: 6, label: 'Heading 6' },
+ { value: 7, label: 'Heading 7' },
+ { value: 8, label: 'Heading 8' },
+ { value: 9, label: 'Heading 9' },
+ { value: 10, label: 'Heading 10' }
+ ]
+
+ const [selectedOption, setSelectedOption] = useState(options[0])
+ const [selectValue, setSelectValue] = useState(options[0])
+
+ useEffect(() => {
+ if (editor.isActive('contentHeading', { level: 1 })) setSelectValue(options[1])
+ else if (editor.isActive('contentHeading', { level: 2 })) setSelectValue(options[2])
+ else if (editor.isActive('contentHeading', { level: 3 })) setSelectValue(options[3])
+ else if (editor.isActive('contentHeading', { level: 4 })) setSelectValue(options[4])
+ else if (editor.isActive('contentHeading', { level: 5 })) setSelectValue(options[5])
+ else if (editor.isActive('contentHeading', { level: 6 })) setSelectValue(options[6])
+ else if (editor.isActive('contentHeading', { level: 7 })) setSelectValue(options[7])
+ else if (editor.isActive('contentHeading', { level: 8 })) setSelectValue(options[8])
+ else if (editor.isActive('contentHeading', { level: 9 })) setSelectValue(options[9])
+ else if (editor.isActive('contentHeading', { level: 10 })) setSelectValue(options[10])
+ else setSelectValue(options[0])
+ }, [
+ editor.isActive('contentHeading', { level: 1 }),
+ editor.isActive('contentHeading', { level: 2 }),
+ editor.isActive('contentHeading', { level: 3 }),
+ editor.isActive('contentHeading', { level: 4 }),
+ editor.isActive('contentHeading', { level: 5 }),
+ editor.isActive('contentHeading', { level: 6 }),
+ editor.isActive('contentHeading', { level: 7 }),
+ editor.isActive('contentHeading', { level: 8 }),
+ editor.isActive('contentHeading', { level: 9 }),
+ editor.isActive('contentHeading', { level: 10 }),
+ editor.isActive('paragraph')
+ ])
+
+ const onchangeValue = (e) => {
+ setSelectedOption(e)
+ const value = e.value
+
+ if (value === 0) editor.chain().focus().normalText().run()
+ else editor.chain().focus().wrapBlock({ level: +value }).run()
+ }
+
+ let indentSetting = Boolean(localStorage.getItem('setting.indentHeading') || false)
+ let h1SectionBreakSetting = Boolean(localStorage.getItem('setting.h1SectionBreakSetting') || false)
+
+ if (!indentSetting) {
+ localStorage.setItem('setting.indentHeading', '')
+ indentSetting = false
+ }
+
+ if (!h1SectionBreakSetting) {
+ console.log('indentSetting', indentSetting)
+ localStorage.setItem('setting.h1SectionBreakSetting', 'true')
+ h1SectionBreakSetting = true
+ document.body.classList.add('h1SectionBreak')
+ }
+
+ const [indented, setIndented] = React.useState(indentSetting)
+ const [h1SectionBreak, setH1SectionBreak] = React.useState(h1SectionBreakSetting)
+
+ const toggleHeadingIndent = (e) => {
+ setIndented(preState => {
+ const newState = !preState
+
+ if (newState) { document.body.classList.add('indentHeading') } else { document.body.classList.remove('indentHeading') }
+
+ localStorage.setItem('setting.indentHeading', newState ? 'yes' : '')
+
+ return newState
+ })
+ }
+
+ const toggleH1SectionBreak = (e) => {
+ setH1SectionBreak(preState => {
+ const newState = !preState
+
+ if (newState) { document.body.classList.add('h1SectionBreak') } else { document.body.classList.remove('h1SectionBreak') }
+
+ localStorage.setItem('setting.h1SectionBreakSetting', newState ? 'yes' : '')
+
+ return newState
+ })
+ }
+
+ const toggleSettingModal = () => {
+ console.log('toggleSettingModal')
+ document.querySelector('.gearModal').classList.toggle('active')
+ }
+
+ const hideModals = (e) => {
+ console.log()
+ if (e.target.closest('.btn_modal') || e.target.closest('.nd_modal')) return
+ document.querySelector('.gearModal').classList.remove('active')
+ }
+
+ useEffect(() => {
+ const newIndent = Boolean(localStorage.getItem('setting.indentHeading'))
+ const newHsectionBreak = Boolean(localStorage.getItem('setting.indentHeading'))
+
+ if (newIndent) document.body.classList.add('indentHeading')
+ else document.body.classList.remove('indentHeading')
+
+ if (newHsectionBreak) document.body.classList.add('h1SectionBreak')
+ else document.body.classList.remove('h1SectionBreak')
+ }, [])
+
+ return (
+
+
+
+
editor.chain().focus().undo().run()}>
+
+
+
editor.chain().focus().redo().run()}>
+
+
+
window.print()}>
+
+
+
+
+
+
+
+
+
+
editor.chain().focus().toggleBold().run()}
+ >
+
+
+
+
editor.chain().focus().toggleItalic().run()}
+ >
+
+
+
+
editor.chain().focus().toggleUnderline().run()}
+ >
+
+
+
+
+ editor.chain().focus().toggleStrike().run()}
+ >
+
+
+
+
+
+
+
+
+
editor.chain().focus().toggleOrderedList().run()}
+ >
+
+
+
+
+
editor.chain().focus().toggleBulletList().run()}
+ >
+
+
+
+
+
+
+ editor.chain().focus().toggleTaskList().run()}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ editor.chain().focus().toggleHighlight().run()}
+ >
+
+
+
+
+
+
+
+
+ {
+ const range = editor.view.state.selection.ranges[0]
+
+ if (range.$from === range.$to) {
+ editor.chain().focus().clearNodes().run()
+ } else {
+ editor.chain().focus().unsetAllMarks().run()
+ }
+ }}>
+
+
+
+
+
+
+
+
+
+
+ Settings:
+
+
+
+ toggleHeadingIndent(e.target)} />
+
+ Toggle heading indent
+
+
+
+
+ toggleH1SectionBreak(e.target)} />
+
+ Toggle H1 section break
+
+
+
+
+
+ )
+}
+
+export default Toolbar
diff --git a/packages/next.js/components/TipTap/extentions/ContentHeading.jsx b/packages/next.js/components/TipTap/extentions/ContentHeading.jsx
new file mode 100644
index 000000000..7a4505c56
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/ContentHeading.jsx
@@ -0,0 +1,241 @@
+import { Selection, Plugin, TextSelection, PluginKey } from 'prosemirror-state'
+import { Decoration, DecorationSet } from 'prosemirror-view'
+import { Node, mergeAttributes } from '@tiptap/core'
+
+import { db } from '../../../db'
+
+import { copyToClipboard } from './helper'
+import onHeading from './normalText/onHeading'
+
+function extractContentHeadingBlocks (doc) {
+ const result = []
+ const record = (from, to, headingId, node) => {
+ result.push({ from, to, headingId, node })
+ }
+ let lastHeadingId
+
+ // For each node in the document
+ doc.descendants((node, pos) => {
+ if (node.type.name === 'heading') {
+ lastHeadingId = node.attrs.id
+ }
+ if (node.type.name === 'contentHeading') {
+ const nodeSize = node.nodeSize
+
+ record(pos, pos + nodeSize, lastHeadingId, node)
+ }
+ })
+
+ return result
+}
+
+const buttonWrapper = (editor, { headingId, from, node }) => {
+ const buttonWrapper = document.createElement('div')
+ const btnToggleHeading = document.createElement('button')
+ const btnChatBox = document.createElement('button')
+
+ buttonWrapper.classList.add('buttonWrapper')
+
+ btnChatBox.setAttribute('class', 'btn_openChatBox')
+ btnChatBox.setAttribute('type', 'button')
+ btnToggleHeading.classList.add('btnFold')
+ btnToggleHeading.setAttribute('type', 'button')
+
+ buttonWrapper.append(btnToggleHeading)
+ buttonWrapper.append(btnChatBox)
+
+ const toggleHeadingContent = (el) => {
+ console.log('coming heaidng', {
+ el
+ })
+ const headingId = el.getAttribute('data-id')
+ const detailsContent = el.querySelector('div.contentWrapper')
+ const event = new CustomEvent('toggleHeadingsContent', { detail: { headingId, el: detailsContent } })
+
+ detailsContent === null || detailsContent === void 0 ? void 0 : detailsContent.dispatchEvent(event)
+ }
+
+ const foldAndUnfold = (e) => {
+ const el = e.target
+ const headingNodeEl = el.closest('.heading')
+ const headingId = headingNodeEl.getAttribute('data-id')
+
+ editor.commands.focus(from + node.nodeSize - 1)
+ if (editor.isEditable) {
+ const { tr } = editor.state
+ const pos = from
+ const currentNode = tr.doc.nodeAt(pos)
+ const headingNode = tr.doc.nodeAt(pos - 1)
+
+ if (currentNode && currentNode.type.name === 'contentHeading') {
+ tr.setNodeMarkup(pos, undefined, {
+ ...currentNode.attrs,
+ level: currentNode.attrs.level,
+ id: headingNode.attrs.id
+ })
+ }
+
+ if (headingNode && headingNode.type.name === 'heading') {
+ tr.setNodeMarkup(pos - 1, undefined, {
+ ...headingNode.attrs,
+ level: currentNode.attrs.level,
+ id: headingNode.attrs.id
+ })
+ }
+
+ tr.setMeta('addToHistory', false)
+
+ const documentId = localStorage.getItem('docId')
+ const headingMap = JSON.parse(localStorage.getItem('headingMap')) || []
+ const nodeState = headingMap.find(h => h.headingId === headingId) || { crinkleOpen: true }
+
+ db.meta
+ .put({ docId: documentId, headingId, crinkleOpen: !nodeState.crinkleOpen, level: currentNode.attrs.level })
+ .then((data, ddd) => {
+ db.meta.where({ docId: documentId }).toArray().then((data) => {
+ localStorage.setItem('headingMap', JSON.stringify(data))
+ })
+ })
+
+ editor.view.dispatch(tr)
+ toggleHeadingContent(headingNodeEl)
+ }
+ }
+
+ btnToggleHeading.addEventListener('click', foldAndUnfold)
+
+ // copy the link to clipboard
+ const href = document.createElement('a')
+
+ href.innerHTML = '#'
+ href.setAttribute('href', `#${headingId}`)
+ href.addEventListener('click', (e) => {
+ e.preventDefault()
+ const url = new URL(window.location)
+
+ url.searchParams.set('id', headingId)
+ window.history.pushState({}, '', url)
+ copyToClipboard(url)
+ editor
+ .chain()
+ .focus(from + node.nodeSize - 1)
+ .run()
+ })
+ href.contenteditable = false
+ buttonWrapper.append(href)
+
+ return buttonWrapper
+}
+
+const appendButtonsDec = (doc, editor) => {
+ const decos = []
+ const contentWrappers = extractContentHeadingBlocks(doc)
+
+ contentWrappers.forEach(prob => {
+ const decorationWidget = Decoration.widget(prob.to, buttonWrapper(editor, prob), {
+ side: -1,
+ key: prob.headingId
+ })
+
+ decos.push(decorationWidget)
+ })
+
+ return DecorationSet.create(doc, decos)
+}
+
+const HeadingsTitle = Node.create({
+ name: 'contentHeading',
+ content: 'inline*',
+ group: 'block',
+ defining: true,
+ // draggable: false,
+ // selectable: false,
+ // isolating: true,
+ allowGapCursor: false,
+ addOptions () {
+ return {
+ HTMLAttributes: {
+ class: 'title'
+ },
+ levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ id: null
+ }
+ },
+ addAttributes () {
+ return {
+ level: {
+ default: 1,
+ rendered: false
+ },
+ id: {
+ default: null,
+ rendered: false
+ }
+ }
+ },
+ parseHTML () {
+ return this.options.levels
+ .map((level) => ({
+ tag: `h${level}`,
+ attrs: { level }
+ }))
+ },
+ renderHTML (state) {
+ const { node, HTMLAttributes } = state
+ const hasLevel = this.options.levels.includes(node.attrs.level)
+ const level = hasLevel
+ ? node.attrs.level
+ : this.options.levels[0]
+
+ return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, level }), 0]
+ },
+ addKeyboardShortcuts () {
+ return {
+ Backspace: ({ editor }) => {
+ const { schema, selection, doc } = this.editor.state
+ const { empty, $anchor, $head, $from, $to, from, to } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ // current node
+ const node = doc.nodeAt(from)
+ // parent node
+ const parent = $anchor.parent
+
+ // if the current node is empty(means there is not content) and the parent is contentHeading
+ if (
+ (node !== null || $anchor.parentOffset !== 0) || // this node block contains content
+ parent.type.name !== schema.nodes.contentHeading.name ||
+ $anchor.pos === 2 // || // if the caret is in the first heading
+ // parent.attrs.open === false // if the heading is closed
+ ) return false
+
+ console.info('[Heading]: remove the heading node')
+
+ return onHeading({
+ editor,
+ state: editor.state,
+ tr: editor.state.tr,
+ view: editor.view
+ })
+ }
+ }
+ },
+ addProseMirrorPlugins () {
+ return [
+ new Plugin({
+ key: new PluginKey('HeadingButtons'),
+ state: {
+ init: (_, { doc }) => appendButtonsDec(doc, this.editor),
+ apply: (tr, old) => tr.docChanged ? appendButtonsDec(tr.doc, this.editor) : old
+ },
+ props: {
+ decorations (state) {
+ return this.getState(state)
+ }
+ }
+ })
+ ]
+ }
+})
+
+export { HeadingsTitle, HeadingsTitle as default }
diff --git a/packages/next.js/components/TipTap/extentions/ContentWrapper.jsx b/packages/next.js/components/TipTap/extentions/ContentWrapper.jsx
new file mode 100644
index 000000000..0bbea8ff3
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/ContentWrapper.jsx
@@ -0,0 +1,304 @@
+import { Node, mergeAttributes, findParentNode, defaultBlockAt } from '@tiptap/core'
+import { Selection, Plugin, TextSelection, PluginKey } from 'prosemirror-state'
+import { Decoration, DecorationSet } from 'prosemirror-view'
+import PubSub from 'pubsub-js'
+
+import { getNodeState } from './helper'
+
+function extractContentWrapperBlocks (doc) {
+ const result = []
+ const record = (from, to, nodeSize, childCount, headingId) => {
+ result.push({ from, to, nodeSize, childCount, headingId })
+ }
+ let lastHeadingId
+
+ // For each node in the document
+ doc.descendants((node, pos) => {
+ if (node.type.name === 'heading') {
+ lastHeadingId = node.attrs.id
+ }
+ if (node.type.name === 'contentWrapper') {
+ const nodeSize = node.content.size
+ const childCount = node.childCount
+
+ record(pos, pos + nodeSize, nodeSize, childCount, lastHeadingId)
+ }
+ })
+
+ return result
+}
+
+function crinkleNode (prob) {
+ const foldEl = document.createElement('div')
+
+ foldEl.classList.add('foldWrapper')
+ const step = 1000
+ const lines = 3 + Math.floor(prob.nodeSize / step)
+ const clampedLines = Math.min(Math.max(lines, 3), 10)
+
+ for (let i = 0; i <= clampedLines; i++) {
+ const line = document.createElement('div')
+
+ line.classList.add('fold')
+ line.classList.add(`l${i}`)
+ foldEl.append(line)
+ }
+ foldEl.setAttribute('data-clampedLines', clampedLines + 1)
+ foldEl.addEventListener('click', (e) => {
+ if (!e.target.closest('.heading').classList.contains('closed')) return
+ e.target.parentElement.parentElement.querySelector('.btnFold')?.click()
+ })
+
+ return foldEl
+}
+
+function lintDeco (doc) {
+ const decos = []
+ const contentWrappers = extractContentWrapperBlocks(doc)
+
+ contentWrappers.forEach(prob => {
+ const decorationWidget = Decoration.widget(prob.from, crinkleNode(prob), {
+ side: -1,
+ key: prob.headingId
+ })
+
+ decos.push(decorationWidget)
+ })
+
+ return DecorationSet.create(doc, decos)
+}
+
+function expandElement (elem, collapseClass, headingId, open) {
+ // debugger;
+ elem.style.height = ''
+ elem.style.transition = 'none'
+ elem.style.transitionTimingFunction = 'ease-in-out'
+ const startHeight = window.getComputedStyle(elem).height
+ const contentWrapper = document.querySelector(`.heading[data-id="${headingId}"]`)
+
+ contentWrapper.classList.remove('opend')
+ contentWrapper.classList.remove('closed')
+ contentWrapper.classList.remove('closing')
+ contentWrapper.classList.remove('opening')
+ elem.classList.add('overflow-hidden')
+
+ contentWrapper.classList.add(open ? 'opening' : 'closing')
+
+ // Remove the collapse class, and force a layout calculation to get the final height
+ elem.classList.toggle(collapseClass)
+ const height = window.getComputedStyle(elem).height
+
+ // Set the start height to begin the transition
+ elem.style.height = startHeight
+
+ // wait until the next frame so that everything has time to update before starting the transition
+ requestAnimationFrame(() => {
+ elem.style.transition = ''
+
+ requestAnimationFrame(() => {
+ elem.style.height = height
+ })
+ })
+
+ function callback () {
+ elem.style.height = ''
+ if (open) {
+ contentWrapper.classList.remove('closed')
+ contentWrapper.classList.remove('closing')
+ contentWrapper.classList.add('opend')
+ elem.classList.remove('overflow-hidden')
+ } else {
+ contentWrapper.classList.remove('opening')
+ contentWrapper.classList.remove('opend')
+ contentWrapper.classList.add('closed')
+ elem.classList.add('overflow-hidden')
+ }
+
+ elem.removeEventListener('transitionend', callback)
+ }
+
+ // Clear the saved height values after the transition
+ elem.addEventListener('transitionend', callback)
+}
+
+const HeadingsContent = Node.create({
+ name: 'contentWrapper',
+ content: '(heading|paragraph|block)*',
+ defining: true,
+ selectable: false,
+ isolating: true,
+ draggable: false,
+ allowGapCursor: false,
+ addOptions () {
+ return {
+ persist: true,
+ id: null,
+ HTMLAttributes: {}
+ }
+ },
+ parseHTML () {
+ return [
+ {
+ tag: `div[data-type="${this.name}"]`
+ }
+ ]
+ },
+ addAttributes () {
+ return {
+ id: {
+ default: null,
+ rendered: false
+ }
+ }
+ },
+ renderHTML ({ HTMLAttributes }) {
+ return [
+ 'div',
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 'data-type': this.name }),
+ 0
+ ]
+ },
+ addNodeView () {
+ return ({ editor, getPos, node, HTMLAttributes }) => {
+ const dom = document.createElement('div')
+
+ // get parent node
+ const upperNode = editor.state.doc.nodeAt(getPos() - 2)
+ const parentNode = editor.state.doc.nodeAt(getPos() - upperNode.nodeSize - 2)
+ const headingId = parentNode?.attrs.id
+
+ const nodeState = getNodeState(headingId)
+
+ dom.setAttribute('class', 'contentWrapper')
+
+ const attrs = {
+ 'data-type': this.name
+ }
+ const attributes = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, attrs)
+
+ if (!nodeState.crinkleOpen) {
+ dom.classList.add('overflow-hidden')
+ dom.classList.add('collapsed')
+ dom.classList.add('closed')
+ } else {
+ dom.classList.remove('overflow-hidden')
+ dom.classList.remove('collapsed')
+ dom.classList.remove('closed')
+ dom.classList.add('opend')
+ }
+
+ const content = document.createElement('div')
+
+ content.classList.add('contents')
+ dom.append(content)
+
+ Object.entries(attributes).forEach(([key, value]) => dom.setAttribute(key, value))
+ dom.addEventListener('toggleHeadingsContent', ({ detail }) => {
+ const section = detail.el
+ const headingMap = JSON.parse(localStorage.getItem('headingMap')) || []
+ const nodeState = headingMap.find(h => h.headingId === detail.headingId) || { crinkleOpen: true }
+
+ editor.commands.focus()
+
+ if (editor.isEditable && typeof getPos === 'function') {
+ const { tr } = editor.state
+
+ const pos = getPos()
+ const currentNode = tr.doc.nodeAt(pos)
+
+ if ((currentNode === null || currentNode === void 0 ? void 0 : currentNode.type) !== this.type) {
+ return false
+ }
+
+ if (nodeState.crinkleOpen) {
+ section.classList.add('overflow-hidden')
+ }
+
+ tr.setMeta('addToHistory', false)
+ // for trigger table of contents
+ tr.setMeta('fold&unfold', true)
+ editor.view.dispatch(tr)
+
+ PubSub.publish('toggleHeadingsContent', { headingId: detail.headingId, crinkleOpen: !nodeState.crinkleOpen })
+ expandElement(section, 'collapsed', detail.headingId, !nodeState.crinkleOpen)
+ }
+ })
+
+ return {
+ dom,
+ contentDOM: dom,
+ ignoreMutation (mutation) {
+ if (mutation.type === 'selection') {
+ return false
+ }
+
+ return !dom.contains(mutation.target) || dom === mutation.target
+ },
+ update: updatedNode => {
+ if (updatedNode.type !== this.type) {
+ return false
+ }
+
+ return true
+ }
+ }
+ }
+ },
+ addKeyboardShortcuts () {
+ return {
+ Backspace: (data) => {
+ const { schema, selection } = this.editor.state
+ const { empty, $anchor, $head, $from, $to } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ // if backspace hit in the node that not have any content
+ if ($anchor.parentOffset !== 0) return false
+ const contentWrapper = $anchor.doc?.nodeAt($from?.before(depth))
+
+ // if Backspace is in the contentWrapper
+ if (contentWrapper.type.name !== schema.nodes.contentHeading.name) {
+ if (contentWrapper.type.name !== schema.nodes.contentWrapper.name) return
+ // INFO: if the contentWrapper block has one child just change textSelection
+ // Otherwise remove the current line and move the textSelection to the
+
+ if (contentWrapper.childCount === 1) {
+ return this.editor.chain()
+ .setTextSelection(start - 2)
+ .scrollIntoView()
+ .run()
+ } else {
+ return this.editor.chain()
+ .deleteRange({ from: start, to: end })
+ .setTextSelection(start - 2)
+ .scrollIntoView()
+ .run()
+ }
+ }
+ },
+ // Escape node on double enter
+ Enter: ({ editor }) => { }
+ }
+ },
+ addProseMirrorPlugins () {
+ return [
+ new Plugin({
+ key: new PluginKey('crinkle'),
+ state: {
+ init (_, { doc }) {
+ return lintDeco(doc)
+ },
+ apply (tr, old) {
+ return tr.docChanged ? lintDeco(tr.doc) : old
+ }
+ },
+ props: {
+ decorations (state) {
+ return this.getState(state)
+ }
+ }
+ })
+ ]
+ }
+})
+
+export { HeadingsContent, HeadingsContent as default }
diff --git a/packages/next.js/components/TipTap/extentions/Heading.jsx b/packages/next.js/components/TipTap/extentions/Heading.jsx
new file mode 100644
index 000000000..7e71241ba
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/Heading.jsx
@@ -0,0 +1,230 @@
+import { Node, mergeAttributes, findChildren, isActive } from '@tiptap/core'
+import { Slice, Fragment } from 'prosemirror-model'
+import { Selection, Plugin, PluginKey, TextSelection } from 'prosemirror-state'
+
+import changeHeadingLevel from './changeHeadingLevel'
+import wrapContenWithHeading from './wrapContenWithHeading'
+import clipboardPast from './clipboardPast'
+import changeHeading2paragraphs from './changeHeading2paragraphs'
+import { getSelectionBlocks, getNodeState } from './helper'
+
+const Blockquote = Node.create({
+ name: 'heading',
+ content: 'contentHeading+ contentWrapper*',
+ group: 'contentWrapper',
+ defining: true,
+ isolating: true,
+ allowGapCursor: false,
+ addOptions () {
+ return {
+ levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ persist: false,
+ openClassName: 'opend',
+ id: 1,
+ HTMLAttributes: {
+ class: 'heading',
+ level: 1
+ }
+ }
+ },
+ addAttributes () {
+ return {
+ level: {
+ default: 1,
+ rendered: false
+ }
+ }
+ },
+ addNodeView () {
+ return ({ editor, getPos, node, HTMLAttributes }) => {
+ const dom = document.createElement('div')
+ const attributes = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
+ 'data-type': this.name,
+ level: node.firstChild?.attrs.level,
+ 'data-id': HTMLAttributes['data-id'] || this.options.id
+ })
+
+ const headingId = !node.attrs.id ? '1' : HTMLAttributes['data-id']
+
+ const nodeState = getNodeState(headingId)
+
+ Object.entries(attributes).forEach(([key, value]) => dom.setAttribute(key, value))
+
+ dom.classList.add(nodeState.crinkleOpen ? 'opend' : 'closed')
+
+ const content = document.createElement('div')
+
+ content.classList.add('wrapBlock')
+ content.setAttribute('data-id', headingId)
+ dom.append(content)
+
+ return {
+ dom,
+ contentDOM: content,
+ ignoreMutation: mutation => {
+ if (mutation.type === 'selection') return false
+
+ return !dom.contains(mutation.target) || dom === mutation.target
+ },
+ update: updatedNode => {
+ if (updatedNode.type.name !== this.name) return false
+
+ return true
+ }
+ }
+ }
+ },
+ parseHTML () {
+ return [
+ { tag: 'div' }
+ ]
+ },
+ renderHTML ({ node, HTMLAttributes }) {
+ // console.log(node, "coming render html")
+ const hasLevel = this.options.levels.includes(node.attrs.level)
+ const level = hasLevel
+ ? node.attrs.level
+ : this.options.levels[0]
+
+ return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
+ },
+ addCommands () {
+ return {
+ normalText: () => (arrg) => {
+ return changeHeading2paragraphs(arrg)
+ },
+ wrapBlock: (attributes) => (arrg) => {
+ const { can, chain, commands, dispatch, editor, state, tr, view } = arrg
+ const { schema, selection, doc } = state
+ const { $from, $to, $anchor, $cursor } = selection
+
+ // TODO: change heading level
+ // First get the content of heading
+ // then copy the contentWrapper of the heading
+ if ($anchor.parent.type.name === schema.nodes.contentHeading.name) {
+ return changeHeadingLevel(arrg, attributes, dispatch)
+ }
+
+ return wrapContenWithHeading(arrg, attributes, dispatch)
+ }
+ }
+ },
+ addKeyboardShortcuts () {
+ return {
+ Backspace: (data) => { },
+ Enter: ({ editor, chain }) => {
+ const { state, view } = editor
+ const { schema, selection, doc, tr } = state
+ const { $head, $anchor, $from, $to } = selection
+
+ // TODO: limited just for contentHeading, contentWrapper
+ if ($head.parent.type.name !== schema.nodes.contentHeading.name) {
+ return false
+ }
+
+ const { start, end, depth } = $from.blockRange($to)
+
+ // if a user Enter in the contentHeading block,
+ // should go to the next block, which is contentWrapper
+ const parent = $head.path.filter(x => x?.type?.name)
+ .findLast(x => x.type.name === this.name)
+
+ // INFO: if the content is hide, do not anything
+ // ! this open in the Heading block is wrong and Have to change, It's opposite
+ const headingId = parent.attrs.id
+ const nodeState = getNodeState(headingId)
+
+ if (!nodeState.crinkleOpen) return true
+
+ console.log('yes new', {
+ $head,
+ state,
+ $anchor,
+ parent,
+ content: parent?.lastChild?.firstChild?.type.name,
+ sd: Selection.near(state.doc.resolve($from.pos), 1),
+ // after: $head.start(depth + 1),
+ // newResolve: $head.node(depth + 1)
+ isHeading: parent
+ })
+
+ // FIXME: not working
+ // some times the contentWrapper cleaned up, so it should be create first
+ // otherwise just the cursour must move to contnetWrapper
+ // TODO: find better way for this 4
+ if (parent?.content?.content.length === 1 || parent.lastChild?.firstChild?.type.name === 'heading') {
+ // console.log("yes iminininin", parent.lastChild.firstChild.contentsize === 0, parent.lastChild.firstChild)
+ // If there is not any contentWrapper
+ console.log(parent.lastChild)
+ // if first child of the heading is another heading
+ // console.log(parent.lastChild.type.name === "contentWrapper")
+ // console.log(parent.lastChild.content.lastChild.type.name === "heading")
+ // if the contentWrapper does not contain any content
+ if (parent.lastChild.content.size === 0 || parent.lastChild?.firstChild?.content.size === 0) {
+ return editor.commands.insertContentAt($anchor.pos, {
+ type: 'contentWrapper',
+ content: [
+ {
+ type: 'paragraph'
+ }
+ ]
+
+ })
+ }
+ console.log('move to contetnWrapper', {
+ after: $anchor.after(depth + 1),
+ start,
+ start1: $anchor.start(depth + 2) + 1
+ })
+ // move to contentWrapper
+ editor.commands
+ .insertContentAt($anchor.start(depth + 2) + 1, '
')
+
+ return true
+ }
+
+ // INFO: 1 mean start of the next line
+ const nextLine = end + 1
+
+ return editor.chain()
+ .insertContentAt(nextLine, '
')
+ .scrollIntoView()
+ .run()
+ }
+ }
+ },
+ addProseMirrorPlugins () {
+ return [
+ // https://github.com/pageboard/pagecut/blob/bd91a17986978d560cc78642e442655f4e09ce06/src/editor.js#L234-L241
+ new Plugin({
+ key: new PluginKey('copy&pasteHeading'),
+ props: {
+ // INFO: Div turn confuses the schema service;
+ // INFO:if there is a div in the clipboard, the docsplus schema will not serialize as a must.
+ transformPastedHTML: (html, event) => html.replace(/div/g, 'span'),
+ transformPasted: (slice) => clipboardPast(slice, this.editor),
+ transformCopied: (slice, view) => {
+ // Can be used to transform copied or cut content before it is serialized to the clipboard.
+ const { selection, doc } = this.editor.state
+ const { from, to } = selection
+
+ // TODO: this function retrive blocks level from the selection, I need to block characters level from the selection
+ const contentWrapper = getSelectionBlocks(doc.cut(from, to), null, null, true, true)
+
+ // convert Json Block to Node Block
+ let serializeSelection = contentWrapper
+ .map(x => this.editor.state.schema.nodeFromJSON(x))
+
+ // convert Node Block to Fragment
+ serializeSelection = Fragment.fromArray(serializeSelection)
+
+ // convert Fragment to Slice and save it to clipboard
+ return Slice.maxOpen(serializeSelection)
+ }
+ }
+ })
+ ]
+ }
+})
+
+export { Blockquote, Blockquote as default }
diff --git a/packages/next.js/components/TipTap/extentions/UniqueId.jsx b/packages/next.js/components/TipTap/extentions/UniqueId.jsx
new file mode 100644
index 000000000..ebf3ada26
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/UniqueId.jsx
@@ -0,0 +1,254 @@
+import { Extension, findChildren, combineTransactionSteps, getChangedRanges, findChildrenInRange } from '@tiptap/core'
+import { Plugin, PluginKey } from 'prosemirror-state'
+import { Slice, Fragment } from 'prosemirror-model'
+import { v4 } from 'uuid'
+
+/**
+ * Removes duplicated values within an array.
+ * Supports numbers, strings and objects.
+ */
+function removeDuplicates (array, by = JSON.stringify) {
+ const seen = {}
+
+ return array.filter(item => {
+ const key = by(item)
+
+ return Object.prototype.hasOwnProperty.call(seen, key)
+ ? false
+ : (seen[key] = true)
+ })
+}
+
+/**
+ * Returns a list of duplicated items within an array.
+ */
+function findDuplicates (items) {
+ const filtered = items.filter((el, index) => items.indexOf(el) !== index)
+ const duplicates = removeDuplicates(filtered)
+
+ return duplicates
+}
+
+const UniqueID = Extension.create({
+ name: 'uniqueID',
+ // we’ll set a very high priority to make sure this runs first
+ // and is compatible with `appendTransaction` hooks of other extensions
+ priority: 10000,
+ addOptions () {
+ return {
+ attributeName: 'id',
+ types: [],
+ generateID: () => v4(),
+ filterTransaction: null
+ }
+ },
+ addGlobalAttributes () {
+ return [
+ {
+ types: this.options.types,
+ attributes: {
+ [this.options.attributeName]: {
+ default: null,
+ parseHTML: element => element.getAttribute(`data-${this.options.attributeName}`),
+ renderHTML: attributes => {
+ if (!attributes[this.options.attributeName]) {
+ return {}
+ }
+
+ return {
+ [`data-${this.options.attributeName}`]: attributes[this.options.attributeName]
+ }
+ }
+ }
+ }
+ }
+ ]
+ },
+ // check initial content for missing ids
+ onCreate () {
+ // Don’t do this when the collaboration extension is active
+ // because this may update the content, so Y.js tries to merge these changes.
+ // This leads to empty block nodes.
+ // See: https://github.com/ueberdosis/tiptap/issues/2400
+ // console.log(this.editor.extensionManager.extensions.filter(x => x.type === 'extension'))
+ if (this.editor.extensionManager.extensions.find(extension => extension.name === 'collaboration')) {
+ return
+ }
+ console.log('once update========>>>>')
+ const { view, state } = this.editor
+ const { tr, doc } = state
+ const { types, attributeName, generateID } = this.options
+ const nodesWithoutId = findChildren(doc, node => {
+ return types.includes(node.type.name) &&
+ node.attrs[attributeName] === null
+ })
+
+ nodesWithoutId.forEach(({ node, pos }) => {
+ tr.setNodeMarkup(pos, undefined, {
+ ...node.attrs,
+ [attributeName]: generateID()
+ })
+ })
+ tr.setMeta('addToHistory', false)
+ view.dispatch(tr)
+ },
+ addProseMirrorPlugins () {
+ let dragSourceElement = null
+ let transformPasted = false
+
+ return [
+ new Plugin({
+ key: new PluginKey('uniqueID'),
+ appendTransaction: (transactions, oldState, newState) => {
+ const docChanges = transactions.some(transaction => transaction.docChanged) &&
+ !oldState.doc.eq(newState.doc)
+ const filterTransactions = this.options.filterTransaction &&
+ transactions.some(tr => {
+ let _a, _b
+
+ return !((_b = (_a = this.options).filterTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, tr))
+ })
+
+ if (!docChanges || filterTransactions) {
+ return
+ }
+ const { tr } = newState
+ const { types, attributeName, generateID } = this.options
+ const transform = combineTransactionSteps(oldState.doc, transactions)
+ const { mapping } = transform
+
+ // get changed ranges based on the old state
+ const changes = getChangedRanges(transform)
+
+ changes.forEach(({ newRange }) => {
+ const newNodes = findChildrenInRange(newState.doc, newRange, node => {
+ return types.includes(node.type.name)
+ })
+ const newIds = newNodes
+ .map(({ node }) => node.attrs[attributeName])
+ .filter(id => id !== null)
+ const duplicatedNewIds = findDuplicates(newIds)
+
+ newNodes.forEach(({ node, pos }) => {
+ let _a
+ // instead of checking `node.attrs[attributeName]` directly
+ // we look at the current state of the node within `tr.doc`.
+ // this helps to prevent adding new ids to the same node
+ // if the node changed multiple times within one transaction
+ const id = (_a = tr.doc.nodeAt(pos)) === null || _a === void 0 ? void 0 : _a.attrs[attributeName]
+
+ if (id === null) {
+ tr.setNodeMarkup(pos, undefined, {
+ ...node.attrs,
+ [attributeName]: generateID()
+ })
+
+ return
+ }
+ // check if the node doesn’t exist in the old state
+ const { deleted } = mapping.invert().mapResult(pos)
+ const newNode = deleted && duplicatedNewIds.includes(id)
+
+ if (newNode) {
+ tr.setNodeMarkup(pos, undefined, {
+ ...node.attrs,
+ [attributeName]: generateID()
+ })
+ }
+ })
+ })
+ if (!tr.steps.length) {
+ return
+ }
+
+ return tr
+ },
+ // we register a global drag handler to track the current drag source element
+ view (view) {
+ const handleDragstart = (event) => {
+ let _a
+
+ dragSourceElement = ((_a = view.dom.parentElement) === null || _a === void 0 ? void 0 : _a.contains(event.target))
+ ? view.dom.parentElement
+ : null
+ }
+
+ window.addEventListener('dragstart', handleDragstart)
+
+ return {
+ destroy () {
+ window.removeEventListener('dragstart', handleDragstart)
+ }
+ }
+ },
+ props: {
+ // `handleDOMEvents` is called before `transformPasted`
+ // so we can do some checks before
+ handleDOMEvents: {
+ // only create new ids for dropped content while holding `alt`
+ // or content is dragged from another editor
+ drop: (view, event) => {
+ let _a
+
+ if (dragSourceElement !== view.dom.parentElement ||
+ ((_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.effectAllowed) === 'copy') {
+ dragSourceElement = null
+ transformPasted = true
+ }
+
+ return false
+ },
+ // always create new ids on pasted content
+ paste: () => {
+ transformPasted = true
+
+ return false
+ }
+ },
+ // we’ll remove ids for every pasted node
+ // so we can create a new one within `appendTransaction`
+ transformPasted: slice => {
+ if (!transformPasted) {
+ return slice
+ }
+ const { types, attributeName } = this.options
+ const removeId = (fragment) => {
+ const list = []
+
+ fragment.forEach(node => {
+ // don’t touch text nodes
+ if (node.isText) {
+ list.push(node)
+
+ return
+ }
+ // check for any other child nodes
+ if (!types.includes(node.type.name)) {
+ list.push(node.copy(removeId(node.content)))
+
+ return
+ }
+ // remove id
+ const nodeWithoutId = node.type.create({
+ ...node.attrs,
+ [attributeName]: null
+ }, removeId(node.content), node.marks)
+
+ list.push(nodeWithoutId)
+ })
+
+ return Fragment.from(list)
+ }
+
+ // reset check
+ transformPasted = false
+
+ return new Slice(removeId(slice.content), slice.openStart, slice.openEnd)
+ }
+ }
+ })
+ ]
+ }
+})
+
+export { UniqueID, UniqueID as default }
diff --git a/packages/next.js/components/TipTap/extentions/changeHeading2paragraphs.js b/packages/next.js/components/TipTap/extentions/changeHeading2paragraphs.js
new file mode 100644
index 000000000..4f75a0fe2
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/changeHeading2paragraphs.js
@@ -0,0 +1,18 @@
+import onHeading from './normalText/onHeading'
+import onSelection from './normalText/onSelection'
+
+export default (arrg) => {
+ const { state } = arrg
+ const { selection } = state
+ const { $anchor, $head } = selection
+
+ if ($anchor.pos === $head.pos) {
+ console.info('[Heading], normalText on Heading block')
+
+ return onHeading(arrg)
+ } else {
+ console.info('[Heading], normalText on selection')
+
+ return onSelection(arrg)
+ }
+}
diff --git a/packages/next.js/components/TipTap/extentions/changeHeadingLevel-backward.js b/packages/next.js/components/TipTap/extentions/changeHeadingLevel-backward.js
new file mode 100644
index 000000000..26f7bc283
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/changeHeadingLevel-backward.js
@@ -0,0 +1,70 @@
+import { TextSelection } from 'prosemirror-state'
+
+import { getRangeBlocks, getHeadingsBlocksMap, createThisBlockMap } from './helper'
+
+export default (arrg, attributes, asWrapper = false) => {
+ const { can, chain, commands, dispatch, editor, state, tr, view } = arrg
+ const { schema, selection, doc } = state
+ const { $from, $to, $anchor, $cursor, from } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ console.log('[Heading]: Backward process, comingLevel < currentHLevel')
+
+ const comingLevel = attributes.level
+ const caretSelectionTextBlock = { type: 'text', text: doc?.nodeAt($anchor.pos)?.text || $anchor.nodeBefore?.text || ' ' }
+
+ const block = createThisBlockMap($from, depth, caretSelectionTextBlock)
+ const titleNode = $from.doc.nodeAt($from.start(1) - 1)
+ const titleStartPos = $from.start(1) - 1
+ const titleEndPos = titleStartPos + titleNode.content.size
+ const contentWrapper = getRangeBlocks(doc, start, titleEndPos)
+ const titleHMap = getHeadingsBlocksMap(doc, start, titleEndPos)
+
+ const sliceTargetContent = contentWrapper.filter(x => {
+ if (x.type !== 'heading') return x
+
+ return x.le > comingLevel
+ })
+
+ // remove the first paragraph, if the request is to wrap the content
+ if (asWrapper) {
+ const pickedNode = sliceTargetContent.shift()
+
+ if (sliceTargetContent.length === 0) {
+ sliceTargetContent.push({ ...pickedNode, content: [block.paragraph] })
+ }
+ }
+
+ const endSliceBlocPos = sliceTargetContent[sliceTargetContent.length - 1].endBlockPos
+ const insertPos = titleHMap
+ .filter(x => endSliceBlocPos <= x.endBlockPos)
+ .find(x => x.le >= comingLevel)?.endBlockPos
+
+ const jsonNewBlock = {
+ type: 'heading',
+ content: [
+ {
+ type: 'contentHeading',
+ content: [block.headingContent],
+ attrs: {
+ level: attributes.level
+ }
+ },
+ {
+ type: 'contentWrapper',
+ content: sliceTargetContent
+ }
+ ]
+ }
+
+ const node = state.schema.nodeFromJSON(jsonNewBlock)
+
+ const newTr = tr.insert(insertPos, node)
+
+ const newTextSelection = new TextSelection(newTr.doc.resolve(from))
+
+ newTr.setSelection(newTextSelection)
+ newTr.deleteRange(start - 1, insertPos)
+
+ return true
+}
diff --git a/packages/next.js/components/TipTap/extentions/changeHeadingLevel-forward.js b/packages/next.js/components/TipTap/extentions/changeHeadingLevel-forward.js
new file mode 100644
index 000000000..5a14157af
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/changeHeadingLevel-forward.js
@@ -0,0 +1,110 @@
+import { TextSelection } from 'prosemirror-state'
+
+import { getRangeBlocks, getHeadingsBlocksMap, createThisBlockMap, getPrevHeadingList, getPrevHeadingPos } from './helper'
+
+export default (arrg, attributes) => {
+ const { can, chain, commands, dispatch, editor, state, tr, view } = arrg
+ const { schema, selection, doc } = state
+ const { $from, $to, $anchor, $cursor, $head, from } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ console.log('[Heading]: change heading level forwarding')
+
+ const commingLevel = attributes.level
+ const caretSelectionTextBlock = { type: 'text', text: doc?.nodeAt($anchor.pos)?.text || $anchor.nodeBefore?.text || ' ' }
+
+ const block = createThisBlockMap($from, depth, caretSelectionTextBlock)
+ const titleNode = $from.doc.nodeAt($from.start(1) - 1)
+ const titleStartPos = $from.start(1) - 1
+ const titleEndPos = titleStartPos + titleNode.content.size
+ const contentWrapper = getRangeBlocks(doc, start, titleEndPos)
+ const titleHMap = getHeadingsBlocksMap(doc, titleStartPos, titleEndPos)
+
+ const contentWrapperParagraphs = contentWrapper.filter(x => x.type !== 'heading')
+ const contentWrapperHeadings = contentWrapper.filter(x => x.type === 'heading')
+
+ const { prevHStartPos, prevHEndPos } = getPrevHeadingPos(doc, titleStartPos, start - 1)
+
+ let mapHPost = titleHMap.filter(x =>
+ x.startBlockPos < start - 1 &&
+ x.startBlockPos >= prevHStartPos
+ )
+
+ let shouldNested = false
+
+ // let prevBlock = mapHPost.find(x => x.le >= commingLevel)
+ // FIXME: this is heavy! I need to find better solotion with less loop
+ const prevBlockEqual = mapHPost.findLast(x => x.le === commingLevel)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= commingLevel)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= commingLevel)
+ const lastBlock = mapHPost.at(-1)
+ let prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+
+ if (lastBlock?.le <= commingLevel) prevBlock = lastBlock
+ if (!prevBlock) prevBlock = lastBlock
+ shouldNested = prevBlock.le < commingLevel
+
+ if (prevBlock.le === 1) shouldNested = false
+
+ const jsonNode = {
+ type: 'heading',
+ content: [
+ {
+ type: 'contentHeading',
+ content: [block.headingContent],
+ attrs: {
+ level: attributes.level
+ }
+ },
+ {
+ type: 'contentWrapper',
+ content: contentWrapperParagraphs
+ }
+ ]
+ }
+
+ const node = state.schema.nodeFromJSON(jsonNode)
+
+ // remove content from the current positon to the end of the heading
+ tr.delete(start - 1, titleEndPos)
+ // then add the new heading with the content
+ const insertPos = (contentWrapperHeadings.length === 0 ? prevBlock.endBlockPos : start - 1) - (shouldNested ? 2 : 0)
+
+ tr.insert(tr.mapping.map(insertPos), node)
+
+ // set the cursor to the end of the heading
+ const newSelection = new TextSelection(tr.doc.resolve(from))
+
+ tr.setSelection(newSelection)
+
+ // after all that, we need to loop through the heading to append
+ for (const heading of contentWrapperHeadings) {
+ const commingLevel = heading.le
+
+ mapHPost = getPrevHeadingList(
+ tr,
+ mapHPost.at(0).startBlockPos,
+ tr.mapping.map(mapHPost.at(0).endBlockPos)
+ )
+
+ mapHPost = mapHPost.filter(x =>
+ x.startBlockPos < heading.startBlockPos &&
+ x.startBlockPos >= prevHStartPos
+ )
+
+ const prevBlockEqual = mapHPost.findLast(x => x.le === commingLevel)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= commingLevel)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= commingLevel)
+ const lastBlock = mapHPost.at(-1)
+
+ prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+ if (lastBlock.le <= commingLevel) prevBlock = lastBlock
+ shouldNested = prevBlock.le < commingLevel
+
+ const node = state.schema.nodeFromJSON(heading)
+
+ tr.insert(prevBlock.endBlockPos - (shouldNested ? 2 : 0), node)
+ }
+
+ return true
+}
diff --git a/packages/next.js/components/TipTap/extentions/changeHeadingLevel-h1.js b/packages/next.js/components/TipTap/extentions/changeHeadingLevel-h1.js
new file mode 100644
index 000000000..d4c4be94d
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/changeHeadingLevel-h1.js
@@ -0,0 +1,118 @@
+import { TextSelection } from 'prosemirror-state'
+
+import { getPrevHeadingList, createThisBlockMap, getHeadingsBlocksMap, getRangeBlocks, getPrevHeadingPos } from './helper'
+
+export default (arrg, attributes) => {
+ const { can, chain, commands, dispatch, editor, state, tr, view } = arrg
+ const { schema, selection, doc } = state
+ const { $from, $to, $anchor, $cursor, from } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ console.log('[Heading]: change heading Level h1')
+
+ const commingLevel = attributes.level
+ const caretSelectionTextBlock = { type: 'text', text: doc?.nodeAt($anchor.pos)?.text || $anchor.nodeBefore?.text || ' ' }
+
+ const block = createThisBlockMap($from, depth, caretSelectionTextBlock)
+ const currentHLevel = $from.doc.nodeAt(block.start).attrs.level
+
+ let titleStartPos = 0
+ let titleEndPos = 0
+
+ doc.nodesBetween($from.start(0), start - 1, function (node, pos, parent, index) {
+ if (node.type.name === 'heading') {
+ const headingLevel = node.firstChild?.attrs?.level
+
+ if (headingLevel === currentHLevel) {
+ titleStartPos = pos
+ // INFO: I need the pos of last content in contentWrapper
+ titleEndPos = pos + node.content.size
+ }
+ }
+ })
+
+ const contentWrapper = getRangeBlocks(doc, start, $from.start(1) - 1 + $from.doc.nodeAt($from.start(1) - 1).content.size)
+ const titleHMap = getHeadingsBlocksMap(doc, titleStartPos, titleEndPos)
+
+ const contentWrapperParagraphs = contentWrapper.filter(x => x.type !== 'heading')
+ const contentWrapperHeadings = contentWrapper.filter(x => x.type === 'heading')
+
+ const { prevHStartPos, prevHEndPos } = getPrevHeadingPos(doc, titleStartPos, start - 1)
+
+
+ let mapHPost = titleHMap.filter(x =>
+ x.startBlockPos < start - 1 &&
+ x.startBlockPos >= prevHStartPos
+ )
+
+ let shouldNested = false
+
+ // FIXME: this is heavy! I need to find better solotion with less loop
+ const prevBlockEqual = mapHPost.findLast(x => x.le === commingLevel)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= commingLevel)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= commingLevel)
+ const lastbloc = mapHPost.at(-1)
+ let prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+
+ if (lastbloc.le <= commingLevel) prevBlock = lastbloc
+ shouldNested = prevBlock.le < commingLevel
+
+ const jsonNode = {
+ type: 'heading',
+ content: [
+ {
+ type: 'contentHeading',
+ content: [block.headingContent],
+ attrs: {
+ level: attributes.level
+ }
+ },
+ {
+ type: 'contentWrapper',
+ content: contentWrapperParagraphs
+ }
+ ]
+ }
+ const node = state.schema.nodeFromJSON(jsonNode)
+
+ // remove content from the current positon to the end of the heading
+ tr.delete(start - 1, $from.start(1) - 1 + $from.doc.nodeAt($from.start(1) - 1).content.size)
+
+ // then add the new heading with the content
+ const insertPos = prevBlock.endBlockPos - (shouldNested ? 2 : 0)
+
+ tr.insert(tr.mapping.map(insertPos), node)
+
+ // set the cursor to the end of the heading
+ const newSelection = new TextSelection(tr.doc.resolve(from))
+
+ tr.setSelection(newSelection)
+
+ // FIXME: this loop so much heavy, I need to find better solotion!
+ // then loop through the heading to append
+ for (const heading of contentWrapperHeadings) {
+ mapHPost = getPrevHeadingList(
+ tr,
+ mapHPost.at(0).startBlockPos,
+ mapHPost.at(0).startBlockPos + doc.nodeAt(mapHPost.at(0).startBlockPos).nodeSize + 2
+ )
+
+ mapHPost = mapHPost.filter(x =>
+ x.startBlockPos < heading.startBlockPos &&
+ x.startBlockPos >= prevHStartPos
+ )
+
+ const node = state.schema.nodeFromJSON(heading)
+
+ const prevBlockEqual = mapHPost.findLast(x => x.le === heading.le)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= heading.le)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= heading.le)
+ const lastbloc = mapHPost.at(-1)
+ let prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+
+ if (lastbloc.le <= heading.le) prevBlock = lastbloc
+ shouldNested = prevBlock.le < heading.le
+
+ tr.insert(prevBlock.endBlockPos - (shouldNested ? 2 : 0), node)
+ }
+}
diff --git a/packages/next.js/components/TipTap/extentions/changeHeadingLevel.js b/packages/next.js/components/TipTap/extentions/changeHeadingLevel.js
new file mode 100644
index 000000000..2933700fd
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/changeHeadingLevel.js
@@ -0,0 +1,36 @@
+import changeHeadingLevelBackward from './changeHeadingLevel-backward'
+import changeHeadingLevelForward from './changeHeadingLevel-forward'
+import changeHeadingLevelForwardH1 from './changeHeadingLevel-h1'
+
+export default (arrg, attributes) => {
+ const { can, chain, commands, dispatch, editor, state, tr, view } = arrg
+ const { schema, selection, doc } = state
+ const { $from, $to, $anchor, $cursor } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ const commingLevel = attributes.level
+ const currentHLevel = $from.doc.nodeAt(start).attrs.level
+
+ if (commingLevel === currentHLevel) {
+ console.log('[heading]: commingLevel === nextSiblingLevel')
+
+ return true
+ }
+
+ // H1
+ if (currentHLevel === 1) {
+ return changeHeadingLevelForwardH1(arrg, attributes)
+ }
+
+ // H2 > H3 || H4 > H3
+ if (commingLevel > currentHLevel) {
+ return changeHeadingLevelForward(arrg, attributes)
+ }
+
+ // H2 < H3 || H4 < H3
+ if (commingLevel < currentHLevel) {
+ return changeHeadingLevelBackward(arrg, attributes)
+ }
+
+ return true
+}
diff --git a/packages/next.js/components/TipTap/extentions/clipboardPast.js b/packages/next.js/components/TipTap/extentions/clipboardPast.js
new file mode 100644
index 000000000..8bc59b859
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/clipboardPast.js
@@ -0,0 +1,213 @@
+import { Slice, Fragment, NodeRange, NodeType, Mark, ContentMatch } from 'prosemirror-model'
+import { TextSelection, Selection } from 'prosemirror-state'
+
+import { getRangeBlocks, getHeadingsBlocksMap } from './helper'
+
+const normalizeClipboardContents = (clipboardContents, editor) => {
+ const paragraphs = []
+ const headings = []
+ let heading = null
+
+ for (const node of clipboardContents) {
+ if (!heading && node.type !== 'contentHeading') {
+ paragraphs.push(editor.schema.nodeFromJSON(node))
+ }
+
+ if (node.type === 'contentHeading') {
+ // if new heading is found, push the previous heading into the heading list
+ // and reset the heading
+ if (heading) {
+ headings.push(editor.schema.nodeFromJSON(heading))
+ heading = null
+ }
+ heading = {
+ type: 'heading',
+ attrs: { level: node?.attrs.level },
+ content: [
+ node,
+ {
+ type: 'contentWrapper',
+ content: []
+ }
+ ]
+ }
+ } else {
+ heading?.content.at(1).content.push(node)
+ }
+ }
+
+ if (heading) {
+ headings.push(editor.schema.nodeFromJSON(heading))
+ }
+
+ return [paragraphs, headings]
+}
+
+export default (slice, editor) => {
+ const { state, view } = editor
+ const { schema, selection, doc, tr } = state
+ const { from, to, $anchor } = selection
+
+ let newPosResolver
+ let $from = selection.$from
+ let start = $from.pos
+
+ // if user cursor is in the heading,
+ // move the cursor to the contentWrapper and do the rest
+ if ($from.parent.type.name === 'contentHeading') {
+ const firstLine = doc.nodeAt(start + 2)
+
+ let resolveNextBlock = tr.doc.resolve(start + 2)
+
+ newPosResolver = resolveNextBlock
+
+ // if the heading block does not contain contentWrapper as a first child
+ // then create a contentWrapper block
+ if (firstLine.type.name === 'heading') {
+ const contentWrapperBlock = {
+ type: 'contentWrapper',
+ content: [
+ {
+ type: 'paragraph'
+ }
+ ]
+
+ }
+
+ const node = state.schema.nodeFromJSON(contentWrapperBlock)
+
+ tr.insert(start, node)
+ resolveNextBlock = tr.doc.resolve(start + 2)
+ }
+
+ // put the selection to the first line of contentWrapper block
+ if (resolveNextBlock.parent.type.name === 'contentWrapper') {
+ tr.setSelection(TextSelection.near(resolveNextBlock))
+ }
+ }
+
+ // If caret selection move to contentWrapper, create a new selection
+ if (newPosResolver) {
+ $from = (new Selection(
+ newPosResolver,
+ newPosResolver
+ )).$from
+ }
+
+ start = $from.pos
+
+ // return Slice.empty
+
+ const titleNode = $from.doc.nodeAt($from.start(1) - 1)
+ let titleStartPos = $from.start(1) - 1
+ let titleEndPos = titleStartPos + titleNode.content.size
+ const contentWrapper = getRangeBlocks(doc, start, titleEndPos)
+
+ let prevHStartPos = 0
+ let prevHEndPos = 0
+
+ doc.nodesBetween(titleStartPos, start - 1, function (node, pos, parent, index) {
+ if (node.type.name === 'heading') {
+ const depth = doc.resolve(pos).depth
+
+ // INFO: this the trick I've looking for
+ if (depth === 2) {
+ prevHStartPos = pos
+ prevHEndPos = pos + node.content.size
+ }
+ }
+ })
+
+ const clipboardContentJson = slice.toJSON().content
+
+ const [paragraphs, headings] = normalizeClipboardContents(clipboardContentJson, state)
+
+ // if there is no heading block, then just return
+ if (headings.length <= 0) return slice
+
+ // return Slice.empty
+ let shouldNested = false
+
+ tr.delete(to, titleEndPos)
+
+ if (paragraphs.length !== 0) {
+ // first append the paragraphs in the current selection
+ tr.replaceWith(tr.mapping.map(from), tr.mapping.map(from), paragraphs)
+
+ const newSelection = new TextSelection(tr.doc.resolve(selection.from))
+
+ tr.setSelection(newSelection)
+ }
+
+ let lastBlockPos = paragraphs.length === 0 ? start : tr.mapping.map(start)
+
+ const headingContent = contentWrapper.filter(x => x.type === 'heading')
+
+ if (headingContent.length > 0) {
+ headingContent.forEach(heading => headings.push(state.schema.nodeFromJSON(heading)))
+ }
+
+ let mapHPost = {}
+ // return Slice.empty
+
+ const lastH1Inserted = {
+ startBlockPos: 0,
+ endBlockPos: 0,
+ }
+
+ // paste the headings
+ for (const heading of headings) {
+ const comingLevel = heading.content.firstChild.attrs.level
+
+ const startBlock = lastH1Inserted.startBlockPos === 0 ? tr.mapping.map(start) : lastH1Inserted.startBlockPos
+ const endBlock = lastH1Inserted.endBlockPos === 0 ? tr.mapping.map(titleEndPos) : tr.doc.nodeAt(lastH1Inserted.startBlockPos).content.size + lastH1Inserted.startBlockPos
+
+ if (lastH1Inserted.startBlockPos !== 0) {
+ lastH1Inserted.startBlockPos = 0
+ lastH1Inserted.endBlockPos = 0
+ }
+
+ mapHPost = getHeadingsBlocksMap(tr.doc, startBlock, endBlock)
+
+ mapHPost = mapHPost.filter(x =>
+ x.startBlockPos >= (comingLevel === 1 ? titleStartPos : prevHStartPos)
+ )
+
+ const prevBlockEqual = mapHPost.findLast(x => x.le === comingLevel)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= comingLevel)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= comingLevel)
+
+ const lastBlock = mapHPost.at(-1)
+ let prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+
+ if (!prevBlock) prevBlock = lastBlock
+
+ if (lastBlock.le <= comingLevel) prevBlock = lastBlock
+
+ shouldNested = prevBlock.le < comingLevel
+
+ // find prevBlock.le in mapHPost
+ const robob = mapHPost.filter(x => prevBlock.le === x.le)
+
+ if (robob.length > 1) {
+ prevBlock = robob.at(-1)
+ }
+
+ lastBlockPos = prevBlock.endBlockPos
+
+ if (prevBlock && prevBlock.depth === 2) {
+ prevHStartPos = prevBlock.startBlockPos
+ }
+
+ tr.insert(lastBlockPos - (shouldNested ? 2 : 0), heading)
+
+ if (comingLevel === 1) {
+ lastH1Inserted.startBlockPos = lastBlockPos
+ lastH1Inserted.endBlockPos = tr.mapping.map(lastBlockPos + heading.content.size)
+ }
+ }
+ tr.setMeta('paste', true)
+ view.dispatch(tr)
+
+ return Slice.empty
+}
diff --git a/packages/next.js/components/TipTap/extentions/crinkle.event.js b/packages/next.js/components/TipTap/extentions/crinkle.event.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/next.js/components/TipTap/extentions/helper.js b/packages/next.js/components/TipTap/extentions/helper.js
new file mode 100644
index 000000000..1500eed40
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/helper.js
@@ -0,0 +1,239 @@
+import { Slice, Fragment, NodeRange, NodeType, Mark, ContentMatch } from 'prosemirror-model'
+
+/**
+ *
+ * @param {Object} tr transform object
+ * @param {Number} start start pos
+ * @param {Number} from end pos
+ * @returns
+ */
+export const getPrevHeadingList = (tr, start, from) => {
+ const titleHMap = []
+
+ if (from < start) throw new Error("[Heading]: position is invalid 'from < start'")
+ try {
+ tr.doc.nodesBetween(start, from, function (node, pos, parent, index) {
+ if (node.type.name === 'heading') {
+ const headingLevel = node.firstChild?.attrs?.level
+ const depth = tr.doc.resolve(pos).depth
+
+ titleHMap.push({
+ le: headingLevel,
+ node: node.toJSON(),
+ depth,
+ startBlockPos: pos,
+ endBlockPos: pos + node.nodeSize
+ })
+ }
+ })
+ } catch (error) {
+ console.error('[Heading]: getPrevHeadingList', error, { tr, start, from })
+ // return Slice.empty
+ } finally {
+ return titleHMap
+ }
+}
+
+/**
+ *
+ * @param {Object} doc prosemirror doc
+ * @param {Number} start start pos
+ * @param {Number} end end pos
+ * @returns Array of Selection Block
+ */
+export const getSelectionBlocks = (doc, start, end, includeContentHeading = false, range = false) => {
+ const firstHEading = true
+ const prevDepth = 0
+ const selectedContents = []
+
+ if (range) {
+ console.log(doc)
+ doc.descendants(function (node, pos, parent) {
+ if (firstHEading && node.type.name !== 'heading' && parent.type.name === 'contentWrapper') {
+ const depth = doc.resolve(pos).depth
+
+ selectedContents.push({ depth, startBlockPos: pos, endBlockPos: pos + node.nodeSize, ...node.toJSON() })
+ }
+
+ if (node.type.name === 'contentHeading') {
+ const depth = doc.resolve(pos).depth
+
+ selectedContents.push({
+ depth,
+ level: node.attrs?.level,
+ attrs: includeContentHeading ? node.attrs : {},
+ startBlockPos: pos,
+ endBlockPos: pos + node.nodeSize,
+ type: includeContentHeading ? node.type.name : 'paragraph',
+ content: node.toJSON().content
+ })
+ }
+ })
+
+ return selectedContents
+ }
+
+ doc.nodesBetween(start, end, function (node, pos, parent, index) {
+ if (pos < start) return
+
+ if (firstHEading && node.type.name !== 'heading' && parent.type.name === 'contentWrapper') {
+ const depth = doc.resolve(pos).depth
+
+ selectedContents.push({ depth, startBlockPos: pos, endBlockPos: pos + node.nodeSize, ...node.toJSON() })
+ }
+
+ if (node.type.name === 'contentHeading') {
+ const depth = doc.resolve(pos).depth
+
+ selectedContents.push({
+ depth,
+ level: node.attrs?.level,
+ attrs: includeContentHeading ? node.attrs : {},
+ startBlockPos: pos,
+ endBlockPos: pos + node.nodeSize,
+ type: includeContentHeading ? node.type.name : 'paragraph',
+ content: node.toJSON().content
+ })
+ }
+ })
+
+ return selectedContents
+}
+
+/**
+ *
+ * @param {Object} doc prosemirror doc
+ * @param {Number} start start pos
+ * @param {Number} end end pos
+ * @returns Array of Selection Block
+ */
+export const getRangeBlocks = (doc, start, end) => {
+ let firstHEading = true
+ let prevDepth = 0
+ const selectedContents = []
+
+ doc.nodesBetween(start, end, function (node, pos, parent, index) {
+ if (pos < start) return
+
+ if (firstHEading && node.type.name !== 'heading' && parent.type.name === 'contentWrapper') {
+ const depth = doc.resolve(pos).depth
+
+ selectedContents.push({ depth, startBlockPos: pos, endBlockPos: pos + node.nodeSize, ...node.toJSON() })
+ }
+ if (node.type.name === 'heading') {
+ firstHEading = false
+ const headingLevel = node.firstChild?.attrs?.level
+ const depth = doc.resolve(pos).depth
+
+ if (prevDepth === 0) prevDepth = depth
+
+ if (prevDepth >= depth) {
+ selectedContents.push({ le: headingLevel, depth, startBlockPos: pos, endBlockPos: pos + node.nodeSize, ...node.toJSON() })
+ prevDepth = depth
+ }
+ }
+ })
+
+ return selectedContents
+}
+
+/**
+ * Returns a map of headings and the blocks that fall under them
+ *
+ * @param {Object} doc
+ * @param {number} start
+ * @param {number} end
+ * @returns {Map}
+ */
+export const getHeadingsBlocksMap = (doc, start, end) => {
+ const titleHMap = []
+
+ doc.nodesBetween(start, end, function (node, pos, parent, index) {
+ if (node.type.name === 'heading') {
+ const headingLevel = node.firstChild?.attrs?.level
+ const depth = doc.resolve(pos).depth
+
+ titleHMap.push({ le: headingLevel, depth, startBlockPos: pos, endBlockPos: pos + node.nodeSize, index })
+ }
+ })
+
+ return titleHMap
+}
+
+/**
+ * Creates a block map of the editor state.
+ * @param {Object} $from - The current selection's start position in the editor state.
+ * @param {number} depth - The current selection's depth.
+ * @param {Object} caretSelectionTextBlock - The current selection's text block.
+ * @returns {Object} blockMap - The current selection's block map.
+ */
+
+export const createThisBlockMap = ($from, depth, caretSelectionTextBlock = ' ') => {
+ return {
+ parent: {
+ end: $from.end(depth - 1),
+ start: $from.start(depth - 1)
+ },
+ edge: {
+ end: $from.end(depth - 1) + 1,
+ start: $from.start(depth - 1) - 1
+ },
+ ancesster: {
+ start: $from.start(1),
+ end: $from.end(1)
+ },
+ end: $from.end(depth),
+ start: $from.start(depth),
+ nextLevel: 0,
+ depth,
+ headingContent: caretSelectionTextBlock,
+ empty: {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'text',
+ text: ' '
+ }
+ ]
+ },
+ paragraph: { type: 'paragraph' }
+ }
+}
+
+/**
+ * This method copies a text to clipboard
+ * @param {string} text - the text to copy
+ * @param {function} callback - a callback to execute after the text is copied
+ */
+export const copyToClipboard = (text, callback) => {
+ navigator.clipboard.writeText(text).then(() => {
+ alert('Copied to clipboard')
+ if (callback) callback()
+ })
+}
+
+export const getNodeState = (headingId) => {
+ const headingMap = JSON.parse(localStorage.getItem('headingMap')) || []
+ const nodeState = headingMap.find(h => h.headingId === headingId) || { crinkleOpen: true }
+
+ return nodeState
+}
+
+export const getPrevHeadingPos = (doc, startPos, endPos) => {
+ let prevHStartPos = 0
+ let prevHEndPos = 0
+
+ doc.nodesBetween(startPos, endPos, function (node, pos, parent, index) {
+ if (node.type.name === 'heading') {
+ const depth = doc.resolve(pos).depth
+
+ // INFO: this the trick I've looking for
+ if (depth === 2) {
+ prevHStartPos = pos
+ prevHEndPos = pos + node.content.size
+ }
+ }
+ })
+
+ return { prevHStartPos, prevHEndPos }
+}
diff --git a/packages/next.js/components/TipTap/extentions/normalText/onHeading.js b/packages/next.js/components/TipTap/extentions/normalText/onHeading.js
new file mode 100644
index 000000000..d110d2e7c
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/normalText/onHeading.js
@@ -0,0 +1,115 @@
+import { TextSelection } from 'prosemirror-state'
+
+import { getRangeBlocks, getPrevHeadingList } from '../helper'
+
+export default (arrg) => {
+ const { can, chain, commands, dispatch, editor, state, tr, view } = arrg
+ const { schema, selection, doc } = state
+ const { $from, $to, $anchor, $cursor, $head, from } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ if (doc?.nodeAt(start).type.name !== 'contentHeading') {
+ return console.info('[Heading]: not heading')
+ }
+
+ const headingText = { type: 'text', text: doc?.nodeAt($anchor.pos)?.text || $anchor.nodeBefore?.text || ' ' }
+
+ const titleNode = $from.doc.nodeAt($from.start(1) - 1)
+ let titleStartPos = $from.start(1) - 1
+ let titleEndPos = titleStartPos + titleNode.content.size
+ const contentWrapper = getRangeBlocks(doc, start, titleEndPos)
+ const currentHLevel = $from.parent.attrs.level
+
+ let prevHStartPos = 0
+ let prevHEndPos = 0
+
+ const backspaceAction = doc.nodeAt(from) === null && $anchor.parentOffset === 0
+
+ tr.delete(backspaceAction ? start - 1 : start - 1, titleEndPos)
+
+ // if the current heading is not H1, otherwise we need to find the previous H1
+ if (currentHLevel !== 1) {
+ doc.nodesBetween(titleStartPos, start - 1, function (node, pos, parent, index) {
+ if (node.type.name === 'heading') {
+ const depth = doc.resolve(pos).depth
+
+ // INFO: this the trick I've looking for
+ if (depth === 2) {
+ prevHStartPos = pos
+ prevHEndPos = pos + node.content.size
+ }
+ }
+ })
+ } else {
+ doc.nodesBetween($from.start(0), start - 1, function (node, pos, parent, index) {
+ if (node.type.name === 'heading') {
+ const headingLevel = node.firstChild?.attrs?.level
+
+ if (headingLevel === currentHLevel) {
+ titleStartPos = pos
+ titleEndPos = pos + node.content.size
+ }
+ }
+ })
+ }
+
+ const contentWrapperParagraphs = contentWrapper.filter(x => x.type !== 'heading')
+ const contentWrapperHeadings = contentWrapper.filter(x => x.type === 'heading')
+ const normalContents = [headingText, ...contentWrapperParagraphs]
+ .map(x => editor.schema.nodeFromJSON(x))
+
+ // this is for backspace, if the node is empty remove the TextNode
+ if (backspaceAction) normalContents.shift()
+
+ const titleHMap = getPrevHeadingList(tr, titleStartPos, tr.mapping.map(titleEndPos))
+
+ let mapHPost = titleHMap.filter(x =>
+ x.startBlockPos < start - 1 &&
+ x.startBlockPos >= prevHStartPos
+ )
+
+ let shouldNested = false
+
+ const comingLevel = mapHPost.at(-1).le + 1
+
+ const prevBlockEqual = mapHPost.findLast(x => x.le === comingLevel)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= comingLevel)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= comingLevel)
+ const lastbloc = mapHPost.at(-1)
+ let prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+
+ if (lastbloc.le <= comingLevel) prevBlock = lastbloc
+ shouldNested = prevBlock.le < comingLevel
+
+ const insertPos = prevBlock.endBlockPos - (shouldNested ? 2 : 0)
+
+ tr.insert(tr.mapping.map(insertPos), normalContents)
+
+ const focusSelection = new TextSelection(tr.doc.resolve(insertPos + 1))
+
+ tr.setSelection(focusSelection)
+
+ for (let heading of contentWrapperHeadings) {
+ if (!heading.le) heading = { ...heading, le: heading.content[0].attrs.level, startBlockPos: 0 }
+
+ const startBlock = mapHPost[0].startBlockPos
+ const endBlock = tr.mapping.map(mapHPost.at(0).endBlockPos)
+
+ mapHPost = getPrevHeadingList(tr, startBlock, endBlock)
+
+ const node = state.schema.nodeFromJSON(heading)
+
+ const prevBlockEqual = mapHPost.findLast(x => x.le === heading.le)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= heading.le)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= heading.le)
+ const lastbloc = mapHPost[mapHPost.length - 1]
+ let prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+
+ if (lastbloc.le <= heading.le) prevBlock = lastbloc
+ shouldNested = prevBlock.le < heading.le
+
+ tr.insert(prevBlock.endBlockPos - (shouldNested ? 2 : 0), node)
+ }
+
+ return (backspaceAction) ? view.dispatch(tr) : true
+}
diff --git a/packages/next.js/components/TipTap/extentions/normalText/onSelection.js b/packages/next.js/components/TipTap/extentions/normalText/onSelection.js
new file mode 100644
index 000000000..b084c458d
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/normalText/onSelection.js
@@ -0,0 +1,94 @@
+import { TextSelection } from 'prosemirror-state'
+
+import { getSelectionBlocks, getRangeBlocks, getPrevHeadingList } from '../helper'
+
+export default (arrg) => {
+ const { can, chain, commands, dispatch, editor, state, tr, view } = arrg
+ const { schema, selection, doc } = state
+ const { $from, $to, $anchor, $cursor, $head, from, to } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ const currentHLevel = $from.parent.attrs.level
+ const titleNode = $from.doc.nodeAt($from.start(1) - 1)
+ let titleStartPos = $from.start(1) - 1
+ let titleEndPos = titleStartPos + titleNode.content.size
+ const prevHStartPos = 0
+ const prevHEndPos = 0
+
+ const selectionFirstLinePos = $from.pos - $from.parentOffset
+
+ const selectedContents = getSelectionBlocks(doc, selectionFirstLinePos - 1, to)
+ const lastHeadingInSelection = selectedContents.findLast(x => x.hasOwnProperty('attrs'))
+ const lastHeadingIndex = selectedContents.findLastIndex(x => x.hasOwnProperty('attrs'))
+
+ // on selection we have Heading level 1
+ if (titleEndPos < to) {
+ const getTitleBlock = selectedContents.findLast(x => x.level === 1)
+
+ titleEndPos = (getTitleBlock.startBlockPos + doc.nodeAt(getTitleBlock.startBlockPos - 1).nodeSize) - 1
+ }
+
+ // select rest of contents
+ const contentWrapper = getRangeBlocks(doc, lastHeadingInSelection.startBlockPos, titleEndPos)
+ const contentWrapperParagraphs = contentWrapper.filter(x => x.type !== 'heading')
+ const contentWrapperHeadings = contentWrapper.filter(x => x.type === 'heading')
+
+ const normalizeSelectedContents = [
+ ...[...selectedContents].splice(0, lastHeadingIndex + 1),
+ ...contentWrapperParagraphs
+ ]
+
+ doc.nodesBetween(titleStartPos, start - 1, function (node, pos, parent, index) {
+ if (node.type.name === 'heading') {
+ const headingLevel = node.firstChild?.attrs?.level
+
+ if (headingLevel === currentHLevel) {
+ titleStartPos = pos
+ // INFO: I need the pos of last content in contentWrapper
+ titleEndPos = pos + node.content.size
+ }
+ }
+ })
+
+ const normalizeSelectedContentsBlocks = normalizeSelectedContents.map(node => state.schema.nodeFromJSON(node))
+
+ tr.delete(selectionFirstLinePos, titleEndPos)
+
+ tr.insert(tr.mapping.map(selectionFirstLinePos) - 1, normalizeSelectedContentsBlocks)
+
+ const focusSelection = new TextSelection(tr.doc.resolve(from))
+
+ tr.setSelection(focusSelection)
+
+ const titleHMap = getPrevHeadingList(tr, tr.mapping.map(titleStartPos), tr.mapping.map(titleEndPos))
+ let shouldNested = false
+
+ let mapHPost = titleHMap.filter(x =>
+ x.startBlockPos < start - 1 &&
+ x.startBlockPos >= prevHStartPos
+ )
+
+ if (!mapHPost.length) mapHPost = titleHMap
+
+ for (const heading of contentWrapperHeadings) {
+ const startBlock = mapHPost[0].startBlockPos
+ const endBlock = tr.mapping.map(mapHPost.at(0).endBlockPos)
+
+ mapHPost = getPrevHeadingList(tr, startBlock, endBlock)
+
+ const node = state.schema.nodeFromJSON(heading)
+
+ const prevBlockEqual = mapHPost.findLast(x => x.le === heading.le)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= heading.le)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= heading.le)
+ const lastbloc = mapHPost[mapHPost.length - 1]
+ let prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+
+ if (lastbloc.le <= heading.le) prevBlock = lastbloc
+ shouldNested = prevBlock.le < heading.le
+
+ tr.insert(prevBlock.endBlockPos - (shouldNested ? 2 : 0), node)
+ }
+
+ return true
+}
diff --git a/packages/next.js/components/TipTap/extentions/wrapContenWithHeading.js b/packages/next.js/components/TipTap/extentions/wrapContenWithHeading.js
new file mode 100644
index 000000000..5fbc022e2
--- /dev/null
+++ b/packages/next.js/components/TipTap/extentions/wrapContenWithHeading.js
@@ -0,0 +1,136 @@
+import { TextSelection } from 'prosemirror-state'
+
+import changeHeadingLevelBackward from './changeHeadingLevel-backward'
+import {
+ getPrevHeadingList,
+ createThisBlockMap,
+ getHeadingsBlocksMap,
+ getRangeBlocks
+} from './helper'
+
+export default (arrg, attributes) => {
+ const { can, chain, commands, dispatch, editor, state, tr, view } = arrg
+ const { schema, selection, doc } = state
+ const { $from, $to, $anchor, $cursor, to } = selection
+ const { start, end, depth } = $from.blockRange($to)
+
+ const cominglevel = attributes.level
+ const caretSelectionTextBlock = { type: 'text', text: doc?.nodeAt($anchor.pos)?.text || $anchor.nodeBefore?.text || ' ' }
+ const block = createThisBlockMap($from, depth, caretSelectionTextBlock)
+
+ // TODO: check this statment for comment, (block.edge.start !== -1)
+ const parentLevel = block.edge.start !== -1 && doc?.nodeAt(block.edge.start)?.content?.content[0]?.attrs.level
+
+ // Create a new heading with the same level
+ // in this case all content below must cut and wrapp with the new heading
+ // And the depth should be the same as the sibling Heading
+ if (cominglevel === parentLevel) {
+ console.info('[Heading]: create a new heading with same level')
+ const contents = getRangeBlocks(doc, end, block.parent.end)
+ const contentWrapper = contents.length === 0 ? [block.paragraph] : contents
+
+ const jsonNode = {
+ type: 'heading',
+ content: [
+ {
+ type: 'contentHeading',
+ content: [block.headingContent],
+ attrs: {
+ level: attributes.level
+ }
+ },
+ {
+ type: 'contentWrapper',
+ content: contentWrapper
+ }
+ ]
+ }
+
+ const headingNode = state.schema.nodeFromJSON(jsonNode)
+ const insertPos = start + 1
+
+ tr.delete(insertPos, block.edge.end)
+ tr.insert(tr.mapping.map(block.edge.end), headingNode)
+
+ const newSelection = new TextSelection(tr.doc.resolve(insertPos))
+
+ tr.setSelection(newSelection)
+
+ return true
+ }
+
+ // Create a new Heading block as a child of the current Heading block
+ if (cominglevel > parentLevel) {
+ console.info('[Heading]: Create a new Heading block as a child of the current Heading block')
+
+ const titleHMap = getHeadingsBlocksMap(doc, block.start, block.parent.end)
+ const contentWrapper = getRangeBlocks(doc, end, block.parent.end)
+ const contentWrapperParagraphs = contentWrapper.filter(x => x.type !== 'heading')
+ const contentWrapperHeadings = contentWrapper.filter(x => x.type === 'heading')
+
+ const jsonNode = {
+ type: 'heading',
+ content: [
+ {
+ type: 'contentHeading',
+ content: [block.headingContent],
+ attrs: {
+ level: cominglevel
+ }
+ },
+ {
+ type: 'contentWrapper',
+ content: contentWrapperParagraphs
+ }
+ ]
+ }
+ const newHeadingNode = state.schema.nodeFromJSON(jsonNode)
+
+ tr.delete(start, block.parent.end)
+ tr.insert(tr.mapping.map(start), newHeadingNode)
+
+ const newSelection = new TextSelection(tr.doc.resolve(end))
+
+ tr.setSelection(newSelection)
+
+ let mapHPost = titleHMap
+
+ for (const heading of contentWrapperHeadings) {
+ const cominglevel = heading.le
+
+ mapHPost = getPrevHeadingList(
+ tr,
+ mapHPost.at(0).startBlockPos,
+ tr.mapping.map(mapHPost.at(0).endBlockPos)
+ )
+
+ mapHPost = mapHPost.filter(x =>
+ x.startBlockPos < heading.startBlockPos &&
+ x.startBlockPos >= block.parent.start
+ )
+
+ const prevBlockEqual = mapHPost.findLast(x => x.le === cominglevel)
+ const prevBlockGratherFromFirst = mapHPost.find(x => x.le >= cominglevel)
+ const prevBlockGratherFromLast = mapHPost.findLast(x => x.le <= cominglevel)
+ const lastBlock = mapHPost.at(-1)
+
+ let prevBlock = prevBlockEqual || prevBlockGratherFromLast || prevBlockGratherFromFirst
+
+ if (lastBlock.le <= cominglevel) prevBlock = lastBlock
+
+ const shouldNested = prevBlock.le < cominglevel
+
+ const node = state.schema.nodeFromJSON(heading)
+
+ tr.insert(prevBlock.endBlockPos - (shouldNested ? 2 : 0), node)
+ }
+
+ return true
+ }
+
+ if (cominglevel < parentLevel) {
+ console.info('[Heading]: break the current Heading chain, cominglevel is grether than parentLevel')
+
+ return changeHeadingLevelBackward(arrg, attributes, true)
+ }
+}
diff --git a/packages/next.js/components/TipTap/styles/_blocks.scss b/packages/next.js/components/TipTap/styles/_blocks.scss
new file mode 100644
index 000000000..7a2dca02d
--- /dev/null
+++ b/packages/next.js/components/TipTap/styles/_blocks.scss
@@ -0,0 +1,517 @@
+.pad {
+ position: relative;
+ @apply h-full;
+
+ .header {
+ // height: $Pad_Header__height;
+ }
+
+ .toolbars {
+ // height: $Pad_Toolbar__height;
+ }
+
+ .editor {
+ background-color: #f8f9fa;
+ overflow: auto;
+ // @apply absolute;
+
+ .tipta__editor {
+ @apply max-w-4xl w-full;
+
+ .heading {
+ background-color: #fff;
+ // padding: 0 1rem;
+ }
+
+ }
+ }
+}
+
+.toc {
+ background: rgba(black, 0.1);
+ border-radius: 0.5rem;
+ opacity: 0.75;
+ padding: 0.75rem;
+ min-width: 200px;
+ text-align: left;
+ line-height: 2rem;
+
+ &__list {
+ list-style: none;
+ padding: 0;
+
+ &::before {
+ content: "Table of Contents";
+ display: block;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.025rem;
+ opacity: 0.5;
+ text-transform: uppercase;
+ padding-bottom: 20px;
+ }
+ }
+
+ &__item {
+ padding-left: 1.5rem;
+
+ &.active > span {
+ a {
+ color: #4285f4;
+ }
+
+ a::after {
+
+ opacity: 1;
+ }
+ }
+
+ a {
+ font-weight: normal;
+ position: relative;
+ @apply text-sm text-gray-400;
+
+ &::after {
+ content: "";
+ display: block;
+ width: 100%;
+ border: 1px solid #4285f4;
+ position: absolute;
+ opacity: 0;
+ transition: all 0.1s cubic-bezier(0.39, 0.575, 0.565, 1);
+ }
+ }
+
+ a:hover {
+ opacity: 0.5;
+ color: #0D0D0D
+ }
+
+ &--1 {
+ padding-left: 0!important;
+ > span > a {
+ @apply text-base font-medium text-black;
+ }
+ }
+
+ }
+}
+
+.drag_box {
+ position: relative;
+
+ &:hover {
+ >#dragspan {
+ opacity: 1;
+ }
+ }
+
+ #dragspan {
+ display: block;
+ position: absolute;
+ left: -20px;
+ top: 0;
+ opacity: 0;
+ transition: all .1s cubic-bezier(0.39, 0.575, 0.565, 1);
+
+ &:hover {
+ opacity: 1;
+ }
+
+ &::after {
+ content: '';
+ display: block;
+ flex: 0 0 auto;
+ position: relative;
+ width: 1rem;
+ height: 1rem;
+ top: 0.3rem;
+ margin-right: 0.5rem;
+ cursor: grab;
+ background-image: url('data:image/svg+xml;charset=UTF-8, ');
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ }
+ }
+
+}
+
+.tiptap__toolbar {
+ @apply border-t border-b;
+
+ .divided {
+ display: inline-block;
+ height: 20px;
+ @apply border-l mx-[10px] my-[6px];
+ // float: left;
+ }
+
+ button {
+ float: left;
+ margin: 4px 2px;
+ // padding: 2px 6px;
+ background-color: transparent;
+ outline: none;
+ cursor: pointer;
+ height: 28px;
+ width: 28px;
+ @apply flex rounded items-center justify-center relative;
+
+ &.btn_settingModal {
+ @apply ml-auto
+ }
+
+ svg {}
+
+ &:hover {
+ background: #eee;
+ color: black;
+ }
+
+ &.is-active {
+ background-color: #e8f0fe;
+ color: #1a73e8;
+
+ svg {
+ fill: #1a73e8;
+ }
+ }
+
+ &[disabled]:hover {
+ color: #aaa;
+ background-color: transparent;
+ }
+ }
+
+ .gearModal {
+ width: 300px;
+ padding: 8px;
+ border: 1px solid #ddd;
+ position: absolute;
+ right: 14px;
+ background: #fff;
+ border-radius: 4px;
+ position: absolute;
+ display: none;
+ top: 38px;
+
+ &.active {
+ display: block;
+ }
+
+ .content {
+ display: flex;
+ align-items: center;
+ }
+ }
+}
+
+.nodeStyle__control,
+.nodeStyle__value-container {
+ min-height: 28px !important;
+ height: 28px;
+}
+
+.nodeStyle__control {
+ border-width: 0 !important;
+ padding: 0 0 0 4px !important;
+}
+
+.nodeStyle__control:hover {
+ background-color: #eee;
+}
+
+.nodeStyle__input-container,
+.nodeStyle__value-container {
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+.nodeStyle__indicator-separator {
+ margin: 5px 0 !important;
+ display: none !important;
+}
+
+.nodeStyle__indicator {
+ padding: 0 !important;
+}
+
+.nodeStyle__menu {
+ padding: 0 !important;
+ margin: 0 !important;
+}
+
+.ProseMirror {
+ text-align: left;
+ outline: none;
+ min-height: 800px;
+
+ *.unselectable {
+ -moz-user-select: -moz-none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+
+ >*+* {
+ margin-top: 0.75em;
+ }
+
+ /* Placeholder (at the top) */
+ .ProseMirror p.is-editor-empty:first-child::before {
+ color: #adb5bd;
+ content: attr(data-placeholder);
+ float: left;
+ height: 0;
+ pointer-events: none;
+ }
+
+ /* Give a remote user a caret */
+ .collaboration-cursor__caret {
+ border-left: 1px solid #0d0d0d;
+ border-right: 1px solid #0d0d0d;
+ margin-left: -1px;
+ margin-right: -1px;
+ pointer-events: none;
+ position: relative;
+ word-break: normal;
+ }
+
+ /* Render the username above the caret */
+ .collaboration-cursor__label {
+ border-radius: 3px 3px 3px 0;
+ color: #0d0d0d;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 600;
+ left: -1px;
+ line-height: normal;
+ padding: 0.1rem 0.3rem;
+ position: absolute;
+ top: -1.4em;
+ user-select: none;
+ white-space: nowrap;
+ }
+
+ ul[data-type="taskList"] {
+ list-style: none;
+ padding: 0;
+
+ p {
+ margin: 0;
+ }
+
+ li {
+ display: flex;
+
+ >label {
+ flex: 0 0 auto;
+ margin-right: 0.5rem;
+ user-select: none;
+ }
+
+ >div {
+ flex: 1 1 auto;
+ }
+ }
+ }
+
+ ul {
+ list-style-type: revert;
+ }
+
+ ol {
+ list-style-type: decimal;
+ }
+
+ ul,
+ ol {
+ padding: 0 1rem;
+ }
+
+ :is(h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) {
+ font-weight: bold;
+ @apply font-medium mt-0 mb-2;
+ }
+
+ h1 {
+ @apply text-6xl;
+ }
+
+ h2 {
+ @apply text-5xl;
+ }
+
+ h3 {
+ @apply text-4xl;
+ }
+
+ h4 {
+ @apply text-3xl;
+ }
+
+ h5 {
+ @apply text-2xl;
+ }
+
+ :is(h6, h7, h8, h9, h10) {
+ @apply text-xl;
+ }
+
+ code {
+ background-color: rgba(#616161, 0.1);
+ color: #616161;
+ }
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ hr {
+ border: none;
+ border-top: 2px solid #fff;
+ margin: 2rem 0;
+ }
+
+ .is-empty::before {
+ content: attr(data-placeholder);
+ float: left;
+ color: #adb5bd;
+ pointer-events: none;
+ height: 0;
+ }
+
+ pre {
+ background: #0d0d0d;
+ border-radius: 0.5rem;
+ color: #fff;
+ font-family: "JetBrainsMono", monospace;
+ padding: 0.75rem 1rem;
+
+ code {
+ background: none;
+ color: inherit;
+ font-size: 0.8rem;
+ padding: 0;
+ }
+
+ .hljs-comment,
+ .hljs-quote {
+ color: #616161;
+ }
+
+ .hljs-variable,
+ .hljs-template-variable,
+ .hljs-attribute,
+ .hljs-tag,
+ .hljs-name,
+ .hljs-regexp,
+ .hljs-link,
+ .hljs-name,
+ .hljs-selector-id,
+ .hljs-selector-class {
+ color: #f98181;
+ }
+
+ .hljs-number,
+ .hljs-meta,
+ .hljs-built_in,
+ .hljs-builtin-name,
+ .hljs-literal,
+ .hljs-type,
+ .hljs-params {
+ color: #fbbc88;
+ }
+
+ .hljs-string,
+ .hljs-symbol,
+ .hljs-bullet {
+ color: #b9f18d;
+ }
+
+ .hljs-title,
+ .hljs-section {
+ color: #faf594;
+ }
+
+ .hljs-keyword,
+ .hljs-selector-tag {
+ color: #70cff8;
+ }
+
+ .hljs-emphasis {
+ font-style: italic;
+ }
+
+ .hljs-strong {
+ font-weight: 700;
+ }
+ }
+
+ table {
+ border-collapse: collapse;
+ margin: 0;
+ overflow: hidden;
+ table-layout: fixed;
+ width: 100%;
+
+ td,
+ th {
+ border: 2px solid #ced4da;
+ box-sizing: border-box;
+ min-width: 1em;
+ padding: 3px 5px;
+ position: relative;
+ vertical-align: top;
+
+ >* {
+ margin-bottom: 0;
+ }
+ }
+
+ th {
+ background-color: #f1f3f5;
+ font-weight: bold;
+ text-align: left;
+ }
+
+ .selectedCell:after {
+ background: rgba(200, 200, 255, 0.4);
+ content: "";
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ pointer-events: none;
+ position: absolute;
+ z-index: 2;
+ }
+
+ .column-resize-handle {
+ background-color: #adf;
+ bottom: -2px;
+ position: absolute;
+ right: -2px;
+ pointer-events: none;
+ top: 0;
+ width: 4px;
+ }
+
+ p {
+ margin: 0;
+ }
+ }
+
+ .tableWrapper {
+ padding: 1rem 0;
+ overflow-x: auto;
+ }
+
+ .resize-cursor {
+ cursor: ew-resize;
+ cursor: col-resize;
+ }
+}
diff --git a/packages/next.js/components/TipTap/styles/_headings.scss b/packages/next.js/components/TipTap/styles/_headings.scss
new file mode 100644
index 000000000..b17608fab
--- /dev/null
+++ b/packages/next.js/components/TipTap/styles/_headings.scss
@@ -0,0 +1,127 @@
+body.indentHeading {
+ &.h1SectionBreak{
+ div.heading {
+ &[level="1"] {
+ box-shadow: 8px 0 34px 0 rgb(60 64 67 / 10%);
+ }
+ }
+ }
+ div.heading {
+ border-left: 3px solid rgba(#0d0d0d, 0.1);
+ padding-left: 1rem;
+ position: relative;
+ margin-top: 1rem;
+
+ .title::before {
+ content: attr(level);
+ position: absolute;
+ background-color: tomato;
+ font-size: 12px !important;
+ height: 20px;
+ width: 20px;
+ top: -16px;
+ left: 33px;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ }
+ }
+}
+
+body.h1SectionBreak{
+ .ProseMirror {
+ background-color: transparent;
+
+ .heading {
+ margin-bottom: 1rem;
+ &[level="1"] {
+ box-shadow: 0 1px 3px 1px rgb(00 64 67 / 15%);
+ padding: 1rem;
+ padding-bottom: 0;
+ border-bottom: 1px solid #e1e6e7;
+
+ }
+ }
+ }
+}
+
+.ProseMirror {
+ .heading {
+ &[level="1"] {
+ padding: 1rem;
+ margin-top: 0;
+ border: 1px solid #e1e6e7;
+ border-bottom: none;
+ border-top:none;
+
+ &:first-of-type{
+ border-top: 1px solid #e1e6e7;
+ }
+
+ &:last-of-type{
+ border-bottom: 1px solid #e1e6e7;
+ }
+ }
+ }
+
+ .title {
+ z-index: 9;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ &.is-empty::before {
+ position: absolute;
+ line-height: 0;
+ }
+
+ &[level="1"].is-empty::before {
+ font-size: 3.75rem;
+ }
+
+ &[level="2"].is-empty::before {
+ font-size: 3rem;
+ }
+
+ &[level="3"].is-empty::before {
+ font-size: 2.25rem;
+ }
+
+ &[level="4"].is-empty::before {
+ font-size: 1.875rem;
+ }
+
+ &[level="5"].is-empty::before {
+ font-size: 1.5rem;
+ }
+
+ &[level="6"].is-empty::before {
+ font-size: 1.25rem;
+ }
+
+ &[level="7"].is-empty::before {
+ font-size: 1.125rem;
+ }
+
+ &[level="8"].is-empty::before {
+ font-size: 1.125rem;
+ }
+
+ &[level="9"].is-empty::before {
+ font-size: 1.125rem;
+ }
+
+ &[level="10"].is-empty::before {
+ font-size: 1.125rem;
+ }
+
+ :is(h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) {
+ margin-left: -33px;
+ width: 100%;
+ }
+
+ }
+}
diff --git a/packages/next.js/components/TipTap/styles/styles.scss b/packages/next.js/components/TipTap/styles/styles.scss
new file mode 100644
index 000000000..de940080b
--- /dev/null
+++ b/packages/next.js/components/TipTap/styles/styles.scss
@@ -0,0 +1,374 @@
+@use './blocks';
+@use './headings';
+
+$pad-header-height: 64px;
+$pad-toolbar-height: 38px;
+$crinkle-time: 0.9s;
+$crinkle-transition: ease-out;
+$crinkle-fold-bg: #bbb;
+$crinkle-unfold-bg: #efefef;
+
+.foldWrapper {
+ background-color: #f8f9fa;
+ cursor: pointer;
+ height: 100%;
+ position: absolute;
+ width: 100%;
+ bottom: 0;
+ height: calc(100% - 90px);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ transition: all $crinkle-time $crinkle-transition;
+
+ .fold {
+ width: calc(100% + 2rem + 6px);
+ position: relative;
+ z-index: 2;
+ left: -19px;
+ background-color: #f8f9fa;
+ padding-right: 4px;
+ height: 100%;
+ margin-top: -2px;
+ transition: all $crinkle-time $crinkle-transition;
+
+ &::after {
+ content: "";
+ display: block;
+ max-height: 50%;
+ height: 100%;
+ min-height: 5px;
+ background-color: #fff;
+ width: 100%;
+ transform: skew(44deg);
+ transition: all $crinkle-time $crinkle-transition;
+ border-right: 0;
+ border-left: 0;
+ }
+
+ &::before {
+ content: "";
+ display: block;
+ max-height: 50%;
+ height: 100%;
+ min-height: 5px;
+ background-color: $crinkle-fold-bg;
+ width: 100%;
+ transform: skew(-44deg);
+ border-bottom: none;
+ transition: all $crinkle-time $crinkle-transition;
+ box-shadow: 0 1px 5px 1px rgb(0 64 67 / 15%);
+ border-right: 0;
+ border-left: 0;
+ }
+
+ &:last-of-type {
+
+ &::after {
+ border: 1px solid #e1e6e7;
+ box-shadow: none;
+ }
+ }
+ }
+}
+
+.ProseMirror {
+
+ // buttons in heading
+ div.heading {
+ padding-bottom: 1rem;
+ position: relative;
+ height: 100%;
+
+ &.opend,
+ &.opening {
+ .buttonWrapper {
+ button.btnFold::before {
+ content: '\25BC';
+ }
+ }
+ }
+
+ &.closed,
+ &.closing {
+ .buttonWrapper {
+ button.btnFold::before {
+ content: '\25B6';
+ }
+ }
+ }
+
+ &[level="1"] {
+ > .wrapBlock > .foldWrapper .fold {
+ width: calc(100% + 6px);
+ left: 14px;
+ }
+ }
+
+ &[level="2"] {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 60px);
+ }
+ }
+
+ &[level="3"] {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 50px);
+ }
+ }
+
+ &[level="4"] {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 40px);
+ }
+ }
+
+ &[level="5"] {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 40px);
+ }
+ }
+
+ &:is([level="6"], [level="7"], [level="8"], [level="9"], [level="10"]) {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 40px);
+ }
+ }
+
+ @keyframes foldCrinkle_after {
+ 0% {
+ transform: skew(0deg);
+ }
+
+ 100% {
+ transform: skew(44deg);
+ }
+ }
+
+ @keyframes foldCrinkle_before {
+ 0% {
+ transform: skew(0deg);
+ background-color: $crinkle-unfold-bg;
+ }
+
+ 100% {
+ transform: skew(-44deg);
+ background-color: $crinkle-fold-bg;
+ }
+ }
+
+ @keyframes unfoldCrinkle_after {
+ 0% {
+ transform: skew(44deg);
+ }
+
+ 100% {
+ transform: skew(0deg);
+ }
+ }
+
+ @keyframes unfoldCrinkle_before {
+ 0% {
+ transform: skew(-44deg);
+ background-color: $crinkle-fold-bg;
+ }
+
+ 100% {
+ transform: skew(0deg);
+ background-color: $crinkle-unfold-bg;
+ }
+ }
+
+ @keyframes unfoldWrapper {
+ 0% {
+ background-color: inherit;
+ }
+
+ 100% {
+ background-color: transparent;
+ }
+ }
+
+ &.closed {
+ > .wrapBlock > .foldWrapper {
+ z-index: 2;
+ }
+
+ &:hover {
+
+ > .wrapBlock > .foldWrapper ~ .contentWrapper {
+ height: 70px !important;
+ }
+
+ height: calc(100% - 70px);
+
+ > .wrapBlock > .foldWrapper .fold::before {
+ background-color: #ccc;
+ }
+ }
+
+ }
+
+ &.closing {
+ > .wrapBlock > .foldWrapper {
+ z-index: 4;
+ }
+ }
+
+ &.closed,
+ &.closing {
+ > .wrapBlock > .foldWrapper {
+ display: flex;
+ z-index: 4;
+
+ .fold::after {
+ animation: foldCrinkle_after $crinkle-time $crinkle-transition;
+ }
+
+ .fold::before {
+ animation: foldCrinkle_before $crinkle-time $crinkle-transition;
+ }
+ }
+ }
+
+ &.opening {
+ > .wrapBlock > .foldWrapper {
+ .fold {}
+ }
+ }
+
+ &.opend,
+ &.opening {
+ > .wrapBlock > .foldWrapper {
+ .fold::after {
+ animation: unfoldCrinkle_after $crinkle-time $crinkle-transition;
+ }
+
+ .fold::before {
+ animation: unfoldCrinkle_before $crinkle-time $crinkle-transition;
+ }
+ }
+ }
+
+ &.opend {
+ > .wrapBlock > .foldWrapper {
+ display: none;
+ }
+ }
+ }
+
+ .wrapBlock {
+ height: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row-reverse;
+
+ .title {
+ flex: 1;
+ padding-left: 60px;
+ margin-left: -100px;
+ position: relative;
+ z-index: 1;
+ min-width: 100%;
+
+ &:hover ~ .buttonWrapper {
+ visibility: visible;
+ }
+ }
+
+ .foldWrapper,
+ .contentWrapper {
+ width: 100%;
+ }
+
+ .buttonWrapper {
+ display: flex;
+ width: 40px;
+ align-items: center;
+ position: relative;
+ left: -60px;
+ z-index: 2;
+ visibility: hidden;
+
+ &:hover {
+ visibility: visible;
+ }
+
+ > a {
+ margin-left: 14px;
+ color: #ccc;
+
+ &:hover {
+ color: #646cff
+ }
+ }
+
+ }
+
+ .contentWrapper {
+ height: auto;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-height: 40px;
+ z-index: 0;
+ padding-bottom: 10px;
+ // NOTE: only transition duration is work here! other property are in the js file
+ transition: all $crinkle-time $crinkle-transition;
+
+ .foldWrapper {
+ transition: all $crinkle-time $crinkle-transition;
+
+ .fold {
+ transition: height $crinkle-time $crinkle-transition;
+ }
+ }
+
+ &.collapsed {
+ height: 0;
+ padding: 0;
+ overflow: hidden;
+ }
+ }
+ }
+}
+
+
+
+.tiptap__toc{
+ .toc__item{
+ position: relative;
+ width: 100%;
+ margin: 6px 0;
+ > span{
+ display: flex;
+ align-items: center;
+ }
+
+ &.closed{
+ > span .btnFold{
+ &::before{
+ content: '\25B6';
+ }
+ }
+ }
+ > span .btnFold{
+ &::before{
+ content: '\25BC';
+ display: block;
+ margin-right: 6px;
+ width: 16px;
+ height: 16px;
+ // border: 2px solid red;
+ font-size: 0.7rem;
+ text-align: center;
+ opacity: 0;
+ cursor: pointer;
+ }
+ }
+
+ &:hover > span > .btnFold::before{
+ opacity: 1;
+ }
+ }
+}
diff --git a/packages/next.js/components/icons/Icons.jsx b/packages/next.js/components/icons/Icons.jsx
new file mode 100644
index 000000000..1adf00d3c
--- /dev/null
+++ b/packages/next.js/components/icons/Icons.jsx
@@ -0,0 +1,243 @@
+// Source: google Icons for G Suits https://ssl.gstatic.com/docs/common/material_common_sprite413_blue.svg
+// https://ssl.gstatic.com/docs/documents/share/images/sprite-24.svg
+import React from 'react'
+
+export const Bold = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Italic = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Stric = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const HighlightMarker = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const ClearMark = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Underline = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const CheckList = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const OrderList = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const BulletList = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Link = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const UnLink = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const CheckBox = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Undo = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Redo = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Printer = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Plus = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const AddComment = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Video = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+ )
+}
+
+export const Image = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const Gear = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+ )
+}
+
+export const OfflineCloud = ({ size = 18, fill = 'black', className }) => {
+ return (
+
+
+
+ )
+}
+
+export const OnlineCloud = ({ size = 18, fill = 'black', className }) => {
+ return (
+
+
+
+ )
+}
+
+export const ArrowLeft = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+
+
+ )
+}
+
+export const Doc = ({ size = 18, fill = 'black', className }) => {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const DocsPlus = ({ size = 18, fill = 'black', className }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * Renders an SVG icon "PrivateShare".
+ *
+ * @param {object} props - The component props.
+ * @param {number} [props.size=18] - The width and height of the SVG icon.
+ * @param {string} [props.fill='black'] - The color of the SVG icon.
+ * @returns {JSX.Element} The rendered SVG icon.
+ */
+export const PrivateShare = ({ size = 18, fill = 'black' }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/packages/next.js/components/icons/croper.html b/packages/next.js/components/icons/croper.html
new file mode 100644
index 000000000..31fdd8eaa
--- /dev/null
+++ b/packages/next.js/components/icons/croper.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
+
diff --git a/packages/next.js/components/icons/logo copy.svg b/packages/next.js/components/icons/logo copy.svg
new file mode 100644
index 000000000..f55d441bc
--- /dev/null
+++ b/packages/next.js/components/icons/logo copy.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/packages/next.js/components/icons/logo.svg b/packages/next.js/components/icons/logo.svg
new file mode 100644
index 000000000..ac2e3241c
--- /dev/null
+++ b/packages/next.js/components/icons/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/next.js/components/icons/material_common_sprite413_blue.svg b/packages/next.js/components/icons/material_common_sprite413_blue.svg
new file mode 100644
index 000000000..a7ff41068
--- /dev/null
+++ b/packages/next.js/components/icons/material_common_sprite413_blue.svg
@@ -0,0 +1,2029 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/next.js/components/icons/sprite-24.svg b/packages/next.js/components/icons/sprite-24.svg
new file mode 100644
index 000000000..7c1693cbb
--- /dev/null
+++ b/packages/next.js/components/icons/sprite-24.svg
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Artboard
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/next.js/db.js b/packages/next.js/db.js
new file mode 100644
index 000000000..e787009fd
--- /dev/null
+++ b/packages/next.js/db.js
@@ -0,0 +1,15 @@
+import Dexie from 'dexie'
+
+export let db
+
+export const initDB = (dbName, t11) => {
+ const DexieDB = new Dexie(dbName)
+
+ const newdb = DexieDB.version(1).stores({
+ meta: 'headingId, *docId, text, crinkleOpen, level'
+ })
+
+ db = newdb.db
+
+ return db
+}
diff --git a/packages/next.js/next.config.js b/packages/next.js/next.config.js
new file mode 100644
index 000000000..40fafe0b2
--- /dev/null
+++ b/packages/next.js/next.config.js
@@ -0,0 +1,17 @@
+/** @type {import('next').NextConfig} */
+
+const runtimeCaching = require('next-pwa/cache')
+const isProduction = process.env.NODE_ENV === 'production';
+
+const withPWA = require('next-pwa')({
+ dest: 'public',
+ register: true,
+ skipWaiting: false,
+ runtimeCaching
+})
+
+
+module.exports = withPWA({
+ // config
+
+})
diff --git a/packages/next.js/package.json b/packages/next.js/package.json
new file mode 100644
index 000000000..8e2f935f7
--- /dev/null
+++ b/packages/next.js/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "@docsplus/next.js",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@hocuspocus/provider": "^2.0.1",
+ "@tanstack/react-query": "^4.28.0",
+ "@tiptap/core": "^2.0.1",
+ "@tiptap/extension-blockquote": "^2.0.0-beta.209",
+ "@tiptap/extension-bold": "^2.0.0-beta.209",
+ "@tiptap/extension-bullet-list": "^2.0.0-beta.209",
+ "@tiptap/extension-code-block": "^2.0.0-beta.209",
+ "@tiptap/extension-code-block-lowlight": "^2.0.0-beta.209",
+ "@tiptap/extension-collaboration": "^2.0.1",
+ "@tiptap/extension-collaboration-cursor": "^2.0.1",
+ "@tiptap/extension-document": "^2.0.0-beta.209",
+ "@tiptap/extension-dropcursor": "^2.0.0-beta.209",
+ "@tiptap/extension-gapcursor": "^2.0.0-beta.209",
+ "@tiptap/extension-heading": "^2.0.0-beta.209",
+ "@tiptap/extension-highlight": "^2.0.0-beta.209",
+ "@tiptap/extension-image": "^2.0.0-beta.209",
+ "@tiptap/extension-italic": "^2.0.0-beta.209",
+ "@tiptap/extension-link": "^2.0.0-beta.209",
+ "@tiptap/extension-list-item": "^2.0.0-beta.209",
+ "@tiptap/extension-paragraph": "^2.0.0-beta.209",
+ "@tiptap/extension-placeholder": "^2.0.0-beta.209",
+ "@tiptap/extension-strike": "^2.0.0-beta.209",
+ "@tiptap/extension-subscript": "^2.0.0-beta.209",
+ "@tiptap/extension-superscript": "^2.0.0-beta.209",
+ "@tiptap/extension-table": "^2.0.0-beta.209",
+ "@tiptap/extension-table-cell": "^2.0.0-beta.209",
+ "@tiptap/extension-table-header": "^2.0.0-beta.209",
+ "@tiptap/extension-table-row": "^2.0.0-beta.209",
+ "@tiptap/extension-task-item": "^2.0.0-beta.209",
+ "@tiptap/extension-task-list": "^2.0.0-beta.209",
+ "@tiptap/extension-text": "^2.0.0-beta.209",
+ "@tiptap/extension-text-align": "^2.0.0-beta.209",
+ "@tiptap/extension-typography": "^2.0.0-beta.209",
+ "@tiptap/extension-underline": "^2.0.0-beta.209",
+ "@tiptap/pm": "^2.0.1",
+ "@tiptap/prosemirror-tables": "^1.1.4",
+ "@tiptap/react": "^2.0.1",
+ "@tiptap/starter-kit": "^2.0.1",
+ "dexie": "^3.2.3",
+ "lowlight": "^2.8.1",
+ "next": "latest",
+ "next-pwa": "^5.6.0",
+ "prosemirror-commands": "^1.5.0",
+ "prosemirror-dropcursor": "^1.6.1",
+ "prosemirror-gapcursor": "^1.3.1",
+ "prosemirror-history": "^1.3.0",
+ "prosemirror-schema-list": "^1.2.2",
+ "prosemirror-state": "^1.4.2",
+ "prosemirror-transform": "^1.7.0",
+ "pubsub-js": "^1.9.4",
+ "randomcolor": "^0.6.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-github-btn": "^1.4.0",
+ "react-select": "^5.7.2",
+ "short-unique-id": "^4.4.4",
+ "slugify": "^1.6.6",
+ "y-indexeddb": "^9.0.9",
+ "y-prosemirror": "^1.2.0",
+ "y-webrtc": "^10.2.5",
+ "y-websocket": "^1.4.5",
+ "yjs": "^13.5.51"
+ },
+ "devDependencies": {
+ "@hocuspocus/cli": "^1.0.1",
+ "@types/node": "17.0.4",
+ "@types/react": "17.0.38",
+ "autoprefixer": "^10.4.14",
+ "postcss": "^8.4.21",
+ "sass": "^1.60.0",
+ "tailwindcss": "^3.3.0",
+ "typescript": "4.5.4"
+ }
+}
diff --git a/packages/next.js/pages/404.jsx b/packages/next.js/pages/404.jsx
new file mode 100644
index 000000000..b0d9d8e46
--- /dev/null
+++ b/packages/next.js/pages/404.jsx
@@ -0,0 +1,14 @@
+
+import Link from 'next/link'
+export default function Custom404() {
+ return (
+
+
+
Oops! You seem to be lost. 404
+
+ Home
+
+
+
+ )
+ }
diff --git a/packages/next.js/pages/_app.jsx b/packages/next.js/pages/_app.jsx
new file mode 100644
index 000000000..474dc21e1
--- /dev/null
+++ b/packages/next.js/pages/_app.jsx
@@ -0,0 +1,87 @@
+import Head from 'next/head'
+import { AppProps } from 'next/app'
+import dynamic from 'next/dynamic'
+import '../styles/styles.scss'
+import '../styles/globals.scss'
+
+import {
+ useQuery,
+ useMutation,
+ useQueryClient,
+ QueryClient,
+ QueryClientProvider,
+} from '@tanstack/react-query'
+
+
+const RelpadPrompt = dynamic(() => import(`../components/ReloadPrompt`), { ssr: false });
+
+
+// Create a client
+const queryClient = new QueryClient()
+
+export default function MyApp({ Component, pageProps }) {
+ return (
+
+
+
+
+
+
+
+ {/* Chrome, Firefox OS and Opera */}
+
+
+
+
+
+
+
+
+ {/* Sets whether a web application runs in full-screen mode. See: https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html#//apple_ref/doc/uid/TP40008193-SW3 */}
+
+ {/* Web clip icon for Android homescreen shortcuts. Available since Chrome 31+ for Android.See: https://developers.google.com/chrome/mobile/docs/installtohomescreen */}
+
+
+
+
+ {/*
+ Disables automatic detection of possible phone numbers in a webpage in Safari on iOS.
+ See: https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html#//apple_ref/doc/uid/TP40008193-SW5
+ See: https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html#//apple_ref/html/const/format-detection
+ */}
+
+
+
+
+
+
Docs Plus
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/next.js/pages/index.jsx b/packages/next.js/pages/index.jsx
new file mode 100644
index 000000000..c36de86d2
--- /dev/null
+++ b/packages/next.js/pages/index.jsx
@@ -0,0 +1,164 @@
+import Head from 'next/head'
+import { useState, useRef, useContext, useEffect } from 'react'
+
+import Link from 'next/link'
+import GitHubButton from 'react-github-btn'
+
+import { DocsPlus } from '../components/icons/Icons'
+import Button from '../components/Button'
+import slugify from 'slugify'
+
+import { useRouter } from 'next/router'
+
+export default function Home({hostname}) {
+ // const { signInWithOtp, signIn, signOut, user, profile } = useAuth()
+ const user = null
+ const profile = null
+
+ const router = useRouter()
+ const docNameRef = useRef()
+ const [loadingDoc, setLoadingDoc] = useState(false)
+ const [error, setError] = useState(null)
+ const [namespace, setNamespace] = useState(`${hostname}/${profile?.doc_namespace ? profile?.doc_namespace : 'open'}`)
+
+ // slugify the docNameRef
+ const validateDocName = (docSlug) => {
+ const slug = slugify(docSlug, { lower: true, strict: true })
+
+ console.log(slug)
+ if (docSlug.length <= 0 || docSlug !== slug) {
+ setError('Only lowercase letters, numbers and dashes are allowed')
+
+ return [false, slug]
+ }
+
+ return [true, slug]
+ }
+
+ const enterToPad = (target) => {
+ setLoadingDoc(true)
+ let docSlug = docNameRef.current.value
+
+ if (target === 'random') docSlug = (Math.random() + 1).toString(36).substring(2)
+
+ const [error, slug] = validateDocName(docSlug)
+
+ if (!error) {
+ setLoadingDoc(false)
+
+ return
+ }
+
+ if (!user) return router.push(`/open/${slug}`)
+
+ // check if profile has a namespace
+ if (!profile?.doc_namespace) {
+ return router.push('/auth/username_needed')
+ }
+
+ router.push(`/${profile?.doc_namespace}/${slug}`)
+ }
+
+ const handleKeyDown = event => {
+ if (event.key === 'Enter') {
+ enterToPad(event)
+ }
+ }
+
+ useEffect(() => {
+ setNamespace(`${location.host}/${profile?.doc_namespace ? profile?.doc_namespace : 'open'}`)
+ }, [])
+
+ return (
+
+
+
Docs Plus
+
+
+
+
+
+
+
docs plus
+
Get everyone on the same page
+
+
+
+
+
+
+ {user &&
+ signOut()}>Signout
+ Dashboard
+
}
+ {user?.id && user.email &&
+
+
Continue As:
+
{profile?.display_name || user?.email}
+
}
+
enterToPad('random')}>Create a new public doc
+
or
+
+
+
+ {error &&
*Only lowercase letters, numbers and dashes are allowed
}
+
Open public doc
+
+
+
+ {/* {!user && !user?.id && !user?.email &&
+
+
+ Sign up for private docs
+ navigate('/auth/signup')}>Sign up
+
+
+ Already have an account? Log in
+
+
} */}
+
+
+
+
+
+
+ Start exploring our open-source project on GitHub , Join our discussions and help make it even better!
+
+
+
+
+
+ Star
+
+
+ Discuss
+
+
+
+
+
+
+
+
+ )
+}
+
+
+export async function getServerSideProps({req, res}) {
+ return {
+ props: {name: "hassan", hostname: req?.headers?.host }, // will be passed to the page component as props
+ }
+}
diff --git a/packages/next.js/pages/open/[slug].jsx b/packages/next.js/pages/open/[slug].jsx
new file mode 100644
index 000000000..8ef3acb31
--- /dev/null
+++ b/packages/next.js/pages/open/[slug].jsx
@@ -0,0 +1,187 @@
+import React, { useState, useMemo, useEffect } from 'react'
+import { useRouter } from 'next/router'
+import Head from 'next/head'
+
+import { useEditor, EditorContent } from '@tiptap/react'
+import StarterKit from '@tiptap/starter-kit'
+import * as Y from 'yjs'
+import { IndexeddbPersistence } from 'y-indexeddb'
+import { HocuspocusProvider } from '@hocuspocus/provider'
+import editorConfig from '../../components/TipTap/TipTap'
+import { useQuery } from '@tanstack/react-query'
+
+import Toolbar from '../../components/TipTap/Toolbar'
+import PadTitle from '../../components/TipTap/PadTitle'
+import TableOfContents from '../../components/TipTap/TableOfContents'
+import { db, initDB } from '../../db'
+
+const useCustomeHook = (documentSlug) => {
+ // NOTE: This is a hack to get the correct URL in the build time
+ const url = `${process.env.NEXT_PUBLIC_RESTAPI_URL}/documents/${documentSlug}`
+
+ const { isLoading, error, data, isSuccess } = useQuery({
+ queryKey: ['getDocumentMetadataByDocName'],
+ queryFn: () => {
+ return fetch(url)
+ .then(res => res.json())
+ }
+ })
+
+ return { isLoading, error, data, isSuccess }
+}
+
+const OpenDocuments = ({ docTitle, docSlug}) => {
+ const [loading, setLoading] = useState(false)
+ const [ydoc, setYdoc] = useState(null)
+ const [provider, setProvider] = useState(null)
+ const [loadedData, setLoadedData] = useState(false)
+ const { isLoading, error, data, isSuccess } = useCustomeHook(docSlug)
+ const [documentTitle, setDocumentTitle] = useState(docTitle)
+ const [docId, setDocId] = useState(null)
+
+ useEffect(() => {
+ // Use the data returned by useCustomHook in useEffect
+ if (data?.data?.documentId) {
+ const { documentId, isPrivate } = data?.data
+
+ setDocumentTitle(data?.data.title)
+ setDocId(`${isPrivate ? 'private' : 'public'}.${documentId}`)
+ localStorage.setItem('docId', documentId)
+ localStorage.setItem('padName', `${isPrivate ? 'private' : 'public'}.${documentId}`)
+ localStorage.setItem('slug', docSlug)
+ localStorage.setItem('title', data?.data.title)
+
+ initDB(`meta.${documentId}`, `${isPrivate ? 'private' : 'public'}.${documentId}`)
+
+ db.meta.where({ docId: documentId }).toArray().then((data) => {
+ localStorage.setItem('headingMap', JSON.stringify(data))
+ })
+ }
+ }, [data])
+
+ useEffect(() => {
+ if (docId) {
+ const ydoc = new Y.Doc()
+
+ setYdoc(ydoc)
+
+ const colabProvider = new HocuspocusProvider({
+ url: `${process.env.NEXT_PUBLIC_PROVIDER_URL}`,
+ name: docId,
+ document: ydoc,
+ onStatus: (data) => {
+ console.log('onStatus', data)
+ },
+ onSynced: (data) => {
+ console.log('onSynced', data)
+ // console.log(`content loaded from Server, pad name: ${ docId }`, provider.isSynced)
+ if (data?.state) setLoading(false)
+ },
+ documentUpdateHandler: (update) => {
+ console.log('documentUpdateHandler', update)
+ },
+ onDisconnect: (data) => {
+ // console.log("onDisconnect", data)
+ },
+ onMessage: (data) => {
+ // console.log('onMessage', data)
+ }
+ })
+
+ setProvider(colabProvider)
+
+ console.log(colabProvider)
+
+ // Store the Y document in the browser
+ const indexDbProvider = new IndexeddbPersistence(docId, colabProvider.document)
+
+ indexDbProvider.on('synced', () => {
+ if (!loadedData) return
+ // console.log(`content loaded from indexdb, pad name: ${ docId }`)
+ setLoadedData(true)
+ })
+ }
+ }, [docId])
+
+ const editor = useEditor(editorConfig({ padName: docId, provider, ydoc }), [provider])
+
+ // useEffect(() => {
+ // if (!loading && editor?.isEmpty) {
+ // console.log('editor is empty', editor?.isEmpty)
+
+ // editor?.chain().focus().insertContentAt(2, '' +
+ // ' ' +
+ // '
' +
+ // '
' +
+ // '
' +
+ // '
',
+ // {
+ // updateSelection: false
+ // }).setTextSelection(0).run()
+ // }
+ // }, [loading, editor])
+
+ const scrollHeadingSelection = (event) => {
+ const scrollTop = event.currentTarget.scrollTop
+ const toc = document.querySelector('.toc__list')
+ const tocLis = [...toc.querySelectorAll('.toc__item')]
+ const closest = tocLis
+ .map(li => {
+ li.classList.remove('active')
+
+ return li
+ })
+ .filter(li => {
+ const thisOffsetTop = +li.getAttribute('data-offsettop') - 220
+ // const nextSiblingOffsetTop = +li.querySelector('div > .toc__item')?.getAttribute('data-offsettop') - 220
+
+ return thisOffsetTop <= scrollTop // && nextSiblingOffsetTop >= scrollTop
+ })
+
+ // if (event.target.offsetHeight === (event.target.scrollHeight - scrollTop) - 0.5) {
+ // tocLis.at(-1)?.classList.add('active')
+ // tocLis.at(-1)?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
+
+ // return
+ // }
+
+ // console.log(closest)
+ closest.at(-1)?.classList.add('active')
+ closest.at(-1)?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
+ }
+
+ return (
+ <>
+
+ {documentTitle}
+
+
+
+
+
+ { editor ? : 'Loading...'}
+
+
+ {loading ? 'loading...' : editor ?
: 'Loading...'}
+
+ {loading ? 'loading...' : editor ? : 'Loading...'}
+
+
+
+ >
+ )
+}
+
+export default OpenDocuments
+
+
+export async function getServerSideProps(context) {
+ const documentSlug = context.query.slug
+ const res = await fetch(`${process.env.NEXT_PUBLIC_RESTAPI_URL}/documents/${documentSlug}`)
+ const data = await res.json()
+ return {
+ props: {name: "hassan", docTitle: data.data.title, docSlug: documentSlug}, // will be passed to the page component as props
+ }
+}
diff --git a/packages/next.js/pages/open/_error.jsx b/packages/next.js/pages/open/_error.jsx
new file mode 100644
index 000000000..92f524e7b
--- /dev/null
+++ b/packages/next.js/pages/open/_error.jsx
@@ -0,0 +1,16 @@
+function Error({ statusCode }) {
+ return (
+
+ {statusCode
+ ? `An error ${statusCode} occurred on server`
+ : 'An error occurred on client'}
+
+ )
+ }
+
+ Error.getInitialProps = ({ res, err }) => {
+ const statusCode = res ? res.statusCode : err ? err.statusCode : 404
+ return { statusCode }
+ }
+
+ export default Error
diff --git a/packages/next.js/postcss.config.js b/packages/next.js/postcss.config.js
new file mode 100644
index 000000000..33ad091d2
--- /dev/null
+++ b/packages/next.js/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/next.js/public/favicon.ico b/packages/next.js/public/favicon.ico
new file mode 100644
index 000000000..b0b10bd81
Binary files /dev/null and b/packages/next.js/public/favicon.ico differ
diff --git a/packages/next.js/public/icons/android-chrome-192x192.png b/packages/next.js/public/icons/android-chrome-192x192.png
new file mode 100644
index 000000000..e9fd2ba8d
Binary files /dev/null and b/packages/next.js/public/icons/android-chrome-192x192.png differ
diff --git a/packages/next.js/public/icons/android-chrome-512x512.png b/packages/next.js/public/icons/android-chrome-512x512.png
new file mode 100644
index 000000000..884b1034e
Binary files /dev/null and b/packages/next.js/public/icons/android-chrome-512x512.png differ
diff --git a/packages/next.js/public/icons/apple-touch-icon.png b/packages/next.js/public/icons/apple-touch-icon.png
new file mode 100644
index 000000000..691349dca
Binary files /dev/null and b/packages/next.js/public/icons/apple-touch-icon.png differ
diff --git a/packages/next.js/public/icons/favicon-16x16.png b/packages/next.js/public/icons/favicon-16x16.png
new file mode 100644
index 000000000..3b788ca05
Binary files /dev/null and b/packages/next.js/public/icons/favicon-16x16.png differ
diff --git a/packages/next.js/public/icons/favicon-32x32.png b/packages/next.js/public/icons/favicon-32x32.png
new file mode 100644
index 000000000..39738e56c
Binary files /dev/null and b/packages/next.js/public/icons/favicon-32x32.png differ
diff --git a/packages/next.js/public/icons/favicon.ico b/packages/next.js/public/icons/favicon.ico
new file mode 100644
index 000000000..b0b10bd81
Binary files /dev/null and b/packages/next.js/public/icons/favicon.ico differ
diff --git a/packages/next.js/public/icons/logo.svg b/packages/next.js/public/icons/logo.svg
new file mode 100644
index 000000000..844057320
--- /dev/null
+++ b/packages/next.js/public/icons/logo.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/next.js/public/icons/maskable_icon.png b/packages/next.js/public/icons/maskable_icon.png
new file mode 100644
index 000000000..ff16bc55d
Binary files /dev/null and b/packages/next.js/public/icons/maskable_icon.png differ
diff --git a/packages/next.js/public/icons/maskable_icon_x192.png b/packages/next.js/public/icons/maskable_icon_x192.png
new file mode 100644
index 000000000..88af3640b
Binary files /dev/null and b/packages/next.js/public/icons/maskable_icon_x192.png differ
diff --git a/packages/next.js/public/icons/maskable_icon_x384.png b/packages/next.js/public/icons/maskable_icon_x384.png
new file mode 100644
index 000000000..0458a6c7e
Binary files /dev/null and b/packages/next.js/public/icons/maskable_icon_x384.png differ
diff --git a/packages/next.js/public/icons/maskable_icon_x512.png b/packages/next.js/public/icons/maskable_icon_x512.png
new file mode 100644
index 000000000..bb8cdfcbd
Binary files /dev/null and b/packages/next.js/public/icons/maskable_icon_x512.png differ
diff --git a/packages/next.js/public/icons/screenshot1.png b/packages/next.js/public/icons/screenshot1.png
new file mode 100644
index 000000000..38cd842e9
Binary files /dev/null and b/packages/next.js/public/icons/screenshot1.png differ
diff --git a/packages/next.js/public/manifest.json b/packages/next.js/public/manifest.json
new file mode 100644
index 000000000..6e82e463d
--- /dev/null
+++ b/packages/next.js/public/manifest.json
@@ -0,0 +1,58 @@
+{
+ "name": "Docs.plus",
+ "short_name": "Docs.plus",
+ "background_color": "#3367D6",
+ "display": "standalone",
+ "theme_color": "#3367D6",
+ "description": "A real-time collaborative",
+ "start_url": "/",
+ "scope": "/",
+ "icons": [
+ {
+ "src": "icons/favicon.ico",
+ "type": "image/x-icon",
+ "sizes": "any"
+ },
+ {
+ "src": "icons/logo.svg",
+ "type": "image/svg+xml",
+ "sizes": "any"
+ },
+ {
+ "src": "icons/android-chrome-192x192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "icons/android-chrome-512x512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ },
+ {
+ "src": "icons/maskable_icon_x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/maskable_icon_x384.png",
+ "sizes": "384x384",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/maskable_icon_x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "screenshots": [
+ {
+ "src": "icons/screenshot1.png",
+ "type": "image/png",
+ "sizes": "1244x1370"
+ }
+ ],
+ "splash_pages": null
+}
diff --git a/packages/next.js/styles/_blocks.scss b/packages/next.js/styles/_blocks.scss
new file mode 100644
index 000000000..7a2dca02d
--- /dev/null
+++ b/packages/next.js/styles/_blocks.scss
@@ -0,0 +1,517 @@
+.pad {
+ position: relative;
+ @apply h-full;
+
+ .header {
+ // height: $Pad_Header__height;
+ }
+
+ .toolbars {
+ // height: $Pad_Toolbar__height;
+ }
+
+ .editor {
+ background-color: #f8f9fa;
+ overflow: auto;
+ // @apply absolute;
+
+ .tipta__editor {
+ @apply max-w-4xl w-full;
+
+ .heading {
+ background-color: #fff;
+ // padding: 0 1rem;
+ }
+
+ }
+ }
+}
+
+.toc {
+ background: rgba(black, 0.1);
+ border-radius: 0.5rem;
+ opacity: 0.75;
+ padding: 0.75rem;
+ min-width: 200px;
+ text-align: left;
+ line-height: 2rem;
+
+ &__list {
+ list-style: none;
+ padding: 0;
+
+ &::before {
+ content: "Table of Contents";
+ display: block;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.025rem;
+ opacity: 0.5;
+ text-transform: uppercase;
+ padding-bottom: 20px;
+ }
+ }
+
+ &__item {
+ padding-left: 1.5rem;
+
+ &.active > span {
+ a {
+ color: #4285f4;
+ }
+
+ a::after {
+
+ opacity: 1;
+ }
+ }
+
+ a {
+ font-weight: normal;
+ position: relative;
+ @apply text-sm text-gray-400;
+
+ &::after {
+ content: "";
+ display: block;
+ width: 100%;
+ border: 1px solid #4285f4;
+ position: absolute;
+ opacity: 0;
+ transition: all 0.1s cubic-bezier(0.39, 0.575, 0.565, 1);
+ }
+ }
+
+ a:hover {
+ opacity: 0.5;
+ color: #0D0D0D
+ }
+
+ &--1 {
+ padding-left: 0!important;
+ > span > a {
+ @apply text-base font-medium text-black;
+ }
+ }
+
+ }
+}
+
+.drag_box {
+ position: relative;
+
+ &:hover {
+ >#dragspan {
+ opacity: 1;
+ }
+ }
+
+ #dragspan {
+ display: block;
+ position: absolute;
+ left: -20px;
+ top: 0;
+ opacity: 0;
+ transition: all .1s cubic-bezier(0.39, 0.575, 0.565, 1);
+
+ &:hover {
+ opacity: 1;
+ }
+
+ &::after {
+ content: '';
+ display: block;
+ flex: 0 0 auto;
+ position: relative;
+ width: 1rem;
+ height: 1rem;
+ top: 0.3rem;
+ margin-right: 0.5rem;
+ cursor: grab;
+ background-image: url('data:image/svg+xml;charset=UTF-8, ');
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ }
+ }
+
+}
+
+.tiptap__toolbar {
+ @apply border-t border-b;
+
+ .divided {
+ display: inline-block;
+ height: 20px;
+ @apply border-l mx-[10px] my-[6px];
+ // float: left;
+ }
+
+ button {
+ float: left;
+ margin: 4px 2px;
+ // padding: 2px 6px;
+ background-color: transparent;
+ outline: none;
+ cursor: pointer;
+ height: 28px;
+ width: 28px;
+ @apply flex rounded items-center justify-center relative;
+
+ &.btn_settingModal {
+ @apply ml-auto
+ }
+
+ svg {}
+
+ &:hover {
+ background: #eee;
+ color: black;
+ }
+
+ &.is-active {
+ background-color: #e8f0fe;
+ color: #1a73e8;
+
+ svg {
+ fill: #1a73e8;
+ }
+ }
+
+ &[disabled]:hover {
+ color: #aaa;
+ background-color: transparent;
+ }
+ }
+
+ .gearModal {
+ width: 300px;
+ padding: 8px;
+ border: 1px solid #ddd;
+ position: absolute;
+ right: 14px;
+ background: #fff;
+ border-radius: 4px;
+ position: absolute;
+ display: none;
+ top: 38px;
+
+ &.active {
+ display: block;
+ }
+
+ .content {
+ display: flex;
+ align-items: center;
+ }
+ }
+}
+
+.nodeStyle__control,
+.nodeStyle__value-container {
+ min-height: 28px !important;
+ height: 28px;
+}
+
+.nodeStyle__control {
+ border-width: 0 !important;
+ padding: 0 0 0 4px !important;
+}
+
+.nodeStyle__control:hover {
+ background-color: #eee;
+}
+
+.nodeStyle__input-container,
+.nodeStyle__value-container {
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+.nodeStyle__indicator-separator {
+ margin: 5px 0 !important;
+ display: none !important;
+}
+
+.nodeStyle__indicator {
+ padding: 0 !important;
+}
+
+.nodeStyle__menu {
+ padding: 0 !important;
+ margin: 0 !important;
+}
+
+.ProseMirror {
+ text-align: left;
+ outline: none;
+ min-height: 800px;
+
+ *.unselectable {
+ -moz-user-select: -moz-none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+
+ >*+* {
+ margin-top: 0.75em;
+ }
+
+ /* Placeholder (at the top) */
+ .ProseMirror p.is-editor-empty:first-child::before {
+ color: #adb5bd;
+ content: attr(data-placeholder);
+ float: left;
+ height: 0;
+ pointer-events: none;
+ }
+
+ /* Give a remote user a caret */
+ .collaboration-cursor__caret {
+ border-left: 1px solid #0d0d0d;
+ border-right: 1px solid #0d0d0d;
+ margin-left: -1px;
+ margin-right: -1px;
+ pointer-events: none;
+ position: relative;
+ word-break: normal;
+ }
+
+ /* Render the username above the caret */
+ .collaboration-cursor__label {
+ border-radius: 3px 3px 3px 0;
+ color: #0d0d0d;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 600;
+ left: -1px;
+ line-height: normal;
+ padding: 0.1rem 0.3rem;
+ position: absolute;
+ top: -1.4em;
+ user-select: none;
+ white-space: nowrap;
+ }
+
+ ul[data-type="taskList"] {
+ list-style: none;
+ padding: 0;
+
+ p {
+ margin: 0;
+ }
+
+ li {
+ display: flex;
+
+ >label {
+ flex: 0 0 auto;
+ margin-right: 0.5rem;
+ user-select: none;
+ }
+
+ >div {
+ flex: 1 1 auto;
+ }
+ }
+ }
+
+ ul {
+ list-style-type: revert;
+ }
+
+ ol {
+ list-style-type: decimal;
+ }
+
+ ul,
+ ol {
+ padding: 0 1rem;
+ }
+
+ :is(h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) {
+ font-weight: bold;
+ @apply font-medium mt-0 mb-2;
+ }
+
+ h1 {
+ @apply text-6xl;
+ }
+
+ h2 {
+ @apply text-5xl;
+ }
+
+ h3 {
+ @apply text-4xl;
+ }
+
+ h4 {
+ @apply text-3xl;
+ }
+
+ h5 {
+ @apply text-2xl;
+ }
+
+ :is(h6, h7, h8, h9, h10) {
+ @apply text-xl;
+ }
+
+ code {
+ background-color: rgba(#616161, 0.1);
+ color: #616161;
+ }
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ hr {
+ border: none;
+ border-top: 2px solid #fff;
+ margin: 2rem 0;
+ }
+
+ .is-empty::before {
+ content: attr(data-placeholder);
+ float: left;
+ color: #adb5bd;
+ pointer-events: none;
+ height: 0;
+ }
+
+ pre {
+ background: #0d0d0d;
+ border-radius: 0.5rem;
+ color: #fff;
+ font-family: "JetBrainsMono", monospace;
+ padding: 0.75rem 1rem;
+
+ code {
+ background: none;
+ color: inherit;
+ font-size: 0.8rem;
+ padding: 0;
+ }
+
+ .hljs-comment,
+ .hljs-quote {
+ color: #616161;
+ }
+
+ .hljs-variable,
+ .hljs-template-variable,
+ .hljs-attribute,
+ .hljs-tag,
+ .hljs-name,
+ .hljs-regexp,
+ .hljs-link,
+ .hljs-name,
+ .hljs-selector-id,
+ .hljs-selector-class {
+ color: #f98181;
+ }
+
+ .hljs-number,
+ .hljs-meta,
+ .hljs-built_in,
+ .hljs-builtin-name,
+ .hljs-literal,
+ .hljs-type,
+ .hljs-params {
+ color: #fbbc88;
+ }
+
+ .hljs-string,
+ .hljs-symbol,
+ .hljs-bullet {
+ color: #b9f18d;
+ }
+
+ .hljs-title,
+ .hljs-section {
+ color: #faf594;
+ }
+
+ .hljs-keyword,
+ .hljs-selector-tag {
+ color: #70cff8;
+ }
+
+ .hljs-emphasis {
+ font-style: italic;
+ }
+
+ .hljs-strong {
+ font-weight: 700;
+ }
+ }
+
+ table {
+ border-collapse: collapse;
+ margin: 0;
+ overflow: hidden;
+ table-layout: fixed;
+ width: 100%;
+
+ td,
+ th {
+ border: 2px solid #ced4da;
+ box-sizing: border-box;
+ min-width: 1em;
+ padding: 3px 5px;
+ position: relative;
+ vertical-align: top;
+
+ >* {
+ margin-bottom: 0;
+ }
+ }
+
+ th {
+ background-color: #f1f3f5;
+ font-weight: bold;
+ text-align: left;
+ }
+
+ .selectedCell:after {
+ background: rgba(200, 200, 255, 0.4);
+ content: "";
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ pointer-events: none;
+ position: absolute;
+ z-index: 2;
+ }
+
+ .column-resize-handle {
+ background-color: #adf;
+ bottom: -2px;
+ position: absolute;
+ right: -2px;
+ pointer-events: none;
+ top: 0;
+ width: 4px;
+ }
+
+ p {
+ margin: 0;
+ }
+ }
+
+ .tableWrapper {
+ padding: 1rem 0;
+ overflow-x: auto;
+ }
+
+ .resize-cursor {
+ cursor: ew-resize;
+ cursor: col-resize;
+ }
+}
diff --git a/packages/next.js/styles/_headings.scss b/packages/next.js/styles/_headings.scss
new file mode 100644
index 000000000..b17608fab
--- /dev/null
+++ b/packages/next.js/styles/_headings.scss
@@ -0,0 +1,127 @@
+body.indentHeading {
+ &.h1SectionBreak{
+ div.heading {
+ &[level="1"] {
+ box-shadow: 8px 0 34px 0 rgb(60 64 67 / 10%);
+ }
+ }
+ }
+ div.heading {
+ border-left: 3px solid rgba(#0d0d0d, 0.1);
+ padding-left: 1rem;
+ position: relative;
+ margin-top: 1rem;
+
+ .title::before {
+ content: attr(level);
+ position: absolute;
+ background-color: tomato;
+ font-size: 12px !important;
+ height: 20px;
+ width: 20px;
+ top: -16px;
+ left: 33px;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ }
+ }
+}
+
+body.h1SectionBreak{
+ .ProseMirror {
+ background-color: transparent;
+
+ .heading {
+ margin-bottom: 1rem;
+ &[level="1"] {
+ box-shadow: 0 1px 3px 1px rgb(00 64 67 / 15%);
+ padding: 1rem;
+ padding-bottom: 0;
+ border-bottom: 1px solid #e1e6e7;
+
+ }
+ }
+ }
+}
+
+.ProseMirror {
+ .heading {
+ &[level="1"] {
+ padding: 1rem;
+ margin-top: 0;
+ border: 1px solid #e1e6e7;
+ border-bottom: none;
+ border-top:none;
+
+ &:first-of-type{
+ border-top: 1px solid #e1e6e7;
+ }
+
+ &:last-of-type{
+ border-bottom: 1px solid #e1e6e7;
+ }
+ }
+ }
+
+ .title {
+ z-index: 9;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ &.is-empty::before {
+ position: absolute;
+ line-height: 0;
+ }
+
+ &[level="1"].is-empty::before {
+ font-size: 3.75rem;
+ }
+
+ &[level="2"].is-empty::before {
+ font-size: 3rem;
+ }
+
+ &[level="3"].is-empty::before {
+ font-size: 2.25rem;
+ }
+
+ &[level="4"].is-empty::before {
+ font-size: 1.875rem;
+ }
+
+ &[level="5"].is-empty::before {
+ font-size: 1.5rem;
+ }
+
+ &[level="6"].is-empty::before {
+ font-size: 1.25rem;
+ }
+
+ &[level="7"].is-empty::before {
+ font-size: 1.125rem;
+ }
+
+ &[level="8"].is-empty::before {
+ font-size: 1.125rem;
+ }
+
+ &[level="9"].is-empty::before {
+ font-size: 1.125rem;
+ }
+
+ &[level="10"].is-empty::before {
+ font-size: 1.125rem;
+ }
+
+ :is(h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) {
+ margin-left: -33px;
+ width: 100%;
+ }
+
+ }
+}
diff --git a/packages/next.js/styles/globals.scss b/packages/next.js/styles/globals.scss
new file mode 100644
index 000000000..7ff0a2706
--- /dev/null
+++ b/packages/next.js/styles/globals.scss
@@ -0,0 +1,101 @@
+.dark {
+ --color-bg-primary: #2d3748;
+ --color-bg-secondary: #283141;
+ --color-text-primary: #f7fafc;
+ --color-text-secondary: #e2e8f0;
+ --color-text-accent: #81e6d9;
+}
+
+.light {
+ --color-bg-primary: #ffffff;
+ --color-bg-secondary: #edf2f7;
+ --color-text-primary: #2d3748;
+ --color-text-secondary: #4a5568;
+ --color-text-accent: #2b6cb0;
+}
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* #root {
+ @apply h-full
+} */
+
+* {
+ @apply box-border
+}
+
+.dark * {
+ @apply bg-slate-800 text-slate-400;
+
+}
+
+.dark {
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h4 {
+ @apply text-white font-medium tracking-tight
+ }
+
+ border-color: #283141;
+
+ svg {
+ color: #fff;
+ fill: #fff;
+ }
+
+ p {
+ @apply text-sm text-slate-400
+ }
+
+ button {
+ @apply text-white
+ }
+}
+
+
+.ProseMirror {
+ @apply h-full
+}
+
+body,
+html {
+ @apply p-0 m-0 max-h-full min-h-full h-full
+}
+
+#root, #__next {
+ @apply h-full
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+
+a:hover {
+ color: #535bf2;
+}
+
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+@media (prefers-color-scheme: light) {
+ /* :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+
+ a:hover {
+ color: #747bff;
+ } */
+
+}
diff --git a/packages/next.js/styles/styles.scss b/packages/next.js/styles/styles.scss
new file mode 100644
index 000000000..de940080b
--- /dev/null
+++ b/packages/next.js/styles/styles.scss
@@ -0,0 +1,374 @@
+@use './blocks';
+@use './headings';
+
+$pad-header-height: 64px;
+$pad-toolbar-height: 38px;
+$crinkle-time: 0.9s;
+$crinkle-transition: ease-out;
+$crinkle-fold-bg: #bbb;
+$crinkle-unfold-bg: #efefef;
+
+.foldWrapper {
+ background-color: #f8f9fa;
+ cursor: pointer;
+ height: 100%;
+ position: absolute;
+ width: 100%;
+ bottom: 0;
+ height: calc(100% - 90px);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ transition: all $crinkle-time $crinkle-transition;
+
+ .fold {
+ width: calc(100% + 2rem + 6px);
+ position: relative;
+ z-index: 2;
+ left: -19px;
+ background-color: #f8f9fa;
+ padding-right: 4px;
+ height: 100%;
+ margin-top: -2px;
+ transition: all $crinkle-time $crinkle-transition;
+
+ &::after {
+ content: "";
+ display: block;
+ max-height: 50%;
+ height: 100%;
+ min-height: 5px;
+ background-color: #fff;
+ width: 100%;
+ transform: skew(44deg);
+ transition: all $crinkle-time $crinkle-transition;
+ border-right: 0;
+ border-left: 0;
+ }
+
+ &::before {
+ content: "";
+ display: block;
+ max-height: 50%;
+ height: 100%;
+ min-height: 5px;
+ background-color: $crinkle-fold-bg;
+ width: 100%;
+ transform: skew(-44deg);
+ border-bottom: none;
+ transition: all $crinkle-time $crinkle-transition;
+ box-shadow: 0 1px 5px 1px rgb(0 64 67 / 15%);
+ border-right: 0;
+ border-left: 0;
+ }
+
+ &:last-of-type {
+
+ &::after {
+ border: 1px solid #e1e6e7;
+ box-shadow: none;
+ }
+ }
+ }
+}
+
+.ProseMirror {
+
+ // buttons in heading
+ div.heading {
+ padding-bottom: 1rem;
+ position: relative;
+ height: 100%;
+
+ &.opend,
+ &.opening {
+ .buttonWrapper {
+ button.btnFold::before {
+ content: '\25BC';
+ }
+ }
+ }
+
+ &.closed,
+ &.closing {
+ .buttonWrapper {
+ button.btnFold::before {
+ content: '\25B6';
+ }
+ }
+ }
+
+ &[level="1"] {
+ > .wrapBlock > .foldWrapper .fold {
+ width: calc(100% + 6px);
+ left: 14px;
+ }
+ }
+
+ &[level="2"] {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 60px);
+ }
+ }
+
+ &[level="3"] {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 50px);
+ }
+ }
+
+ &[level="4"] {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 40px);
+ }
+ }
+
+ &[level="5"] {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 40px);
+ }
+ }
+
+ &:is([level="6"], [level="7"], [level="8"], [level="9"], [level="10"]) {
+ > .wrapBlock > .foldWrapper {
+ height: calc(100% - 40px);
+ }
+ }
+
+ @keyframes foldCrinkle_after {
+ 0% {
+ transform: skew(0deg);
+ }
+
+ 100% {
+ transform: skew(44deg);
+ }
+ }
+
+ @keyframes foldCrinkle_before {
+ 0% {
+ transform: skew(0deg);
+ background-color: $crinkle-unfold-bg;
+ }
+
+ 100% {
+ transform: skew(-44deg);
+ background-color: $crinkle-fold-bg;
+ }
+ }
+
+ @keyframes unfoldCrinkle_after {
+ 0% {
+ transform: skew(44deg);
+ }
+
+ 100% {
+ transform: skew(0deg);
+ }
+ }
+
+ @keyframes unfoldCrinkle_before {
+ 0% {
+ transform: skew(-44deg);
+ background-color: $crinkle-fold-bg;
+ }
+
+ 100% {
+ transform: skew(0deg);
+ background-color: $crinkle-unfold-bg;
+ }
+ }
+
+ @keyframes unfoldWrapper {
+ 0% {
+ background-color: inherit;
+ }
+
+ 100% {
+ background-color: transparent;
+ }
+ }
+
+ &.closed {
+ > .wrapBlock > .foldWrapper {
+ z-index: 2;
+ }
+
+ &:hover {
+
+ > .wrapBlock > .foldWrapper ~ .contentWrapper {
+ height: 70px !important;
+ }
+
+ height: calc(100% - 70px);
+
+ > .wrapBlock > .foldWrapper .fold::before {
+ background-color: #ccc;
+ }
+ }
+
+ }
+
+ &.closing {
+ > .wrapBlock > .foldWrapper {
+ z-index: 4;
+ }
+ }
+
+ &.closed,
+ &.closing {
+ > .wrapBlock > .foldWrapper {
+ display: flex;
+ z-index: 4;
+
+ .fold::after {
+ animation: foldCrinkle_after $crinkle-time $crinkle-transition;
+ }
+
+ .fold::before {
+ animation: foldCrinkle_before $crinkle-time $crinkle-transition;
+ }
+ }
+ }
+
+ &.opening {
+ > .wrapBlock > .foldWrapper {
+ .fold {}
+ }
+ }
+
+ &.opend,
+ &.opening {
+ > .wrapBlock > .foldWrapper {
+ .fold::after {
+ animation: unfoldCrinkle_after $crinkle-time $crinkle-transition;
+ }
+
+ .fold::before {
+ animation: unfoldCrinkle_before $crinkle-time $crinkle-transition;
+ }
+ }
+ }
+
+ &.opend {
+ > .wrapBlock > .foldWrapper {
+ display: none;
+ }
+ }
+ }
+
+ .wrapBlock {
+ height: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row-reverse;
+
+ .title {
+ flex: 1;
+ padding-left: 60px;
+ margin-left: -100px;
+ position: relative;
+ z-index: 1;
+ min-width: 100%;
+
+ &:hover ~ .buttonWrapper {
+ visibility: visible;
+ }
+ }
+
+ .foldWrapper,
+ .contentWrapper {
+ width: 100%;
+ }
+
+ .buttonWrapper {
+ display: flex;
+ width: 40px;
+ align-items: center;
+ position: relative;
+ left: -60px;
+ z-index: 2;
+ visibility: hidden;
+
+ &:hover {
+ visibility: visible;
+ }
+
+ > a {
+ margin-left: 14px;
+ color: #ccc;
+
+ &:hover {
+ color: #646cff
+ }
+ }
+
+ }
+
+ .contentWrapper {
+ height: auto;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-height: 40px;
+ z-index: 0;
+ padding-bottom: 10px;
+ // NOTE: only transition duration is work here! other property are in the js file
+ transition: all $crinkle-time $crinkle-transition;
+
+ .foldWrapper {
+ transition: all $crinkle-time $crinkle-transition;
+
+ .fold {
+ transition: height $crinkle-time $crinkle-transition;
+ }
+ }
+
+ &.collapsed {
+ height: 0;
+ padding: 0;
+ overflow: hidden;
+ }
+ }
+ }
+}
+
+
+
+.tiptap__toc{
+ .toc__item{
+ position: relative;
+ width: 100%;
+ margin: 6px 0;
+ > span{
+ display: flex;
+ align-items: center;
+ }
+
+ &.closed{
+ > span .btnFold{
+ &::before{
+ content: '\25B6';
+ }
+ }
+ }
+ > span .btnFold{
+ &::before{
+ content: '\25BC';
+ display: block;
+ margin-right: 6px;
+ width: 16px;
+ height: 16px;
+ // border: 2px solid red;
+ font-size: 0.7rem;
+ text-align: center;
+ opacity: 0;
+ cursor: pointer;
+ }
+ }
+
+ &:hover > span > .btnFold::before{
+ opacity: 1;
+ }
+ }
+}
diff --git a/packages/next.js/tailwind.config.js b/packages/next.js/tailwind.config.js
new file mode 100644
index 000000000..cf140077c
--- /dev/null
+++ b/packages/next.js/tailwind.config.js
@@ -0,0 +1,29 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ "./app/**/*.{js,ts,jsx,tsx}",
+ "./pages/**/*.{js,ts,jsx,tsx}",
+ "./components/**/*.{js,ts,jsx,tsx}",
+
+ // Or if using `src` directory:
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ plugins: [],
+ darkMode: 'class',
+ theme: {
+ extend: {
+ spacing: {
+ },
+ backgroundColor: {
+ primary: 'var(--color-bg-primary)',
+ secondary: 'var(--color-bg-secondary)',
+ },
+ textColor: {
+ accent: 'var(--color-text-accent)',
+ primary: 'var(--color-text-primary)',
+ secondary: 'var(--color-text-secondary)',
+ },
+ },
+ },
+}
+
diff --git a/packages/next.js/tsconfig.json b/packages/next.js/tsconfig.json
new file mode 100644
index 000000000..4627fc3bc
--- /dev/null
+++ b/packages/next.js/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ "pages/index.jsx",
+ "pages/_app.jsx",
+ "pages/api/hello.js"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}