diff --git a/manifest.json b/manifest.json
index 5606e54..64e20a7 100755
--- a/manifest.json
+++ b/manifest.json
@@ -1,6 +1,6 @@
{
"manifest_version": 3,
- "version": "1.2.4",
+ "version": "1.2.5",
"name": "PlaceNoter",
"description": "Turns browser's home page to a Note-Taking-App.",
"offline_enabled": true,
diff --git a/package-lock.json b/package-lock.json
index c6d9302..f33d909 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "placenoter",
- "version": "1.2.4",
+ "version": "1.2.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "placenoter",
- "version": "1.2.4",
+ "version": "1.2.5",
"license": "MIT",
"dependencies": {
"@dnd-kit/core": "^6.0.3",
diff --git a/package.json b/package.json
index 5d0ca99..1742642 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "placenoter",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "New tab replaced by note taking app.",
"license": "MIT",
"repository": {
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 0433556..f69e9bd 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -133,6 +133,8 @@ const Sidebar = () => {
// Adding note to `binNotes`
// TODO: don't need so much vars/consts
setBinNotes(JSON.parse(JSON.stringify([note, ...binNotes])))
+
+ if (id === activeNote?.id) setActiveNote(undefined)
}
const initiateMoveToBin = (e: any, note: Note) => {
diff --git a/src/components/editor/Menubar.tsx b/src/components/editor/Menubar.tsx
index 5dc212d..bc00d71 100644
--- a/src/components/editor/Menubar.tsx
+++ b/src/components/editor/Menubar.tsx
@@ -6,7 +6,7 @@ import { Button, Input, Tooltip, Text } from '@nextui-org/react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
import { debounce } from 'lodash';
import { RiSearch2Line, RiArrowDownSLine } from 'react-icons/ri'
-import { MdSpellcheck } from 'react-icons/md'
+import { MdEdit, MdPreview, MdSpellcheck } from 'react-icons/md'
import { useLocalStorage } from 'react-use';
import { stopPrevent } from '../../utils'
@@ -159,9 +159,17 @@ type MenubarProps = {
editor: Editor,
isLocalSearchVisible: boolean,
onSearchTooltipClose: () => any
+ isPreview: boolean,
+ toggleIsPreview: () => any
}
-const Menubar = ({ editor, isLocalSearchVisible, onSearchTooltipClose }: MenubarProps) => {
+const Menubar = ({
+ editor,
+ isLocalSearchVisible,
+ onSearchTooltipClose,
+ isPreview,
+ toggleIsPreview,
+}: MenubarProps) => {
if (!editor) return null
const activeNote = useRecoilValue(activeNoteState)
@@ -362,6 +370,22 @@ const Menubar = ({ editor, isLocalSearchVisible, onSearchTooltipClose }: Menubar
+
+
+
+
{LinkModal({ visible: linkModalVisible, onClose: closeLinkModalAndUpdateLink })}
{BubbleMenu({ editor, debouncedCalculateIsActiveStates, isActiveStates, openLinkModal })}
diff --git a/src/components/editor/Tiptap.scss b/src/components/editor/Tiptap.scss
index 55a74b5..b06e7c0 100644
--- a/src/components/editor/Tiptap.scss
+++ b/src/components/editor/Tiptap.scss
@@ -33,6 +33,12 @@
pointer-events: none;
}
+ h1,
+ h2,
+ h3 {
+ margin: 0;
+ }
+
ul,
ol {
padding: 0 1rem;
diff --git a/src/components/editor/Tiptap.tsx b/src/components/editor/Tiptap.tsx
index 3f2ae96..dc1be32 100644
--- a/src/components/editor/Tiptap.tsx
+++ b/src/components/editor/Tiptap.tsx
@@ -18,7 +18,7 @@ import TableRow from '@tiptap/extension-table-row';
import './Tiptap.scss'
import Menubar from './Menubar'
-import { suggestions, Commands, SearchAndReplace, SmilieReplacer } from './extensions'
+import { suggestions, Commands, SearchAndReplace, SmilieReplacer, Doc, DBlock, NodeMover } from './extensions'
import { CodeBlockLowLight } from './extensions/CodeBlockLowLight';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currentLinkUrlState, linkModalState, spellCheckState } from '../../Store';
@@ -36,6 +36,8 @@ const Tiptap = ({ onUpdate, content, isNoteInBin }: TiptapProps) => {
const [isLocalSearchVisible, setIsLocalSearchVisible] = useState(false)
+ const [isPreview, setIsPreview] = useState(false)
+
const spellcheckRecoilState = useRecoilValue(spellCheckState)
const focusSearchInput = async (): Promise => {
@@ -56,7 +58,17 @@ const Tiptap = ({ onUpdate, content, isNoteInBin }: TiptapProps) => {
const editor = useEditor({
extensions: [
- StarterKit.configure({ codeBlock: false }),
+ Doc,
+ StarterKit.configure({
+ codeBlock: false,
+ document: false,
+ dropcursor: {
+ color: 'skyblue',
+ width: 2
+ }
+ }),
+ DBlock,
+ NodeMover,
Placeholder.configure({
placeholder: "Type '/' for commands…"
}),
@@ -126,6 +138,17 @@ const Tiptap = ({ onUpdate, content, isNoteInBin }: TiptapProps) => {
})
}, [spellcheckRecoilState])
+ useEffect(() => {
+ editor?.setEditable(!isPreview)
+ }, [isPreview])
+
+ const [editorContentKey, setEditorContentKey] = useState(`${Math.random()}`)
+
+ const toggleIsPreview = () => {
+ setIsPreview(!isPreview)
+ setEditorContentKey(`${Math.random()}`)
+ }
+
return (
<>
{
@@ -134,11 +157,18 @@ const Tiptap = ({ onUpdate, content, isNoteInBin }: TiptapProps) => {
editor={editor}
isLocalSearchVisible={isLocalSearchVisible}
onSearchTooltipClose={() => setIsLocalSearchVisible(false)}
+ isPreview={isPreview}
+ toggleIsPreview={toggleIsPreview}
/>
)
}
-
+
diff --git a/src/components/editor/extensions/dBlock/DBlockNodeView.tsx b/src/components/editor/extensions/dBlock/DBlockNodeView.tsx
new file mode 100644
index 0000000..24f8327
--- /dev/null
+++ b/src/components/editor/extensions/dBlock/DBlockNodeView.tsx
@@ -0,0 +1,111 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+
+import { Button, Tooltip, styled } from '@nextui-org/react';
+import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
+import React, { useMemo } from "react";
+import { BiPlus } from 'react-icons/bi';
+import { MdClose, MdDragIndicator } from 'react-icons/md';
+import { RiArrowDownLine, RiArrowUpLine } from 'react-icons/ri';
+
+import './DBlockStyle.scss'
+
+const LeftSection = styled('section', {
+ display: 'flex',
+ gap: '4px',
+ alignItems: 'center'
+})
+
+const MoveButtonsContainer = styled('div', {
+ display: 'flex',
+ justifyContent: 'space-between'
+})
+
+export const DBlockNodeView: React.FC = ({
+ node,
+ getPos,
+ editor,
+ deleteNode
+}) => {
+ const isTable = useMemo(() => {
+ const { content } = node.content as any;
+
+ return content[0].type.name === "table";
+ }, [node.content]);
+
+ const createNodeAfter = () => {
+ const pos = getPos() + node.nodeSize;
+
+ editor
+ .chain()
+ .insertContentAt(pos, {
+ type: "dBlock",
+ content: [
+ {
+ type: "paragraph",
+ },
+ ],
+ })
+ .focus(pos + 2)
+ .run();
+ };
+
+ const onDeleteClicked = () => {
+ deleteNode()
+
+ setTimeout(() => editor.commands.focus())
+ }
+
+ const moveNode = (dir: 'up' | 'down') => {
+ const { from, to } = editor.state.selection
+ const [nodeFrom, nodeTo] = [getPos(), getPos() + node.nodeSize]
+
+ if (!(nodeFrom <= from && to <= nodeTo)) editor.commands.focus(getPos() + 2)
+
+ setTimeout(() => editor.chain().moveNode(dir).focus().run())
+ }
+
+ return (
+
+ {
+ editor.isEditable && (
+
+
+
+
+ } onPress={() => moveNode('up')} />
+ } onPress={onDeleteClicked} />
+ } onPress={() => moveNode('down')} />
+
+
+ )}
+ trigger={'click'}
+ hideArrow
+ placement='bottom'
+ >
+
+
+
+
+
+ )
+ }
+
+
+
+ );
+};
diff --git a/src/components/editor/extensions/dBlock/DBlockStyle.scss b/src/components/editor/extensions/dBlock/DBlockStyle.scss
new file mode 100644
index 0000000..edcaf2d
--- /dev/null
+++ b/src/components/editor/extensions/dBlock/DBlockStyle.scss
@@ -0,0 +1,47 @@
+.dBlockWrapper {
+ gap: 8px;
+ width: 100%;
+ position: relative;
+
+ &:hover {
+ .d-block-button {
+ opacity: 1;
+ transition: none;
+ }
+ }
+
+ .d-block-button {
+ opacity: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: none;
+ border: none !important;
+ height: 28px;
+ padding: 2px;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: all 0.25s ease-in-out;
+
+ &:hover {
+ background: var(--nextui-colors-selection);
+ }
+
+ &.drag-handle {
+ cursor: grab;
+
+ &:hover:active {
+ cursor: grabbing;
+ }
+ }
+ }
+
+ .content {
+ width: 100%;
+ min-width: 4px;
+
+ &.is-table {
+ margin-left: 2rem;
+ }
+ }
+}
diff --git a/src/components/editor/extensions/dBlock/dBlock.ts b/src/components/editor/extensions/dBlock/dBlock.ts
new file mode 100644
index 0000000..1832c12
--- /dev/null
+++ b/src/components/editor/extensions/dBlock/dBlock.ts
@@ -0,0 +1,128 @@
+import { Node, mergeAttributes } from "@tiptap/core";
+import { ReactNodeViewRenderer } from "@tiptap/react";
+
+import { DBlockNodeView } from "./DBlockNodeView";
+
+export interface DBlockOptions {
+ HTMLAttributes: Record;
+}
+
+declare module "@tiptap/core" {
+ interface Commands {
+ dBlock: {
+ /**
+ * Toggle a dBlock
+ */
+ setDBlock: (position?: number) => ReturnType;
+ };
+ }
+}
+
+export const DBlock = Node.create({
+ name: "dBlock",
+
+ priority: 1000,
+
+ group: "dBlock",
+
+ content: "block",
+
+ draggable: true,
+
+ selectable: false,
+
+ inline: false,
+
+ addOptions() {
+ return {
+ HTMLAttributes: {},
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: 'div[data-type="d-block"]' }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "div",
+ mergeAttributes(HTMLAttributes, { "data-type": "d-block" }),
+ 0,
+ ];
+ },
+
+ addCommands() {
+ return {
+ setDBlock:
+ (position) =>
+ ({ state, chain }) => {
+ const {
+ selection: { from },
+ } = state;
+
+ const pos =
+ position !== undefined || position !== null ? from : position;
+
+ return chain()
+ .insertContentAt(pos, {
+ type: this.name,
+ content: [
+ {
+ type: "paragraph",
+ },
+ ],
+ })
+ .focus(pos + 2)
+ .run();
+ },
+ };
+ },
+
+ addNodeView() {
+ return ReactNodeViewRenderer(DBlockNodeView);
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ "Mod-Alt-0": () => this.editor.commands.setDBlock(),
+ Enter: ({ editor }) => {
+ const { selection: { $head, from, to }, doc } = editor.state
+
+ const parent = $head.node($head.depth - 1)
+
+ if (parent.type.name !== 'dBlock') return false
+
+ let currentActiveNodeTo = -1
+
+ doc.descendants((node, pos) => {
+ if (currentActiveNodeTo !== -1) return false
+ if (node.type.name === this.name) return
+
+ const [ nodeFrom, nodeTo ] = [ pos, pos + node.nodeSize ]
+
+ if (nodeFrom <= from && to <= nodeTo) currentActiveNodeTo = nodeTo
+
+ return false
+ })
+
+ const content = doc.slice(from, currentActiveNodeTo)?.toJSON().content
+
+ if (content[0]?.type) {
+ delete content[0].attrs
+ content[0].type = 'paragraph'
+ }
+
+ return editor.chain()
+ .insertContentAt(
+ { from, to: currentActiveNodeTo },
+ {
+ type: this.name,
+ content
+ }
+ )
+ .focus(from + 4)
+ .run()
+ },
+ };
+ },
+});
diff --git a/src/components/editor/extensions/dBlock/index.ts b/src/components/editor/extensions/dBlock/index.ts
new file mode 100644
index 0000000..18d98bf
--- /dev/null
+++ b/src/components/editor/extensions/dBlock/index.ts
@@ -0,0 +1 @@
+export * from './dBlock'
diff --git a/src/components/editor/extensions/doc.ts b/src/components/editor/extensions/doc.ts
new file mode 100644
index 0000000..dd5dcc3
--- /dev/null
+++ b/src/components/editor/extensions/doc.ts
@@ -0,0 +1,5 @@
+import Document from '@tiptap/extension-document'
+
+export const Doc = Document.extend({
+ content: 'dBlock+'
+})
diff --git a/src/components/editor/extensions/index.ts b/src/components/editor/extensions/index.ts
index b9abf97..2400882 100644
--- a/src/components/editor/extensions/index.ts
+++ b/src/components/editor/extensions/index.ts
@@ -1,3 +1,6 @@
+export * from './dBlock'
+export * from './doc'
export * from './searchAndReplace'
-export * from './smilieReplacer'
export * from './slash-menu'
+export * from './smilieReplacer'
+export * from './nodeMover'
diff --git a/src/components/editor/extensions/nodeMover.ts b/src/components/editor/extensions/nodeMover.ts
new file mode 100644
index 0000000..f45ebb2
--- /dev/null
+++ b/src/components/editor/extensions/nodeMover.ts
@@ -0,0 +1,148 @@
+import { Extension } from '@tiptap/core'
+import { Node, NodeType } from 'prosemirror-model'
+import { Fragment, Slice, ResolvedPos } from 'prosemirror-model'
+import { ReplaceStep } from 'prosemirror-transform'
+import { Selection } from 'prosemirror-state'
+
+export const equalNodeType = (nodeType: NodeType, node: Node) => {
+ return (Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1) || node.type === nodeType
+}
+
+export const findParentNodeClosestToPos = ($pos: ResolvedPos, predicate: (node: Node) => any) => {
+ for (let i = $pos.depth; i > 0; i--) {
+ const node = $pos.node(i)
+ if (predicate(node)) {
+ return {
+ pos: i > 0 ? $pos.before(i) : 0,
+ start: $pos.start(i),
+ depth: i,
+ node,
+ }
+ }
+ }
+}
+
+export const findParentNode =
+ (predicate: (node: Node) => any) =>
+ ({ $from }: { $from: ResolvedPos }) =>
+ findParentNodeClosestToPos($from, predicate)
+
+export const findParentNodeOfType = (nodeType: NodeType) => (selection: Selection) => {
+ return findParentNode((node) => equalNodeType(nodeType, node))(selection)
+}
+
+function mapChildren(node: Node, callback: (...args: any[]) => any) {
+ const array = []
+ for (let i = 0; i < node.childCount; i++) {
+ array.push(callback(node.child(i), i, node instanceof Fragment ? node : node.content))
+ }
+
+ return array
+}
+
+interface MoveNodeOptions {
+ types: string[];
+}
+
+declare module '@tiptap/core' {
+ interface Commands {
+ move: {
+ moveNode: (direction: 'up' | 'down') => ReturnType;
+ };
+ }
+}
+
+function isListNode(node: Node): boolean {
+ return ['listItem', 'taskItem'].includes(node.type.name)
+}
+
+export const NodeMover = Extension.create({
+ name: 'nodeMover',
+
+ addOptions() {
+ return {
+ types: ['dBlock'],
+ }
+ },
+
+ addCommands() {
+ return {
+ moveNode: (direction: 'up' | 'down') =>
+ ({ tr, state, dispatch }) => {
+ const { doc, selection } = tr
+ if (!doc || !selection) return false
+
+ const { from, to } = selection
+
+ doc.nodesBetween(from, to, (node) => {
+ const nodeType = node.type
+
+ if (!this.options.types.includes(nodeType.name)) return false
+
+ const isDown = direction === 'down'
+
+ if (!state.selection.empty) return false
+
+ const { $from } = state.selection
+
+ const currentResolved = findParentNodeOfType(nodeType)(state.selection)
+
+ if (!currentResolved) return false
+
+ const { node: currentNode } = currentResolved
+ const parentDepth = currentResolved.depth - 1
+ const parent = $from.node(parentDepth)
+
+ if (isListNode(parent)) return false
+
+ const parentPos = $from.start(parentDepth)
+
+ if (currentNode.type !== nodeType) return false
+
+ const arr = mapChildren(parent, (node) => node)
+
+ const index = arr.indexOf(currentNode)
+
+ const swapWith = isDown ? index + 1 : index - 1
+
+ if (swapWith >= arr.length || swapWith < 0) return false
+
+ const swapWithNodeSize = arr[swapWith]!.nodeSize
+
+ const temp = arr[index]
+ arr[index] = arr[swapWith]
+ arr[swapWith] = temp
+
+ let tr = state.tr
+ const replaceStart = parentPos
+ const replaceEnd = $from.end(parentDepth)
+
+ const slice = new Slice(Fragment.fromArray(arr), 0, 0)
+
+ tr = tr.step(new ReplaceStep(replaceStart, replaceEnd, slice, false))
+
+ const resolvedPos = tr.doc.resolve(
+ isDown
+ ? $from.pos + swapWithNodeSize
+ : $from.pos - swapWithNodeSize
+ )
+
+ tr = tr.setSelection(Selection.near(resolvedPos))
+
+ if (dispatch) dispatch(tr.scrollIntoView())
+
+ return true
+ })
+
+ return false
+ },
+ }
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ 'Alt-ArrowUp': () => this.editor.commands.moveNode('up'),
+ 'Alt-ArrowDown': () => this.editor.commands.moveNode('down'),
+ }
+ },
+})
diff --git a/src/utils/eventModifiers.ts b/src/utils/eventModifiers.ts
index aa8ee26..bfc7f13 100644
--- a/src/utils/eventModifiers.ts
+++ b/src/utils/eventModifiers.ts
@@ -1,6 +1,6 @@
export const stopPrevent = (e: T): T => {
- (e as Event).stopPropagation();
- (e as Event).preventDefault()
+ (e as Event).stopPropagation?.();
+ (e as Event).preventDefault?.();
return e
}