├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── README.md
├── assets
├── icons
│ └── index.tsx
└── scss
│ ├── _custom.scss
│ ├── _tab-group-col.scss
│ └── theme.scss
├── next-env.d.ts
├── next.config.js
├── package.json
├── src
├── assets
│ └── theme.scss
├── notes
│ ├── components
│ │ └── editor
│ │ │ ├── blocks
│ │ │ ├── headings
│ │ │ │ ├── h1.tsx
│ │ │ │ ├── h2.tsx
│ │ │ │ ├── h3.tsx
│ │ │ │ └── index.ts
│ │ │ ├── index.tsx
│ │ │ └── paragraph.tsx
│ │ │ ├── constants
│ │ │ ├── block-list.tsx
│ │ │ ├── highlight-colors.ts
│ │ │ ├── initial-state.ts
│ │ │ └── mark-list.tsx
│ │ │ ├── text-editor.tsx
│ │ │ ├── toolbar
│ │ │ ├── block-button.tsx
│ │ │ ├── index.tsx
│ │ │ ├── mark-button.tsx
│ │ │ └── toolbar.tsx
│ │ │ └── utils
│ │ │ ├── convert-to-doc.ts
│ │ │ ├── custom-delete.tsx
│ │ │ ├── custom-insert-break.tsx
│ │ │ ├── default-selection.ts
│ │ │ ├── get-closest-range.ts
│ │ │ ├── get-text-ranges.ts
│ │ │ ├── index-of-range.ts
│ │ │ ├── on-key-down.ts
│ │ │ ├── render-element.tsx
│ │ │ ├── render-leaf.tsx
│ │ │ ├── toggle-block.ts
│ │ │ ├── toggle-mark.ts
│ │ │ └── with-custom-normalize.ts
│ └── index.tsx
├── pages
│ ├── _app.tsx
│ └── index.tsx
└── shared
│ └── theme-utils.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es2021": true
4 | },
5 | "settings": {
6 | "react": {
7 | "version": "detect"
8 | }
9 | },
10 | "extends": [
11 | "eslint:recommended",
12 | "prettier",
13 | "plugin:promise/recommended",
14 | "plugin:sonarjs/recommended"
15 | ],
16 | "parser": "@typescript-eslint/parser",
17 | "parserOptions": {
18 | "ecmaFeatures": {
19 | "jsx": true
20 | },
21 | "ecmaVersion": 12,
22 | "project": "tsconfig.eslint.json"
23 | },
24 | "plugins": ["react", "@typescript-eslint", "promise", "sonarjs", "prettier"],
25 | "rules": {
26 | "prefer-const": "error"
27 | },
28 | "overrides": [
29 | /**
30 | * CLIENT SIDE CODE
31 | */
32 | {
33 | "files": ["src/**/*.{ts,js,jsx,tsx}"],
34 |
35 | "env": {
36 | "browser": true,
37 | "es2021": true
38 | },
39 | "rules": {
40 | "react/prop-types": "off",
41 | "react/no-children-prop": "off"
42 | },
43 | "extends": [
44 | "eslint:recommended",
45 | "plugin:react/recommended",
46 | "prettier/react"
47 | ]
48 | },
49 | /**
50 | * SERVER SIDE CODE
51 | */
52 | {
53 | "extends": ["plugin:node/recommended"],
54 | "files": [
55 | "config/**/*.js",
56 | "babel.config.js",
57 | "tailwind.config.js",
58 | "postcss.config.js",
59 | "server/**/*.js"
60 | ],
61 | "env": { "commonjs": true, "node": true }
62 | },
63 | /**
64 | * TYPESCRIPT CODE
65 | */
66 | {
67 | "files": ["{src,tests}/**/*.{ts,tsx}"],
68 | "extends": [
69 | "prettier/@typescript-eslint",
70 | "plugin:@typescript-eslint/recommended",
71 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
72 | ],
73 | "rules": {
74 | "no-unused-vars": "off",
75 | "@typescript-eslint/no-unsafe-call": "off",
76 | "@typescript-eslint/restrict-template-expressions": "off",
77 | "@typescript-eslint/no-unsafe-member-access": "off",
78 | "@typescript-eslint/no-unsafe-assignment": "off",
79 | "@typescript-eslint/no-unsafe-return": "off",
80 | "@typescript-eslint/no-explicit-any": "off"
81 | }
82 | },
83 | /**
84 | * TESTS
85 | */
86 | {
87 | "files": ["tests/**/*.{js,jsx,ts,tsx}"],
88 | "extends": [],
89 | "env": { "node": true, "jest": true }
90 | }
91 | ]
92 | }
93 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "semi": false,
4 | "singleQuote": true,
5 | "jsxBracketSameLine": true,
6 | "endOfLine": "lf",
7 | "trailingComma": "none",
8 | "arrowParens": "avoid"
9 | }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **_work in progress_**
2 |
3 | A try to make slate js paged.
4 |
5 | demo : https://slate-paged-demo.vercel.app
6 |
7 |
8 |
9 | slate:
10 | https://github.com/ianstormtaylor/slate
11 |
12 |
13 | done:
14 | basic editor setup
15 | basic functionalities
16 | figure out the nodes length and paging
17 |
18 | TODO:
19 | testing and yea the page is breaking in some conditions need to figure out
20 |
21 | ## Getting Started
22 | after `yarn dev` visit http://localhost:3000 for notes
23 |
24 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/assets/icons/index.tsx:
--------------------------------------------------------------------------------
1 | export const Administrative = () => (
2 |
16 | )
17 |
18 | export const Logo = () => (
19 |
39 | )
40 |
41 | export const logoAuth = () => (
42 |
62 | )
63 |
64 | export const Preferences = () => (
65 |
86 | )
87 |
88 | export const Patients = () => (
89 |
100 | )
101 |
--------------------------------------------------------------------------------
/assets/scss/_custom.scss:
--------------------------------------------------------------------------------
1 | .cursor-pointer {
2 | cursor: pointer;
3 | }
4 |
5 | .auth-bg {
6 | background: #1d39c4;
7 | }
8 |
--------------------------------------------------------------------------------
/assets/scss/_tab-group-col.scss:
--------------------------------------------------------------------------------
1 | @import '../../../node_modules/bootstrap/scss/functions';
2 | @import '../../../node_modules/bootstrap/scss/variables';
3 | @import '../../../node_modules/bootstrap/scss/mixins';
4 |
5 | @mixin make-tab-group-grid-columns(
6 | $columns: $grid-columns,
7 | $gutter: $grid-gutter-width,
8 | $breakpoints: $grid-breakpoints
9 | ) {
10 | // Common properties for all breakpoints
11 | %grid-column {
12 | position: relative;
13 | width: 100%;
14 | padding-right: $gutter / 2;
15 | padding-left: $gutter / 2;
16 | }
17 |
18 | @each $breakpoint in map-keys($breakpoints) {
19 | $infix: breakpoint-infix($breakpoint, $breakpoints);
20 |
21 | @if $columns > 0 {
22 | // Allow columns to stretch full width below their breakpoints
23 | @for $i from 1 through $columns {
24 | .tg-col#{$infix}-#{$i} {
25 | @extend %grid-column;
26 | }
27 | }
28 | }
29 |
30 | .tg-col#{$infix},
31 | .tg-col#{$infix}-auto {
32 | @extend %grid-column;
33 | }
34 |
35 | @include media-breakpoint-up($breakpoint, $breakpoints) {
36 | // Provide basic `.col-{bp}` classes for equal-width flexbox columns
37 | .tg-col#{$infix} {
38 | flex-basis: 0;
39 | flex-grow: 1;
40 | min-width: 0; // See https://github.com/twbs/bootstrap/issues/25410
41 | max-width: 100%;
42 | }
43 |
44 | @if $grid-row-columns > 0 {
45 | @for $i from 1 through $grid-row-columns {
46 | .tg-row-cols#{$infix}-#{$i} {
47 | @include row-cols($i);
48 | }
49 | }
50 | }
51 |
52 | .tg-col#{$infix}-auto {
53 | @include make-col-auto();
54 | }
55 |
56 | @if $columns > 0 {
57 | @for $i from 1 through $columns {
58 | .tg-col#{$infix}-#{$i} {
59 | @include make-col($i, $columns);
60 | }
61 | }
62 | }
63 |
64 | .tg-order#{$infix}-first {
65 | order: -1;
66 | }
67 |
68 | .tg-order#{$infix}-last {
69 | order: $columns + 1;
70 | }
71 |
72 | @for $i from 0 through $columns {
73 | .tg-order#{$infix}-#{$i} {
74 | order: $i;
75 | }
76 | }
77 |
78 | @if $columns > 0 {
79 | // `$columns - 1` because offsetting by the width of an entire row isn't possible
80 | @for $i from 0 through ($columns - 1) {
81 | @if not($infix == '' and $i == 0) {
82 | // Avoid emitting useless .offset-0
83 | .tg-offset#{$infix}-#{$i} {
84 | @include make-col-offset($i, $columns);
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | @function calc-breakpoints($multipier: 1, $breakpoints: $grid-breakpoints) {
94 | $new-breakpoints: ();
95 |
96 | @each $breakpoint, $pixel in $breakpoints {
97 | $new-breakpoints: map-merge(
98 | $map1: $new-breakpoints,
99 | $map2: (
100 | $breakpoint: $pixel * $multipier
101 | )
102 | );
103 | }
104 |
105 | @return $new-breakpoints;
106 | }
107 |
108 | .tab-group-12 {
109 | @include make-tab-group-grid-columns();
110 | }
111 |
112 | .tab-group-6 {
113 | @include make-tab-group-grid-columns(
114 | $grid-columns,
115 | $grid-gutter-width,
116 | calc-breakpoints(2)
117 | );
118 | }
119 |
120 | .tab-group-4 {
121 | @include make-tab-group-grid-columns(
122 | $grid-columns,
123 | $grid-gutter-width,
124 | calc-breakpoints(3)
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/assets/scss/theme.scss:
--------------------------------------------------------------------------------
1 | // font
2 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
3 |
4 | //node modules
5 | @import '../../../node_modules/react-grid-layout/css/styles.css';
6 | @import '../../../node_modules/react-resizable/css/styles.css';
7 | @import '../../../node_modules/bootstrap/scss/bootstrap';
8 |
9 | // custom scss
10 | @import './tab-group-col';
11 | @import './custom';
12 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | target: 'serverless',
3 | webpack: config => {
4 | return config
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text-editor",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@ant-design/icons": "^4.2.2",
12 | "@theme-ui/typography": "^0.3.0",
13 | "bootstrap": "^4.5.3",
14 | "docx": "^5.3.0",
15 | "file-saver": "^2.0.2",
16 | "is-hotkey": "^0.1.6",
17 | "next": "9.5.5",
18 | "node-sass": "^4.14.1",
19 | "react": "16.13.1",
20 | "react-dom": "16.13.1",
21 | "react-slate": "^0.5.1",
22 | "slate": "^0.59.0",
23 | "slate-hyperscript": "^0.59.0",
24 | "slate-plugins-next": "^0.58.13",
25 | "slate-react": "^0.59.0",
26 | "styled-components": "^5.2.0",
27 | "theme-ui": "^0.3.1"
28 | },
29 | "devDependencies": {
30 | "@types/file-saver": "^2.0.1",
31 | "@types/node": "^14.11.8",
32 | "@types/react": "^16.9.52",
33 | "@types/theme-ui": "^0.3.7",
34 | "@typescript-eslint/eslint-plugin": "^4.4.1",
35 | "@typescript-eslint/parser": "^4.4.1",
36 | "@zeit/next-css": "^1.0.1",
37 | "@zeit/next-less": "^1.0.1",
38 | "@zeit/next-sass": "^1.0.1",
39 | "eslint": "^7.11.0",
40 | "eslint-plugin-react": "^7.21.4",
41 | "less": "^3.12.2",
42 | "prettier": "^2.1.2",
43 | "typescript": "^4.0.3"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/assets/theme.scss:
--------------------------------------------------------------------------------
1 | @import '../../node_modules/bootstrap/scss/bootstrap';
2 |
--------------------------------------------------------------------------------
/src/notes/components/editor/blocks/headings/h1.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { RenderElementProps } from 'slate-react'
4 |
5 | const H1 = (props: RenderElementProps) => {
6 | return
{props.children}
7 | }
8 | export default H1
9 |
--------------------------------------------------------------------------------
/src/notes/components/editor/blocks/headings/h2.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { RenderElementProps } from 'slate-react'
4 |
5 | const H2 = (props: RenderElementProps) => {
6 | return {props.children}
7 | }
8 |
9 | export default H2
10 |
--------------------------------------------------------------------------------
/src/notes/components/editor/blocks/headings/h3.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { RenderElementProps } from 'slate-react'
4 |
5 | const H3 = (props: RenderElementProps) => {
6 | return {props.children}
7 | }
8 |
9 | export default H3
10 |
--------------------------------------------------------------------------------
/src/notes/components/editor/blocks/headings/index.ts:
--------------------------------------------------------------------------------
1 | export { default as H1 } from './h1'
2 | export { default as H2 } from './h2'
3 | export { default as H3 } from './h3'
4 |
--------------------------------------------------------------------------------
/src/notes/components/editor/blocks/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as Paragraph } from './paragraph'
2 | export * from './headings'
3 |
--------------------------------------------------------------------------------
/src/notes/components/editor/blocks/paragraph.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { RenderElementProps } from 'slate-react'
3 |
4 | const Paragraph = (props: RenderElementProps) => {
5 | return {props.children}
6 | }
7 |
8 | export default Paragraph
9 |
--------------------------------------------------------------------------------
/src/notes/components/editor/constants/block-list.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { OrderedListOutlined, UnorderedListOutlined } from '@ant-design/icons'
3 | import { CSSProperties } from 'react'
4 | import { RenderElementProps } from 'slate-react'
5 | import { H3, H2, H1 } from '../blocks'
6 |
7 | export interface ToolbarBlockProps {
8 | type: string
9 | title: string
10 | icon: React.ReactElement
11 | styles?: CSSProperties
12 | modkey?: string
13 | renderBlock: (props: RenderElementProps) => React.ReactElement
14 | isHiddenInToolbar?: boolean
15 | }
16 |
17 | export const LIST_TYPES = ['numbered-list', 'bulleted-list']
18 | export const HEADING_TYPES = ['h1', 'h2', 'h3']
19 |
20 | const blocks: ToolbarBlockProps[] = [
21 | {
22 | type: 'h1',
23 | title: 'Heading 1',
24 | icon: H1,
25 | renderBlock: (props: RenderElementProps) =>
26 | },
27 | {
28 | type: 'h2',
29 | title: 'Heading 2',
30 | icon: H2,
31 | renderBlock: (props: RenderElementProps) =>
32 | },
33 | {
34 | type: 'h3',
35 | title: 'Heading 1',
36 | icon: H3,
37 | renderBlock: (props: RenderElementProps) =>
38 | },
39 | {
40 | type: 'numbered-list',
41 | title: 'Heading 1',
42 | icon: ,
43 | renderBlock: (props: RenderElementProps) => (
44 | {props.children}
45 | )
46 | },
47 | {
48 | type: 'list-item',
49 | title: 'list Item',
50 | icon: ,
51 | isHiddenInToolbar: true,
52 | renderBlock: ({ attributes, children }: RenderElementProps) => (
53 | {children}
54 | )
55 | },
56 | {
57 | type: 'bulleted-list',
58 | title: 'Bullet List',
59 | icon: ,
60 | renderBlock: (props: RenderElementProps) => (
61 |
62 | )
63 | },
64 | {
65 | type: 'page',
66 | title: 'Page',
67 | icon:<>p> ,
68 | isHiddenInToolbar: true,
69 | renderBlock: ({ attributes, children }: RenderElementProps) => (
70 |
82 | {children}
83 |
84 | )
85 | },
86 | ]
87 |
88 | export default blocks
89 |
--------------------------------------------------------------------------------
/src/notes/components/editor/constants/highlight-colors.ts:
--------------------------------------------------------------------------------
1 | const highlightColors: { [key: string]: string } = {
2 | mandatoryReplaceColor: 'rgb(230 29 131)',
3 | searchHighlightColor: '#ffeeba'
4 | }
5 |
6 | export default highlightColors
7 |
--------------------------------------------------------------------------------
/src/notes/components/editor/constants/initial-state.ts:
--------------------------------------------------------------------------------
1 | const nodes = [
2 | {
3 | type: 'page',
4 | children: [
5 | {
6 | type: 'h1',
7 | children: [
8 | {
9 | type: 'text',
10 | text: 'Hey there! '
11 | },
12 |
13 |
14 | ]
15 | },
16 | {
17 | type: 'paragraph',
18 | children: [
19 | {
20 | type: 'text',
21 | text: 'This is first page of whatever you want to write! continue writing!'
22 | },
23 | {
24 | type: 'text',
25 | text: 'anything you want and download the button on top!'
26 | },
27 | ]
28 | },
29 | ]
30 | },
31 |
32 |
33 | ]
34 |
35 |
36 | export default nodes
37 |
--------------------------------------------------------------------------------
/src/notes/components/editor/constants/mark-list.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ItalicOutlined,
3 | StrikethroughOutlined,
4 | UnderlineOutlined,
5 | BoldOutlined,
6 | HighlightOutlined
7 | } from '@ant-design/icons'
8 | import { CSSProperties } from 'react'
9 | import highlightColors from './highlight-colors'
10 | import { Styled } from 'theme-ui'
11 |
12 | export interface IToolbarMark {
13 | type: string
14 | title: string
15 | icon: React.ReactElement
16 | styles?: CSSProperties
17 | modkey?: string
18 | renderChildren?: (children: any, leaf?: any) => React.ReactElement
19 | }
20 |
21 | /**toolbarMarks
22 | * @description toolbarmarks for editor.
23 | *
24 | * type: this is the entry point of leaf. leaf will be defined by this. checkout render-leaf.tsx to know more about leaf.
25 | *
26 | * renderChildren: while using renderChildren be careful to not return a div or a block element which can break the editor flow
27 | *
28 | * styles: by using styles key you can return styles that can be applied when the mark type is present,
29 | * be careful that these styles can be overriden by next type.
30 | * for example : the styles you define on 0 index of the array is { fontWeight:bold } and on the 1st element(i.e index 1 of the array) it is {fontWeight:normal} and the leaf has both the types, then the index 1 will ovveride 0
31 | * if you want to define styles that cannot be ovveriden then define it in renderChildren as {chilren}
32 | * or use html tag which corresponds to the style such as
33 | *
34 | * icon: icon for display
35 | *
36 | *TODO: modkey: use this as shortcut for applying this type on the leaf
37 | */
38 | const toolbarMarks: IToolbarMark[] = [
39 | {
40 | type: 'bold',
41 | title: 'BOLD',
42 | icon: ,
43 | renderChildren: (children: any) => {children}
44 | },
45 | {
46 | type: 'italics',
47 | title: 'Italics',
48 | icon: ,
49 | renderChildren: (children: any) => {children}
50 | },
51 | {
52 | type: 'strike',
53 | title: 'Stike Through',
54 | icon: ,
55 | renderChildren: (children: any) => {children}
56 | },
57 | {
58 | type: 'underlined',
59 | title: 'UnderLine',
60 | icon: ,
61 | renderChildren: (children: any) => {children}
62 | },
63 | {
64 | type: 'highlight',
65 | title: 'highlight',
66 | icon: ,
67 | renderChildren: (children: any, leaf: any) => (
68 |
74 | {children}
75 |
76 | )
77 | }
78 | ]
79 |
80 | export default toolbarMarks
81 |
--------------------------------------------------------------------------------
/src/notes/components/editor/text-editor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState, useCallback, useEffect} from 'react'
2 |
3 | // Import the Slate editor factory.
4 | import { createEditor, Node, Text, NodeEntry, Range, Transforms } from 'slate'
5 | import {
6 | Slate,
7 | Editable,
8 | withReact,
9 | RenderLeafProps,
10 | ReactEditor
11 | } from 'slate-react'
12 |
13 | import { BalloonToolbar, ToolbarMark } from 'slate-plugins-next' // TODO ; plan to remove this dependency and use own balloon toolbar
14 |
15 | import Toolbar from './toolbar'
16 |
17 | import defaultSelection from './utils/default-selection'
18 | import { getTextRanges } from './utils/get-text-ranges'
19 | import onKeyDownCustom from './utils/on-key-down'
20 | import renderElement from './utils/render-element'
21 | import renderLeaf from './utils/render-leaf'
22 | import toggleMark from './utils/toggle-mark'
23 |
24 | import toolbarMarks, { IToolbarMark } from './constants/mark-list'
25 | import intialState from './constants/initial-state'
26 | import highlightColors from './constants/highlight-colors'
27 | import withCustomNormalize from './utils/with-custom-normalize'
28 | import WithCustomInsertBreak from './utils/custom-insert-break'
29 | import WithCustomDelete from './utils/custom-delete'
30 |
31 | interface TextEditorState {
32 | value: Node[]
33 | search: string | undefined
34 | lastBlurSelection: Range | null
35 | }
36 |
37 | function TextEditor() {
38 | const editor = useMemo(
39 | () =>
40 | WithCustomDelete(
41 | WithCustomInsertBreak(withCustomNormalize(withReact(createEditor())))
42 | ),
43 | []
44 | )
45 |
46 | const [state, setState] = useState({
47 | value: [...intialState],
48 | search: '',
49 | lastBlurSelection: defaultSelection
50 | })
51 |
52 | const handleDecorate = ([node, path]: NodeEntry) => {
53 | const ranges: Range[] = []
54 | if (state.search && Text.isText(node)) {
55 | const currentRanges = getTextRanges(node, path, state.search)
56 | const rangesWithHighlights: Range[] = []
57 | currentRanges.forEach((text: Range) => {
58 | rangesWithHighlights.push({
59 | ...text,
60 | highlight: true,
61 | highlightColor: highlightColors.searchHighlightColor
62 | })
63 | })
64 | ranges.push(...rangesWithHighlights)
65 | }
66 | return ranges
67 | }
68 |
69 | const handleRenderLeaf: any = useCallback(
70 | (props: RenderLeafProps) => renderLeaf(props),
71 | [state.search]
72 | )
73 |
74 | const decorate = useCallback((entry: NodeEntry) => handleDecorate(entry), [
75 | state.search
76 | ])
77 |
78 | useEffect(() => {
79 | ReactEditor.focus(editor)
80 | }, [])
81 |
82 | const handleOnPaste=(e:React.ClipboardEvent)=>{
83 | e.preventDefault()
84 | const text= e.clipboardData?.getData('text')
85 | console.log(text,'YES, this text was pasted but i need to insert the page break so i disabled it for now, WORK IN PROGRESS')
86 | // if(text){
87 | // Transforms.insertText(editor,text)
88 | // }
89 | }
90 |
91 | return (
92 |
93 |
setState({ ...state, value })}>
97 |
98 | {toolbarMarks.map((mark: IToolbarMark) => {
99 | return (
100 | {
104 | e.preventDefault()
105 | toggleMark(editor, mark.type)
106 | }}
107 | />
108 | )
109 | })}
110 |
111 |
112 |
113 |
115 | setState({ ...state, search: value })
116 | }
117 | search={state.search || ''}
118 | lastBlurSelection={state.lastBlurSelection}
119 | />
120 | onKeyDownCustom(editor, event)}
124 | renderElement={renderElement}
125 | renderLeaf={handleRenderLeaf}
126 | onBlur={() =>
127 | setState({ ...state, lastBlurSelection: editor.selection })
128 | }
129 | />
130 |
131 | {/*
132 |
133 |
*/}
134 |
135 |
136 |
137 | )
138 | }
139 |
140 | export default TextEditor
141 |
--------------------------------------------------------------------------------
/src/notes/components/editor/toolbar/block-button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Button } from 'theme-ui'
4 | import { useSlate } from 'slate-react'
5 |
6 | import toggleBlock, { isBlockActive } from '../utils/toggle-block'
7 |
8 | interface BlockButtonProps {
9 | type: string
10 | icon: React.ReactElement
11 | }
12 |
13 | const BlockButton = ({ type, icon }: BlockButtonProps) => {
14 | const editor = useSlate()
15 |
16 | return (
17 |
26 | )
27 | }
28 |
29 | export default BlockButton
30 |
--------------------------------------------------------------------------------
/src/notes/components/editor/toolbar/index.tsx:
--------------------------------------------------------------------------------
1 | import Toolbar from './toolbar'
2 |
3 | export default Toolbar
4 |
--------------------------------------------------------------------------------
/src/notes/components/editor/toolbar/mark-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react'
2 |
3 | import { Button,ButtonProps } from 'theme-ui'
4 | import { useSlate } from 'slate-react'
5 |
6 | import toggleMark, { isMarkActive } from '../utils/toggle-mark'
7 |
8 | interface MarkButtonProps extends JSX.IntrinsicAttributes {
9 | type: string
10 | icon: React.ReactElement
11 | }
12 |
13 | const MarkButton = ({ type, icon }:MarkButtonProps) => {
14 | const editor = useSlate()
15 |
16 | return (
17 |
25 | )
26 | }
27 |
28 | export default MarkButton
29 |
--------------------------------------------------------------------------------
/src/notes/components/editor/toolbar/toolbar.tsx:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx, Link } from 'theme-ui'
3 |
4 | import React, { useEffect, useState } from 'react'
5 | import {saveAs} from 'file-saver'
6 | import { Button, Flex, Box } from 'theme-ui'
7 | import { DownOutlined, FileWordOutlined, GithubOutlined, SearchOutlined, UpOutlined } from '@ant-design/icons'
8 | import { ReactEditor, useSlate } from 'slate-react'
9 | import { Range, Transforms } from 'slate'
10 |
11 | import toolbarMarks, { IToolbarMark } from '../constants/mark-list'
12 | import blocks, { ToolbarBlockProps } from '../constants/block-list'
13 | import MarkButton from './mark-button'
14 | import BlockButton from './block-button'
15 | import indexOf from '../utils/index-of-range'
16 | import {
17 | getNextClosestRange,
18 | getPreviousClosestRange
19 | } from '../utils/get-closest-range'
20 | import defaultSelection from '../utils/default-selection'
21 | import { getEditorTextRanges } from '../utils/get-text-ranges'
22 | import convertToDoc from '../utils/convert-to-doc'
23 |
24 | interface ToolbarProps {
25 | search: string
26 | setSearch: (e: string) => void
27 | lastBlurSelection: Range | null
28 | }
29 | const Toolbar = ({ setSearch, search, lastBlurSelection }: ToolbarProps) => {
30 | const editor = useSlate()
31 | const [Ranges, setRanges] = useState([])
32 | const [isPreviousActive, setIsPreviousActive] = useState(false)
33 | const [isNextActive, setisNextActive] = useState(false)
34 |
35 | useEffect(() => {
36 | if (search) {
37 | const textRanges = getEditorTextRanges(editor, search)
38 |
39 | if (textRanges) {
40 | setRanges(textRanges)
41 | } else {
42 | setRanges([])
43 | }
44 | }
45 | }, [search, editor])
46 |
47 | useEffect(() => {
48 | let { selection } = editor
49 | if (selection == null) selection = lastBlurSelection
50 | if (search) {
51 | if (getPreviousClosestRange(Ranges, selection) !== null) {
52 | setIsPreviousActive(true)
53 | } else {
54 | setIsPreviousActive(false)
55 | }
56 | if (getNextClosestRange(Ranges, selection) !== null) {
57 | setisNextActive(true)
58 | } else {
59 | setisNextActive(false)
60 | }
61 | }
62 | }, [search, Ranges, editor.selection])
63 |
64 | const goToPrevious = () => {
65 | if (!Ranges) return
66 |
67 | if (!isPreviousActive) {
68 | // Transforms.select(editor, lastBlurSelection || defaultSelection)
69 | return
70 | }
71 |
72 | const { selection } = editor
73 | if (selection == null) {
74 | Transforms.select(editor, Ranges[0])
75 | } else {
76 | Transforms.deselect(editor)
77 |
78 | const indexofRange = indexOf(Ranges, selection)
79 |
80 | if (indexofRange && indexofRange != -1) {
81 | Transforms.select(editor, Ranges[indexofRange - 1])
82 | } else {
83 | Transforms.select(
84 | editor,
85 | getPreviousClosestRange(Ranges, selection) || defaultSelection
86 | )
87 | }
88 | }
89 |
90 | ReactEditor.focus(editor)
91 | }
92 |
93 | const goToNext = () => {
94 | if (!Ranges) return
95 | if (!isNextActive) {
96 | Transforms.select(editor, lastBlurSelection || defaultSelection)
97 | return
98 | }
99 |
100 | const { selection } = editor
101 | if (selection == null) {
102 | Transforms.select(editor, Ranges[0])
103 | } else {
104 | Transforms.deselect(editor)
105 |
106 | const l: Range = Ranges[0]
107 |
108 | const indexofRange = indexOf(Ranges, selection || l)
109 |
110 | if (indexofRange && indexofRange != -1) {
111 | Transforms.select(editor, Ranges[indexofRange + 1] || l)
112 | } else {
113 | Transforms.select(editor, getNextClosestRange(Ranges, selection) || l)
114 | }
115 | }
116 |
117 | ReactEditor.focus(editor)
118 | }
119 |
120 | useEffect(() => {
121 | let { selection } = editor
122 | if (selection == null) selection = lastBlurSelection
123 | if (search) {
124 | if (getPreviousClosestRange(Ranges, selection) !== null) {
125 | setIsPreviousActive(true)
126 | } else {
127 | setIsPreviousActive(false)
128 | }
129 | if (getNextClosestRange(Ranges, selection) !== null) {
130 | setisNextActive(true)
131 | } else {
132 | setisNextActive(false)
133 | }
134 | }
135 | }, [search, Ranges, editor.selection])
136 |
137 | const downloadAsDoc =async ()=>{
138 | const doc=await convertToDoc(editor)
139 | saveAs(doc,"YO THIS IS YOUR FILE.docx")
140 | }
141 |
142 | return (
143 |
150 |
155 | {toolbarMarks.map((mark: IToolbarMark) => {
156 | return
157 | })}
158 | {blocks.map((block: ToolbarBlockProps) => {
159 | if (block.isHiddenInToolbar) return
160 | return (
161 |
162 | )
163 | })}
164 |
165 |
166 |
167 | setSearch(e.target.value)}
171 | className="d-inline-block border-0"
172 | />
173 |
181 |
190 |
199 |
200 |
201 |
202 |
205 |
206 |
207 |
208 | )
209 | }
210 |
211 | export default Toolbar
212 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/convert-to-doc.ts:
--------------------------------------------------------------------------------
1 | import { ReactEditor } from "slate-react"
2 | import { Document, Packer, Paragraph, TextRun } from "docx";
3 |
4 |
5 | const toText=(doc:any,text:any)=>{
6 | return new TextRun({
7 | text: text.text|| text,
8 | ...text
9 | })
10 | }
11 |
12 | const toParagraph=(doc:any,para:any)=>{
13 | return new Paragraph({
14 | children: para.children.map((paragraph:any)=>{
15 | return toText(doc,paragraph)
16 | })
17 | })
18 | }
19 |
20 | const toPage=(doc:any,page:any)=>{
21 | doc.addSection({
22 | properties: {},
23 | children: page.children.map((paragraph:any)=>{
24 | return toParagraph(doc,paragraph)
25 | })
26 | });
27 | }
28 |
29 | // Used to export the file into a .docx file
30 |
31 | const convertToDoc=async (editor:ReactEditor)=>{
32 |
33 | const children=editor.children;
34 | const doc = new Document();
35 |
36 | children.forEach(child=>{
37 | toPage(doc,child)
38 | })
39 |
40 | const docBuffer = await Packer.toBlob(doc)
41 | // saveAs(blob, "example.docx");
42 | return docBuffer
43 |
44 | }
45 | export default convertToDoc
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/custom-delete.tsx:
--------------------------------------------------------------------------------
1 | import { Editor, Transforms, Node } from 'slate'
2 | import { ReactEditor } from 'slate-react'
3 | import { LIST_TYPES } from '../constants/block-list'
4 | import toggleBlock from './toggle-block'
5 |
6 | const WithCustomDelete = (editor: ReactEditor) => {
7 | const { deleteBackward } = editor
8 |
9 | editor.deleteBackward = () => {
10 | ReactEditor.focus(editor)
11 | const { selection } = editor
12 |
13 | const [match] = Editor.nodes(editor, {
14 | match: n => LIST_TYPES.includes(n.type as string)
15 | })
16 |
17 | if (!!match && selection?.anchor.offset == 0) {
18 | toggleBlock(editor, 'paragraph')
19 | } else {
20 | deleteBackward('character')
21 | }
22 | }
23 |
24 | return editor
25 | }
26 | export default WithCustomDelete
27 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/custom-insert-break.tsx:
--------------------------------------------------------------------------------
1 | import { Editor, Transforms, Node } from 'slate'
2 | import { ReactEditor } from 'slate-react'
3 | import { HEADING_TYPES } from '../constants/block-list'
4 |
5 | const WithCustomInsertBreak = (editor: ReactEditor) => {
6 | const { insertBreak } = editor
7 |
8 | // editor.insertBreak = () => {
9 | // const [match] = Editor.nodes(editor, {
10 | // match: n => HEADING_TYPES.includes(n.type as string)
11 | // })
12 | // if (!!match) {
13 | // Transforms.setNodes(
14 | // editor,
15 | // { type: 'paragraph' },
16 | // { match: (n: Node) => HEADING_TYPES.includes(n.type as string) }
17 | // )
18 | // }
19 | // insertBreak()
20 | // console.log(match)
21 | // }
22 |
23 | return editor
24 | }
25 | export default WithCustomInsertBreak
26 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/default-selection.ts:
--------------------------------------------------------------------------------
1 | import { Range } from 'slate'
2 |
3 | const defaultSelection: Range = {
4 | anchor: {
5 | path: [0, 0],
6 | offset: 0
7 | },
8 | focus: {
9 | path: [0, 0],
10 | offset: 0
11 | }
12 | }
13 |
14 | export default defaultSelection
15 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/get-closest-range.ts:
--------------------------------------------------------------------------------
1 | import { Range, Point } from 'slate'
2 |
3 | export const getPreviousClosestRange = (
4 | ranges: Range[],
5 | current: Range | null
6 | ) => {
7 | if (current == null) return null
8 | let closestRange = null
9 | for (const range in ranges) {
10 | const rangeEnd = Range.end(ranges[range])
11 |
12 | const currentRangeStart = Range.start(current)
13 |
14 | if (Point.compare(rangeEnd, currentRangeStart) == -1) {
15 | closestRange = ranges[range]
16 | } else {
17 | return closestRange
18 | }
19 | }
20 | return closestRange
21 | }
22 |
23 | export const getNextClosestRange = (ranges: Range[], current: Range | null) => {
24 | if (current == null) return null
25 |
26 | for (const range in ranges) {
27 | const rangeStart = Range.start(ranges[range])
28 | const currentRangeEnd = Range.end(current)
29 |
30 | if (Point.compare(rangeStart, currentRangeEnd) == 1) {
31 | return ranges[range]
32 | }
33 | }
34 | return null
35 | }
36 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/get-text-ranges.ts:
--------------------------------------------------------------------------------
1 | import { Editor, Text, Range, Path } from 'slate'
2 | import { ReactEditor } from 'slate-react'
3 |
4 | export const getEditorTextRanges = (editor: ReactEditor, search: string) => {
5 | const ranges = []
6 | for (const [node, path] of Editor.nodes(editor, {
7 | at: [],
8 | match: Text.isText
9 | })) {
10 | if (search && Text.isText(node)) {
11 | ranges.push(...getTextRanges(node, path, search))
12 | }
13 | }
14 | return ranges
15 | }
16 |
17 | export const getTextRanges = (node: Text, path: Path, search: string) => {
18 | const ranges: Range[] = []
19 | const { text } = node
20 |
21 | const parts: string[] = text.split(search)
22 |
23 | let offset = 0
24 | parts.forEach((part, index) => {
25 | if (index !== 0) {
26 | ranges.push({
27 | anchor: { path, offset: offset - search.length },
28 | focus: { path, offset }
29 | })
30 | }
31 |
32 | offset = offset + part.length + search.length
33 | })
34 |
35 | return ranges
36 | }
37 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/index-of-range.ts:
--------------------------------------------------------------------------------
1 | import { Range } from 'slate'
2 |
3 | function indexOf(ranges: Range[], compareToRange: Range): number {
4 | for (const range in ranges) {
5 | if (Range.equals(ranges[range], compareToRange)) {
6 | return Number(range)
7 | }
8 | }
9 |
10 | return -1
11 | }
12 |
13 | export default indexOf
14 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/on-key-down.ts:
--------------------------------------------------------------------------------
1 | import { ReactEditor } from 'slate-react'
2 | import { Editor, Transforms, Text, Range, Point, Path } from 'slate'
3 |
4 | import { isKeyHotkey } from 'is-hotkey'
5 |
6 | import toggleMark from './toggle-mark'
7 |
8 | const HOTKEYS: { [key: string]: string | string[] } = {
9 | bold: 'mod+b',
10 | compose: ['down', 'left', 'right', 'up', 'backspace', 'enter'],
11 | moveBackward: 'left',
12 | moveForward: 'right',
13 | moveWordBackward: 'ctrl+left',
14 | moveWordForward: 'ctrl+right',
15 | deleteBackward: 'shift?+backspace',
16 | deleteForward: 'shift?+delete',
17 | extendBackward: 'shift+left',
18 | extendForward: 'shift+right',
19 | italic: 'mod+i',
20 | splitBlock: 'shift?+enter',
21 | undo: 'mod+z',
22 | selectAll: 'mod+a'
23 | }
24 |
25 | const create = (key: string) => {
26 | const generic = HOTKEYS[key]
27 | const isGeneric = generic && isKeyHotkey(generic)
28 |
29 | return (event: KeyboardEvent) => {
30 | if (isGeneric && isGeneric(event)) return true
31 | return false
32 | }
33 | }
34 |
35 | const isHotKey = {
36 | isBold: create('bold'),
37 | isSplitBlock: create('splitBlock'),
38 | isSelectAll: create('selectAll')
39 | }
40 |
41 | //still very much to do here.
42 | const onKeyDown = (editor: ReactEditor, event: KeyboardEvent) => {
43 | //TODO use isHotKey here
44 | if (isHotKey.isSelectAll(event)) {
45 | event.preventDefault()
46 | const [match] = Editor.nodes(editor, {
47 | match: n => Text.isText(n)
48 | })
49 |
50 | if (!!match) {
51 | const anchor = Editor.start(editor, match[1])
52 | const focus = Editor.end(editor, match[1])
53 | const currentSelectedRange = { anchor, focus }
54 |
55 | if (editor.selection == null) {
56 | Transforms.select(editor, currentSelectedRange)
57 | return
58 | }
59 |
60 | if (Range.equals(editor.selection, currentSelectedRange)) {
61 | const EditorStartAnchor = Editor.start(editor, [])
62 | const EditorEndAnchor = Editor.end(editor, [])
63 | const EditorRange = {
64 | anchor: EditorStartAnchor,
65 | focus: EditorEndAnchor
66 | }
67 |
68 | Transforms.select(editor, EditorRange)
69 |
70 | return
71 | } else {
72 | Transforms.select(editor, currentSelectedRange)
73 | return
74 | }
75 | }
76 | }
77 | switch (event.key) {
78 | // When "`" is pressed, keep our existing code block logic.
79 | case '`': {
80 | event.preventDefault()
81 | const [match] = Editor.nodes(editor, {
82 | match: n => n.type === 'h1'
83 | })
84 | Transforms.setNodes(
85 | editor,
86 | { type: match ? 'paragraph' : 'h1' },
87 | { match: n => Editor.isBlock(editor, n) }
88 | )
89 | break
90 | }
91 |
92 | // When "B" is pressed, bold the text in the selection.
93 | case 'b': {
94 | event.preventDefault()
95 | return toggleMark(editor, 'bold')
96 | break
97 | }
98 | }
99 | }
100 |
101 | export default onKeyDown
102 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/render-element.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { RenderElementProps } from 'slate-react'
3 | import { Paragraph } from '../blocks'
4 | import blocks, { ToolbarBlockProps } from '../constants/block-list'
5 |
6 | const renderElement = ({ element, ...props }: RenderElementProps) => {
7 | const block = blocks.find(
8 | (block: ToolbarBlockProps) => block.type == element.type
9 | )
10 | if (block) {
11 | return block.renderBlock({ element, ...props })
12 | } else {
13 | return
14 | }
15 | }
16 |
17 | export default renderElement
18 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/render-leaf.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react'
2 |
3 | import { RenderLeafProps } from 'slate-react'
4 |
5 | import toolbarMarks, { IToolbarMark } from '../constants/mark-list'
6 |
7 | /**
8 | * Leaf is the lowest form of text in the editor.
9 | * access text from leaf.text and type from other than that all other keys in leaf are either for styling or information passing keys even leaf.type
10 | *
11 | * this is common rendering component for a leaf. To define a custom leaf check mark-list.tsx
12 | */
13 |
14 | const renderLeaf = ({ attributes, leaf, ...props }: RenderLeafProps) => {
15 | let styles: CSSProperties = {}
16 | let children = props.children
17 | const leafKeys = Object.keys(leaf)
18 |
19 | toolbarMarks.forEach((toolbarMark: IToolbarMark) => {
20 | if (leafKeys.includes(toolbarMark.type)) {
21 | if (toolbarMark.renderChildren) {
22 | children = toolbarMark.renderChildren(children, leaf)
23 | }
24 | styles = { ...styles, ...toolbarMark.styles }
25 | }
26 | })
27 |
28 | return (
29 |
30 | {children}
31 |
32 | )
33 | }
34 |
35 | export default renderLeaf
36 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/toggle-block.ts:
--------------------------------------------------------------------------------
1 | import { Editor, Transforms } from 'slate'
2 | import { ReactEditor } from 'slate-react'
3 | import { LIST_TYPES } from '../constants/block-list'
4 |
5 | export const isBlockActive = (editor: ReactEditor, type: string) => {
6 | const [match] = Editor.nodes(editor, {
7 | match: (n: any) => n.type === type
8 | })
9 |
10 | return !!match
11 | }
12 |
13 | const toggleBlock = (editor: ReactEditor, type: string) => {
14 | const isActive = isBlockActive(editor, type)
15 | const isList = LIST_TYPES.includes(type)
16 |
17 | Transforms.unwrapNodes(editor, {
18 | match: n => {
19 | return LIST_TYPES.includes(n.type as string)
20 | },
21 | split: true
22 | })
23 |
24 | Transforms.setNodes(editor, {
25 | type: isActive ? 'paragraph' : isList ? 'list-item' : type
26 | })
27 |
28 | if (!isActive && isList) {
29 | const block = { type: type, children: [] }
30 | Transforms.wrapNodes(editor, block)
31 | }
32 | }
33 | export default toggleBlock
34 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/toggle-mark.ts:
--------------------------------------------------------------------------------
1 | import { Editor } from 'slate'
2 | import { ReactEditor } from 'slate-react'
3 |
4 | export const isMarkActive = (editor: ReactEditor, format: string) => {
5 | const marks = Editor.marks(editor)
6 | return marks ? marks[format] === true : false
7 | }
8 |
9 | const toggleMark = (editor: ReactEditor, format: string) => {
10 | const isActive = isMarkActive(editor, format)
11 |
12 | if (isActive) {
13 | Editor.removeMark(editor, format)
14 | } else {
15 | Editor.addMark(editor, format, true)
16 | }
17 | return ReactEditor.focus(editor)
18 | }
19 |
20 | export default toggleMark
21 |
--------------------------------------------------------------------------------
/src/notes/components/editor/utils/with-custom-normalize.ts:
--------------------------------------------------------------------------------
1 | import { Transforms,Element,Node, Editor ,Text} from 'slate'
2 | import { ReactEditor } from 'slate-react'
3 | import toggleBlock from './toggle-block'
4 |
5 |
6 | const emptyPage ={type:'page',children:[{type:'paragraph',children:[{type:'text',text:''}]}]}
7 |
8 | function withCustomNormalize(editor: ReactEditor) {
9 | // can include custom normalisations---
10 | const {normalizeNode}=editor
11 | editor.normalizeNode = entry => {
12 |
13 | const [node, path] = entry
14 |
15 | if(Text.isText(node)) return normalizeNode(entry)
16 |
17 |
18 |
19 | // if the node is Page
20 | if (Element.isElement(node) && node.type === 'page') {
21 | let PageNode;
22 | //afaik pageNode if inserted as new page is not available here as a dom node because it hasnt rendered yet
23 | try{
24 | PageNode= ReactEditor.toDOMNode(editor,node)
25 | }catch(e){
26 | return
27 | // return normalizeNode(entry)
28 | }
29 |
30 | const style = window.getComputedStyle(PageNode)
31 | const computedHeight = PageNode.offsetHeight
32 | const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)
33 |
34 | let pageHeight=computedHeight - padding
35 |
36 | let CurrentpageHeight=0
37 |
38 | const children=Array.from( PageNode.children)
39 |
40 | children.forEach(child=>{
41 |
42 | const childStyles= window.getComputedStyle(child)
43 | const computedChildHeight = child.clientHeight
44 | const childMargin = parseFloat(childStyles.marginTop) + parseFloat(childStyles.marginBottom)
45 | const childPadding = parseFloat(childStyles.paddingBottom) + parseFloat(childStyles.paddingTop)
46 | const childBorder = parseFloat(childStyles.borderLeftWidth) + parseFloat(childStyles.borderRightWidth)+ parseFloat(childStyles.borderTopWidth) + parseFloat(childStyles.borderBottomWidth)
47 |
48 | const childHeight=computedChildHeight+childMargin+childPadding+childBorder
49 |
50 | CurrentpageHeight=CurrentpageHeight+childHeight
51 |
52 | if(CurrentpageHeight>pageHeight){
53 | Transforms.liftNodes(editor)
54 | Transforms.splitNodes(editor)
55 | Transforms.wrapNodes(editor,emptyPage)
56 | }
57 | })
58 |
59 | }
60 |
61 |
62 | // Fall back to the original `normalizeNode` to enforce other constraints.
63 | return normalizeNode(entry)
64 | }
65 | return editor
66 | }
67 |
68 | export default withCustomNormalize
69 |
--------------------------------------------------------------------------------
/src/notes/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import dynamic from 'next/dynamic'
4 |
5 | const TextEditor = dynamic(
6 | () => import('./components/editor/text-editor') as any,
7 | {
8 | ssr: false
9 | }
10 | )
11 |
12 | const Notes = () => {
13 | return (
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | export default Notes
21 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app'
2 | import React from 'react'
3 |
4 | import { ThemeProvider } from 'theme-ui'
5 |
6 | import { getTheme } from '../shared/theme-utils'
7 |
8 | import '../assets/theme.scss'
9 |
10 | function MyApp({ Component, pageProps }: AppProps) {
11 | return (
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default MyApp
19 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Notes from '../notes'
2 |
3 | export default function Home() {
4 | return (
5 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/shared/theme-utils.ts:
--------------------------------------------------------------------------------
1 | import { Styled } from 'theme-ui'
2 | const colors = {
3 | text: '#000',
4 | background: '#f0efef',
5 | primary: '#000',
6 | secondary: '#3f3f3f',
7 | muted: '#e0e0e0',
8 | highlight: '#9f9f9f',
9 | gray: '#6c6c6c',
10 | accent: '#3f3f3f',
11 | modes: {
12 | dark: {
13 | text: '#fff',
14 | background: '#060606',
15 | primary: '#d2d2d2',
16 | secondary: '#b2b2b2',
17 | muted: '#191919',
18 | highlight: '#3c3c3c',
19 | gray: '#999',
20 | accent: '#e0e0e0'
21 | }
22 | }
23 | }
24 |
25 | const fonts = {
26 | body: 'Silom, monospace',
27 | heading: 'Silom, monospace',
28 | monospace: 'Silom, monospace'
29 | }
30 |
31 | const fontSizes = [12, 14, 16, 20, 24, 32, 48, 64, 72]
32 |
33 | const fontWeights = {
34 | body: 200,
35 | heading: 700,
36 | display: 100
37 | }
38 |
39 | const lineHeights = {
40 | body: 1.5,
41 | heading: 1.25
42 | }
43 |
44 | const textStyles = {
45 | heading: {
46 | fontFamily: 'heading',
47 | fontWeight: 'heading',
48 | lineHeight: 'heading'
49 | },
50 | display: {
51 | variant: 'textStyles.heading',
52 | fontSize: [5, 6],
53 | fontWeight: 'display',
54 | letterSpacing: '-0.03em',
55 | mt: 3
56 | }
57 | }
58 |
59 | const theme = {
60 | initialColorMode: 'light',
61 | colors,
62 | fonts,
63 | fontSizes,
64 | fontWeights,
65 | lineHeights,
66 | textStyles,
67 | styles: {
68 | Container: {
69 | p: 3,
70 | maxWidth: 1024
71 | },
72 | root: {
73 | fontFamily: 'body',
74 | lineHeight: 'body',
75 | fontWeight: 'body',
76 | overflowX:'hidden'
77 | },
78 | h1: {
79 | variant: 'textStyles.display'
80 | },
81 | h2: {
82 | variant: 'textStyles.heading',
83 | fontSize: 5
84 | },
85 | h3: {
86 | variant: 'textStyles.heading',
87 | fontSize: 4
88 | },
89 | h4: {
90 | variant: 'textStyles.heading',
91 | fontSize: 3
92 | },
93 | h5: {
94 | variant: 'textStyles.heading',
95 | fontSize: 2
96 | },
97 | h6: {
98 | variant: 'textStyles.heading',
99 | fontSize: 1
100 | },
101 | p: {
102 | fontSize: 2
103 | },
104 | a: {
105 | color: 'primary',
106 | '&:hover': {
107 | color: 'secondary'
108 | }
109 | },
110 | pre: {
111 | fontFamily: 'monospace',
112 | fontSize: 1,
113 | p: 3,
114 | color: 'text',
115 | bg: 'muted',
116 | borderColor: 'text',
117 | borderStyle: 'solid',
118 | borderTopWidth: 0,
119 | borderLeftWidth: 0,
120 | borderRightWidth: 8,
121 | borderBottomWidth: 8,
122 | overflow: 'auto',
123 | code: {
124 | color: 'inherit'
125 | }
126 | },
127 | code: {
128 | fontFamily: 'monospace',
129 | fontSize: 1
130 | },
131 | inlineCode: {
132 | fontFamily: 'monospace',
133 | color: 'secondary',
134 | bg: 'muted',
135 | px: 2
136 | },
137 | ul: {
138 | listStyleType: 'square'
139 | },
140 | table: {
141 | width: '100%',
142 | my: 4,
143 | borderCollapse: 'separate',
144 | borderSpacing: 0,
145 | 'th,td': {
146 | textAlign: 'left',
147 | py: '4px',
148 | pr: '4px',
149 | pl: 0,
150 | borderColor: 'text',
151 | borderBottomStyle: 'solid'
152 | }
153 | },
154 | th: {
155 | backgroundColor: 'muted',
156 | verticalAlign: 'bottom',
157 | borderBottomWidth: 8
158 | },
159 | td: {
160 | verticalAlign: 'top',
161 | borderBottomWidth: 4
162 | },
163 | hr: {
164 | border: 0,
165 | borderBottom: '8px solid',
166 | borderColor: 'text'
167 | },
168 | strong: {
169 | fontWeight: '900'
170 | }
171 | },
172 | "buttons":{
173 | "primary":{
174 | "bg":"primary",
175 | "margin-right":'10px'
176 | }
177 | }
178 | }
179 |
180 | export const getTheme = () => {
181 | return theme
182 | }
183 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 | /* Basic Options */
5 | // "incremental": true, /* Enable incremental compilation */
6 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | // "outDir": "./", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */,
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 | /* Strict Type-Checking Options */
25 | "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
32 | /* Additional Checks */
33 | // "noUnusedLocals": true, /* Report errors on unused locals. */
34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
37 | /* Module Resolution Options */
38 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
39 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
40 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
41 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
42 | // "typeRoots": [], /* List of folders to include type definitions from. */
43 | // "types": [], /* Type declaration files to be included in compilation. */
44 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
45 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
46 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
47 | /* Source Map Options */
48 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
49 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
50 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
51 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
52 | /* Experimental Options */
53 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
55 | /* Advanced Options */
56 | "skipLibCheck": true /* Skip type checking of declaration files. */,
57 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
58 | "lib": ["dom", "dom.iterable", "esnext"],
59 | "allowJs": true,
60 | "noEmit": true,
61 | "moduleResolution": "node",
62 | "resolveJsonModule": true,
63 | "isolatedModules": true,
64 | "jsx": "preserve"
65 | },
66 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js"],
67 | "exclude": ["node_modules"]
68 | }
69 |
--------------------------------------------------------------------------------