├── .gitignore
├── LICENSE
├── README.md
├── branding
└── cover.png
├── editor
├── .gitignore
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── hello.ts
│ └── index.tsx
├── public
│ ├── favicon.ico
│ └── vercel.svg
├── styles
│ ├── Home.module.css
│ └── globals.css
└── tsconfig.json
├── package.json
├── packages
├── boring-core-action
│ ├── actions.ts
│ ├── index.ts
│ └── package.json
├── boring-core-config
│ ├── index.ts
│ └── package.json
├── boring-core-state
│ ├── index.ts
│ └── package.json
├── boring-core-store
│ ├── index.ts
│ ├── package.json
│ └── store.ts
├── boring-document-model
│ ├── __test__
│ │ ├── page-data-flat.mock.json
│ │ ├── page-data-nested.mock.json
│ │ └── page-data.test.ts
│ ├── content.model.ts
│ ├── document.model.ts
│ ├── index.ts
│ ├── package.json
│ └── title.model.ts
├── boring-loader
│ ├── index.ts
│ ├── initial-loader.ts
│ └── package.json
├── boring-state-app-react
│ ├── index.ts
│ └── package.json
├── boring-template-provider
│ ├── README.md
│ ├── index.ts
│ └── package.json
├── boring-ui-core
│ └── README.md
└── core-react
│ ├── blocks
│ ├── iframe-block.ts
│ ├── index.ts
│ ├── media-block.tsx
│ ├── toc.tsx
│ ├── trailing-node.ts
│ ├── video-block.ts
│ └── wrapper-block.tsx
│ ├── content-editor
│ ├── index.ts
│ └── main-content-editor.tsx
│ ├── drag-handle
│ ├── drag-handle.ts
│ └── index.ts
│ ├── embeding-utils
│ ├── index.ts
│ ├── vimeo.ts
│ └── youtube.ts
│ ├── extension-configs
│ ├── block-quote-config.ts
│ ├── codeblock-config.ts
│ ├── floating-menu-conig.ts
│ ├── index.ts
│ ├── placeholder-config.ts
│ ├── slash-command-config.ts
│ └── underline-config.ts
│ ├── floating-menu
│ ├── index.ts
│ └── side-floating-menu.tsx
│ ├── index.ts
│ ├── inline-toolbar
│ ├── README.md
│ ├── index.ts
│ └── inline-toolbar.tsx
│ ├── key-maps
│ └── index.ts
│ ├── menu
│ ├── add-block-menu
│ │ ├── block-item.tsx
│ │ ├── block-list.tsx
│ │ ├── icons.tsx
│ │ ├── index.tsx
│ │ └── items.ts
│ ├── add-image-block-menu
│ │ ├── README.md
│ │ ├── add-image-block-menu.tsx
│ │ └── index.ts
│ ├── index.ts
│ └── link-input-menu.tsx
│ ├── node-view-wrapper
│ ├── index.ts
│ └── node-view-wrapper.tsx
│ ├── package.json
│ ├── scaffold
│ ├── index.tsx
│ └── scaffold-extensions.ts
│ ├── slash-commands
│ ├── README.md
│ ├── commands.ts
│ ├── index.ts
│ └── render-items.tsx
│ ├── table-of-contents
│ ├── README.md
│ └── index.ts
│ ├── theme
│ ├── font-family.ts
│ └── index.ts
│ ├── title
│ ├── index.ts
│ └── title.tsx
│ └── tsconfig.json
├── tslint.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | package-lock.json
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 |
76 | # parcel-bundler cache (https://parceljs.org/)
77 | .cache
78 | .parcel-cache
79 |
80 | # Next.js build output
81 | .next
82 | out
83 |
84 | # Nuxt.js build / generate output
85 | .nuxt
86 | dist
87 |
88 | # Gatsby files
89 | .cache/
90 | # Comment in the public line in if your project uses Gatsby and not Next.js
91 | # https://nextjs.org/blog/next-9-1#public-directory-support
92 | # public
93 |
94 | # vuepress build output
95 | .vuepress/dist
96 |
97 | # Serverless directories
98 | .serverless/
99 |
100 | # FuseBox cache
101 | .fusebox/
102 |
103 | # DynamoDB Local files
104 | .dynamodb/
105 |
106 | # TernJS port file
107 | .tern-port
108 |
109 | # Stores VSCode versions used for testing VSCode extensions
110 | .vscode-test
111 |
112 | # yarn v2
113 | .yarn/cache
114 | .yarn/unplugged
115 | .yarn/build-state.yml
116 | .yarn/install-state.gz
117 | .pnp.*
118 | .DS_Store
119 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 bridged.xyz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Boring Engine - the boring text editor engine
4 |
5 |
6 | > write your document like you code. write in dedicated environment. manage your text contents.
7 |
8 |
9 |
10 | ## Features
11 | - Table
12 | - md style editing
13 | - text node & api (highlighting)
14 | - variables
15 | - placeholders
16 | - [Bridged's G11n](https://github.com/bridgedxyz/G11n) Support
17 | - text node theme api
18 | - live collaborate compatitable api interface (collaboration is not built-in but provides the availability to just plug-in the live feature)
19 |
20 |
21 | ## Other engine projects
22 |
23 | - [nothing engine](https://github.com/bridgedxyz/nothing) - nothing but drawing engine.
24 |
25 | ## Other Projects
26 | other boring related projects are being managed under [github/boringso](https://github.com/boringso)
27 |
28 | Visit [boring.so](https://boring.so) for more details
29 |
30 |
31 | ## Who uses boring?
32 | - [Bridged Docs](https://github.com/bridgedxyz/docs) - Bridged docs site is built with boring
33 | - [code.surf Docs](https://github.com/surfcodes/website) - github.surf, surf.codes docs site is built with boring
34 |
--------------------------------------------------------------------------------
/branding/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gridaco/boring/e2bf72cc01ddf1a2b8d311fbf3ba2a9e30236892/branding/cover.png
--------------------------------------------------------------------------------
/editor/.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 |
--------------------------------------------------------------------------------
/editor/README.md:
--------------------------------------------------------------------------------
1 | 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).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/editor/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/editor/next.config.js:
--------------------------------------------------------------------------------
1 | const withTM = require("next-transpile-modules")([
2 | //
3 | "@boringso/react-core",
4 | ]);
5 |
6 | module.exports = withTM({
7 | webpack: (config) => {
8 | return config;
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/editor/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "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 | "@boringso/react-core": "0.0.0",
12 | "@boring-ui/embed": "3.3.4",
13 | "@emotion/react": "^11.4.0",
14 | "@emotion/styled": "^11.3.0",
15 | "next": "11.0.0",
16 | "react": "17.0.2",
17 | "react-dom": "17.0.2"
18 | },
19 | "devDependencies": {
20 | "@types/react": "17.0.11",
21 | "next-transpile-modules": "^8.0.0",
22 | "typescript": "4.3.3"
23 | }
24 | }
--------------------------------------------------------------------------------
/editor/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import type { AppProps } from "next/app";
3 | import Head from "next/head";
4 | import { Global, css } from "@emotion/react";
5 |
6 | /**
7 | * Css normalize - reset all default values.
8 | */
9 | function CssNormalized() {
10 | return (
11 |
23 | );
24 | }
25 |
26 | function HeadInjection() {
27 | return (
28 |
29 |
30 |
31 |
36 |
37 | );
38 | }
39 |
40 | function SeoMeta() {
41 | return (
42 | <>
43 |
44 |
45 | >
46 | );
47 | }
48 |
49 | function MyApp({ Component, pageProps }: AppProps) {
50 | return (
51 | <>
52 |
53 |
54 | >
55 | );
56 | }
57 | export default MyApp;
58 |
--------------------------------------------------------------------------------
/editor/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 |
4 | type Data = {
5 | name: string
6 | }
7 |
8 | export default (req: NextApiRequest, res: NextApiResponse) => {
9 | res.status(200).json({ name: 'John Doe' })
10 | }
11 |
--------------------------------------------------------------------------------
/editor/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { Scaffold } from "@boringso/react-core";
3 |
4 | export default function Home() {
5 | return (
6 | <>
7 |
8 | Boring
9 |
10 |
11 |
12 |
13 |
14 |
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/editor/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gridaco/boring/e2bf72cc01ddf1a2b8d311fbf3ba2a9e30236892/editor/public/favicon.ico
--------------------------------------------------------------------------------
/editor/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/editor/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | height: 100vh;
9 | }
10 |
11 | .main {
12 | padding: 5rem 0;
13 | flex: 1;
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | .footer {
21 | width: 100%;
22 | height: 100px;
23 | border-top: 1px solid #eaeaea;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | }
28 |
29 | .footer a {
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | flex-grow: 1;
34 | }
35 |
36 | .title a {
37 | color: #0070f3;
38 | text-decoration: none;
39 | }
40 |
41 | .title a:hover,
42 | .title a:focus,
43 | .title a:active {
44 | text-decoration: underline;
45 | }
46 |
47 | .title {
48 | margin: 0;
49 | line-height: 1.15;
50 | font-size: 4rem;
51 | }
52 |
53 | .title,
54 | .description {
55 | text-align: center;
56 | }
57 |
58 | .description {
59 | line-height: 1.5;
60 | font-size: 1.5rem;
61 | }
62 |
63 | .code {
64 | background: #fafafa;
65 | border-radius: 5px;
66 | padding: 0.75rem;
67 | font-size: 1.1rem;
68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
69 | Bitstream Vera Sans Mono, Courier New, monospace;
70 | }
71 |
72 | .grid {
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | flex-wrap: wrap;
77 | max-width: 800px;
78 | margin-top: 3rem;
79 | }
80 |
81 | .card {
82 | margin: 1rem;
83 | padding: 1.5rem;
84 | text-align: left;
85 | color: inherit;
86 | text-decoration: none;
87 | border: 1px solid #eaeaea;
88 | border-radius: 10px;
89 | transition: color 0.15s ease, border-color 0.15s ease;
90 | width: 45%;
91 | }
92 |
93 | .card:hover,
94 | .card:focus,
95 | .card:active {
96 | color: #0070f3;
97 | border-color: #0070f3;
98 | }
99 |
100 | .card h2 {
101 | margin: 0 0 1rem 0;
102 | font-size: 1.5rem;
103 | }
104 |
105 | .card p {
106 | margin: 0;
107 | font-size: 1.25rem;
108 | line-height: 1.5;
109 | }
110 |
111 | .logo {
112 | height: 1em;
113 | margin-left: 0.5rem;
114 | }
115 |
116 | @media (max-width: 600px) {
117 | .grid {
118 | width: 100%;
119 | flex-direction: column;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/editor/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/editor/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noImplicitAny": false,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve"
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "boring-workspace",
3 | "private": true,
4 | "scripts": {
5 | "editor": "yarn workspace editor dev"
6 | },
7 | "workspaces": [
8 | "packages/*",
9 | "editor"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/boring-core-action/actions.ts:
--------------------------------------------------------------------------------
1 | type TextContent = string;
2 | type RawContent = string;
3 | type CodeContent = RawContent;
4 |
5 | type BlockCreationActionType =
6 | | typeof add_any_block
7 | //
8 | | typeof add_code_block
9 | | HeadingBlockCreationActionType
10 | | typeof add_paragraph_block;
11 |
12 | export interface IAddBlockAction {
13 | type: BlockCreationActionType;
14 | content: T;
15 | }
16 |
17 | const add_any_block = "add-any-block";
18 | export interface AddAnyBlockAction extends IAddBlockAction {
19 | type: typeof add_any_block;
20 | }
21 |
22 | const add_code_block = "add-code-block";
23 | export interface AddCodeBlock extends IAddBlockAction {
24 | type: typeof add_code_block;
25 | content: CodeContent;
26 | }
27 |
28 | export type HeadingBlockCreationActionType =
29 | | typeof add_heading_block
30 | | typeof add_heading1_block
31 | | typeof add_heading2_block
32 | | typeof add_heading3_block
33 | | typeof add_heading4_block
34 | | typeof add_heading5_block
35 | | typeof add_heading6_block;
36 |
37 | const add_heading_block = "add-heading-block";
38 | const add_heading1_block = "add-heading1-block";
39 | const add_heading2_block = "add-heading2-block";
40 | const add_heading3_block = "add-heading3-block";
41 | const add_heading4_block = "add-heading4-block";
42 | const add_heading5_block = "add-heading5-block";
43 | const add_heading6_block = "add-heading6-block";
44 |
45 | export interface IAddHeadingBlockAction extends IAddBlockAction {
46 | type: HeadingBlockCreationActionType;
47 | }
48 | export interface AddHeadingBlock extends IAddHeadingBlockAction {
49 | type: typeof add_heading_block;
50 | level: 1 | 2 | 3 | 4 | 5 | 6;
51 | }
52 | export interface AddHeading1Block extends IAddHeadingBlockAction {
53 | type: typeof add_heading1_block;
54 | }
55 | export interface AddHeading2Block extends IAddHeadingBlockAction {
56 | type: typeof add_heading2_block;
57 | }
58 | export interface AddHeading3Block extends IAddHeadingBlockAction {
59 | type: typeof add_heading3_block;
60 | }
61 | export interface AddHeading4Block extends IAddHeadingBlockAction {
62 | type: typeof add_heading4_block;
63 | }
64 | export interface AddHeading5Block extends IAddHeadingBlockAction {
65 | type: typeof add_heading5_block;
66 | }
67 | export interface AddHeading6Block extends IAddHeadingBlockAction {
68 | type: typeof add_heading6_block;
69 | }
70 |
71 | //
72 |
73 | const add_paragraph_block = "add-paragraph-block";
74 | export interface AddParagraphBlock extends IAddBlockAction {
75 | type: typeof add_paragraph_block;
76 | }
77 |
--------------------------------------------------------------------------------
/packages/boring-core-action/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./actions";
2 |
--------------------------------------------------------------------------------
/packages/boring-core-action/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boring.so/action",
3 | "version": "0.0.0"
4 | }
--------------------------------------------------------------------------------
/packages/boring-core-config/index.ts:
--------------------------------------------------------------------------------
1 | export interface AutosaveOption {
2 | /**
3 | * autosave trigger interval in seconds (not ms)
4 | */
5 | interval: number | (() => number);
6 | dosave: boolean | (() => boolean) | ((prev, curr) => boolean);
7 | }
8 |
9 | export const defaults = {
10 | contentAutosave: {
11 | interval: 3.5,
12 | dosave: true,
13 | },
14 | };
15 |
16 | export interface EditorConfig {
17 | contentAutosave: AutosaveOption;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/boring-core-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boring.so/config",
3 | "version": "0.0.0"
4 | }
--------------------------------------------------------------------------------
/packages/boring-core-state/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gridaco/boring/e2bf72cc01ddf1a2b8d311fbf3ba2a9e30236892/packages/boring-core-state/index.ts
--------------------------------------------------------------------------------
/packages/boring-core-state/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boring.so/state",
3 | "version": "0.0.0"
4 | }
--------------------------------------------------------------------------------
/packages/boring-core-store/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./store";
2 |
--------------------------------------------------------------------------------
/packages/boring-core-store/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boring.so/store",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "idb": "^6.1.2"
6 | },
7 | "devDependencies": {
8 | "@boring.so/document-model": "workspace:^"
9 | },
10 | "peerDependencies": {
11 | "@boring.so/document-model": "workspace:^"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/boring-core-store/store.ts:
--------------------------------------------------------------------------------
1 | import { openDB, deleteDB, wrap, unwrap, IDBPDatabase } from "idb";
2 | import {
3 | BoringContent,
4 | BoringDocument,
5 | BoringDocumentId,
6 | } from "@boring.so/document-model";
7 |
8 | /*no-export*/ const _document_store_db_v = 1;
9 | /*no-export*/ const _document_store_db_n = "documents";
10 | /*no-export*/ const _boring_documents_store_name = "boring.so/documents";
11 | /*no-export*/ const boring_document_store_name = (id: string) =>
12 | `boring.so/documents/${id}`;
13 | export class BoringDocumentsStore {
14 | private _db: IDBPDatabase;
15 | private readonly tmpstore = new Map();
16 | async db(): Promise {
17 | if (this._db) {
18 | return this._db;
19 | }
20 | return await this.prewarm();
21 | }
22 |
23 | constructor() {
24 | this.prewarm();
25 | }
26 |
27 | async prewarm(): Promise {
28 | if (typeof indexedDB === "undefined") {
29 | return;
30 | }
31 |
32 | this._db = await openDB(_document_store_db_n, _document_store_db_v, {
33 | upgrade(db) {
34 | const store = db.createObjectStore(_boring_documents_store_name, {
35 | keyPath: "id",
36 | });
37 | },
38 | });
39 | return this._db;
40 | }
41 |
42 | async get(id: string): Promise {
43 | try {
44 | return this.tmpstore.has(id)
45 | ? this.tmpstore.get(id)
46 | : await (await this.db()).get(_boring_documents_store_name, id);
47 | } catch (e) {}
48 | }
49 |
50 | async set(doc: BoringDocument) {
51 | try {
52 | this.tmpstore.set(doc.id, doc);
53 | await (await this.db()).add(_boring_documents_store_name, doc);
54 | this.tmpstore.delete(doc.id);
55 | } catch (e) {}
56 | }
57 |
58 | async put(doc: BoringDocument) {
59 | try {
60 | await (await this.db()).put(_boring_documents_store_name, doc);
61 | } catch (e) {}
62 | }
63 | }
64 |
65 | export class BoringDocumentStore {
66 | private readonly unique_storename;
67 | readonly service: BoringDocumentsStore;
68 | constructor(readonly id: string) {
69 | this.unique_storename = boring_document_store_name(id);
70 | this.service = new BoringDocumentsStore();
71 | }
72 |
73 | async get() {
74 | const doc = await this.service.get(this.id);
75 | return doc;
76 | }
77 |
78 | async put(doc: BoringDocument, id?: BoringDocumentId) {
79 | if (!doc.id && id) {
80 | doc.id = id;
81 | }
82 | return this.service.put(doc);
83 | }
84 |
85 | async updateContent(content: string) {
86 | try {
87 | const beforeupdate = await this.get();
88 | beforeupdate.content = new BoringContent(content);
89 | /*no-await (no need to await)*/ this.put(beforeupdate);
90 | return beforeupdate;
91 | } catch (e) {}
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/packages/boring-document-model/__test__/page-data-flat.mock.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "1",
4 | "title": "Page name",
5 | "description": "Page description",
6 | "icon": "heart"
7 | }
8 | ]
--------------------------------------------------------------------------------
/packages/boring-document-model/__test__/page-data-nested.mock.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gridaco/boring/e2bf72cc01ddf1a2b8d311fbf3ba2a9e30236892/packages/boring-document-model/__test__/page-data-nested.mock.json
--------------------------------------------------------------------------------
/packages/boring-document-model/__test__/page-data.test.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gridaco/boring/e2bf72cc01ddf1a2b8d311fbf3ba2a9e30236892/packages/boring-document-model/__test__/page-data.test.ts
--------------------------------------------------------------------------------
/packages/boring-document-model/content.model.ts:
--------------------------------------------------------------------------------
1 | export class BoringContent {
2 | constructor(raw: string) {
3 | this.raw = raw;
4 | }
5 | raw: string;
6 | }
7 |
8 | export type BoringContentLike = BoringContent | string;
9 |
10 | export function boringContentLikeAsBoringContent(
11 | p: BoringContentLike
12 | ): BoringContent {
13 | if (p instanceof BoringContent) {
14 | return p;
15 | } else if (typeof p == "string") {
16 | return new BoringContent(p);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/boring-document-model/document.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BoringTitle,
3 | BoringTitleLike,
4 | boringTitleLikeAsBoringTitle,
5 | } from "./title.model";
6 | import {
7 | BoringContent,
8 | BoringContentLike,
9 | boringContentLikeAsBoringContent,
10 | } from "./content.model";
11 | import { nanoid } from "nanoid";
12 | export interface BoringDocumentTemplateConstraint {
13 | /**
14 | * id of the template
15 | * **/
16 | id: string;
17 | constraint: "unconstrained" | "constrained";
18 | }
19 |
20 | export type BoringDocumentId = string;
21 |
22 | export function autoid(): string {
23 | return nanoid();
24 | }
25 | export class BoringDocument {
26 | id: BoringDocumentId;
27 | title: BoringTitle;
28 | content: BoringContent;
29 | template?: BoringDocumentTemplateConstraint;
30 |
31 | constructor({
32 | title,
33 | content,
34 | id = autoid(),
35 | }: {
36 | id?: BoringDocumentId;
37 | title?: BoringTitleLike;
38 | content: BoringContentLike;
39 | }) {
40 | (this.title = boringTitleLikeAsBoringTitle(title)),
41 | (this.content = boringContentLikeAsBoringContent(content)),
42 | (this.id = id);
43 | }
44 | }
45 |
46 | export class EmptyDocument extends BoringDocument {
47 | constructor() {
48 | super({
49 | title: new BoringTitle(""),
50 | content: new BoringContent(""),
51 | });
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/boring-document-model/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./content.model";
2 | export * from "./document.model";
3 | export * from "./title.model";
4 |
--------------------------------------------------------------------------------
/packages/boring-document-model/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boring.so/document-model",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "nanoid": "^3.1.23"
6 | }
7 | }
--------------------------------------------------------------------------------
/packages/boring-document-model/title.model.ts:
--------------------------------------------------------------------------------
1 | type BoringTitleIcon = string;
2 |
3 | // === BoringTitleLike ===
4 | export type BoringTitleLike = string | PageIconAndName | BoringTitle;
5 |
6 | export function boringTitleLikeAsBoringTitle(tl: BoringTitleLike): BoringTitle {
7 | if (!tl) {
8 | return;
9 | }
10 | if (tl instanceof BoringTitle) {
11 | return tl;
12 | }
13 |
14 | return new BoringTitle(tl); // since accepts both `string | PageIconAndName` as initializer
15 | }
16 | // === BoringTitleLike ===
17 |
18 | // === PageIconAndName ===
19 | export interface PageIconAndName {
20 | icon?: BoringTitleIcon;
21 | name: string;
22 | }
23 |
24 | export function isPageIconAndName(p?: PageIconAndName | any): boolean {
25 | if (p) {
26 | const _maybe = p as PageIconAndName;
27 | return _maybe?.name !== undefined;
28 | }
29 | return false;
30 | }
31 | // === PageIconAndName ===
32 |
33 | export interface BoringTitle extends PageIconAndName {
34 | raw: string;
35 | icon?: BoringTitleIcon;
36 | name: string;
37 | }
38 |
39 | export class BoringTitle implements BoringTitle {
40 | constructor(p: PageIconAndName | string) {
41 | switch (typeof p) {
42 | case "string": {
43 | const _p = parseraw(p);
44 | this.icon = _p.icon;
45 | this.name = _p.name;
46 | this.raw = p;
47 | break;
48 | }
49 | case "undefined": {
50 | this.name = "";
51 | this.raw = "";
52 | break;
53 | }
54 | case "object": {
55 | this.icon = p.icon;
56 | this.name = p.name;
57 | this.raw = buildraw(p);
58 | break;
59 | }
60 | }
61 | }
62 | }
63 |
64 | /**
65 | * icon | name single string separator
66 | *
67 | * DO NOT CHANGE THE VALUE
68 | *
69 | * e.g. - `{ icon: ":smile", name: "Title" }` -> `":smile|Title"`
70 | */
71 | const _separator = "|";
72 | /**
73 | * 1. `{ name: `"|Title"` }` -> `"|Title"`
74 | * 2. `{ icon: ":smile", name: "Title" }` -> `":smile|Title"`
75 | * 3. `{ name: "Title" }` -> `"Title"`
76 | */
77 | function buildraw(p: PageIconAndName): string {
78 | return (p.icon ? p.icon + _separator : "") + p.name;
79 | }
80 |
81 | /**
82 | * 1. `"|Title"` -> `{ name: `"|Title"` }`
83 | * 2. `":smile|Title"` -> `{ icon: ":smile", name: "Title" }`
84 | * 3. `"Title"` -> `{ name: "Title" }`
85 | * @param raw
86 | * @returns
87 | */
88 | function parseraw(raw: string): PageIconAndName {
89 | let _icon: BoringTitleIcon;
90 | let _name: string;
91 | if (raw.includes(_separator)) {
92 | if (raw.startsWith(_separator)) {
93 | _name = raw;
94 | } else {
95 | const _splts = raw.split(_separator);
96 | _icon = _splts[0];
97 | _name = _splts[1];
98 | }
99 | } else {
100 | _name = raw;
101 | }
102 | return {
103 | icon: _icon,
104 | name: _name,
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/packages/boring-loader/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./initial-loader";
2 |
--------------------------------------------------------------------------------
/packages/boring-loader/initial-loader.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BoringContent,
3 | BoringContentLike,
4 | boringContentLikeAsBoringContent,
5 | BoringDocument,
6 | BoringTitleLike,
7 | EmptyDocument,
8 | } from "@boring.so/document-model";
9 | export class DocumentInitial {
10 | title: BoringTitleLike;
11 | content: BoringContent;
12 |
13 | constructor({
14 | title,
15 | content,
16 | }: {
17 | title: BoringTitleLike;
18 | content: BoringContentLike;
19 | }) {
20 | this.title = title;
21 | this.content = boringContentLikeAsBoringContent(content);
22 | }
23 | }
24 |
25 | export class StaticDocumentInitial extends DocumentInitial {
26 | constructor({
27 | title,
28 | content,
29 | }: {
30 | title: BoringTitleLike;
31 | content: BoringContentLike;
32 | }) {
33 | super({
34 | title: title,
35 | content: content,
36 | });
37 | }
38 | }
39 |
40 | export class TemplateInitial extends DocumentInitial {
41 | template: string | (() => string);
42 | props: Map;
43 | }
44 |
45 | export function initialize(initial?: DocumentInitial): BoringDocument {
46 | if (initial) {
47 | if (initial instanceof StaticDocumentInitial) {
48 | }
49 | if (initial instanceof TemplateInitial) {
50 | }
51 | /**
52 | * EXECUTION ORDER MATTERS - DocumentInitial is a parent class, needs to be placed at last.
53 | */
54 | if (initial instanceof DocumentInitial) {
55 | }
56 | }
57 | return new EmptyDocument();
58 | }
59 |
--------------------------------------------------------------------------------
/packages/boring-loader/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boring.so/loader",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@boring.so/document-model": "workspace:*"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/boring-state-app-react/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gridaco/boring/e2bf72cc01ddf1a2b8d311fbf3ba2a9e30236892/packages/boring-state-app-react/index.ts
--------------------------------------------------------------------------------
/packages/boring-state-app-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boring.so/react-state",
3 | "version": "0.0.0"
4 | }
--------------------------------------------------------------------------------
/packages/boring-template-provider/README.md:
--------------------------------------------------------------------------------
1 | # Boring template provider
2 |
3 | uses handlebar for simple template rendering
--------------------------------------------------------------------------------
/packages/boring-template-provider/index.ts:
--------------------------------------------------------------------------------
1 | import { BoringContent, BoringTitleLike } from "@boring.so/document-model";
2 | import { DocumentInitial, StaticDocumentInitial } from "@boring.so/loader";
3 | import Handlebars from "handlebars";
4 |
5 | export class TemplateProvider {
6 | register() {} // todo
7 | }
8 |
9 | export interface ITemplateSource {
10 | default?: string;
11 | template: string;
12 | }
13 |
14 | export interface ITemplatePropSource {
15 | default?: T;
16 | props: T;
17 | }
18 |
19 | export class UnconstrainedTemplate extends DocumentInitial {
20 | title: BoringTitleLike;
21 | content: BoringContent;
22 | templateTitleSource: ITemplateSource;
23 | templateContentSource: ITemplateSource;
24 | templateProps: ITemplatePropSource;
25 |
26 | constructor({
27 | templateTitleSource,
28 | templateContentSource,
29 | templateProps,
30 | }: {
31 | templateTitleSource: ITemplateSource;
32 | templateContentSource: ITemplateSource;
33 | templateProps: ITemplatePropSource;
34 | }) {
35 | super({
36 | title: templateTitleSource.default,
37 | content: templateContentSource.default,
38 | });
39 |
40 | this.templateTitleSource = templateTitleSource;
41 | this.templateContentSource = templateContentSource;
42 | this.templateProps = templateProps;
43 | }
44 |
45 | /**
46 | * render template with props
47 | */
48 | render(): StaticDocumentInitial {
49 | const _title_c = Handlebars.compile(this.templateTitleSource.template);
50 | const _content_c = Handlebars.compile(this.templateContentSource.template);
51 | const _title = _title_c(this.templateProps.props);
52 | const _content = _content_c(this.templateProps.props);
53 | return new StaticDocumentInitial({
54 | title: _title,
55 | content: _content,
56 | });
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/packages/boring-template-provider/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boring.so/template-provider",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@boring.so/document-model": "workspace:*",
6 | "handlebars": "^4.7.7"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/boring-ui-core/README.md:
--------------------------------------------------------------------------------
1 | # boring endgine core ui
2 |
--------------------------------------------------------------------------------
/packages/core-react/blocks/iframe-block.ts:
--------------------------------------------------------------------------------
1 | import { Node } from "@tiptap/core";
2 |
3 | export interface IframeOptions {
4 | allowFullscreen: boolean;
5 | HTMLAttributes: {
6 | [key: string]: any;
7 | };
8 | }
9 |
10 | declare module "@tiptap/core" {
11 | interface Commands {
12 | iframe: {
13 | /**
14 | * Add an iframe
15 | */
16 | setIframe: (options: { src: string }) => ReturnType;
17 | };
18 | }
19 | }
20 |
21 | const Iframe = Node.create({
22 | name: "iframe",
23 |
24 | group: "block",
25 |
26 | atom: true,
27 |
28 | addOptions() {
29 | return {
30 | allowFullscreen: true,
31 | HTMLAttributes: {
32 | class: "iframe-wrapper",
33 | },
34 | };
35 | },
36 |
37 | addAttributes() {
38 | return {
39 | src: {
40 | default: null,
41 | },
42 | frameborder: {
43 | default: 0,
44 | },
45 | allowfullscreen: {
46 | default: this.options.allowFullscreen,
47 | parseHTML: () => this.options.allowFullscreen,
48 | },
49 | };
50 | },
51 |
52 | parseHTML() {
53 | return [
54 | {
55 | tag: "iframe",
56 | },
57 | ];
58 | },
59 |
60 | renderHTML({ HTMLAttributes }) {
61 | return ["div", this.options.HTMLAttributes, ["iframe", HTMLAttributes]];
62 | },
63 |
64 | addCommands() {
65 | return {
66 | setIframe:
67 | (options: { src: string }) =>
68 | ({ tr, dispatch }) => {
69 | const { selection } = tr;
70 | const node = this.type.create(options);
71 |
72 | if (dispatch) {
73 | tr.replaceRangeWith(
74 | selection.from,
75 | selection.to,
76 | // @ts-ignore
77 | node
78 | );
79 | }
80 |
81 | return true;
82 | },
83 | };
84 | },
85 | });
86 |
87 | export default Iframe;
88 |
--------------------------------------------------------------------------------
/packages/core-react/blocks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./wrapper-block";
2 | export * from "./media-block";
3 | export * from "./trailing-node";
4 |
--------------------------------------------------------------------------------
/packages/core-react/blocks/media-block.tsx:
--------------------------------------------------------------------------------
1 | import Embed from "@boring-ui/embed";
2 | export function MediaComponent() {
3 | return ;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/core-react/blocks/toc.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from "react";
2 | import css from "@emotion/css";
3 | import { NodeViewWrapper } from "@tiptap/react";
4 | import { Node, mergeAttributes, Editor } from "@tiptap/core";
5 | import { ReactNodeViewRenderer } from "@tiptap/react";
6 | import { getHeadingsFrom } from "../table-of-contents";
7 |
8 | export const TableOfContents = Node.create({
9 | name: "table-of-contents",
10 |
11 | group: "block",
12 |
13 | atom: true,
14 |
15 | parseHTML() {
16 | return [
17 | {
18 | tag: "toc",
19 | },
20 | ];
21 | },
22 |
23 | renderHTML({ HTMLAttributes }) {
24 | return ["toc", mergeAttributes(HTMLAttributes)];
25 | },
26 |
27 | addNodeView() {
28 | return ReactNodeViewRenderer(TocNode);
29 | },
30 |
31 | addGlobalAttributes() {
32 | return [
33 | {
34 | types: ["heading"],
35 | attributes: {
36 | id: {
37 | default: null,
38 | },
39 | },
40 | },
41 | ];
42 | },
43 | });
44 |
45 | const TocNode = ({ editor }: { editor: Editor }) => {
46 | const [items, setItems] = useState([]);
47 |
48 | const handleUpdate = useCallback(() => {
49 | setItems(getHeadingsFrom(editor));
50 | }, [editor]);
51 |
52 | useEffect(handleUpdate, []);
53 |
54 | useEffect(() => {
55 | if (!editor) {
56 | return null;
57 | }
58 |
59 | editor.on("update", handleUpdate);
60 |
61 | return () => {
62 | editor.off("update", handleUpdate);
63 | };
64 | }, [editor]);
65 |
66 | return (
67 |
68 |
69 | {items.map((item, index) => (
70 | -
71 | {item.text}
72 |
73 | ))}
74 |
75 |
76 | );
77 | };
78 |
79 | const style = css`
80 | background: rgba(black, 0.1);
81 | border-radius: 0.5rem;
82 | opacity: 0.75;
83 | padding: 0.75rem;
84 |
85 | list {
86 | list-style: none;
87 | padding: 0;
88 |
89 | &::before {
90 | content: "Table of Contents";
91 | display: block;
92 | font-size: 0.75rem;
93 | font-weight: 700;
94 | letter-spacing: 0.025rem;
95 | opacity: 0.5;
96 | text-transform: uppercase;
97 | }
98 | }
99 |
100 | &item {
101 | a:hover {
102 | opacity: 0.5;
103 | }
104 |
105 | &--3 {
106 | padding-left: 1rem;
107 | }
108 |
109 | &--4 {
110 | padding-left: 2rem;
111 | }
112 |
113 | &--5 {
114 | padding-left: 3rem;
115 | }
116 |
117 | &--6 {
118 | padding-left: 4rem;
119 | }
120 | }
121 | `;
122 |
--------------------------------------------------------------------------------
/packages/core-react/blocks/trailing-node.ts:
--------------------------------------------------------------------------------
1 | import { Extension } from "@tiptap/core";
2 | import { PluginKey, Plugin } from "prosemirror-state";
3 |
4 | // @ts-ignore
5 | function nodeEqualsType({ types, node }) {
6 | return (
7 | (Array.isArray(types) && types.includes(node.type)) || node.type === types
8 | );
9 | }
10 |
11 | /**
12 | * Extension based on:
13 | * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js
14 | * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts
15 | */
16 |
17 | export interface TrailingNodeOptions {
18 | node: string;
19 | notAfter: string[];
20 | }
21 |
22 | const TrailingNode = Extension.create({
23 | name: "trailingNode",
24 |
25 | addOptions() {
26 | return {
27 | node: "paragraph",
28 | notAfter: ["paragraph"],
29 | };
30 | },
31 |
32 | addProseMirrorPlugins() {
33 | const plugin = new PluginKey(this.name);
34 | const disabledNodes = Object.entries(this.editor.schema.nodes)
35 | .map(([, value]) => value)
36 | .filter((node) => this.options.notAfter.includes((node as any).name));
37 |
38 | return [
39 | new Plugin({
40 | key: plugin,
41 | appendTransaction: (_, __, state) => {
42 | const { doc, tr, schema } = state;
43 | const shouldInsertNodeAtEnd = plugin.getState(state);
44 | const endPosition = doc.content.size;
45 | const type = schema.nodes[this.options.node];
46 |
47 | if (!shouldInsertNodeAtEnd) {
48 | return;
49 | }
50 |
51 | return tr.insert(endPosition, type.create());
52 | },
53 | state: {
54 | init: (_, state) => {
55 | const lastNode = state.tr.doc.lastChild;
56 |
57 | return !nodeEqualsType({ node: lastNode, types: disabledNodes });
58 | },
59 | apply: (tr, value) => {
60 | if (!tr.docChanged) {
61 | return value;
62 | }
63 |
64 | const lastNode = tr.doc.lastChild;
65 |
66 | return !nodeEqualsType({ node: lastNode, types: disabledNodes });
67 | },
68 | },
69 | }),
70 | ];
71 | },
72 | });
73 |
74 | export default TrailingNode;
75 |
--------------------------------------------------------------------------------
/packages/core-react/blocks/video-block.ts:
--------------------------------------------------------------------------------
1 | import { Node, mergeAttributes } from "@tiptap/core";
2 |
3 | const Video = Node.create({
4 | name: "video", // unique name for the Node
5 | group: "block", // belongs to the 'block' group of extensions
6 | selectable: true, // so we can select the video
7 | draggable: true, // so we can drag the video
8 | atom: true, // is a single unit
9 |
10 | addAttributes() {
11 | return {
12 | src: {
13 | default: null,
14 | },
15 | controls: {
16 | default: true,
17 | },
18 | };
19 | },
20 |
21 | parseHTML() {
22 | return [
23 | {
24 | tag: "video",
25 | },
26 | ];
27 | },
28 |
29 | renderHTML({ HTMLAttributes }) {
30 | return ["video", mergeAttributes(HTMLAttributes)];
31 | },
32 | });
33 |
34 | export default Video;
35 |
--------------------------------------------------------------------------------
/packages/core-react/blocks/wrapper-block.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled from "@emotion/styled";
3 |
4 | export function WrapperBlock(props: { children?: JSX.Element }) {
5 | const [hover, setHover] = useState(false);
6 | const starthover = () => {
7 | setHover(true);
8 | };
9 | const endhover = () => {
10 | setHover(false);
11 | };
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | {props.children}
19 |
20 | );
21 | }
22 |
23 | const HandleContainer = styled.div`
24 | width: 80px;
25 | height: 20px;
26 | padding-right: 12px;
27 | position: absolute;
28 | left: 280px;
29 | `;
30 |
--------------------------------------------------------------------------------
/packages/core-react/content-editor/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./main-content-editor";
2 |
--------------------------------------------------------------------------------
/packages/core-react/content-editor/main-content-editor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import styled from "@emotion/styled";
3 | import { Node } from "@tiptap/core";
4 | import { Editor, useEditor, EditorContent } from "@tiptap/react";
5 |
6 | // region block components
7 |
8 | // endregion block components
9 | import { DEFAULT_THEME_FONT_FAMILY } from "../theme";
10 | import { InlineToolbar } from "../inline-toolbar";
11 | import { SideFloatingMenu } from "../floating-menu";
12 |
13 | interface MainBodyContentEditorProps {
14 | editor: Editor | null;
15 |
16 | /**
17 | * initial height of interactive area. defaults to 200px.
18 | */
19 | initialHeight?: string;
20 |
21 | readonly: boolean;
22 |
23 | onUploadFile: (file: File) => Promise;
24 | }
25 |
26 | export function MainBodyContentEditor({
27 | initialHeight,
28 | editor,
29 | readonly,
30 | onUploadFile,
31 | }: MainBodyContentEditorProps) {
32 | const focus = () => {
33 | editor?.chain().focus().run();
34 | };
35 |
36 | /**
37 | * this is for focusing to content editor when padding safe area is clicked. (usually bottom of the editor)
38 | */
39 | const onTouchAreaClick = () => {
40 | focus();
41 | };
42 |
43 | return (
44 |
45 | {/* */}
46 |
47 | {/* */}
48 | {editor && (
49 |
50 | )}
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | const TouchArea = styled.div<{
59 | initialHeight?: string;
60 | }>`
61 | cursor: text;
62 | width: 100%;
63 | min-height: ${(p) => p.initialHeight ?? "200px"};
64 | padding-bottom: 120px;
65 | `;
66 |
67 | const MainContentEditorRootWrapper = styled.div`
68 | /* disable outline for contenteditable */
69 | [contenteditable] {
70 | outline: 0px solid transparent;
71 | }
72 |
73 | min-height: 400px;
74 | `;
75 |
76 | const EditorContentInstance = styled(EditorContent)`
77 | /* font */
78 | .ProseMirror {
79 | h1,
80 | h2,
81 | h3,
82 | h4,
83 | h5,
84 | h6,
85 | p {
86 | color: rgba(0, 0, 0, 0.87);
87 | font-family: ${DEFAULT_THEME_FONT_FAMILY};
88 | line-height: 110%;
89 | }
90 |
91 | h1,
92 | h2,
93 | h3 {
94 | line-height: 180%;
95 | }
96 |
97 | h1 {
98 | font-size: 30px;
99 | }
100 |
101 | h2 {
102 | font-size: 24px;
103 | }
104 |
105 | h3 {
106 | font-size: 20px;
107 | }
108 |
109 | /* p only */
110 | p {
111 | font-size: 16px;
112 | line-height: 150%;
113 | }
114 |
115 | img {
116 | border-radius: 2px;
117 | width: 100%;
118 | height: auto;
119 | display: block;
120 | margin-left: auto;
121 | margin-right: auto;
122 | }
123 |
124 | video {
125 | width: 100%;
126 | height: auto;
127 | display: block;
128 | margin-left: auto;
129 | margin-right: auto;
130 | }
131 |
132 | img,
133 | video {
134 | margin-bottom: 4px;
135 | &.ProseMirror-selectednode {
136 | outline: 2px solid rgba(0, 0, 0, 0.2);
137 | cursor: grab;
138 | }
139 | background-color: rgba(0, 0, 0, 0.2);
140 | }
141 |
142 | iframe {
143 | width: 100%;
144 | height: 400px;
145 | }
146 |
147 | hr {
148 | display: flex;
149 | cursor: default;
150 | height: 13px;
151 | outline: none;
152 | margin: 0;
153 | padding: 0;
154 | border: none;
155 | align-items: center;
156 | :after {
157 | display: inline-block;
158 | content: "";
159 | width: 100%;
160 | height: 1px;
161 | border-bottom: solid 1px rgba(0, 0, 0, 0.12);
162 | }
163 | &.ProseMirror-selectednode {
164 | :after {
165 | border-bottom: solid 1px rgba(0, 0, 0, 0.3);
166 | }
167 | }
168 | }
169 | }
170 |
171 | /* placeholder's style - https://www.tiptap.dev/api/extensions/placeholder/#placeholder*/
172 | .ProseMirror p.is-editor-empty:first-of-type::before {
173 | content: attr(data-placeholder);
174 | float: left;
175 | color: rgba(0, 0, 0, 0.3);
176 | pointer-events: none;
177 | height: 0;
178 | }
179 |
180 | /* ================================================================ */
181 | /* region placeholder */
182 | /* Placeholder (only at the top) */
183 | .ProseMirror .is-editor-empty:first-of-type::before {
184 | content: attr(data-placeholder);
185 | float: left;
186 | color: rgba(0, 0, 0, 0.3);
187 | pointer-events: none;
188 | height: 0;
189 | }
190 |
191 | /* Placeholder (on every new line) */
192 | .ProseMirror .is-empty::before {
193 | content: attr(data-placeholder);
194 | float: left;
195 | color: rgba(0, 0, 0, 0.3);
196 | pointer-events: none;
197 | height: 0;
198 | }
199 | /* endregion placeholder */
200 | /* ================================================================ */
201 |
202 | .ProseMirror {
203 | /* ================================================================ */
204 | /* codeblock - https://www.tiptap.dev/api/nodes/code-block-lowlight */
205 | /* */
206 | pre {
207 | background: #5c5c5c;
208 | color: #fff;
209 | font-family: "JetBrainsMono", monospace;
210 | padding: 0.75rem 1rem;
211 | border-radius: 0.5rem;
212 |
213 | code {
214 | color: inherit;
215 | padding: 0;
216 | background: none;
217 | font-size: 0.8rem;
218 | }
219 |
220 | .hljs-comment,
221 | .hljs-quote {
222 | color: #616161;
223 | }
224 |
225 | .hljs-variable,
226 | .hljs-template-variable,
227 | .hljs-attribute,
228 | .hljs-tag,
229 | .hljs-name,
230 | .hljs-regexp,
231 | .hljs-link,
232 | .hljs-name,
233 | .hljs-selector-id,
234 | .hljs-selector-class {
235 | color: #f98181;
236 | }
237 |
238 | .hljs-number,
239 | .hljs-meta,
240 | .hljs-built_in,
241 | .hljs-builtin-name,
242 | .hljs-literal,
243 | .hljs-type,
244 | .hljs-params {
245 | color: #fbbc88;
246 | }
247 |
248 | .hljs-string,
249 | .hljs-symbol,
250 | .hljs-bullet {
251 | color: #b9f18d;
252 | }
253 |
254 | .hljs-title,
255 | .hljs-section {
256 | color: #faf594;
257 | }
258 |
259 | .hljs-keyword,
260 | .hljs-selector-tag {
261 | color: #70cff8;
262 | }
263 |
264 | .hljs-emphasis {
265 | font-style: italic;
266 | }
267 |
268 | .hljs-strong {
269 | font-weight: 700;
270 | }
271 | }
272 | /* ================================================================ */
273 | /* ================================================================ */
274 |
275 | /* ================================================================ */
276 | /* blockquote - https://www.tiptap.dev/api/nodes/blockquote */
277 | /* FIXME - not border-left working */
278 | blockquote {
279 | padding-left: 1rem;
280 | border-left: 2px solid #b0b0b0;
281 | }
282 | /* ================================================================ */
283 | /* ================================================================ */
284 | }
285 |
286 | /* Give a remote user a caret */
287 | .collaboration-cursor__caret {
288 | border-left: 1px solid #0d0d0d;
289 | border-right: 1px solid #0d0d0d;
290 | margin-left: -1px;
291 | margin-right: -1px;
292 | pointer-events: none;
293 | position: relative;
294 | word-break: normal;
295 | }
296 |
297 | /* Render the username above the caret */
298 | .collaboration-cursor__label {
299 | border-radius: 3px 3px 3px 0;
300 | color: #0d0d0d;
301 | font-size: 12px;
302 | font-style: normal;
303 | font-weight: 600;
304 | left: -1px;
305 | line-height: normal;
306 | padding: 0.1rem 0.3rem;
307 | position: absolute;
308 | top: -1.4em;
309 | user-select: none;
310 | white-space: nowrap;
311 | }
312 | `;
313 |
--------------------------------------------------------------------------------
/packages/core-react/drag-handle/drag-handle.ts:
--------------------------------------------------------------------------------
1 | ///
2 | /// forked from https://www.tiptap.dev/experiments/global-drag-handle/#globaldraghandle
3 | ///
4 |
5 | import { Extension } from "@tiptap/core";
6 | import { NodeSelection, Plugin } from "prosemirror-state";
7 | import { serializeForClipboard } from "prosemirror-view/src/clipboard";
8 |
9 | function removeNode(node) {
10 | node.parentNode.removeChild(node);
11 | }
12 |
13 | function absoluteRect(node) {
14 | const data = node.getBoundingClientRect();
15 |
16 | return {
17 | top: data.top,
18 | left: data.left,
19 | width: data.width,
20 | };
21 | }
22 |
23 | export default Extension.create({
24 | addProseMirrorPlugins() {
25 | function blockPosAtCoords(coords, view) {
26 | const pos = view.posAtCoords(coords);
27 | let node = view.domAtPos(pos.pos);
28 |
29 | node = node.node;
30 |
31 | while (node && node.parentNode) {
32 | if (node.parentNode?.classList?.contains("ProseMirror")) {
33 | // todo
34 | break;
35 | }
36 |
37 | node = node.parentNode;
38 | }
39 |
40 | if (node && node.nodeType === 1) {
41 | const desc = view.docView.nearestDesc(node, true);
42 |
43 | if (!(!desc || desc === view.docView)) {
44 | return desc.posBefore;
45 | }
46 | }
47 | return null;
48 | }
49 |
50 | function dragStart(e, view) {
51 | view.composing = true;
52 |
53 | if (!e.dataTransfer) {
54 | return;
55 | }
56 |
57 | const coords = { left: e.clientX + 50, top: e.clientY };
58 | const pos = blockPosAtCoords(coords, view);
59 |
60 | if (pos != null) {
61 | view.dispatch(
62 | view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos))
63 | );
64 |
65 | const slice = view.state.selection.content();
66 |
67 | // console.log({
68 | // from: view.nodeDOM(view.state.selection.from),
69 | // to: view.nodeDOM(view.state.selection.to),
70 | // })
71 | const { dom, text } = serializeForClipboard(view, slice);
72 |
73 | e.dataTransfer.clearData();
74 | e.dataTransfer.setData("text/html", dom.innerHTML);
75 | e.dataTransfer.setData("text/plain", text);
76 |
77 | const el = document.querySelector(".ProseMirror-selectednode");
78 | e.dataTransfer?.setDragImage(el, 0, 0);
79 |
80 | view.dragging = { slice, move: true };
81 | }
82 | }
83 |
84 | let dropElement;
85 | const WIDTH = 28;
86 |
87 | return [
88 | new Plugin({
89 | view(editorView) {
90 | const element = document.createElement("div");
91 | element.draggable = true;
92 | element.classList.add("global-drag-handle");
93 | element.addEventListener("dragstart", (e) =>
94 | dragStart(e, editorView)
95 | );
96 | dropElement = element;
97 | document.body.appendChild(dropElement);
98 |
99 | return {
100 | // update(view, prevState) {
101 | // },
102 | destroy() {
103 | removeNode(dropElement);
104 | dropElement = null;
105 | },
106 | };
107 | },
108 | props: {
109 | handleDrop(view, event, slice, moved): boolean {
110 | if (moved) {
111 | // setTimeout(() => {
112 | // console.log('remove selection')
113 | // view.dispatch(view.state.tr.deleteSelection())
114 | // }, 50)
115 | return true;
116 | }
117 | return false;
118 | },
119 | // handlePaste() {
120 | // alert(2)
121 | // },
122 | handleDOMEvents: {
123 | // drop(view, event) {
124 | // setTimeout(() => {
125 | // const node = document.querySelector('.ProseMirror-hideselection')
126 | // if (node) {
127 | // node.classList.remove('ProseMirror-hideselection')
128 | // }
129 | // }, 50)
130 | // },
131 | mousemove(view, event): boolean {
132 | const coords = {
133 | left: event.clientX + WIDTH + 50,
134 | top: event.clientY,
135 | };
136 | const pos = view.posAtCoords(coords);
137 |
138 | if (pos) {
139 | let node = view.domAtPos(pos?.pos);
140 |
141 | if (node) {
142 | let nnode: HTMLElement = node.node as HTMLElement;
143 | while (nnode && nnode.parentNode) {
144 | if (
145 | (nnode.parentNode as HTMLElement)?.classList?.contains(
146 | "ProseMirror"
147 | )
148 | ) {
149 | // todo
150 | break;
151 | }
152 | nnode = nnode.parentNode as HTMLElement;
153 | }
154 |
155 | if (node instanceof Element) {
156 | const cstyle = window.getComputedStyle(node);
157 | const lineHeight = parseInt(cstyle.lineHeight, 10);
158 | // const top = parseInt(cstyle.marginTop, 10) + parseInt(cstyle.paddingTop, 10)
159 | const top = 0;
160 | const rect = absoluteRect(node);
161 | const win = node.ownerDocument.defaultView;
162 |
163 | rect.top += win.pageYOffset + (lineHeight - 24) / 2 + top;
164 | rect.left += win.pageXOffset;
165 | rect.width = `${WIDTH}px`;
166 |
167 | dropElement.style.left = `${-WIDTH + rect.left}px`;
168 | dropElement.style.top = `${rect.top}px`;
169 | return true;
170 | }
171 | }
172 | }
173 | return false;
174 | },
175 | },
176 | },
177 | }),
178 | ];
179 | },
180 | });
181 |
--------------------------------------------------------------------------------
/packages/core-react/drag-handle/index.ts:
--------------------------------------------------------------------------------
1 | export { default as default } from "./drag-handle";
2 |
--------------------------------------------------------------------------------
/packages/core-react/embeding-utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./youtube";
2 | export * from "./vimeo";
3 |
--------------------------------------------------------------------------------
/packages/core-react/embeding-utils/vimeo.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * parse id from video url
3 | *
4 | * e.g. - from "https://vimeo.com/697387375"
5 | * @param url
6 | * @returns
7 | */
8 | export function get_vimeo_video_id(url: string) {
9 | const regExp =
10 | /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/;
11 | const match = url.match(regExp);
12 | if (match) {
13 | return match[5];
14 | }
15 | }
16 |
17 | export function make_vimeo_video_embed_url(id: string) {
18 | return `https://player.vimeo.com/video/${id}`;
19 | }
20 |
--------------------------------------------------------------------------------
/packages/core-react/embeding-utils/youtube.ts:
--------------------------------------------------------------------------------
1 | export function get_youtube_video_id(url: string) {
2 | const regExp =
3 | /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
4 | const match = url.match(regExp);
5 | return match && match[7].length === 11 ? match[7] : false;
6 | }
7 |
8 | export function make_youtube_video_embed_url(id: string) {
9 | return `https://www.youtube.com/embed/${id}`;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/core-react/extension-configs/block-quote-config.ts:
--------------------------------------------------------------------------------
1 | import Blockquote from "@tiptap/extension-blockquote";
2 | export const BlockQuoteConfig = Blockquote.configure({});
3 |
--------------------------------------------------------------------------------
/packages/core-react/extension-configs/codeblock-config.ts:
--------------------------------------------------------------------------------
1 | import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
2 | /* load all highlight.js languages */ import lowlight from "lowlight";
3 |
4 | export const CodeblockConfig = CodeBlockLowlight.configure({
5 | lowlight,
6 | });
7 |
--------------------------------------------------------------------------------
/packages/core-react/extension-configs/floating-menu-conig.ts:
--------------------------------------------------------------------------------
1 | import FloatingMenu, {
2 | FloatingMenuOptions,
3 | } from "@tiptap/extension-floating-menu";
4 |
5 | export const FloatingMenuConfig = FloatingMenu.configure({
6 | //
7 | });
8 |
--------------------------------------------------------------------------------
/packages/core-react/extension-configs/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./placeholder-config";
2 | export * from "./codeblock-config";
3 | export * from "./block-quote-config";
4 | export * from "./underline-config";
5 | export * from "./slash-command-config";
6 |
--------------------------------------------------------------------------------
/packages/core-react/extension-configs/placeholder-config.ts:
--------------------------------------------------------------------------------
1 | import Placeholder from "@tiptap/extension-placeholder";
2 |
3 | export const PlaceholderConfig = Placeholder.configure({
4 | showOnlyCurrent: true,
5 | placeholder: ({ node, editor, hasAnchor }) => {
6 | const headingPlaceholders = {
7 | 1: "Heading 1",
8 | 2: "Heading 2",
9 | 3: "Heading 3",
10 | };
11 |
12 | if (node.type.name === "heading") {
13 | return headingPlaceholders[node.attrs.level];
14 | }
15 |
16 | if (node.type.name === "paragraph") {
17 | return "Type '/' for commands";
18 | }
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/packages/core-react/extension-configs/slash-command-config.ts:
--------------------------------------------------------------------------------
1 | import { Commands, getSuggestionItems, renderItems } from "../slash-commands";
2 |
3 | export const SlashCommandConfig = ({
4 | onUploadFile,
5 | }: {
6 | onUploadFile: (file: File) => Promise;
7 | }) =>
8 | Commands.configure({
9 | suggestion: {
10 | items: (args) => getSuggestionItems({ ...args, onUploadFile }),
11 | render: renderItems,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/packages/core-react/extension-configs/underline-config.ts:
--------------------------------------------------------------------------------
1 | import Underline from "@tiptap/extension-underline";
2 |
3 | export const UnderlineConfig = Underline.configure({});
4 |
--------------------------------------------------------------------------------
/packages/core-react/floating-menu/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./side-floating-menu";
2 |
--------------------------------------------------------------------------------
/packages/core-react/floating-menu/side-floating-menu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect } from "react";
2 | import styled from "@emotion/styled";
3 | import { FloatingMenu as TiptapFloatingMenu, Editor } from "@tiptap/react";
4 | import {
5 | useFloating,
6 | offset,
7 | useInteractions,
8 | useDismiss,
9 | autoUpdate,
10 | } from "@floating-ui/react-dom-interactions";
11 | import { AddBlockMenu } from "../menu";
12 |
13 | export function SideFloatingMenu({
14 | editor,
15 | onUploadFile,
16 | }: {
17 | editor: Editor;
18 | onUploadFile;
19 | }) {
20 | const rectref = useRef();
21 | const [addMenuShown, setAddMenuShown] = useState(false);
22 | const { x, y, reference, floating, context } = useFloating({
23 | middleware: [offset({ mainAxis: 4, crossAxis: 40 })],
24 | strategy: "absolute",
25 | open: addMenuShown,
26 | });
27 | const { getReferenceProps, getFloatingProps } = useInteractions([
28 | useDismiss(context, {
29 | enabled: true,
30 | escapeKey: true,
31 | referencePointerDown: true,
32 | outsidePointerDown: true,
33 | }),
34 | ]);
35 |
36 | const showAddMenu = () => setAddMenuShown(true);
37 | const hideAddMenu = () => setAddMenuShown(false);
38 |
39 | useEffect(() => {
40 | setAddMenuShown(false);
41 | }, [editor.state.selection.anchor]);
42 |
43 | // dismiss manager
44 | useEffect(() => {
45 | const hideonclickoutside = (event: MouseEvent) => {
46 | if (rectref.current && !rectref.current.contains(event.target as Node)) {
47 | hideAddMenu();
48 | }
49 | };
50 |
51 | const hideonkeydown = (event: KeyboardEvent) => {
52 | if (event.key === "Escape") {
53 | hideAddMenu();
54 | }
55 |
56 | // TODO: pass event to add block menu
57 | };
58 | if (addMenuShown) {
59 | document.addEventListener("mousedown", hideonclickoutside);
60 | document.addEventListener("keydown", hideonkeydown);
61 | }
62 | return () => {
63 | document.removeEventListener("mousedown", hideonclickoutside);
64 | document.removeEventListener("keydown", hideonkeydown);
65 | };
66 | }, [addMenuShown]);
67 |
68 | return (
69 | <>
70 | true}
77 | >
78 |
79 |
90 | {addMenuShown && (
91 |
96 | )}
97 |
98 |
99 |
100 |
105 |
118 |
119 |
120 |
121 | >
122 | );
123 | }
124 |
125 | const Container = styled.div`
126 | display: flex;
127 | `;
128 |
129 | const AddButton = styled.button`
130 | outline: none;
131 | border: none;
132 | text-align: center;
133 | border-radius: 4px;
134 | background-color: transparent;
135 |
136 | :hover {
137 | background-color: rgba(0, 0, 0, 0.1);
138 | }
139 | :active {
140 | background-color: rgba(0, 0, 0, 0.2);
141 | }
142 | `;
143 |
--------------------------------------------------------------------------------
/packages/core-react/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./scaffold";
2 | export * from "./node-view-wrapper";
3 |
--------------------------------------------------------------------------------
/packages/core-react/inline-toolbar/README.md:
--------------------------------------------------------------------------------
1 | # Inline toolbar
2 |
--------------------------------------------------------------------------------
/packages/core-react/inline-toolbar/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./inline-toolbar";
2 |
--------------------------------------------------------------------------------
/packages/core-react/inline-toolbar/inline-toolbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from "react";
2 | import styled from "@emotion/styled";
3 | import { Editor, BubbleMenu } from "@tiptap/react";
4 | import {
5 | useFloating,
6 | offset,
7 | useInteractions,
8 | useDismiss,
9 | } from "@floating-ui/react-dom-interactions";
10 | import { BoringBubbleLinkInput } from "../menu";
11 |
12 | /* TODO: import icon somewhere else */
13 | export function InlineToolbar(props: { editor: Editor | null }) {
14 | const { editor } = props;
15 | if (!editor) {
16 | return <>>;
17 | }
18 |
19 | const { view, state } = editor;
20 | const { from, to } = view.state.selection;
21 | const text = state.doc.textBetween(from, to, "");
22 |
23 | // - on text
24 | // - on image
25 | // - on embeddings
26 | // - on divider
27 |
28 | const isText = text && text !== "";
29 | const isImage = editor.isActive("image");
30 |
31 | return (
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | function LinkItem({ editor }: { editor: Editor }) {
39 | const { x, y, reference, floating, context } = useFloating({
40 | middleware: [offset({ mainAxis: 50 })],
41 | strategy: "absolute",
42 | });
43 | const { getReferenceProps, getFloatingProps } = useInteractions([
44 | useDismiss(context, {
45 | enabled: true,
46 | escapeKey: true,
47 | referencePointerDown: true,
48 | outsidePointerDown: true,
49 | }),
50 | ]);
51 | const [isOpen, setIsOpen] = React.useState(false);
52 |
53 | const link = editor.getAttributes("link")["href"];
54 |
55 | return (
56 | <>
57 |
68 | {isOpen && (
69 | {
72 | if (v) {
73 | editor
74 | .chain()
75 | .focus()
76 | .setLink({ href: v, target: "_blank" })
77 | .run();
78 | } else {
79 | // if input is empty, remove link
80 | editor.chain().focus().unsetLink().run();
81 | }
82 | }}
83 | />
84 | )}
85 |
86 | - {
90 | setIsOpen(true);
91 | }}
92 | className={editor.isActive("link") ? "is-active" : ""}
93 | >
94 |
101 |
107 |
108 | Link
109 |
110 | >
111 | );
112 | }
113 |
114 | function Items({
115 | editor,
116 | type,
117 | }: {
118 | editor: Editor;
119 | type: "text" | "image" | "other";
120 | }) {
121 | const isLink = editor.isActive("link");
122 | const isBlockquote = editor.isActive("blockquote");
123 | const isBold = editor.isActive("bold");
124 | const isItalic = editor.isActive("italic");
125 | const isUnderline = editor.isActive("underline");
126 | const isStrike = editor.isActive("strike");
127 | const isH1 = editor.isActive("heading", { level: 1 });
128 | const isH2 = editor.isActive("heading", { level: 2 });
129 |
130 | switch (type) {
131 | case "text": {
132 | return (
133 | <>
134 |
135 |
136 | - editor.chain().focus().toggleBlockquote().run()}
138 | className={isBlockquote ? "is-active" : ""}
139 | >
140 |
141 |
145 |
146 |
147 | -
149 | editor
150 | .chain()
151 | .focus()
152 | // @ts-ignore
153 | .toggleBold()
154 | .run()
155 | }
156 | className={isBold ? "is-active" : ""}
157 | >
158 |
159 |
163 |
164 |
165 | -
167 | editor
168 | .chain()
169 | .focus()
170 | // @ts-ignore
171 | .toggleItalic()
172 | .run()
173 | }
174 | className={isItalic ? "is-active" : ""}
175 | >
176 |
182 |
186 |
187 |
188 | - editor.chain().focus().toggleUnderline().run()}
190 | className={isUnderline ? "is-active" : ""}
191 | >
192 |
198 |
202 |
206 |
207 |
208 | -
210 | editor
211 | .chain()
212 | .focus()
213 | // @ts-ignore
214 | .toggleStrike()
215 | .run()
216 | }
217 | className={isStrike ? "is-active" : ""}
218 | >
219 |
225 |
229 |
230 |
231 |
232 | -
234 | editor
235 | .chain()
236 | .focus()
237 | // @ts-ignore
238 | .toggleHeading({ level: 1 })
239 | .run()
240 | }
241 | className={isH1 ? "is-active" : ""}
242 | >
243 |
259 |
260 | -
262 | editor
263 | .chain()
264 | .focus()
265 | // @ts-ignore
266 | .toggleHeading({ level: 2 })
267 | .run()
268 | }
269 | className={isH2 ? "is-active" : ""}
270 | >
271 |
287 |
288 | >
289 | );
290 | }
291 | case "image": {
292 | return <>>;
293 | }
294 | case "other": {
295 | return <>>;
296 | }
297 | }
298 | }
299 |
300 | // https://www.tiptap.dev/examples/menus
301 | // @ts-ignore
302 | const MenuWrapper = styled(BubbleMenu)`
303 | display: flex;
304 | justify-content: flex-start;
305 | flex-direction: row;
306 | align-items: center;
307 | flex: none;
308 | border: 1px solid rgba(0, 0, 0, 0.02);
309 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
310 | border-radius: 4px;
311 | background-color: white;
312 | box-sizing: border-box;
313 | overflow: hidden;
314 | `;
315 |
316 | const Item = styled.button`
317 | display: flex;
318 | border-radius: 0px;
319 | justify-content: center;
320 | flex-direction: row;
321 | align-items: center;
322 | border: none;
323 | background: none;
324 | gap: 4px;
325 | align-self: stretch;
326 | box-sizing: border-box;
327 | padding: 10px 6px;
328 | flex-shrink: 0;
329 | background-color: transparent;
330 |
331 | :hover {
332 | background-color: rgba(0, 0, 0, 0.1);
333 | }
334 | :active {
335 | background-color: rgba(0, 0, 0, 0.12);
336 | }
337 | &.is-active {
338 | background-color: rgba(0, 0, 0, 0.1);
339 | }
340 |
341 | transition: background-color 0.1s ease-in-out;
342 | `;
343 |
344 | const Icon = styled.svg`
345 | width: 18px;
346 | height: 18px;
347 | `;
348 |
349 | const Link = styled.span`
350 | color: rgb(83, 84, 85);
351 | text-overflow: ellipsis;
352 | font-size: 16px;
353 | font-family: Inter, sans-serif;
354 | font-weight: 500;
355 | text-align: left;
356 | `;
357 |
358 | const Divider = styled.div`
359 | width: 1px;
360 | margin-left: 1px;
361 | border-left: solid 1px rgb(237, 237, 236);
362 | align-self: stretch;
363 | flex-shrink: 0;
364 | `;
365 |
--------------------------------------------------------------------------------
/packages/core-react/key-maps/index.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function keyBindingFn(e: React.KeyboardEvent) {
4 | if (e.key === "/") {
5 | return "show-boring-shortcut-menu";
6 | }
7 | if (e.key === "Enter") {
8 | return "split-block";
9 | }
10 | }
11 |
12 | export function handleKeyCommand(command: string) {
13 | switch (command) {
14 | case "show-boring-shortcut-menu":
15 | return "handled";
16 | case "split-block":
17 | // const currentContent = editorState.getCurrentContent();
18 | // const selection = editorState.getSelection();
19 | // const textWithEntity = Modifier.splitBlock(currentContent, selection);
20 | // setEditorState(
21 | // EditorState.push(editorState, textWithEntity, "split-block")
22 | // );
23 | return "handled";
24 | }
25 |
26 | // We do this by telling Draft we haven't handled it.
27 | return "not-handled";
28 | }
29 |
--------------------------------------------------------------------------------
/packages/core-react/menu/add-block-menu/block-item.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "@emotion/styled";
3 |
4 | export function BoringBlockSuggestionItemOnBubbleMenu({
5 | label,
6 | helptext,
7 | preview,
8 | onClick,
9 | selected,
10 | }: {
11 | selected?: boolean;
12 | label: string;
13 | helptext?: string;
14 | preview?: JSX.Element;
15 | onClick: () => void;
16 | }) {
17 | return (
18 |
19 | {preview && {preview}}
20 |
21 |
22 | {helptext && {helptext}}
23 |
24 |
25 | );
26 | }
27 |
28 | const Container = styled.div`
29 | cursor: pointer;
30 | display: flex;
31 | justify-content: flex-start;
32 | flex-direction: row;
33 | align-items: center;
34 | flex: none;
35 | gap: 12px;
36 | border-radius: 4px;
37 | box-sizing: border-box;
38 | padding: 8px 16px;
39 | &[data-selected="true"],
40 | :hover {
41 | background-color: rgba(0, 0, 0, 0.05);
42 | /* #efefef; */
43 | }
44 | `;
45 |
46 | const PreviewContainer = styled.div`
47 | display: flex;
48 | justify-content: center;
49 | flex-direction: row;
50 | align-items: center;
51 | flex: none;
52 | border: solid 1px rgba(0, 0, 0, 0.08);
53 | border-radius: 4px;
54 | width: 60px;
55 | height: 60px;
56 | box-sizing: border-box;
57 | `;
58 |
59 | const TextContainer = styled.div`
60 | display: flex;
61 | justify-content: flex-start;
62 | flex-direction: column;
63 | align-items: flex-start;
64 | flex: 1;
65 | gap: 10px;
66 | box-sizing: border-box;
67 | `;
68 |
69 | const Label = styled.span`
70 | color: black;
71 | text-overflow: ellipsis;
72 | font-size: 18px;
73 | font-family: Inter, sans-serif;
74 | font-weight: 500;
75 | text-align: left;
76 | `;
77 |
78 | const Helptext = styled.span`
79 | color: rgba(0, 0, 0, 0.5);
80 | text-overflow: ellipsis;
81 | font-size: 16px;
82 | font-family: Inter, sans-serif;
83 | font-weight: 500;
84 | text-align: left;
85 | `;
86 |
--------------------------------------------------------------------------------
/packages/core-react/menu/add-block-menu/block-list.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import type { Editor } from "@tiptap/react";
3 | import { BoringBlockSuggestionItemOnBubbleMenu } from "./block-item";
4 | import styled from "@emotion/styled";
5 | import { CommandItem } from "./items";
6 | import { BoringBlockIcon } from "./icons";
7 |
8 | export class BlockList extends Component<{
9 | items: CommandItem[];
10 | command?: (c: CommandItem) => void;
11 | }> {
12 | state = {
13 | selectedIndex: 0,
14 | };
15 |
16 | items: [];
17 | off: boolean;
18 |
19 | constructor(props) {
20 | super(props);
21 | this.items = props.items;
22 | this.off = this.items.length === 0;
23 | }
24 |
25 | componentDidUpdate(oldProps) {
26 | if (this.props.items !== oldProps.items) {
27 | this.setState({
28 | selectedIndex: 0,
29 | });
30 | }
31 | }
32 |
33 | onKeyDown({ event }) {
34 | if (this.off) {
35 | return false;
36 | }
37 |
38 | if (event.key === "ArrowUp") {
39 | this.upHandler();
40 | return true;
41 | }
42 |
43 | if (event.key === "ArrowDown") {
44 | this.downHandler();
45 | return true;
46 | }
47 |
48 | if (event.key === "Enter") {
49 | this.enterHandler();
50 | return true;
51 | }
52 |
53 | return false;
54 | }
55 |
56 | upHandler() {
57 | this.setState({
58 | selectedIndex:
59 | (this.state.selectedIndex + this.props.items.length - 1) %
60 | this.props.items.length,
61 | });
62 | }
63 |
64 | downHandler() {
65 | this.setState({
66 | selectedIndex: (this.state.selectedIndex + 1) % this.props.items.length,
67 | });
68 | }
69 |
70 | enterHandler() {
71 | this.selectItem(this.state.selectedIndex);
72 | }
73 |
74 | selectItem(index) {
75 | const item = this.props.items[index];
76 |
77 | if (item) {
78 | this.props.command?.(item);
79 | }
80 | }
81 |
82 | render() {
83 | const { items } = this.props;
84 | return (
85 | <>
86 | {items.length ? (
87 |
88 |
89 | {items.map((item, index) => {
90 | return (
91 | }
95 | label={item.title}
96 | helptext={item.subtitle}
97 | onClick={() => this.selectItem(index)}
98 | />
99 | );
100 | })}
101 |
102 |
103 | ) : (
104 | <>>
105 | )}
106 | >
107 | );
108 | }
109 | }
110 |
111 | const ItemsContainer = styled.div`
112 | z-index: 9;
113 | display: flex;
114 | justify-content: flex-start;
115 | flex-direction: column;
116 | align-items: stretch;
117 | flex: none;
118 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
119 | border: solid 1px rgb(237, 237, 236);
120 | border-radius: 4px;
121 | background-color: white;
122 | box-sizing: border-box;
123 | padding: 0px 8px;
124 | max-height: 400px;
125 | overflow-y: scroll;
126 | `;
127 |
--------------------------------------------------------------------------------
/packages/core-react/menu/add-block-menu/icons.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export type BoringBlockIconType =
4 | | "text"
5 | //
6 | | "h1"
7 | | "h2"
8 | | "h3"
9 | | "hr"
10 | | "image"
11 | | "embed"
12 | | "video";
13 |
14 | export function BoringBlockIcon({ name }: { name: BoringBlockIconType }) {
15 | switch (name) {
16 | case "text":
17 | return <>>;
18 | case "h1":
19 | return ;
20 | case "h2":
21 | return ;
22 | case "h3":
23 | return ;
24 | case "hr":
25 | return ;
26 | case "image":
27 | return ;
28 | case "embed":
29 | return <>>;
30 | case "video":
31 | return ;
32 | default:
33 | return <>>;
34 | }
35 | }
36 |
37 | function Heading1BlockIcon() {
38 | return (
39 |
55 | );
56 | }
57 |
58 | function Heading2BlockIcon() {
59 | return (
60 |
76 | );
77 | }
78 |
79 | function Heading3BlockIcon() {
80 | return (
81 |
97 | );
98 | }
99 |
100 | function HorizontalRuleBlockIcon() {
101 | return (
102 |
111 | );
112 | }
113 |
114 | function ImageBlockIcon() {
115 | return (
116 |
144 | );
145 | }
146 |
147 | function VideoBlockIcon() {
148 | return (
149 |
166 | );
167 | }
168 |
--------------------------------------------------------------------------------
/packages/core-react/menu/add-block-menu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import { BlockList as AddBlockMenuBody } from "./block-list";
3 | import { Editor } from "@tiptap/react";
4 | import getSuggestionItems from "./items";
5 |
6 | export function AddBlockMenu({
7 | editor,
8 | onUploadFile,
9 | onHide,
10 | }: {
11 | editor: Editor;
12 | onUploadFile: (file: File) => Promise;
13 | onHide: () => void;
14 | }) {
15 | const ref = useRef();
16 | useEffect(() => {
17 | // focus to this element on mount
18 | ref.current?.focus();
19 | }, [ref]);
20 |
21 | useEffect(() => {
22 | const block = (e) => {
23 | e.stopPropagation();
24 | e.preventDefault();
25 | };
26 | document.addEventListener("keydown", block);
27 | return () => {
28 | document.removeEventListener("keydown", block);
29 | };
30 | }, []);
31 |
32 | return (
33 |
40 | {/* @ts-ignore */}
41 |
{
43 | c.command({
44 | editor,
45 | range: [
46 | editor.view.state.selection.from,
47 | editor.view.state.selection.to,
48 | ],
49 | });
50 | onHide();
51 | }}
52 | items={getSuggestionItems({ editor, onUploadFile })}
53 | />
54 |
55 | );
56 | }
57 |
58 | export { AddBlockMenuBody };
59 |
--------------------------------------------------------------------------------
/packages/core-react/menu/add-block-menu/items.ts:
--------------------------------------------------------------------------------
1 | import { Editor, ReactRenderer } from "@tiptap/react";
2 | import type { BoringBlockIconType } from "./icons";
3 | import { AddImageBlockMenu } from "../add-image-block-menu";
4 | import tippy from "tippy.js";
5 | import {
6 | get_youtube_video_id,
7 | make_youtube_video_embed_url,
8 | get_vimeo_video_id,
9 | make_vimeo_video_embed_url,
10 | } from "../../embeding-utils";
11 |
12 | export interface CommandItem {
13 | title: string;
14 | subtitle?: string;
15 | icon?: BoringBlockIconType;
16 | command: ({ editor, range }: { editor: Editor; range }) => void;
17 | }
18 |
19 | const getSuggestionItems = (
20 | {
21 | editor,
22 | query = "",
23 | onUploadFile,
24 | }: {
25 | editor: Editor;
26 | query?: string;
27 | onUploadFile: (file: File) => Promise;
28 | },
29 | ...args
30 | ): CommandItem[] => {
31 | // [(enable this to disable slash commands on revisited slashes.)]
32 | // const d = JSON.stringify(editor.getJSON());
33 | // const lastd = sessionStorage.getItem("last-content");
34 | // sessionStorage.setItem("last-content", d);
35 | // if (d.length < (lastd?.length ?? 0) || lastd === d) {
36 | // // if removed or same - it means "/" is not typed.
37 | // // somehow the d is not beign updated with '/' value
38 | // // this may not work when removing and re inserting the same value - '/'
39 | // // fix: save the data on every change, compare that with this. (somewhre else)
40 | // return [];
41 | // }
42 |
43 | return [
44 | {
45 | title: "H1",
46 | icon: "h1",
47 | subtitle: "Big section heading",
48 | command: ({ editor, range }) => {
49 | editor
50 | .chain()
51 | .focus()
52 | .deleteRange(range)
53 | .setNode("heading", { level: 1 })
54 | .run();
55 | },
56 | },
57 | {
58 | title: "H2",
59 | icon: "h2",
60 | subtitle: "Medium section heading",
61 | command: ({ editor, range }) => {
62 | editor
63 | .chain()
64 | .focus()
65 | .deleteRange(range)
66 | .setNode("heading", { level: 2 })
67 | .run();
68 | },
69 | },
70 | {
71 | title: "H3",
72 | icon: "h3",
73 | subtitle: "Smaller section heading",
74 | command: ({ editor, range }) => {
75 | editor
76 | .chain()
77 | .focus()
78 | .deleteRange(range)
79 | .setNode("heading", { level: 3 })
80 | .run();
81 | },
82 | },
83 | {
84 | title: "Divider",
85 | icon: "hr",
86 | subtitle: "Divide blocks with new line",
87 | command: ({ editor, range }) => {
88 | editor
89 | .chain()
90 | .focus()
91 | .deleteRange(range)
92 | // @ts-ignore
93 | .setHorizontalRule()
94 | .run();
95 | // create new empty block below (only for divider)
96 | editor.chain().focus().setNode("paragraph").run();
97 | },
98 | },
99 | // {
100 | // title: "Text",
101 | // icon: "text",
102 | // subtitle: "Paragraph text",
103 | // command: ({ editor, range }) => {
104 | // editor.chain().focus().deleteRange(range).setNode("paragraph").run();
105 | // },
106 | // },
107 | {
108 | title: "Image",
109 | icon: "image",
110 | subtitle: "Put an image from url or via upload",
111 | command: ({ editor, range }) => {
112 | // to dismiss the menu
113 | editor.chain().focus().deleteRange(range).run();
114 | // open file input
115 | const input = document.createElement("input");
116 | input.type = "file";
117 | input.accept = "image/*";
118 | input.style.display = "none";
119 | // @ts-ignore
120 | input.onchange = async () => {
121 | // @ts-ignore
122 | const file = input.files[0];
123 | if (!file || !file.type.includes("image/")) {
124 | return;
125 | }
126 |
127 | onUploadFile(file)
128 | ?.then((url) => {
129 | if (url) {
130 | editor
131 | .chain()
132 | .focus()
133 | .deleteRange(range)
134 | .setImage({ src: url })
135 | .run();
136 | } else {
137 | alert("could not upload file, please try again.");
138 | }
139 | })
140 | .finally(() => {
141 | input.remove();
142 | });
143 | };
144 | document.body.appendChild(input);
145 |
146 | input.click();
147 |
148 | return true;
149 | },
150 | },
151 | {
152 | title: "Video",
153 | icon: "video",
154 | subtitle: "Embed a Youtube or Vimeo video",
155 | command: ({ editor, range }) => {
156 | // dismiss the menu
157 | editor.chain().focus().deleteRange(range).run();
158 | setTimeout(() => {
159 | try {
160 | const _url = window.prompt("Enter a Youtube or Vimeo video url");
161 |
162 | if (_url) {
163 | const url = new URL(_url).toString();
164 | // parse the url, make the embed url
165 | if (_url.includes("youtube.com")) {
166 | const id = get_youtube_video_id(url);
167 | if (id) {
168 | const src = make_youtube_video_embed_url(id);
169 | editor
170 | .chain()
171 | .focus()
172 | .deleteRange(range)
173 | .setIframe({ src: src })
174 | .run();
175 | } else {
176 | alert("Not a valid Youtube URL");
177 | }
178 | return;
179 | }
180 |
181 | if (_url.includes("vimeo.com")) {
182 | const id = get_vimeo_video_id(url);
183 | if (id) {
184 | const src = make_vimeo_video_embed_url(id);
185 | editor
186 | .chain()
187 | .focus()
188 | .deleteRange(range)
189 | .setIframe({ src: src })
190 | .run();
191 | } else {
192 | alert("Not a valid Vimeo URL");
193 | }
194 | return;
195 | }
196 | }
197 | } catch (e) {
198 | // dismiss
199 | }
200 | }, 100);
201 | },
202 | },
203 | ]
204 | .filter((item) => item.title.toLowerCase().startsWith(query?.toLowerCase()))
205 | .slice(0, 10);
206 | };
207 |
208 | export default getSuggestionItems;
209 |
--------------------------------------------------------------------------------
/packages/core-react/menu/add-image-block-menu/README.md:
--------------------------------------------------------------------------------
1 | # Add iamge block menu
2 |
3 | this appreas when add image action is triggered (to show a ui).
4 |
5 | once shown, users can do one of below.
6 |
7 | - drop image on menu
8 | - paste link to an image on menu
9 | - dismiss
10 | - search image on image resources provier.
11 |
--------------------------------------------------------------------------------
/packages/core-react/menu/add-image-block-menu/add-image-block-menu.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | //
3 |
4 | export function AddImageBlockMenu() {
5 | return <>Add an image>;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/core-react/menu/add-image-block-menu/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./add-image-block-menu";
2 |
--------------------------------------------------------------------------------
/packages/core-react/menu/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./link-input-menu";
2 | export { AddBlockMenu, AddBlockMenuBody } from "./add-block-menu";
3 |
--------------------------------------------------------------------------------
/packages/core-react/menu/link-input-menu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useLayoutEffect } from "react";
2 | import styled from "@emotion/styled";
3 |
4 | export function BoringBubbleLinkInput({
5 | defaultValue,
6 | onChange,
7 | onSubmit,
8 | }: {
9 | defaultValue?: string;
10 | onChange?: (value: string) => void;
11 | onSubmit?: (url: string) => void;
12 | }) {
13 | // TODO: add url validation
14 | const ref = useRef();
15 | const [value, setValue] = useState(defaultValue);
16 |
17 | useLayoutEffect(() => {
18 | console.log(ref);
19 | if (ref.current) {
20 | ref.current.focus();
21 | }
22 | }, [ref]);
23 |
24 | return (
25 |
26 | {
29 | setValue(e.target.value);
30 | onChange?.(e.target.value);
31 | }}
32 | onKeyDown={(e) => {
33 | if (e.key === "Enter") {
34 | onSubmit?.(value);
35 | }
36 | }}
37 | value={value}
38 | defaultValue={defaultValue}
39 | type="text"
40 | placeholder="Pase link"
41 | autoFocus
42 | />
43 |
44 | );
45 | }
46 |
47 | const Container = styled.div`
48 | display: flex;
49 | justify-content: flex-start;
50 | flex-direction: column;
51 | align-items: flex-start;
52 | flex: none;
53 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
54 | border: solid 1px rgb(237, 237, 236);
55 | border-radius: 4px;
56 | background-color: white;
57 | box-sizing: border-box;
58 | padding: 18px 20px;
59 | `;
60 |
61 | const InputAsInput = styled.input`
62 | color: rgba(0, 0, 0, 0.87);
63 | background-color: rgba(0, 0, 0, 0.02);
64 | border: solid 3px rgba(35, 77, 255, 0.3);
65 | border-radius: 2px;
66 | padding: 4px 8px;
67 | box-sizing: border-box;
68 | font-size: 16px;
69 | font-family: Inter, sans-serif;
70 | font-weight: 500;
71 | text-align: start;
72 | align-self: stretch;
73 | flex-shrink: 0;
74 |
75 | ::placeholder {
76 | color: rgba(0, 0, 0, 0.3);
77 | font-family: Inter, sans-serif;
78 | font-weight: 500;
79 | }
80 | `;
81 |
--------------------------------------------------------------------------------
/packages/core-react/node-view-wrapper/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./node-view-wrapper"
--------------------------------------------------------------------------------
/packages/core-react/node-view-wrapper/node-view-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import { NodeViewWrapper as _NodeViewWrapper } from "@tiptap/react";
2 |
3 | export function NodeViewWrapper(props: {
4 | children: JSX.Element | JSX.Element[];
5 |
6 | /**
7 | * noninteractive is set to false by default, which means interactive by default. this will prevent parent's onclick event.
8 | */
9 | noninteractive?: boolean;
10 | }) {
11 | const eventcallback = !props.noninteractive
12 | ? (event) => {
13 | event.stopPropagation();
14 | }
15 | : undefined;
16 | return (
17 | <_NodeViewWrapper>
18 | {props.children}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/core-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boringso/react-core",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@boring.so/config": "workspace:^",
6 | "@boring.so/document-model": "workspace:^",
7 | "@boring.so/store": "workspace:^",
8 | "@floating-ui/react-dom-interactions": "^0.6.1",
9 | "@tippyjs/react": "^4.2.6",
10 | "@tiptap/core": "^2.1.13",
11 | "@tiptap/extension-blockquote": "^2.1.13",
12 | "@tiptap/extension-bold": "^2.11.2",
13 | "@tiptap/extension-bubble-menu": "^2.1.13",
14 | "@tiptap/extension-code-block-lowlight": "^2.1.13",
15 | "@tiptap/extension-collaboration": "^2.1.13",
16 | "@tiptap/extension-collaboration-cursor": "^2.1.13",
17 | "@tiptap/extension-image": "^2.1.13",
18 | "@tiptap/extension-link": "^2.1.13",
19 | "@tiptap/extension-mention": "^2.1.13",
20 | "@tiptap/extension-placeholder": "^2.1.13",
21 | "@tiptap/extension-underline": "^2.1.13",
22 | "@tiptap/pm": "^2.1.13",
23 | "@tiptap/react": "^2.1.13",
24 | "@tiptap/starter-kit": "^2.1.13",
25 | "@tiptap/suggestion": "^2.1.13",
26 | "lowlight": "^3.1.0",
27 | "prosemirror-state": "^1.4.3",
28 | "prosemirror-view": "^1.33.1",
29 | "react-textarea-autosize": "^8.3.3",
30 | "tippy.js": "^6.3.7",
31 | "y-indexeddb": "^9.0.7",
32 | "y-prosemirror": "^1.2.1",
33 | "y-webrtc": "^10.2.3",
34 | "yjs": "^13.5.38"
35 | },
36 | "devDependencies": {
37 | "@emotion/styled": "^11.11.0",
38 | "@types/react": "18.2.58",
39 | "react": "^18.2.0"
40 | },
41 | "resolutions": {
42 | "prosemirror-model": "1.14.2"
43 | },
44 | "peerDependencies": {
45 | "@emotion/styled": "^11.11.0",
46 | "react": "^18.2.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/core-react/scaffold/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo } from "react";
2 | import styled from "@emotion/styled";
3 | import { Title, TitleProps } from "../title";
4 | import { Node } from "@tiptap/core";
5 | import { MainBodyContentEditor } from "../content-editor";
6 | import type { EditorView } from "prosemirror-view";
7 | import {
8 | BoringContent,
9 | BoringDocument,
10 | BoringDocumentId,
11 | autoid,
12 | } from "@boring.so/document-model";
13 | import { EditorConfig, defaults as DefaultConfig } from "@boring.so/config";
14 | import { Editor, useEditor, EditorContent } from "@tiptap/react";
15 | import { default_extensions } from "./scaffold-extensions";
16 | import { BoringDocumentStore } from "@boring.so/store";
17 | import {
18 | get_youtube_video_id,
19 | make_youtube_video_embed_url,
20 | } from "../embeding-utils";
21 | import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
22 | import Collaboration from "@tiptap/extension-collaboration";
23 | import * as Y from "yjs";
24 | import { WebrtcProvider } from "y-webrtc";
25 |
26 | export type InitialDocumentProp =
27 | | {
28 | title?: string;
29 | content?: string;
30 | }
31 | | BoringDocument
32 | | BoringDocumentId;
33 |
34 | export type OnContentChange = (content: string, transaction?) => void;
35 |
36 | export interface ScaffoldProps {
37 | /**
38 | * defaults to false
39 | */
40 | fullWidth?: boolean;
41 |
42 | /**
43 | * boring in-content extended node blocks configuration
44 | */
45 | extensions?: Node[];
46 | contentmode?: "html" | "json";
47 |
48 | // region document model
49 | initial?: InitialDocumentProp;
50 | onTitleChange?: (title: string) => void;
51 |
52 | onContentChange?: OnContentChange;
53 | // endregion document model
54 |
55 | onTriggerSave?: () => void;
56 |
57 | config?: EditorConfig;
58 |
59 | readonly?: boolean;
60 |
61 | fileUploader?: (file: File) => Promise;
62 |
63 | titleStyle?: TitleProps["style"];
64 | collaboration?: {
65 | enabled: boolean;
66 | };
67 | }
68 |
69 | export function Scaffold({
70 | initial,
71 | fullWidth,
72 | onTitleChange,
73 | onContentChange,
74 | extensions,
75 | contentmode = "html",
76 | config = DefaultConfig,
77 | readonly = false,
78 | onTriggerSave,
79 | fileUploader,
80 | titleStyle,
81 | collaboration,
82 | ...props
83 | }: ScaffoldProps) {
84 | // region doc init
85 | const initializer = handleDocumentInitialization(initial);
86 | const id = initializer.id;
87 | const [title, setTitle] = useState(initializer.loaded?.title?.raw);
88 | const [content, setContent] = useState(
89 | initializer.loaded?.content?.raw
90 | );
91 |
92 | useEffect(() => {
93 | if (initializer.shouldload) {
94 | initializer.shouldload.then((d) => {
95 | // load title
96 | setTitle(d?.title?.raw ?? "");
97 | // load body conditionally
98 | if (collaboration?.enabled) {
99 | // do not set initial data. if already loaed by collaboration module
100 | if (editor && editor.getText()) return;
101 | }
102 | setContent(d?.content?.raw ?? "");
103 | });
104 | }
105 | }, [initializer.id]);
106 |
107 | // endregion doc init
108 |
109 | // store
110 | const service = new BoringDocumentStore(id);
111 |
112 | // region collaboration
113 | // A new Y document
114 | const ydoc = useMemo(
115 | () => (collaboration?.enabled ? new Y.Doc({}) : null),
116 | [id, collaboration?.enabled]
117 | );
118 | // Registered with a WebRTC provider
119 | const provider = useMemo(
120 | () =>
121 | collaboration?.enabled
122 | ? new WebrtcProvider("boring.grida.co" + id, ydoc)
123 | : null,
124 | [id, ydoc, collaboration?.enabled]
125 | );
126 | // endregion collaboration
127 |
128 | const finalcontent = _makecontent(content);
129 | const editor = useEditor(
130 | {
131 | extensions: [
132 | // StarterKit.configure({
133 | // gapcursor: false,
134 | // // history: collaboration?.enabled ? false : undefined,
135 | // history: false,
136 | // }),
137 | ...default_extensions({
138 | onUploadFile: fileUploader,
139 | }),
140 | ...(extensions ?? []),
141 | // region collaboration extensions
142 | ...(collaboration?.enabled
143 | ? [
144 | Collaboration.configure({
145 | document: ydoc,
146 | }),
147 | CollaborationCursor.configure({
148 | provider: provider,
149 | user: {
150 | name: "Editor",
151 | color: randomCursorColor(),
152 | },
153 | }),
154 | ]
155 | : []),
156 | // endregion collaboration extensions
157 | ] as any,
158 | content: finalcontent,
159 | // onTransaction: ({ editor, transaction }) => {
160 | // // editor.state.selection.anchor;
161 | // // editor.view.posAtDOM()
162 | // },
163 | editorProps: {
164 | handlePaste: (view, event: ClipboardEvent, slice) => {
165 | console.log("pasted", event, slice, view);
166 | const text = event.clipboardData.getData("Text");
167 | if (text) {
168 | // parse
169 | const id = get_youtube_video_id(text);
170 | if (id) {
171 | addIframe(make_youtube_video_embed_url(id));
172 | return true;
173 | }
174 | }
175 | return false;
176 | },
177 | handleDrop: function (view, event: DragEvent, slice, moved) {
178 | if (!moved && event.dataTransfer && event.dataTransfer.files) {
179 | const coordinates = view.posAtCoords({
180 | left: event.clientX,
181 | top: event.clientY,
182 | });
183 | // if dropping external files
184 | // the addImage function checks the files are an image upload, and returns the url
185 | for (let i = 0; i < event.dataTransfer.files.length; i++) {
186 | const file = event.dataTransfer.files.item(i);
187 | if (!file) return false
188 | fileUploader?.(file).then((url) => {
189 | if (url) {
190 | if (file.type.includes("image")) {
191 | return addImage(
192 | // @ts-ignore
193 | view,
194 | url,
195 | coordinates.pos
196 | );
197 | }
198 |
199 | if (file.type.includes("video")) {
200 | return addVideo(
201 | // @ts-ignore
202 | view,
203 | url,
204 | coordinates.pos
205 | );
206 | }
207 | } else {
208 | console.error("cannot upload file", event);
209 | return false;
210 | }
211 | });
212 | }
213 | return true; // drop is handled don't do anything else
214 | }
215 | return false; // not handled as wasn't dragging a file so use default behaviour
216 | },
217 | // TODO: when pos = 0, < or ^ key pressed, move to title
218 | // handleKeyPress: function (view, event) {
219 | // return true;
220 | // },
221 | },
222 | onUpdate: ({ editor, transaction }) => {
223 | const content = editor.getHTML();
224 | _oncontentchange(content, transaction);
225 | },
226 | },
227 | [content]
228 | );
229 |
230 | const addIframe = (url: string) => {
231 | if (url) {
232 | editor?.chain().focus().setIframe({ src: url }).run();
233 | }
234 | };
235 |
236 | // this inserts the image with src url into the editor at the position of the drop
237 | const addImage = (view: EditorView, url: string, pos: number) => {
238 | const { schema } = view.state;
239 | const node = schema.nodes.image.create({ src: url });
240 | const transaction = view.state.tr.insert(pos, node);
241 | return view.dispatch(transaction);
242 | };
243 |
244 | const addVideo = (view: EditorView, url: string, pos: number) => {
245 | const { schema } = view.state;
246 | const node = schema.nodes.video.create({ src: url });
247 | const transaction = view.state.tr.insert(pos, node);
248 | return view.dispatch(transaction);
249 | };
250 |
251 | const focustocontent = () => {
252 | editor?.chain().focus().run();
253 | };
254 |
255 | const focustotop = () => {
256 | editor?.chain().focus("start").run();
257 | };
258 |
259 | const _ontitlereturnhit = () => {
260 | focustotop();
261 | };
262 |
263 | const _ontitlechange = (t: string) => {
264 | onTitleChange?.(t);
265 | };
266 |
267 | const _oncontentchange = (c: string, transaction?) => {
268 | service.updateContent(c);
269 | //
270 | onContentChange?.(c, transaction);
271 | };
272 |
273 | return (
274 | {
277 | // if cmd + s ignore.
278 | if (e.metaKey && e.key === "s") {
279 | e.preventDefault();
280 | e.stopPropagation();
281 | onTriggerSave?.();
282 | }
283 | }}
284 | >
285 | {/* */}
286 |
291 | {title}
292 |
293 |
294 |
299 |
300 | );
301 | }
302 |
303 | function handleDocumentInitialization(initial: InitialDocumentProp): {
304 | id: BoringDocumentId;
305 | loaded?: BoringDocument;
306 | shouldload?: Promise;
307 | } {
308 | if (initial instanceof BoringDocument) {
309 | return {
310 | id: initial.id,
311 | loaded: initial,
312 | };
313 | } else if (typeof initial == "string") {
314 | // fetch document
315 | return {
316 | id: initial,
317 | shouldload: new BoringDocumentStore(initial).get(),
318 | };
319 | } else {
320 | const _newly = new BoringDocument({
321 | title: initial?.title,
322 | content: initial?.content,
323 | });
324 | return {
325 | id: _newly.id,
326 | loaded: _newly,
327 | };
328 | }
329 | }
330 |
331 | function _makecontent(raw: string | BoringContent): string {
332 | if (raw) {
333 | if (typeof raw == "string") {
334 | return raw;
335 | }
336 | return raw.raw;
337 | }
338 | }
339 |
340 | function randomCursorColor() {
341 | const colors = [
342 | "#f783ac",
343 | "#f7b7c4",
344 | "#f7d6a7",
345 | "#f7c7a7",
346 | "#f7b7a7",
347 | "#f7a7a7",
348 | "#f7a7b7",
349 | "#f7a7c7",
350 | "#f7a7d6",
351 | "#f7a7e6",
352 | "#f7a7f6",
353 | "#f7a7ff",
354 | "#f7d6ff",
355 | "#f7c6ff",
356 | "#f7b6ff",
357 | "#f7a6ff",
358 | "#f796ff",
359 | "#f7a6f7",
360 | "#f7a6e7",
361 | "#f7a6d7",
362 | "#f7a6c7",
363 | "#f7a6b7",
364 | "#f7a6a7",
365 | "#f7a6a7",
366 | "#f7a7a7",
367 | "#f7b7a7",
368 | "#f7c7a7",
369 | "#f7d6a7",
370 | "#f7e6a7",
371 | "#f7f6a7",
372 | "#ffa7a7",
373 | "#ffa7b7",
374 | "#ffa7c7",
375 | "#ffa7d7",
376 | "#ffa7e7",
377 | "#ffa7f7",
378 | "#ffa7ff",
379 | "#ffd6ff",
380 | "#ffc6ff",
381 | "#ffb6ff",
382 | "#ffa6ff",
383 | "#ff96ff",
384 | "#ffa6f7",
385 | "#ffa6e7",
386 | "#ffa6d7",
387 | "#ffa6c7",
388 | "#ffa6b7",
389 | "#ffa6a7",
390 | "#ffa6a7",
391 | "#ffa7a7",
392 | "#ffb7a7",
393 | "#ffc7a7",
394 | "#ffd6a7",
395 | "#ffe6",
396 | ];
397 | return colors[Math.floor(Math.random() * colors.length)];
398 | }
399 |
400 | const TitleAndEditorSeparator = styled.div`
401 | height: 32px;
402 | `;
403 |
404 | const EditorWrap = styled.div<{
405 | fullWidth?: boolean;
406 | }>`
407 | box-sizing: border-box;
408 | width: 100%;
409 | max-width: 1080px;
410 | height: 100%;
411 | margin: auto;
412 | padding: 160px ${(p) => (p.fullWidth ? "20px" : "80px")};
413 |
414 | @media (max-width: 600px) {
415 | padding: 120px 20px;
416 | }
417 | `;
418 |
--------------------------------------------------------------------------------
/packages/core-react/scaffold/scaffold-extensions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PlaceholderConfig,
3 | // CodeblockConfig,
4 | // BlockQuoteConfig,
5 | UnderlineConfig,
6 | SlashCommandConfig,
7 | } from "../extension-configs";
8 | import Image from "@tiptap/extension-image";
9 | import Link from "@tiptap/extension-link";
10 | import Iframe from "../blocks/iframe-block";
11 | import Video from "../blocks/video-block";
12 | import TrailingNode from "../blocks/trailing-node";
13 | import _StarterKit from "@tiptap/starter-kit";
14 |
15 | const StarterKit = _StarterKit.configure({
16 | gapcursor: false,
17 | });
18 |
19 | interface ExtensionsProps {
20 | onUploadFile: (file: File) => Promise;
21 | }
22 |
23 | export const default_extensions = (props: ExtensionsProps) => [
24 | TrailingNode,
25 | Image,
26 | Iframe,
27 | Video,
28 | SlashCommandConfig(props),
29 | StarterKit,
30 | PlaceholderConfig,
31 | UnderlineConfig,
32 |
33 | // included in starter kit
34 | // CodeblockConfig,
35 | // BlockQuoteConfig,
36 | Link,
37 | ];
38 |
--------------------------------------------------------------------------------
/packages/core-react/slash-commands/README.md:
--------------------------------------------------------------------------------
1 | # WIP - will resume when experiment label removed
2 |
3 | ref: https://www.tiptap.dev/experiments/commands/#commands
4 |
5 | issue: https://github.com/ueberdosis/tiptap/issues/1508
6 |
--------------------------------------------------------------------------------
/packages/core-react/slash-commands/commands.ts:
--------------------------------------------------------------------------------
1 | import { Extension } from "@tiptap/core";
2 | import Suggestion from "@tiptap/suggestion";
3 |
4 | const Commands = Extension.create({
5 | name: "slash-command-suggestion",
6 |
7 | addOptions: () => ({
8 | suggestion: {
9 | char: "/",
10 | allowSpaces: false,
11 | startOfLine: false,
12 | command: ({ editor, range, props }) => {
13 | props.command({ editor, range, props });
14 | },
15 | },
16 | }),
17 |
18 | addProseMirrorPlugins() {
19 | return [
20 | Suggestion({
21 | editor: this.editor,
22 | ...this.options.suggestion,
23 | }),
24 | ];
25 | },
26 | });
27 |
28 | export default Commands;
29 |
--------------------------------------------------------------------------------
/packages/core-react/slash-commands/index.ts:
--------------------------------------------------------------------------------
1 | import Commands from "./commands";
2 | import getSuggestionItems from "../menu/add-block-menu/items";
3 | import renderItems from "./render-items";
4 |
5 | export { Commands, getSuggestionItems, renderItems };
6 |
--------------------------------------------------------------------------------
/packages/core-react/slash-commands/render-items.tsx:
--------------------------------------------------------------------------------
1 | import { ReactRenderer } from "@tiptap/react";
2 | import tippy from "tippy.js";
3 | import { AddBlockMenuBody } from "../menu/add-block-menu";
4 |
5 | const renderItems = () => {
6 | let component;
7 | let popup;
8 | let hidden = false;
9 |
10 | return {
11 | onStart: (props) => {
12 | component = new ReactRenderer(AddBlockMenuBody, {
13 | props,
14 | editor: props.editor,
15 | });
16 |
17 | popup = tippy("body", {
18 | getReferenceClientRect: props.clientRect,
19 | appendTo: () => document.body,
20 | content: component.element,
21 | showOnCreate: true,
22 | interactive: true,
23 | trigger: "manual",
24 | placement: "bottom-start",
25 | });
26 | },
27 | onUpdate(props) {
28 | component.updateProps(props);
29 |
30 | popup[0].setProps({
31 | getReferenceClientRect: props.clientRect,
32 | });
33 | },
34 | onKeyDown(props) {
35 | if (props.event.key === "Escape") {
36 | popup[0].hide();
37 | hidden = true;
38 |
39 | return true;
40 | }
41 |
42 | if (hidden) {
43 | return false;
44 | }
45 |
46 | return component.ref?.onKeyDown(props);
47 | },
48 | onExit() {
49 | popup[0].destroy();
50 | component.destroy();
51 | },
52 | };
53 | };
54 |
55 | export default renderItems;
56 |
--------------------------------------------------------------------------------
/packages/core-react/table-of-contents/README.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | https://tiptap.dev/guide/node-views/examples#table-of-contents
4 |
--------------------------------------------------------------------------------
/packages/core-react/table-of-contents/index.ts:
--------------------------------------------------------------------------------
1 | import type { Editor } from "@tiptap/core";
2 |
3 | /**
4 | * get headings and update the id if required.
5 | * @param editor
6 | * @returns
7 | */
8 | export function getHeadingsFrom(editor: Editor) {
9 | const headings = [];
10 | const transaction = editor.state.tr;
11 |
12 | editor.state.doc.descendants((node, pos) => {
13 | if (node.type.name === "heading") {
14 | const id = `heading-i${headings.length + 1}`;
15 |
16 | if (node.attrs.id !== id) {
17 | transaction.setNodeMarkup(pos, undefined, {
18 | ...node.attrs,
19 | id,
20 | });
21 | }
22 |
23 | headings.push({
24 | level: node.attrs.level,
25 | text: node.textContent,
26 | id,
27 | });
28 | }
29 | });
30 |
31 | transaction.setMeta("addToHistory", false);
32 | transaction.setMeta("preventUpdate", true);
33 |
34 | editor.view.dispatch(transaction);
35 | return headings;
36 | }
37 |
--------------------------------------------------------------------------------
/packages/core-react/theme/font-family.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_THEME_FONT_FAMILY = `ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";`;
2 |
--------------------------------------------------------------------------------
/packages/core-react/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./font-family";
2 |
--------------------------------------------------------------------------------
/packages/core-react/title/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./title";
2 |
--------------------------------------------------------------------------------
/packages/core-react/title/title.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import styled from "@emotion/styled";
3 | import TextareaAutoresize from "react-textarea-autosize";
4 | import { DEFAULT_THEME_FONT_FAMILY } from "../theme";
5 |
6 | export interface TitleProps {
7 | children: string | undefined;
8 |
9 | /**
10 | * placeholder of title. - @todo - not implemented
11 | */
12 | placeholder?: string;
13 | noplaceholder?: boolean;
14 |
15 | style?: {
16 | /**
17 | * @default start
18 | */
19 | textAlign?: React.CSSProperties["textAlign"];
20 | /**
21 | * @default 48px
22 | */
23 | fontSize?: React.CSSProperties["fontSize"];
24 | fontFamily?: React.CSSProperties["fontFamily"];
25 | /**
26 | * @default bold
27 | */
28 | fontWeight?: React.CSSProperties["fontWeight"];
29 | };
30 |
31 | /**
32 | * title is only allowed to be single line. when enter key hit, it should be handled.
33 | */
34 | onReturn?: () => void;
35 | onChange?: (title: string) => void;
36 | }
37 |
38 | const DEFAULT_PLACEHOLDER_TEXT = "Untitled";
39 | export function Title(props: TitleProps) {
40 | const fieldref = useRef();
41 |
42 | const shouldReturn = (e) => {
43 | // about arrow keycodes - https://stackoverflow.com/a/5597114/5463235
44 | // 13 = return key
45 | // 40 = down key
46 | // 39 = right key
47 | const isNewLineEnter = e.keyCode === 13;
48 | const isDownKeyPress = e.keyCode === 40;
49 | const isCursorEnd =
50 | fieldref.current?.selectionEnd === fieldref.current?.value.length;
51 | const isCursorMoveRightOnEnd = e.keyCode === 39 && isCursorEnd;
52 | const isCursorMoveDownOnEnd = isDownKeyPress && isCursorEnd;
53 |
54 | if (isNewLineEnter || isCursorMoveDownOnEnd || isCursorMoveRightOnEnd) {
55 | props.onReturn?.();
56 |
57 | // disable line break
58 | e.preventDefault();
59 | }
60 | };
61 |
62 | const onkeydown = (e) => {
63 | shouldReturn(e);
64 | };
65 |
66 | const onchange = (e) => {
67 | const title = e.target.value;
68 | props.onChange?.(title);
69 | };
70 |
71 | return (
72 | <_Wrap>
73 |
88 |
89 | );
90 | }
91 |
92 | const _Wrap = styled.div`
93 | max-width: 100%;
94 | `;
95 |
96 | const TitleText = styled(TextareaAutoresize)<{
97 | textAlign?: React.CSSProperties["textAlign"];
98 | fontSize?: React.CSSProperties["fontSize"];
99 | fontFamily?: React.CSSProperties["fontFamily"];
100 | fontWeight?: React.CSSProperties["fontWeight"];
101 | }>`
102 | border: none;
103 | background: transparent;
104 | user-select: none;
105 | width: 100%;
106 | resize: none;
107 | text-align: ${(props) => props.textAlign ?? "start"};
108 | font-size: ${(props) => props.fontSize ?? "48px"};
109 | font-weight: ${(props) => props.fontWeight ?? "bold"};
110 | font-family: ${(props) => props.fontFamily ?? DEFAULT_THEME_FONT_FAMILY};
111 |
112 | :focus {
113 | outline: none;
114 | }
115 |
116 | ::placeholder {
117 | color: rgba(0, 0, 0, 0.12);
118 | }
119 | `;
120 |
--------------------------------------------------------------------------------
/packages/core-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "moduleResolution": "Node",
6 | "removeComments": true,
7 | "noImplicitAny": false,
8 | "allowJs": false,
9 | "allowSyntheticDefaultImports": true,
10 | "skipDefaultLibCheck": true,
11 | "skipLibCheck": true,
12 | "experimentalDecorators": true,
13 | "importHelpers": true,
14 | "pretty": true,
15 | "sourceMap": true,
16 | "strict": true,
17 | "jsx": "react",
18 | "esModuleInterop": true,
19 | "forceConsistentCasingInFileNames": true,
20 | "noEmitHelpers": true,
21 | "noEmitOnError": true,
22 | "noErrorTruncation": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "noImplicitReturns": true,
25 | "declaration": true,
26 | "lib": ["es2018", "es2017", "esnext", "dom", "esnext.asynciterable"],
27 | "outDir": "./dist"
28 | },
29 | "exclude": [
30 | "node_modules",
31 | "dist",
32 | "src/__tests__",
33 | "src/**/__tests__/**/*.*",
34 | "src/**/__mocks__/**/*.*",
35 | "*.test.ts",
36 | "*.spec.ts"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint-config-common"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------