├── .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 | 8 | 15 | 16 | ) 17 | 18 | export const Logo = () => ( 19 | 24 | 25 | 31 | 37 | 38 | 39 | ) 40 | 41 | export const logoAuth = () => ( 42 | 47 | 48 | 54 | 60 | 61 | 62 | ) 63 | 64 | export const Preferences = () => ( 65 | 71 | 78 | 85 | 86 | ) 87 | 88 | export const Patients = () => ( 89 | 95 | 99 | 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 |
      {props.children}
    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 |
    6 |
    7 | 8 |
    9 |
    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 | --------------------------------------------------------------------------------