├── .gitignore
├── README.md
├── tiptap-svelte-examples
├── .gitignore
├── README.md
├── appveyor.yml
├── cypress.json
├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── integration
│ │ └── spec.js
│ ├── plugins
│ │ └── index.js
│ └── support
│ │ ├── commands.js
│ │ └── index.js
├── package.json
├── src
│ ├── assets
│ │ ├── images
│ │ │ └── icons
│ │ │ │ ├── add_col_after.svg
│ │ │ │ ├── add_col_before.svg
│ │ │ │ ├── add_row_after.svg
│ │ │ │ ├── add_row_before.svg
│ │ │ │ ├── bold.svg
│ │ │ │ ├── checklist.svg
│ │ │ │ ├── code.svg
│ │ │ │ ├── combine_cells.svg
│ │ │ │ ├── delete_col.svg
│ │ │ │ ├── delete_row.svg
│ │ │ │ ├── delete_table.svg
│ │ │ │ ├── github.svg
│ │ │ │ ├── hr.svg
│ │ │ │ ├── image.svg
│ │ │ │ ├── italic.svg
│ │ │ │ ├── link.svg
│ │ │ │ ├── mention.svg
│ │ │ │ ├── ol.svg
│ │ │ │ ├── paragraph.svg
│ │ │ │ ├── quote.svg
│ │ │ │ ├── redo.svg
│ │ │ │ ├── remove.svg
│ │ │ │ ├── strike.svg
│ │ │ │ ├── table.svg
│ │ │ │ ├── ul.svg
│ │ │ │ ├── underline.svg
│ │ │ │ └── undo.svg
│ │ ├── sass
│ │ │ ├── editor.scss
│ │ │ ├── main.scss
│ │ │ ├── menubar.scss
│ │ │ ├── menububble.scss
│ │ │ └── variables.scss
│ │ └── static
│ │ │ └── images
│ │ │ ├── favicon.ico
│ │ │ └── open-graph.png
│ ├── client.js
│ ├── components
│ │ ├── Hero
│ │ │ ├── index.svelte
│ │ │ └── style.scss
│ │ ├── Icon
│ │ │ ├── index.svelte
│ │ │ └── style.scss
│ │ ├── Navigation
│ │ │ ├── index.svelte
│ │ │ └── style.scss
│ │ └── Subnavigation
│ │ │ ├── index.svelte
│ │ │ └── style.scss
│ ├── helpers
│ │ └── svg-sprite-loader.js
│ ├── routes
│ │ ├── _error.svelte
│ │ ├── _layout.scss
│ │ ├── _layout.svelte
│ │ ├── basic
│ │ │ └── index.svelte
│ │ ├── code-highlighting
│ │ │ ├── examples.js
│ │ │ └── index.svelte
│ │ ├── collaboration
│ │ │ └── index.svelte
│ │ ├── drag-handle
│ │ │ ├── DragItem.js
│ │ │ ├── DragItem.svelte
│ │ │ └── index.svelte
│ │ ├── embeds
│ │ │ ├── Iframe.js
│ │ │ ├── Iframe.svelte
│ │ │ └── index.svelte
│ │ ├── export
│ │ │ └── index.svelte
│ │ ├── floating-menu
│ │ │ └── index.svelte
│ │ ├── focus
│ │ │ └── index.svelte
│ │ ├── hiding-menu-bar
│ │ │ └── index.svelte
│ │ ├── history
│ │ │ └── index.svelte
│ │ ├── images
│ │ │ └── index.svelte
│ │ ├── index.svelte
│ │ ├── links
│ │ │ └── index.svelte
│ │ ├── markdown-shortcuts
│ │ │ └── index.svelte
│ │ ├── menu-bubble
│ │ │ └── index.svelte
│ │ ├── placeholder
│ │ │ └── index.svelte
│ │ ├── read-only
│ │ │ └── index.svelte
│ │ ├── search-and-replace
│ │ │ └── index.svelte
│ │ ├── suggestions
│ │ │ └── index.svelte
│ │ ├── tables
│ │ │ └── index.svelte
│ │ ├── title
│ │ │ ├── Doc.js
│ │ │ ├── Title.js
│ │ │ └── index.svelte
│ │ ├── todo-list
│ │ │ └── index.svelte
│ │ └── trailing-paragraph
│ │ │ └── index.svelte
│ ├── server.js
│ ├── service-worker.js
│ └── template.html
├── static
│ ├── favicon.png
│ ├── global.css
│ ├── logo-192.png
│ ├── logo-512.png
│ └── manifest.json
├── svelte.config.js
├── webpack.config.js
└── yarn.lock
├── tiptap-svelte-extensions
├── README.md
├── package.json
├── src
│ ├── extensions
│ │ ├── Collaboration.js
│ │ ├── Focus.js
│ │ ├── History.js
│ │ ├── Placeholder.js
│ │ ├── Search.js
│ │ └── TrailingNode.js
│ ├── index.js
│ ├── marks
│ │ ├── Bold.js
│ │ ├── Code.js
│ │ ├── Italic.js
│ │ ├── Link.js
│ │ ├── Strike.js
│ │ └── Underline.js
│ ├── nodes
│ │ ├── Blockquote.js
│ │ ├── BulletList.js
│ │ ├── CodeBlock.js
│ │ ├── CodeBlockHighlight.js
│ │ ├── HardBreak.js
│ │ ├── Heading.js
│ │ ├── HorizontalRule.js
│ │ ├── Image.js
│ │ ├── ListItem.js
│ │ ├── Mention.js
│ │ ├── OrderedList.js
│ │ ├── Table.js
│ │ ├── TableCell.js
│ │ ├── TableHeader.js
│ │ ├── TableNodes.js
│ │ ├── TableRow.js
│ │ ├── TodoItem.js
│ │ ├── TodoItem.svelte
│ │ └── TodoList.js
│ └── plugins
│ │ ├── Highlight.js
│ │ └── Suggestions.js
└── yarn.lock
└── tiptap-svelte
├── README.md
├── package.json
├── src
├── Components
│ ├── EditorContent.svelte
│ ├── EditorFloatingMenu.svelte
│ ├── EditorMenuBar.svelte
│ └── EditorMenuBubble.svelte
├── Editor.js
├── Nodes
│ ├── Doc.js
│ ├── Paragraph.js
│ ├── Text.js
│ └── index.js
├── Plugins
│ ├── FloatingMenu.js
│ ├── MenuBar.js
│ └── MenuBubble.js
├── Utils
│ ├── ComponentView.js
│ ├── Emitter.js
│ ├── Extension.js
│ ├── ExtensionManager.js
│ ├── Mark.js
│ ├── Node.js
│ ├── camelCase.js
│ ├── index.js
│ ├── injectCSS.js
│ └── minMax.js
├── index.js
└── style.css
├── test
└── Editor.spec.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist/
4 | coverage/
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw*
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tiptap-svelte
2 |
3 | NOTE: This repository is now archived as tiptap 2.0 has [official support for Svelte](https://tiptap.dev/installation/svelte/).
4 |
5 | ## About
6 |
7 | This editor is ported from [tiptap](https://tiptap.scrumpy.io), which is based on [Prosemirror](https://prosemirror.net).
8 |
9 | It is *fully extendable* and renderless. You can easily add custom nodes as __Svelte components__.
10 |
11 | ## Getting started
12 |
13 | TODO: Package for NPM
14 |
15 | ```bash
16 | yarn add -D tiptap-svelte etc
17 | ```
18 |
19 | ## Running the examples
20 |
21 | ```bash
22 | # clone tiptap-svelte
23 | git clone https://github.com/andrewjk/tiptap-svelte.git
24 | cd tiptap-svelte
25 |
26 | # install the dependencies for each project
27 | cd tiptap-svelte && yarn install && cd ..
28 | cd tiptap-svelte-extensions && yarn install && cd ..
29 | cd tiptap-svelte-examples && yarn install && cd ..
30 |
31 | # run the examples project
32 | cd tiptap-svelte-examples
33 | yarn run dev
34 | ```
35 |
36 | Then point your browser to [http://localhost:3000](http://localhost:3000).
37 |
38 | ## Basic setup
39 |
40 | ```
41 |
60 |
61 | {#if editor}
62 |
63 | {/if}
64 | ```
65 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | yarn-error.log
4 | /cypress/screenshots/
5 | /__sapper__/
6 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/README.md:
--------------------------------------------------------------------------------
1 | # sapper-template
2 |
3 | The default [Sapper](https://github.com/sveltejs/sapper) template, with branches for Rollup and webpack. To clone it and get started:
4 |
5 | ```bash
6 | # for Rollup
7 | npx degit "sveltejs/sapper-template#rollup" my-app
8 | # for webpack
9 | npx degit "sveltejs/sapper-template#webpack" my-app
10 | cd my-app
11 | npm install # or yarn!
12 | npm run dev
13 | ```
14 |
15 | Open up [localhost:3000](http://localhost:3000) and start clicking around.
16 |
17 | Consult [sapper.svelte.dev](https://sapper.svelte.dev) for help getting started.
18 |
19 |
20 | ## Structure
21 |
22 | Sapper expects to find two directories in the root of your project — `src` and `static`.
23 |
24 |
25 | ### src
26 |
27 | The [src](src) directory contains the entry points for your app — `client.js`, `server.js` and (optionally) a `service-worker.js` — along with a `template.html` file and a `routes` directory.
28 |
29 |
30 | #### src/routes
31 |
32 | This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
33 |
34 | **Pages** are Svelte components written in `.svelte` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
35 |
36 | **Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
37 |
38 | There are three simple rules for naming the files that define your routes:
39 |
40 | * A file called `src/routes/about.svelte` corresponds to the `/about` route. A file called `src/routes/blog/[slug].svelte` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
41 | * The file `src/routes/index.svelte` (or `src/routes/index.js`) corresponds to the root of your app. `src/routes/about/index.svelte` is treated the same as `src/routes/about.svelte`.
42 | * Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `src/routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
43 |
44 |
45 | ### static
46 |
47 | The [static](static) directory contains any static assets that should be available. These are served using [sirv](https://github.com/lukeed/sirv).
48 |
49 | In your [service-worker.js](src/service-worker.js) file, you can import these as `files` from the generated manifest...
50 |
51 | ```js
52 | import { files } from '@sapper/service-worker';
53 | ```
54 |
55 | ...so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
56 |
57 |
58 | ## Bundler config
59 |
60 | Sapper uses Rollup or webpack to provide code-splitting and dynamic imports, as well as compiling your Svelte components. With webpack, it also provides hot module reloading. As long as you don't do anything daft, you can edit the configuration files to add whatever plugins you'd like.
61 |
62 |
63 | ## Production mode and deployment
64 |
65 | To start a production version of your app, run `npm run build && npm start`. This will disable live reloading, and activate the appropriate bundler plugins.
66 |
67 | You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
68 |
69 | ```bash
70 | npm install -g now
71 | now
72 | ```
73 |
74 |
75 | ## Using external components
76 |
77 | When using Svelte components installed from npm, such as [@sveltejs/svelte-virtual-list](https://github.com/sveltejs/svelte-virtual-list), Svelte needs the original component source (rather than any precompiled JavaScript that ships with the component). This allows the component to be rendered server-side, and also keeps your client-side app smaller.
78 |
79 | Because of that, it's essential that the bundler doesn't treat the package as an *external dependency*. You can either modify the `external` option under `server` in [rollup.config.js](rollup.config.js) or the `externals` option in [webpack.config.js](webpack.config.js), or simply install the package to `devDependencies` rather than `dependencies`, which will cause it to get bundled (and therefore compiled) with your app:
80 |
81 | ```bash
82 | npm install -D @sveltejs/svelte-virtual-list
83 | ```
84 |
85 |
86 | ## Bugs and feedback
87 |
88 | Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
89 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: "{build}"
2 |
3 | shallow_clone: true
4 |
5 | init:
6 | - git config --global core.autocrlf false
7 |
8 | build: off
9 |
10 | environment:
11 | matrix:
12 | # node.js
13 | - nodejs_version: stable
14 |
15 | install:
16 | - ps: Install-Product node $env:nodejs_version
17 | - npm install cypress
18 | - npm install
19 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3010",
3 | "video": false
4 | }
--------------------------------------------------------------------------------
/tiptap-svelte-examples/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/tiptap-svelte-examples/cypress/integration/spec.js:
--------------------------------------------------------------------------------
1 | describe('Sapper template app', () => {
2 | beforeEach(() => {
3 | cy.visit('/')
4 | });
5 |
6 | it('has the correct
', () => {
7 | cy.contains('h1', 'Great success!')
8 | });
9 |
10 | it('navigates to /about', () => {
11 | cy.get('nav a').contains('about').click();
12 | cy.url().should('include', '/about');
13 | });
14 |
15 | it('navigates to /blog', () => {
16 | cy.get('nav a').contains('blog').click();
17 | cy.url().should('include', '/blog');
18 | });
19 | });
--------------------------------------------------------------------------------
/tiptap-svelte-examples/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | }
18 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tiptap-svelte-examples",
3 | "version": "0.0.0",
4 | "description": "Examples for tiptap-svelte",
5 | "scripts": {
6 | "dev": "sapper dev",
7 | "build": "sapper build",
8 | "export": "sapper export",
9 | "start": "node __sapper__/build",
10 | "cy:run": "cypress run",
11 | "cy:open": "cypress open",
12 | "test": "run-p --race dev cy:run"
13 | },
14 | "dependencies": {
15 | "compression": "^1.7.4",
16 | "polka": "0.5.2",
17 | "sirv": "^0.4.2"
18 | },
19 | "devDependencies": {
20 | "css-loader": "^3.4.2",
21 | "fuse.js": "^3.4.6",
22 | "highlight.js": "^9.18.1",
23 | "node-sass": "^4.13.1",
24 | "npm-run-all": "^4.1.5",
25 | "postcss": "^7.0.26",
26 | "sapper": "^0.27.9",
27 | "sass-loader": "^8.0.2",
28 | "socket.io-client": "^2.3.0",
29 | "style-loader": "^1.1.3",
30 | "svelte": "^3.18.1",
31 | "svelte-loader": "^2.13.6",
32 | "svelte-preprocess": "^3.4.0",
33 | "tippy.js": "^4.3.5",
34 | "tiptap-commands": "^1.12.5",
35 | "tiptap-utils": "^1.8.3",
36 | "webpack": "^4.41.5",
37 | "webpack-svgstore-plugin": "^4.1.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/add_col_after.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/add_col_before.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/add_row_after.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/add_row_before.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/bold.svg:
--------------------------------------------------------------------------------
1 | text-bold
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/checklist.svg:
--------------------------------------------------------------------------------
1 | checklist-alternate
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/code.svg:
--------------------------------------------------------------------------------
1 | angle-brackets
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/combine_cells.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/delete_col.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/delete_row.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/delete_table.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/hr.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/image.svg:
--------------------------------------------------------------------------------
1 | paginate-filter-picture-alternate
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/italic.svg:
--------------------------------------------------------------------------------
1 | text-italic
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/link.svg:
--------------------------------------------------------------------------------
1 | hyperlink-2
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/mention.svg:
--------------------------------------------------------------------------------
1 | read-email-at-alternate
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/ol.svg:
--------------------------------------------------------------------------------
1 | list-numbers
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/paragraph.svg:
--------------------------------------------------------------------------------
1 | paragraph
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/quote.svg:
--------------------------------------------------------------------------------
1 | close-quote
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/redo.svg:
--------------------------------------------------------------------------------
1 | redo
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/remove.svg:
--------------------------------------------------------------------------------
1 | delete-2-alternate
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/strike.svg:
--------------------------------------------------------------------------------
1 | text-strike-through
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/table.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/ul.svg:
--------------------------------------------------------------------------------
1 | list-bullets
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/underline.svg:
--------------------------------------------------------------------------------
1 | text-underline
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/images/icons/undo.svg:
--------------------------------------------------------------------------------
1 | undo
2 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/sass/editor.scss:
--------------------------------------------------------------------------------
1 | .editor {
2 | position: relative;
3 | max-width: 30rem;
4 | margin: 0 auto 5rem auto;
5 |
6 | &__content {
7 |
8 | overflow-wrap: break-word;
9 | word-wrap: break-word;
10 | word-break: break-word;
11 |
12 | * {
13 | caret-color: currentColor;
14 | }
15 |
16 | pre {
17 | padding: 0.7rem 1rem;
18 | border-radius: 5px;
19 | background: $color-black;
20 | color: $color-white;
21 | font-size: 0.8rem;
22 | overflow-x: auto;
23 |
24 | code {
25 | display: block;
26 | }
27 | }
28 |
29 | p code {
30 | display: inline-block;
31 | padding: 0 0.4rem;
32 | border-radius: 5px;
33 | font-size: 0.8rem;
34 | font-weight: bold;
35 | background: rgba($color-black, 0.1);
36 | color: rgba($color-black, 0.8);
37 | }
38 |
39 | ul,
40 | ol {
41 | padding-left: 1rem;
42 | }
43 |
44 | li > p,
45 | li > ol,
46 | li > ul {
47 | margin: 0;
48 | }
49 |
50 | a {
51 | color: inherit;
52 | }
53 |
54 | blockquote {
55 | border-left: 3px solid rgba($color-black, 0.1);
56 | color: rgba($color-black, 0.8);
57 | padding-left: 0.8rem;
58 | font-style: italic;
59 |
60 | p {
61 | margin: 0;
62 | }
63 | }
64 |
65 | img {
66 | max-width: 100%;
67 | border-radius: 3px;
68 | }
69 |
70 | table {
71 | border-collapse: collapse;
72 | table-layout: fixed;
73 | width: 100%;
74 | margin: 0;
75 | overflow: hidden;
76 |
77 | td, th {
78 | min-width: 1em;
79 | border: 2px solid $color-grey;
80 | padding: 3px 5px;
81 | vertical-align: top;
82 | box-sizing: border-box;
83 | position: relative;
84 | > * {
85 | margin-bottom: 0;
86 | }
87 | }
88 |
89 | th {
90 | font-weight: bold;
91 | text-align: left;
92 | }
93 |
94 | .selectedCell:after {
95 | z-index: 2;
96 | position: absolute;
97 | content: "";
98 | left: 0; right: 0; top: 0; bottom: 0;
99 | background: rgba(200, 200, 255, 0.4);
100 | pointer-events: none;
101 | }
102 |
103 | .column-resize-handle {
104 | position: absolute;
105 | right: -2px; top: 0; bottom: 0;
106 | width: 4px;
107 | z-index: 20;
108 | background-color: #adf;
109 | pointer-events: none;
110 | }
111 | }
112 |
113 | .tableWrapper {
114 | margin: 1em 0;
115 | overflow-x: auto;
116 | }
117 |
118 | .resize-cursor {
119 | cursor: ew-resize;
120 | cursor: col-resize;
121 | }
122 |
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/sass/main.scss:
--------------------------------------------------------------------------------
1 | @import "./variables";
2 |
3 | * {
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: border-box;
7 | text-size-adjust: 100%;
8 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
9 | -webkit-touch-callout: none;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | text-rendering: optimizeLegibility;
13 |
14 | &:focus {
15 | outline: none;
16 | }
17 | }
18 |
19 | *::before,
20 | *::after {
21 | box-sizing: border-box;
22 | }
23 |
24 | html {
25 | font-family: -apple-system, BlinkMacSystemFont, San Francisco, Roboto, Segoe UI, Helvetica Neue, sans-serif;
26 | font-size: 18px;
27 | color: $color-black;
28 | line-height: 1.5;
29 | }
30 |
31 | body {
32 | margin: 0;
33 | }
34 |
35 | a {
36 | color: inherit;
37 | }
38 |
39 | h1,
40 | h2,
41 | h3,
42 | p,
43 | ul,
44 | ol,
45 | pre,
46 | blockquote {
47 | margin: 1rem 0;
48 |
49 | &:first-child {
50 | margin-top: 0;
51 | }
52 |
53 | &:last-child {
54 | margin-bottom: 0;
55 | }
56 | }
57 |
58 | h1,
59 | h2,
60 | h3 {
61 | line-height: 1.3;
62 | }
63 |
64 | .button {
65 | font-weight: bold;
66 | display: inline-flex;
67 | background: transparent;
68 | border: 0;
69 | color: $color-black;
70 | padding: 0.2rem 0.5rem;
71 | margin-right: 0.2rem;
72 | border-radius: 3px;
73 | cursor: pointer;
74 | background-color: rgba($color-black, 0.1);
75 |
76 | &:hover {
77 | background-color: rgba($color-black, 0.15);
78 | }
79 | }
80 |
81 | .message {
82 | background-color: rgba($color-black, 0.05);
83 | color: rgba($color-black, 0.7);
84 | padding: 1rem;
85 | border-radius: 6px;
86 | margin-bottom: 1.5rem;
87 | font-style: italic;
88 | }
89 |
90 | @import "./editor";
91 | @import "./menubar";
92 | @import "./menububble";
93 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/sass/menubar.scss:
--------------------------------------------------------------------------------
1 | .menubar {
2 |
3 | margin-bottom: 1rem;
4 | transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
5 |
6 | &.is-hidden {
7 | visibility: hidden;
8 | opacity: 0;
9 | }
10 |
11 | &.focused {
12 | visibility: visible;
13 | opacity: 1;
14 | transition: visibility 0.2s, opacity 0.2s;
15 | }
16 |
17 | &__button {
18 | font-weight: bold;
19 | display: inline-flex;
20 | background: transparent;
21 | border: 0;
22 | color: $color-black;
23 | padding: 0.2rem 0.5rem;
24 | margin-right: 0.2rem;
25 | border-radius: 3px;
26 | cursor: pointer;
27 |
28 | &:hover {
29 | background-color: rgba($color-black, 0.05);
30 | }
31 |
32 | &.active {
33 | background-color: rgba($color-black, 0.1);
34 | }
35 | }
36 |
37 | span#{&}__button {
38 | font-size: 13.3333px;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/sass/menububble.scss:
--------------------------------------------------------------------------------
1 | .menububble {
2 | position: absolute;
3 | display: flex;
4 | z-index: 20;
5 | background: $color-black;
6 | border-radius: 5px;
7 | padding: 0.3rem;
8 | margin-bottom: 0.5rem;
9 | transform: translateX(-50%);
10 | visibility: hidden;
11 | opacity: 0;
12 | transition: opacity 0.2s, visibility 0.2s;
13 |
14 | &.active {
15 | opacity: 1;
16 | visibility: visible;
17 | }
18 |
19 | &__button {
20 | display: inline-flex;
21 | background: transparent;
22 | border: 0;
23 | color: $color-white;
24 | padding: 0.2rem 0.5rem;
25 | margin-right: 0.2rem;
26 | border-radius: 3px;
27 | cursor: pointer;
28 |
29 | &:last-child {
30 | margin-right: 0;
31 | }
32 |
33 | &:hover {
34 | background-color: rgba($color-white, 0.1);
35 | }
36 |
37 | &.active {
38 | background-color: rgba($color-white, 0.2);
39 | }
40 | }
41 |
42 | &__form {
43 | display: flex;
44 | align-items: center;
45 | }
46 |
47 | &__input {
48 | font: inherit;
49 | border: none;
50 | background: transparent;
51 | color: $color-white;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/sass/variables.scss:
--------------------------------------------------------------------------------
1 | $color-black: #000000;
2 | $color-white: #ffffff;
3 | $color-grey: #dddddd;
4 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewjk/tiptap-svelte/6e07b9862186bd6319164a64d8402a81ea539384/tiptap-svelte-examples/src/assets/static/images/favicon.ico
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/assets/static/images/open-graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewjk/tiptap-svelte/6e07b9862186bd6319164a64d8402a81ea539384/tiptap-svelte-examples/src/assets/static/images/open-graph.png
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/client.js:
--------------------------------------------------------------------------------
1 | import * as sapper from '@sapper/app'
2 | import svgSpriteLoader from './helpers/svg-sprite-loader'
3 | import './assets/sass/main.scss'
4 |
5 | const __svg__ = { path: './assets/images/icons/*.svg', name: 'assets/images/[hash].sprite.svg' }
6 | svgSpriteLoader(__svg__.filename)
7 |
8 | sapper.start({
9 | target: document.querySelector('#sapper'),
10 | })
11 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/components/Hero/index.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
13 |
60 |
61 |
tiptap-svelte
62 |
A renderless and extendable rich-text editor for Svelte
63 |
64 |
65 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/components/Hero/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../assets/sass/variables";
2 |
3 | .hero {
4 | background-color: $color-black;
5 | color: $color-white;
6 | text-align: center;
7 | padding: 3rem 1rem;
8 |
9 | &__inner {
10 | margin: 0 auto;
11 | max-width: 30rem;
12 | }
13 |
14 | &__logo {
15 | width: 4rem;
16 | height: 4rem;
17 |
18 | path {
19 | fill: $color-white;
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/components/Icon/index.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
21 |
22 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/components/Icon/style.scss:
--------------------------------------------------------------------------------
1 | .icon {
2 | position: relative;
3 | display: inline-block;
4 | vertical-align: middle;
5 | width: 0.8rem;
6 | height: 0.8rem;
7 | margin: 0 0.3rem;
8 | top: -0.05rem;
9 | fill: currentColor;
10 |
11 | //&.has-align-fix {
12 | // top: -.1rem;
13 | //}
14 |
15 | &__svg {
16 | display: inline-block;
17 | vertical-align: top;
18 | width: 100%;
19 | height: 100%;
20 | }
21 |
22 | &:first-child {
23 | margin-left: 0;
24 | }
25 |
26 | &:last-child {
27 | margin-right: 0;
28 | }
29 | }
30 |
31 | // svg sprite
32 | body > svg,
33 | .icon use > svg,
34 | symbol {
35 | path,
36 | rect,
37 | circle,
38 | g {
39 | fill: currentColor;
40 | stroke: none;
41 | }
42 |
43 | *[d="M0 0h24v24H0z"] {
44 | display: none;
45 | }
46 | }
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/components/Navigation/index.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
tiptap
14 |
19 |
20 |
21 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/components/Navigation/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../assets/sass/variables";
2 |
3 | .navigation {
4 | display: flex;
5 | justify-content: space-between;
6 | align-items: center;
7 | padding: 0.75rem;
8 | background-color: $color-black;
9 | color: $color-white;
10 | flex-wrap: wrap;
11 |
12 | &__logo {
13 | display: inline-block;
14 | vertical-align: middle;
15 | font-size: 1.1rem;
16 | font-weight: bold;
17 | margin: 0;
18 | margin-right: 0.5rem;
19 | }
20 |
21 | &__icon {
22 | width: 1.5rem;
23 | height: 1.5rem;
24 | }
25 |
26 | &__count {
27 | display: inline-block;
28 | vertical-align: middle;
29 | margin-top: 0.3rem;
30 | }
31 |
32 | &__link {
33 | display: inline-block;
34 | color: rgba($color-white, 0.5);
35 | text-decoration: none;
36 | font-weight: bold;
37 | font-size: 0.9rem;
38 | padding: 0.1rem 0.5rem;
39 | border-radius: 3px;
40 |
41 | &:hover {
42 | color: $color-white;
43 | background-color: rgba($color-white, 0.1);
44 | }
45 | }
46 |
47 | &__github-link {
48 | margin-left: 0.5rem;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/components/Subnavigation/index.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
8 |
9 |
40 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/components/Subnavigation/style.scss:
--------------------------------------------------------------------------------
1 | @import "../../assets/sass/variables";
2 |
3 | .subnavigation {
4 |
5 | padding: 0.5rem;
6 | background-color: rgba($color-black, 0.9);
7 | color: $color-white;
8 | text-align: center;
9 |
10 | @media (min-width: 600px) {
11 | position: sticky;
12 | top: 0;
13 | z-index: 1000;
14 | }
15 |
16 | &__link {
17 | display: inline-block;
18 | color: rgba($color-white, 0.5);
19 | text-decoration: none;
20 | font-weight: bold;
21 | font-size: 0.9rem;
22 | padding: 0.1rem 0.5rem;
23 | border-radius: 3px;
24 |
25 | &:hover {
26 | color: $color-white;
27 | background-color: rgba($color-white, 0.1);
28 | }
29 |
30 | &.is-exact-active {
31 | color: $color-white;
32 | background-color: rgba($color-white, 0.2);
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/helpers/svg-sprite-loader.js:
--------------------------------------------------------------------------------
1 | ;(function(window, document) {
2 | 'use strict';
3 |
4 | var isSvg = document.createElementNS && document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ).createSVGRect;
5 | var localStorage = 'localStorage' in window && window['localStorage'] !== null ? window.localStorage : false;
6 |
7 | function svgSpriteInjector(source, opts) {
8 | var file;
9 | opts = opts || {};
10 |
11 | if (source instanceof Node) {
12 | file = source.getAttribute('data-svg-sprite');
13 | opts.revision = source.getAttribute('data-svg-sprite-revision') || opts.revision;
14 | } else if (typeof source === 'string') {
15 | file = source;
16 | }
17 |
18 | if (isSvg) {
19 | if (file) {
20 | injector(file, opts);
21 | } else {
22 | console.error('svg-sprite-injector: undefined sprite filename!');
23 | }
24 | } else {
25 | console.error('svg-sprite-injector require ie9 or greater!');
26 | }
27 | };
28 |
29 | function injector(filepath, opts) {
30 | var name = 'injectedSVGSprite' + filepath,
31 | revision = opts.revision,
32 | request;
33 |
34 | // localStorage cache
35 | if (revision !== undefined && localStorage && localStorage[name + 'Rev'] == revision) {
36 | return injectOnLoad(localStorage[name]);
37 | }
38 |
39 | // Async load
40 | request = new XMLHttpRequest();
41 | request.open('GET', filepath, true);
42 | request.onreadystatechange = function (e) {
43 | var data;
44 |
45 | if (request.readyState === 4 && request.status >= 200 && request.status < 400) {
46 | injectOnLoad(data = request.responseText);
47 | if (revision !== undefined && localStorage) {
48 | localStorage[name] = data;
49 | localStorage[name + 'Rev'] = revision;
50 | }
51 | }
52 | };
53 | request.send();
54 | }
55 |
56 | function injectOnLoad(data) {
57 | if (data) {
58 | if (document.body) {
59 | injectData(data);
60 | } else {
61 | document.addEventListener('DOMContentLoaded', injectData.bind(null, data));
62 | }
63 | }
64 | }
65 |
66 | function injectData(data) {
67 | var body = document.body;
68 | body.insertAdjacentHTML('afterbegin', data);
69 | if (body.firstChild.tagName === 'svg') {
70 | body.firstChild.style.display = 'none';
71 | }
72 | }
73 |
74 | if (typeof exports === 'object') {
75 | module.exports = svgSpriteInjector;
76 | } else {
77 | window.svgSpriteInjector = svgSpriteInjector;
78 | }
79 |
80 | } (window, document));
81 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/_error.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
29 |
30 |
31 | {status}
32 |
33 |
34 | {status}
35 |
36 | {error.message}
37 |
38 | {#if dev && error.stack}
39 | {error.stack}
40 | {/if}
41 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/_layout.scss:
--------------------------------------------------------------------------------
1 | @import "../assets/sass/variables";
2 |
3 | .page {
4 |
5 | &__content {
6 | padding: 4rem 1rem;
7 | }
8 |
9 | &__footer {
10 | text-align: center;
11 | margin-bottom: 2rem;
12 | }
13 |
14 | &__source-link {
15 | display: inline-block;
16 | text-decoration: none;
17 | text-transform: uppercase;
18 | font-weight: bold;
19 | font-size: 0.8rem;
20 | background-color: rgba($color-black, 0.1);
21 | color: $color-black;
22 | border-radius: 5px;
23 | padding: 0.2rem 0.5rem;
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/_layout.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/basic/index.svelte:
--------------------------------------------------------------------------------
1 |
86 |
87 | {#if editor}
88 |
89 |
193 |
194 |
195 | {/if}
196 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/code-highlighting/examples.js:
--------------------------------------------------------------------------------
1 | export const JavaScriptExample = `function $initHighlight(block, flags) {
2 | try {
3 | if (block.className.search(/\bno\-highlight\b/) != -1)
4 | return processBlock(block, true, 0x0F) + ' class=""';
5 | } catch (e) {
6 | /* handle exception */
7 | }
8 | for (var i = 0 / 2; i < classes.length; i++) { // "0 / 2" should not be parsed as regexp
9 | if (checkCondition(classes[i]) === undefined)
10 | return /\d+/g;
11 | }
12 | }`
13 |
14 | export const CSSExample = `@font-face {
15 | font-family: Chunkfive; src: url('Chunkfive.otf');
16 | }
17 |
18 | body, .usertext {
19 | color: #F0F0F0; background: #600;
20 | font-family: Chunkfive, sans;
21 | }
22 |
23 | @import url(print.css);
24 | @media print {
25 | a[href^=http]::after {
26 | content: attr(href)
27 | }
28 | }`
29 |
30 | export const ExplicitImportExample = `import javascript from 'highlight.js/lib/languages/javascript'
31 | import cssx from 'highlight.js/lib/languages/css'
32 | import { Editor } from 'tiptap'
33 | import {
34 | CodeBlockHighlight,
35 | } from 'tiptap-extensions'
36 |
37 | export default {
38 | components: {
39 | Editor,
40 | },
41 | data() {
42 | return {
43 | extensions: [
44 | new CodeBlockHighlight({
45 | languages: {
46 | javascript,
47 | cssx,
48 | },
49 | })
50 | ]
51 | }
52 | }
53 | }`;
54 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/code-highlighting/index.svelte:
--------------------------------------------------------------------------------
1 |
66 |
67 |
133 |
134 | {#if editor}
135 |
136 |
137 |
138 | {/if}
139 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/collaboration/index.svelte:
--------------------------------------------------------------------------------
1 |
79 |
80 |
105 |
106 | {#if editor}
107 |
108 |
Collaborative Editing
109 |
110 | With the Collaboration Extension it's possible for several users to work
111 | on a document at the same time. To make this possible, client-side and
112 | server-side code is required. This example shows this using a
113 |
114 | socket server on glitch.com
115 |
116 | . To keep the demo code clean, only a few nodes and marks are activated.
117 | There is also set a 250ms debounce for all changes. Try it out below:
118 |
119 | {#if editor && !loading}
120 |
121 | {count} {count === 1 ? 'user' : 'users'} connected
122 |
123 |
124 | {:else}
125 |
Connecting to socket server…
126 | {/if}
127 |
128 | {/if}
129 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/drag-handle/DragItem.js:
--------------------------------------------------------------------------------
1 | import { Node } from "../../../../tiptap-svelte/src";
2 | import View from './DragItem.svelte';
3 |
4 | export default class DragItem extends Node {
5 |
6 | get name() {
7 | return 'drag_item'
8 | }
9 |
10 | get schema() {
11 | return {
12 | group: 'block',
13 | draggable: true,
14 | content: 'paragraph+',
15 | toDOM: () => ['div', { 'data-type': this.name }, 0],
16 | parseDOM: [{
17 | tag: `[data-type="${this.name}"]`,
18 | }],
19 | }
20 | }
21 |
22 | get view() {
23 | return View;
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/drag-handle/DragItem.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
23 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/drag-handle/index.svelte:
--------------------------------------------------------------------------------
1 |
41 |
42 |
62 |
63 | {#if editor}
64 |
65 |
66 |
67 | {/if}
68 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/embeds/Iframe.js:
--------------------------------------------------------------------------------
1 | import { Node } from "../../../../tiptap-svelte/src";
2 | import View from './Iframe.svelte'
3 |
4 | export default class Iframe extends Node {
5 |
6 | get name() {
7 | return 'iframe'
8 | }
9 |
10 | get schema() {
11 | return {
12 | attrs: {
13 | src: {
14 | default: null,
15 | },
16 | },
17 | group: 'block',
18 | selectable: false,
19 | parseDOM: [{
20 | tag: 'iframe',
21 | getAttrs: dom => ({
22 | src: dom.getAttribute('src'),
23 | }),
24 | }],
25 | toDOM: node => ['iframe', {
26 | src: node.attrs.src,
27 | frameborder: 0,
28 | allowfullscreen: 'true',
29 | }],
30 | }
31 | }
32 |
33 | get view() {
34 | return View;
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/embeds/Iframe.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 | {#if view.editable}
23 |
29 | {/if}
30 |
31 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/embeds/index.svelte:
--------------------------------------------------------------------------------
1 |
48 |
49 |
70 |
71 | {#if editor}
72 |
73 |
74 |
75 | {/if}
76 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/floating-menu/index.svelte:
--------------------------------------------------------------------------------
1 |
66 |
67 |
88 |
89 | {#if editor}
90 |
91 |
92 |
147 |
148 |
149 |
150 |
151 | {/if}
152 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/focus/index.svelte:
--------------------------------------------------------------------------------
1 |
82 |
83 |
91 |
92 | {#if editor}
93 |
94 |
95 |
96 | {/if}
97 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/hiding-menu-bar/index.svelte:
--------------------------------------------------------------------------------
1 |
70 |
71 | {#if editor}
72 |
73 |
74 |
168 |
169 |
170 |
171 |
172 | {/if}
173 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/history/index.svelte:
--------------------------------------------------------------------------------
1 |
50 |
51 | {#if editor}
52 |
53 |
64 |
65 |
66 |
67 | {/if}
68 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/images/index.svelte:
--------------------------------------------------------------------------------
1 |
53 |
54 | {#if editor}
55 |
56 |
63 |
64 |
65 |
66 | {/if}
67 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/index.svelte:
--------------------------------------------------------------------------------
1 |
2 | tiptap-svelte
3 |
6 |
7 |
8 |
9 |
10 |
Welcome to tiptap-svelte
11 |
12 |
13 | This editor is ported from
14 | tiptap , which is based on
15 | Prosemirror .
16 |
17 |
18 |
19 | It is
20 | fully extendable
21 | and renderless. You can easily add custom nodes as
22 | Svelte components .
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/links/index.svelte:
--------------------------------------------------------------------------------
1 |
97 |
98 | {#if editor}
99 |
100 |
106 |
141 |
142 |
143 |
144 |
145 | {/if}
146 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/markdown-shortcuts/index.svelte:
--------------------------------------------------------------------------------
1 |
63 |
64 | {#if editor}
65 |
66 |
67 |
68 | {/if}
69 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/menu-bubble/index.svelte:
--------------------------------------------------------------------------------
1 |
71 |
72 | {#if editor}
73 |
74 |
75 |
102 |
103 |
104 |
105 |
106 | {/if}
107 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/placeholder/index.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 |
47 |
48 | {#if editor}
49 |
50 |
53 |
54 |
55 | {/if}
56 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/read-only/index.svelte:
--------------------------------------------------------------------------------
1 |
52 |
53 |
58 |
59 | {#if editor}
60 |
61 |
62 |
67 | editable
68 |
69 |
70 |
71 |
72 | {/if}
73 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/title/Doc.js:
--------------------------------------------------------------------------------
1 | import { Doc } from "../../../../tiptap-svelte/src/index.js";
2 |
3 | export default class CustomDoc extends Doc {
4 |
5 | get schema() {
6 | return {
7 | content: 'title block+',
8 | }
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/title/Title.js:
--------------------------------------------------------------------------------
1 | import { Node } from "../../../../tiptap-svelte/src/index.js";
2 |
3 | export default class Title extends Node {
4 |
5 | get name() {
6 | return 'title'
7 | }
8 |
9 | get schema() {
10 | return {
11 | content: 'inline*',
12 | parseDOM: [{
13 | tag: 'h1',
14 | }],
15 | toDOM: () => ['h1', 0],
16 | }
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/title/index.svelte:
--------------------------------------------------------------------------------
1 |
38 |
39 |
50 |
51 | {#if editor}
52 |
53 |
54 |
55 | {/if}
56 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/routes/todo-list/index.svelte:
--------------------------------------------------------------------------------
1 |
70 |
71 |
123 |
124 | {#if editor}
125 |
126 |
157 |
158 |
159 |
160 | {/if}
161 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/server.js:
--------------------------------------------------------------------------------
1 | import sirv from 'sirv';
2 | import polka from 'polka';
3 | import compression from 'compression';
4 | import * as sapper from '@sapper/server';
5 |
6 | const { PORT, NODE_ENV } = process.env;
7 | const dev = NODE_ENV === 'development';
8 |
9 | polka() // You can also use Express
10 | .use(
11 | compression({ threshold: 0 }),
12 | sirv('static', { dev }),
13 | sapper.middleware()
14 | )
15 | .listen(PORT, err => {
16 | if (err) console.log('error', err);
17 | });
18 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/service-worker.js:
--------------------------------------------------------------------------------
1 | import { timestamp, files, shell, routes } from '@sapper/service-worker';
2 |
3 | const ASSETS = `cache${timestamp}`;
4 |
5 | // `shell` is an array of all the files generated by the bundler,
6 | // `files` is an array of everything in the `static` directory
7 | const to_cache = shell.concat(files);
8 | const cached = new Set(to_cache);
9 |
10 | self.addEventListener('install', event => {
11 | event.waitUntil(
12 | caches
13 | .open(ASSETS)
14 | .then(cache => cache.addAll(to_cache))
15 | .then(() => {
16 | self.skipWaiting();
17 | })
18 | );
19 | });
20 |
21 | self.addEventListener('activate', event => {
22 | event.waitUntil(
23 | caches.keys().then(async keys => {
24 | // delete old caches
25 | for (const key of keys) {
26 | if (key !== ASSETS) await caches.delete(key);
27 | }
28 |
29 | self.clients.claim();
30 | })
31 | );
32 | });
33 |
34 | self.addEventListener('fetch', event => {
35 | if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
36 |
37 | const url = new URL(event.request.url);
38 |
39 | // don't try to handle e.g. data: URIs
40 | if (!url.protocol.startsWith('http')) return;
41 |
42 | // ignore dev server requests
43 | if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
44 |
45 | // always serve static files and bundler-generated assets from cache
46 | if (url.host === self.location.host && cached.has(url.pathname)) {
47 | event.respondWith(caches.match(event.request));
48 | return;
49 | }
50 |
51 | // for pages, you might want to serve a shell `service-worker-index.html` file,
52 | // which Sapper has generated for you. It's not right for every
53 | // app, but if it's right for yours then uncomment this section
54 | /*
55 | if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
56 | event.respondWith(caches.match('/service-worker-index.html'));
57 | return;
58 | }
59 | */
60 |
61 | if (event.request.cache === 'only-if-cached') return;
62 |
63 | // for everything else, try the network first, falling back to
64 | // cache if the user is offline. (If the pages never change, you
65 | // might prefer a cache-first approach to a network-first one.)
66 | event.respondWith(
67 | caches
68 | .open(`offline${timestamp}`)
69 | .then(async cache => {
70 | try {
71 | const response = await fetch(event.request);
72 | cache.put(event.request, response.clone());
73 | return response;
74 | } catch(err) {
75 | const response = await cache.match(event.request);
76 | if (response) return response;
77 |
78 | throw err;
79 | }
80 | })
81 | );
82 | });
83 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/src/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | %sapper.base%
10 |
11 |
12 |
13 |
14 |
15 |
18 | %sapper.styles%
19 |
20 |
22 | %sapper.head%
23 |
24 |
25 |
26 |
28 | %sapper.html%
29 |
30 |
33 | %sapper.scripts%
34 |
35 |
36 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewjk/tiptap-svelte/6e07b9862186bd6319164a64d8402a81ea539384/tiptap-svelte-examples/static/favicon.png
--------------------------------------------------------------------------------
/tiptap-svelte-examples/static/global.css:
--------------------------------------------------------------------------------
1 | /*body {
2 | margin: 0;
3 | font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
4 | font-size: 14px;
5 | line-height: 1.5;
6 | color: #333;
7 | }
8 |
9 | h1, h2, h3, h4, h5, h6 {
10 | margin: 0 0 0.5em 0;
11 | font-weight: 400;
12 | line-height: 1.2;
13 | }
14 |
15 | h1 {
16 | font-size: 2em;
17 | }
18 |
19 | a {
20 | color: inherit;
21 | }
22 |
23 | code {
24 | font-family: menlo, inconsolata, monospace;
25 | font-size: calc(1em - 2px);
26 | color: #555;
27 | background-color: #f0f0f0;
28 | padding: 0.2em 0.4em;
29 | border-radius: 2px;
30 | }
31 |
32 | @media (min-width: 400px) {
33 | body {
34 | font-size: 16px;
35 | }
36 | }*/
--------------------------------------------------------------------------------
/tiptap-svelte-examples/static/logo-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewjk/tiptap-svelte/6e07b9862186bd6319164a64d8402a81ea539384/tiptap-svelte-examples/static/logo-192.png
--------------------------------------------------------------------------------
/tiptap-svelte-examples/static/logo-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewjk/tiptap-svelte/6e07b9862186bd6319164a64d8402a81ea539384/tiptap-svelte-examples/static/logo-512.png
--------------------------------------------------------------------------------
/tiptap-svelte-examples/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "background_color": "#ffffff",
3 | "theme_color": "#333333",
4 | "name": "TODO",
5 | "short_name": "TODO",
6 | "display": "minimal-ui",
7 | "start_url": "/",
8 | "icons": [
9 | {
10 | "src": "logo-192.png",
11 | "sizes": "192x192",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "logo-512.png",
16 | "sizes": "512x512",
17 | "type": "image/png"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/svelte.config.js:
--------------------------------------------------------------------------------
1 | // const ts = require('typescript')
2 | const sass = require('node-sass')
3 |
4 | module.exports = {
5 | preprocess: {
6 | // script: async ({ content, attributes }) => {
7 | // if (attributes.lang !== 'typescript') {
8 | // return
9 | // }
10 | // return processTypeScript(content)
11 | // },
12 | style: async ({ content, attributes }) => {
13 | if (attributes.lang !== 'scss') {
14 | return
15 | }
16 | return processSass(content)
17 | },
18 | },
19 | }
20 |
21 | // NOTE: TypeScript doesn't seem to work very well with Svelte atm
22 | // Importing things from stores doesn't import them properly etc
23 | // function processTypeScript(content) {
24 | // return new Promise((resolve, reject) => {
25 | // var options = {
26 | // //module: ts.ModuleKind.CommonJS,
27 | // //inlineSourceMap: true,
28 | // //inlineSources: true
29 | // }
30 | // const result = ts.transpileModule(content, options)
31 | // console.log(result)
32 | // resolve({
33 | // code: result.outputText,
34 | // map: result.sourceMapText
35 | // })
36 | // })
37 | // }
38 |
39 | function processSass(content) {
40 | return new Promise((resolve, reject) => {
41 | sass.render(
42 | {
43 | data: content,
44 | sourceMap: true,
45 | outFile: 'x', // this is necessary, but is ignored
46 | },
47 | (err, result) => {
48 | if (err) {
49 | return reject(err)
50 | }
51 | resolve({
52 | code: result.css.toString(),
53 | map: result.map.toString(),
54 | })
55 | },
56 | )
57 | })
58 | }
59 |
--------------------------------------------------------------------------------
/tiptap-svelte-examples/webpack.config.js:
--------------------------------------------------------------------------------
1 | const SvgStore = require('webpack-svgstore-plugin')
2 |
3 | const webpack = require('webpack')
4 | const path = require('path')
5 | const config = require('sapper/config/webpack.js')
6 | const preprocess = require('svelte-preprocess')
7 | const pkg = require('./package.json')
8 |
9 | const mode = process.env.NODE_ENV
10 | const dev = mode === 'development'
11 |
12 | const alias = {
13 | svelte: path.resolve('node_modules', 'svelte'),
14 | 'prosemirror-model': path.resolve('node_modules', 'prosemirror-model'),
15 | 'prosemirror-state': path.resolve('node_modules', 'prosemirror-state'),
16 | 'prosemirror-tables': path.resolve('node_modules', 'prosemirror-tables')
17 | }
18 | const extensions = ['.mjs', '.js', '.json', '.svelte', '.html']
19 | const mainFields = ['svelte', 'module', 'browser', 'main']
20 |
21 | module.exports = {
22 | client: {
23 | entry: config.client.entry(),
24 | output: config.client.output(),
25 | resolve: { alias, extensions, mainFields },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.(svelte|html)$/,
30 | use: {
31 | loader: 'svelte-loader',
32 | options: {
33 | dev,
34 | hydratable: true,
35 | hotReload: false, // pending https://github.com/sveltejs/svelte/issues/2377
36 | preprocess: preprocess({
37 | /* options */
38 | }),
39 | },
40 | },
41 | },
42 | {
43 | test: /\.(css|scss)$/,
44 | use: [
45 | 'style-loader',
46 | 'css-loader',
47 | 'sass-loader',
48 | ],
49 | },
50 | ],
51 | },
52 | mode,
53 | plugins: [
54 | // pending https://github.com/sveltejs/svelte/issues/2377
55 | // dev && new webpack.HotModuleReplacementPlugin(),
56 | new webpack.DefinePlugin({
57 | 'process.browser': true,
58 | 'process.env.NODE_ENV': JSON.stringify(mode),
59 | }),
60 | // svg icons
61 | new SvgStore({
62 | prefix: 'icon--',
63 | svgoOptions: {
64 | plugins: [
65 | { cleanupIDs: false },
66 | { collapseGroups: false },
67 | { removeTitle: true },
68 | ],
69 | },
70 | }),
71 | ].filter(Boolean),
72 | devtool: dev && 'inline-source-map',
73 | },
74 |
75 | server: {
76 | entry: config.server.entry(),
77 | output: config.server.output(),
78 | target: 'node',
79 | resolve: { alias, extensions, mainFields },
80 | externals: Object.keys(pkg.dependencies).concat('encoding'),
81 | module: {
82 | rules: [
83 | {
84 | test: /\.(svelte|html)$/,
85 | use: {
86 | loader: 'svelte-loader',
87 | options: {
88 | css: false,
89 | generate: 'ssr',
90 | dev,
91 | preprocess: preprocess({
92 | /* options */
93 | }),
94 | },
95 | },
96 | },
97 | ],
98 | },
99 | mode: process.env.NODE_ENV,
100 | performance: {
101 | hints: false, // it doesn't matter if server.js is large
102 | },
103 | },
104 |
105 | serviceworker: {
106 | entry: config.serviceworker.entry(),
107 | output: config.serviceworker.output(),
108 | mode: process.env.NODE_ENV,
109 | },
110 | }
111 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/README.md:
--------------------------------------------------------------------------------
1 | # tiptap-extensions
2 | This is a collection of extensions for [tiptap](https://www.npmjs.com/package/tiptap).
3 |
4 | [](https://www.npmjs.com/package/tiptap-extensions)
5 | [](https://npmcharts.com/compare/tiptap-extensions?minimal=true)
6 | [](https://www.npmjs.com/package/tiptap-extensions)
7 | [](https://www.npmjs.com/package/tiptap-extensions)
8 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tiptap-svelte-extensions",
3 | "version": "0.0.0",
4 | "description": "Extensions for tiptap-svelte",
5 | "homepage": "",
6 | "license": "MIT",
7 | "main": "dist/extensions.common.js",
8 | "module": "dist/extensions.esm.js",
9 | "unpkg": "dist/extensions.js",
10 | "jsdelivr": "dist/extensions.js",
11 | "sideEffects": false,
12 | "files": [
13 | "src",
14 | "dist"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/andrewjk/tiptap-svelte.git"
19 | },
20 | "bugs": {
21 | "url": "https://github.com/andrewjk/tiptap-svelte/issues"
22 | },
23 | "dependencies": {
24 | "lowlight": "^1.13.1",
25 | "prosemirror-collab": "^1.2.2",
26 | "prosemirror-history": "^1.1.3",
27 | "prosemirror-model": "^1.9.1",
28 | "prosemirror-state": "^1.3.2",
29 | "prosemirror-tables": "^1.0.0",
30 | "prosemirror-transform": "^1.2.3",
31 | "prosemirror-utils": "^0.9.6",
32 | "prosemirror-view": "^1.13.11",
33 | "tiptap-commands": "^1.12.5"
34 | },
35 | "peerDependencies": {
36 | "svelte": "^3.18.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/extensions/Collaboration.js:
--------------------------------------------------------------------------------
1 | import { Extension } from '../../../tiptap-svelte/src/index.js'
2 | import { Step } from 'prosemirror-transform'
3 | import {
4 | collab,
5 | sendableSteps,
6 | getVersion,
7 | receiveTransaction,
8 | } from 'prosemirror-collab'
9 |
10 | export default class Collaboration extends Extension {
11 |
12 | get name() {
13 | return 'collaboration'
14 | }
15 |
16 | init() {
17 | this.getSendableSteps = this.debounce(state => {
18 | const sendable = sendableSteps(state)
19 |
20 | if (sendable) {
21 | this.options.onSendable({
22 | editor: this.editor,
23 | sendable: {
24 | version: sendable.version,
25 | steps: sendable.steps.map(step => step.toJSON()),
26 | clientID: sendable.clientID,
27 | },
28 | })
29 | }
30 | }, this.options.debounce)
31 |
32 | this.editor.on('transaction', ({ state }) => {
33 | this.getSendableSteps(state)
34 | })
35 | }
36 |
37 | get defaultOptions() {
38 | return {
39 | version: 0,
40 | clientID: Math.floor(Math.random() * 0xFFFFFFFF),
41 | debounce: 250,
42 | onSendable: () => {},
43 | update: ({ steps, version }) => {
44 | const { state, view, schema } = this.editor
45 |
46 | if (getVersion(state) > version) {
47 | return
48 | }
49 |
50 | view.dispatch(receiveTransaction(
51 | state,
52 | steps.map(item => Step.fromJSON(schema, item.step)),
53 | steps.map(item => item.clientID),
54 | ))
55 | },
56 | }
57 | }
58 |
59 | get plugins() {
60 | return [
61 | collab({
62 | version: this.options.version,
63 | clientID: this.options.clientID,
64 | }),
65 | ]
66 | }
67 |
68 | debounce(fn, delay) {
69 | let timeout
70 | return function (...args) {
71 | if (timeout) {
72 | clearTimeout(timeout)
73 | }
74 | timeout = setTimeout(() => {
75 | fn(...args)
76 | timeout = null
77 | }, delay)
78 | }
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/extensions/Focus.js:
--------------------------------------------------------------------------------
1 | import { Extension, Plugin } from '../../../tiptap-svelte/src/index.js'
2 | import { DecorationSet, Decoration } from 'prosemirror-view'
3 |
4 | export default class Focus extends Extension {
5 |
6 | get name() {
7 | return 'focus'
8 | }
9 |
10 | get defaultOptions() {
11 | return {
12 | className: 'has-focus',
13 | nested: false,
14 | }
15 | }
16 |
17 | get plugins() {
18 | return [
19 | new Plugin({
20 | props: {
21 | decorations: ({ doc, plugins, selection }) => {
22 | const editablePlugin = plugins.find(plugin => plugin.key.startsWith('editable$'))
23 | const editable = editablePlugin.props.editable()
24 | const active = editable && this.options.className
25 | const { focused } = this.editor
26 | const { anchor } = selection
27 | const decorations = []
28 |
29 | if (!active || !focused) {
30 | return false
31 | }
32 |
33 | doc.descendants((node, pos) => {
34 | const hasAnchor = anchor >= pos && anchor <= (pos + node.nodeSize)
35 |
36 | if (hasAnchor && !node.isText) {
37 | const decoration = Decoration.node(pos, pos + node.nodeSize, {
38 | class: this.options.className,
39 | })
40 | decorations.push(decoration)
41 | }
42 |
43 | return this.options.nested
44 | })
45 |
46 | return DecorationSet.create(doc, decorations)
47 | },
48 | },
49 | }),
50 | ]
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/extensions/History.js:
--------------------------------------------------------------------------------
1 | import { Extension } from '../../../tiptap-svelte/src/index.js'
2 | import { history, undo, redo, undoDepth, redoDepth } from 'prosemirror-history'
3 |
4 | export default class History extends Extension {
5 |
6 | get name() {
7 | return 'history'
8 | }
9 |
10 | get defaultOptions() {
11 | return {
12 | depth: '',
13 | newGroupDelay: '',
14 | }
15 | }
16 |
17 | keys() {
18 | const keymap = {
19 | 'Mod-z': undo,
20 | 'Mod-y': redo,
21 | 'Shift-Mod-z': redo,
22 | }
23 |
24 | return keymap
25 | }
26 |
27 | get plugins() {
28 | return [
29 | history({
30 | depth: this.options.depth,
31 | newGroupDelay: this.options.newGroupDelay,
32 | }),
33 | ]
34 | }
35 |
36 | commands() {
37 | return {
38 | undo: () => undo,
39 | redo: () => redo,
40 | undoDepth: () => undoDepth,
41 | redoDepth: () => redoDepth,
42 | }
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/extensions/Placeholder.js:
--------------------------------------------------------------------------------
1 | import { Extension, Plugin } from '../../../tiptap-svelte/src/index.js'
2 | import { Decoration, DecorationSet } from 'prosemirror-view'
3 |
4 | export default class Placeholder extends Extension {
5 |
6 | get name() {
7 | return 'placeholder'
8 | }
9 |
10 | get defaultOptions() {
11 | return {
12 | emptyEditorClass: 'is-editor-empty',
13 | emptyNodeClass: 'is-empty',
14 | emptyNodeText: 'Write something …',
15 | showOnlyWhenEditable: true,
16 | showOnlyCurrent: true,
17 | }
18 | }
19 |
20 | get update() {
21 | return view => {
22 | view.updateState(view.state)
23 | }
24 | }
25 |
26 | get plugins() {
27 | return [
28 | new Plugin({
29 | props: {
30 | decorations: ({ doc, plugins, selection }) => {
31 | const editablePlugin = plugins.find(plugin => plugin.key.startsWith('editable$'))
32 | const editable = editablePlugin.props.editable()
33 | const active = editable || !this.options.showOnlyWhenEditable
34 | const { anchor } = selection
35 | const decorations = []
36 | const isEditorEmpty = doc.textContent.length === 0
37 |
38 | if (!active) {
39 | return false
40 | }
41 |
42 | doc.descendants((node, pos) => {
43 | const hasAnchor = anchor >= pos && anchor <= (pos + node.nodeSize)
44 | const isNodeEmpty = node.content.size === 0
45 |
46 | if ((hasAnchor || !this.options.showOnlyCurrent) && isNodeEmpty) {
47 | const classes = [this.options.emptyNodeClass]
48 |
49 | if (isEditorEmpty) {
50 | classes.push(this.options.emptyEditorClass)
51 | }
52 |
53 | const decoration = Decoration.node(pos, pos + node.nodeSize, {
54 | class: classes.join(' '),
55 | 'data-empty-text': typeof this.options.emptyNodeText === 'function'
56 | ? this.options.emptyNodeText(node)
57 | : this.options.emptyNodeText,
58 | })
59 | decorations.push(decoration)
60 | }
61 |
62 | return false
63 | })
64 |
65 | return DecorationSet.create(doc, decorations)
66 | },
67 | },
68 | }),
69 | ]
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/extensions/Search.js:
--------------------------------------------------------------------------------
1 | import { Extension, Plugin } from '../../../tiptap-svelte/src/index.js'
2 | import { Decoration, DecorationSet } from 'prosemirror-view'
3 |
4 | export default class Search extends Extension {
5 |
6 | constructor(options = {}) {
7 | super(options)
8 |
9 | this.results = []
10 | this.searchTerm = null
11 | this._updating = false
12 | }
13 |
14 | get name() {
15 | return 'search'
16 | }
17 |
18 | get defaultOptions() {
19 | return {
20 | autoSelectNext: true,
21 | findClass: 'find',
22 | searching: false,
23 | caseSensitive: false,
24 | disableRegex: true,
25 | alwaysSearch: false,
26 | }
27 | }
28 |
29 | commands() {
30 | return {
31 | find: attrs => this.find(attrs),
32 | replace: attrs => this.replace(attrs),
33 | replaceAll: attrs => this.replaceAll(attrs),
34 | clearSearch: () => this.clear(),
35 | }
36 | }
37 |
38 | get findRegExp() {
39 | return RegExp(this.searchTerm, !this.options.caseSensitive ? 'gui' : 'gu')
40 | }
41 |
42 | get decorations() {
43 | return this.results.map(deco => (
44 | Decoration.inline(deco.from, deco.to, { class: this.options.findClass })
45 | ))
46 | }
47 |
48 | _search(doc) {
49 | this.results = []
50 | const mergedTextNodes = []
51 | let index = 0
52 |
53 | if (!this.searchTerm) {
54 | return
55 | }
56 |
57 | doc.descendants((node, pos) => {
58 | if (node.isText) {
59 | if (mergedTextNodes[index]) {
60 | mergedTextNodes[index] = {
61 | text: mergedTextNodes[index].text + node.text,
62 | pos: mergedTextNodes[index].pos,
63 | }
64 | } else {
65 | mergedTextNodes[index] = {
66 | text: node.text,
67 | pos,
68 | }
69 | }
70 | } else {
71 | index += 1
72 | }
73 | })
74 |
75 | mergedTextNodes.forEach(({ text, pos }) => {
76 | const search = this.findRegExp
77 | let m
78 | // eslint-disable-next-line no-cond-assign
79 | while ((m = search.exec(text))) {
80 | if (m[0] === '') {
81 | break
82 | }
83 |
84 | this.results.push({
85 | from: pos + m.index,
86 | to: pos + m.index + m[0].length,
87 | })
88 | }
89 | })
90 | }
91 |
92 | replace(replace) {
93 | return (state, dispatch) => {
94 | const firstResult = this.results[0]
95 |
96 | if (!firstResult) {
97 | return
98 | }
99 |
100 | const { from, to } = this.results[0]
101 | dispatch(state.tr.insertText(replace, from, to))
102 | this.editor.commands.find(this.searchTerm)
103 | }
104 | }
105 |
106 | rebaseNextResult(replace, index, lastOffset = 0) {
107 | const nextIndex = index + 1
108 |
109 | if (!this.results[nextIndex]) {
110 | return null
111 | }
112 |
113 | const { from: currentFrom, to: currentTo } = this.results[index]
114 | const offset = (currentTo - currentFrom - replace.length) + lastOffset
115 | const { from, to } = this.results[nextIndex]
116 |
117 | this.results[nextIndex] = {
118 | to: to - offset,
119 | from: from - offset,
120 | }
121 |
122 | return offset
123 | }
124 |
125 | replaceAll(replace) {
126 | return ({ tr }, dispatch) => {
127 | let offset
128 |
129 | if (!this.results.length) {
130 | return
131 | }
132 |
133 | this.results.forEach(({ from, to }, index) => {
134 | tr.insertText(replace, from, to)
135 | offset = this.rebaseNextResult(replace, index, offset)
136 | })
137 |
138 | dispatch(tr)
139 |
140 | this.editor.commands.find(this.searchTerm)
141 | }
142 | }
143 |
144 | find(searchTerm) {
145 | return (state, dispatch) => {
146 | this.searchTerm = this.options.disableRegex
147 | ? searchTerm.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
148 | : searchTerm
149 |
150 | this.updateView(state, dispatch)
151 | }
152 | }
153 |
154 | clear() {
155 | return (state, dispatch) => {
156 | this.searchTerm = null
157 |
158 | this.updateView(state, dispatch)
159 | }
160 | }
161 |
162 | updateView({ tr }, dispatch) {
163 | this._updating = true
164 | dispatch(tr)
165 | this._updating = false
166 | }
167 |
168 | createDeco(doc) {
169 | this._search(doc)
170 | return this.decorations
171 | ? DecorationSet.create(doc, this.decorations)
172 | : []
173 | }
174 |
175 | get plugins() {
176 | return [
177 | new Plugin({
178 | state: {
179 | init() {
180 | return DecorationSet.empty
181 | },
182 | apply: (tr, old) => {
183 | if (this._updating
184 | || this.options.searching
185 | || (tr.docChanged && this.options.alwaysSearch)
186 | ) {
187 | return this.createDeco(tr.doc)
188 | }
189 |
190 | if (tr.docChanged) {
191 | return old.map(tr.mapping, tr.doc)
192 | }
193 |
194 | return old
195 | },
196 | },
197 | props: {
198 | decorations(state) {
199 | return this.getState(state)
200 | },
201 | },
202 | }),
203 | ]
204 | }
205 |
206 | }
207 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/extensions/TrailingNode.js:
--------------------------------------------------------------------------------
1 | import { Extension, Plugin, PluginKey } from '../../../tiptap-svelte/src/index.js'
2 | import { nodeEqualsType } from 'tiptap-utils'
3 |
4 | export default class TrailingNode extends Extension {
5 |
6 | get name() {
7 | return 'trailing_node'
8 | }
9 |
10 | get defaultOptions() {
11 | return {
12 | node: 'paragraph',
13 | notAfter: [
14 | 'paragraph',
15 | ],
16 | }
17 | }
18 |
19 | get plugins() {
20 | const plugin = new PluginKey(this.name)
21 | const disabledNodes = Object.entries(this.editor.schema.nodes)
22 | .map(([, value]) => value)
23 | .filter(node => this.options.notAfter.includes(node.name))
24 |
25 | return [
26 | new Plugin({
27 | key: plugin,
28 | view: () => ({
29 | update: view => {
30 | const { state } = view
31 | const insertNodeAtEnd = plugin.getState(state)
32 |
33 | if (!insertNodeAtEnd) {
34 | return
35 | }
36 |
37 | const { doc, schema, tr } = state
38 | const type = schema.nodes[this.options.node]
39 | const transaction = tr.insert(doc.content.size, type.create())
40 | view.dispatch(transaction)
41 | },
42 | }),
43 | state: {
44 | init: (_, state) => {
45 | const lastNode = state.tr.doc.lastChild
46 | return !nodeEqualsType({ node: lastNode, types: disabledNodes })
47 | },
48 | apply: (tr, value) => {
49 | if (!tr.docChanged) {
50 | return value
51 | }
52 |
53 | const lastNode = tr.doc.lastChild
54 | return !nodeEqualsType({ node: lastNode, types: disabledNodes })
55 | },
56 | },
57 | }),
58 | ]
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Blockquote } from './nodes/Blockquote'
2 | export { default as BulletList } from './nodes/BulletList'
3 | export { default as CodeBlock } from './nodes/CodeBlock'
4 | export { default as CodeBlockHighlight } from './nodes/CodeBlockHighlight'
5 | export { default as HardBreak } from './nodes/HardBreak'
6 | export { default as Heading } from './nodes/Heading'
7 | export { default as HorizontalRule } from './nodes/HorizontalRule'
8 | export { default as Image } from './nodes/Image'
9 | export { default as ListItem } from './nodes/ListItem'
10 | export { default as Mention } from './nodes/Mention'
11 | export { default as OrderedList } from './nodes/OrderedList'
12 | export { default as Table } from './nodes/Table'
13 | export { default as TableHeader } from './nodes/TableHeader'
14 | export { default as TableCell } from './nodes/TableCell'
15 | export { default as TableRow } from './nodes/TableRow'
16 | export { default as TodoItem } from './nodes/TodoItem'
17 | export { default as TodoList } from './nodes/TodoList'
18 |
19 | export { default as Bold } from './marks/Bold'
20 | export { default as Code } from './marks/Code'
21 | export { default as Italic } from './marks/Italic'
22 | export { default as Link } from './marks/Link'
23 | export { default as Strike } from './marks/Strike'
24 | export { default as Underline } from './marks/Underline'
25 |
26 | export { default as Collaboration } from './extensions/Collaboration'
27 | export { default as Focus } from './extensions/Focus'
28 | export { default as History } from './extensions/History'
29 | export { default as Placeholder } from './extensions/Placeholder'
30 | export { default as Search } from './extensions/Search'
31 | export { default as TrailingNode } from './extensions/TrailingNode'
32 |
33 | export { default as Suggestions } from './plugins/Suggestions'
34 | export { default as Highlight } from './plugins/Highlight'
35 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/marks/Bold.js:
--------------------------------------------------------------------------------
1 | import { Mark } from '../../../tiptap-svelte/src/index.js'
2 | import { toggleMark, markInputRule, markPasteRule } from 'tiptap-commands'
3 |
4 | export default class Bold extends Mark {
5 |
6 | get name() {
7 | return 'bold'
8 | }
9 |
10 | get schema() {
11 | return {
12 | parseDOM: [
13 | {
14 | tag: 'strong',
15 | },
16 | {
17 | tag: 'b',
18 | getAttrs: node => node.style.fontWeight !== 'normal' && null,
19 | },
20 | {
21 | style: 'font-weight',
22 | getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
23 | },
24 | ],
25 | toDOM: () => ['strong', 0],
26 | }
27 | }
28 |
29 | keys({ type }) {
30 | return {
31 | 'Mod-b': toggleMark(type),
32 | }
33 | }
34 |
35 | commands({ type }) {
36 | return () => toggleMark(type)
37 | }
38 |
39 | inputRules({ type }) {
40 | return [
41 | markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, type),
42 | ]
43 | }
44 |
45 | pasteRules({ type }) {
46 | return [
47 | markPasteRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)/g, type),
48 | ]
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/marks/Code.js:
--------------------------------------------------------------------------------
1 | import { Mark } from '../../../tiptap-svelte/src/index.js'
2 | import { toggleMark, markInputRule, markPasteRule } from 'tiptap-commands'
3 |
4 | export default class Code extends Mark {
5 |
6 | get name() {
7 | return 'code'
8 | }
9 |
10 | get schema() {
11 | return {
12 | excludes: '_',
13 | parseDOM: [
14 | { tag: 'code' },
15 | ],
16 | toDOM: () => ['code', 0],
17 | }
18 | }
19 |
20 | keys({ type }) {
21 | return {
22 | 'Mod-`': toggleMark(type),
23 | }
24 | }
25 |
26 | commands({ type }) {
27 | return () => toggleMark(type)
28 | }
29 |
30 | inputRules({ type }) {
31 | return [
32 | markInputRule(/(?:`)([^`]+)(?:`)$/, type),
33 | ]
34 | }
35 |
36 | pasteRules({ type }) {
37 | return [
38 | markPasteRule(/(?:`)([^`]+)(?:`)/g, type),
39 | ]
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/marks/Italic.js:
--------------------------------------------------------------------------------
1 | import { Mark } from '../../../tiptap-svelte/src/index.js'
2 | import { toggleMark, markInputRule, markPasteRule } from 'tiptap-commands'
3 |
4 | export default class Italic extends Mark {
5 |
6 | get name() {
7 | return 'italic'
8 | }
9 |
10 | get schema() {
11 | return {
12 | parseDOM: [
13 | { tag: 'i' },
14 | { tag: 'em' },
15 | { style: 'font-style=italic' },
16 | ],
17 | toDOM: () => ['em', 0],
18 | }
19 | }
20 |
21 | keys({ type }) {
22 | return {
23 | 'Mod-i': toggleMark(type),
24 | }
25 | }
26 |
27 | commands({ type }) {
28 | return () => toggleMark(type)
29 | }
30 |
31 | inputRules({ type }) {
32 | return [
33 | markInputRule(/(?:^|[^_])(_([^_]+)_)$/, type),
34 | markInputRule(/(?:^|[^*])(\*([^*]+)\*)$/, type),
35 | ]
36 | }
37 |
38 | pasteRules({ type }) {
39 | return [
40 | markPasteRule(/_([^_]+)_/g, type),
41 | markPasteRule(/\*([^*]+)\*/g, type),
42 | ]
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/marks/Link.js:
--------------------------------------------------------------------------------
1 | import { Mark, Plugin } from '../../../tiptap-svelte/src/index.js'
2 | import { updateMark, removeMark, pasteRule } from 'tiptap-commands'
3 | import { getMarkAttrs } from 'tiptap-utils'
4 |
5 | export default class Link extends Mark {
6 |
7 | get name() {
8 | return 'link'
9 | }
10 |
11 | get defaultOptions() {
12 | return {
13 | openOnClick: true,
14 | }
15 | }
16 |
17 | get schema() {
18 | return {
19 | attrs: {
20 | href: {
21 | default: null,
22 | },
23 | },
24 | inclusive: false,
25 | parseDOM: [
26 | {
27 | tag: 'a[href]',
28 | getAttrs: dom => ({
29 | href: dom.getAttribute('href'),
30 | }),
31 | },
32 | ],
33 | toDOM: node => ['a', {
34 | ...node.attrs,
35 | rel: 'noopener noreferrer nofollow',
36 | }, 0],
37 | }
38 | }
39 |
40 | commands({ type }) {
41 | return attrs => {
42 | if (attrs.href) {
43 | return updateMark(type, attrs)
44 | }
45 |
46 | return removeMark(type)
47 | }
48 | }
49 |
50 | pasteRules({ type }) {
51 | return [
52 | pasteRule(
53 | /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-zA-Z]{2,}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g,
54 | type,
55 | url => ({ href: url }),
56 | ),
57 | ]
58 | }
59 |
60 | get plugins() {
61 | if (!this.options.openOnClick) {
62 | return []
63 | }
64 |
65 | return [
66 | new Plugin({
67 | props: {
68 | handleClick: (view, pos, event) => {
69 | const { schema } = view.state
70 | const attrs = getMarkAttrs(view.state, schema.marks.link)
71 |
72 | if (attrs.href && event.target instanceof HTMLAnchorElement) {
73 | event.stopPropagation()
74 | window.open(attrs.href)
75 | }
76 | },
77 | },
78 | }),
79 | ]
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/marks/Strike.js:
--------------------------------------------------------------------------------
1 | import { Mark } from '../../../tiptap-svelte/src/index.js'
2 | import { toggleMark, markInputRule, markPasteRule } from 'tiptap-commands'
3 |
4 | export default class Strike extends Mark {
5 |
6 | get name() {
7 | return 'strike'
8 | }
9 |
10 | get schema() {
11 | return {
12 | parseDOM: [
13 | {
14 | tag: 's',
15 | },
16 | {
17 | tag: 'del',
18 | },
19 | {
20 | tag: 'strike',
21 | },
22 | {
23 | style: 'text-decoration',
24 | getAttrs: value => value === 'line-through',
25 | },
26 | ],
27 | toDOM: () => ['s', 0],
28 | }
29 | }
30 |
31 | keys({ type }) {
32 | return {
33 | 'Mod-d': toggleMark(type),
34 | }
35 | }
36 |
37 | commands({ type }) {
38 | return () => toggleMark(type)
39 | }
40 |
41 | inputRules({ type }) {
42 | return [
43 | markInputRule(/~([^~]+)~$/, type),
44 | ]
45 | }
46 |
47 | pasteRules({ type }) {
48 | return [
49 | markPasteRule(/~([^~]+)~/g, type),
50 | ]
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/marks/Underline.js:
--------------------------------------------------------------------------------
1 | import { Mark } from '../../../tiptap-svelte/src/index.js'
2 | import { toggleMark } from 'tiptap-commands'
3 |
4 | export default class Underline extends Mark {
5 |
6 | get name() {
7 | return 'underline'
8 | }
9 |
10 | get schema() {
11 | return {
12 | parseDOM: [
13 | {
14 | tag: 'u',
15 | },
16 | {
17 | style: 'text-decoration',
18 | getAttrs: value => value === 'underline',
19 | },
20 | ],
21 | toDOM: () => ['u', 0],
22 | }
23 | }
24 |
25 | keys({ type }) {
26 | return {
27 | 'Mod-u': toggleMark(type),
28 | }
29 | }
30 |
31 | commands({ type }) {
32 | return () => toggleMark(type)
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/Blockquote.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { wrappingInputRule, toggleWrap } from 'tiptap-commands'
3 |
4 | export default class Blockquote extends Node {
5 |
6 | get name() {
7 | return 'blockquote'
8 | }
9 |
10 | get schema() {
11 | return {
12 | content: 'block*',
13 | group: 'block',
14 | defining: true,
15 | draggable: false,
16 | parseDOM: [
17 | { tag: 'blockquote' },
18 | ],
19 | toDOM: () => ['blockquote', 0],
20 | }
21 | }
22 |
23 | commands({ type, schema }) {
24 | return () => toggleWrap(type, schema.nodes.paragraph)
25 | }
26 |
27 | keys({ type }) {
28 | return {
29 | 'Ctrl->': toggleWrap(type),
30 | }
31 | }
32 |
33 | inputRules({ type }) {
34 | return [
35 | wrappingInputRule(/^\s*>\s$/, type),
36 | ]
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/BulletList.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { wrappingInputRule, toggleList } from 'tiptap-commands'
3 |
4 | export default class BulletList extends Node {
5 |
6 | get name() {
7 | return 'bullet_list'
8 | }
9 |
10 | get schema() {
11 | return {
12 | content: 'list_item+',
13 | group: 'block',
14 | parseDOM: [
15 | { tag: 'ul' },
16 | ],
17 | toDOM: () => ['ul', 0],
18 | }
19 | }
20 |
21 | commands({ type, schema }) {
22 | return () => toggleList(type, schema.nodes.list_item)
23 | }
24 |
25 | keys({ type, schema }) {
26 | return {
27 | 'Shift-Ctrl-8': toggleList(type, schema.nodes.list_item),
28 | }
29 | }
30 |
31 | inputRules({ type }) {
32 | return [
33 | wrappingInputRule(/^\s*([-+*])\s$/, type),
34 | ]
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/CodeBlock.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-commands'
3 |
4 | export default class CodeBlock extends Node {
5 |
6 | get name() {
7 | return 'code_block'
8 | }
9 |
10 | get schema() {
11 | return {
12 | content: 'text*',
13 | marks: '',
14 | group: 'block',
15 | code: true,
16 | defining: true,
17 | draggable: false,
18 | parseDOM: [
19 | { tag: 'pre', preserveWhitespace: 'full' },
20 | ],
21 | toDOM: () => ['pre', ['code', 0]],
22 | }
23 | }
24 |
25 | commands({ type, schema }) {
26 | return () => toggleBlockType(type, schema.nodes.paragraph)
27 | }
28 |
29 | keys({ type }) {
30 | return {
31 | 'Shift-Ctrl-\\': setBlockType(type),
32 | }
33 | }
34 |
35 | inputRules({ type }) {
36 | return [
37 | textblockTypeInputRule(/^```$/, type),
38 | ]
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/CodeBlockHighlight.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import low from 'lowlight/lib/core'
3 | import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-commands'
4 | import HighlightPlugin from '../plugins/Highlight'
5 |
6 | export default class CodeBlockHighlight extends Node {
7 |
8 | constructor(options = {}) {
9 | super(options)
10 | try {
11 | Object.entries(this.options.languages).forEach(([name, mapping]) => {
12 | low.registerLanguage(name, mapping)
13 | })
14 | } catch (err) {
15 | throw new Error('Invalid syntax highlight definitions: define at least one highlight.js language mapping')
16 | }
17 | }
18 |
19 | get name() {
20 | return 'code_block'
21 | }
22 |
23 | get defaultOptions() {
24 | return {
25 | languages: {},
26 | }
27 | }
28 |
29 | get schema() {
30 | return {
31 | content: 'text*',
32 | marks: '',
33 | group: 'block',
34 | code: true,
35 | defining: true,
36 | draggable: false,
37 | parseDOM: [
38 | { tag: 'pre', preserveWhitespace: 'full' },
39 | ],
40 | toDOM: () => ['pre', ['code', 0]],
41 | }
42 | }
43 |
44 | commands({ type, schema }) {
45 | return () => toggleBlockType(type, schema.nodes.paragraph)
46 | }
47 |
48 | keys({ type }) {
49 | return {
50 | 'Shift-Ctrl-\\': setBlockType(type),
51 | }
52 | }
53 |
54 | inputRules({ type }) {
55 | return [
56 | textblockTypeInputRule(/^```$/, type),
57 | ]
58 | }
59 |
60 | get plugins() {
61 | return [
62 | HighlightPlugin({ name: this.name }),
63 | ]
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/HardBreak.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { chainCommands, exitCode } from 'tiptap-commands'
3 |
4 | export default class HardBreak extends Node {
5 |
6 | get name() {
7 | return 'hard_break'
8 | }
9 |
10 | get schema() {
11 | return {
12 | inline: true,
13 | group: 'inline',
14 | selectable: false,
15 | parseDOM: [
16 | { tag: 'br' },
17 | ],
18 | toDOM: () => ['br'],
19 | }
20 | }
21 |
22 | keys({ type }) {
23 | const command = chainCommands(exitCode, (state, dispatch) => {
24 | dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
25 | return true
26 | })
27 | return {
28 | 'Mod-Enter': command,
29 | 'Shift-Enter': command,
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/Heading.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { setBlockType, textblockTypeInputRule, toggleBlockType } from 'tiptap-commands'
3 |
4 | export default class Heading extends Node {
5 |
6 | get name() {
7 | return 'heading'
8 | }
9 |
10 | get defaultOptions() {
11 | return {
12 | levels: [1, 2, 3, 4, 5, 6],
13 | }
14 | }
15 |
16 | get schema() {
17 | return {
18 | attrs: {
19 | level: {
20 | default: 1,
21 | },
22 | },
23 | content: 'inline*',
24 | group: 'block',
25 | defining: true,
26 | draggable: false,
27 | parseDOM: this.options.levels
28 | .map(level => ({
29 | tag: `h${level}`,
30 | attrs: { level },
31 | })),
32 | toDOM: node => [`h${node.attrs.level}`, 0],
33 | }
34 | }
35 |
36 | commands({ type, schema }) {
37 | return attrs => toggleBlockType(type, schema.nodes.paragraph, attrs)
38 | }
39 |
40 | keys({ type }) {
41 | return this.options.levels.reduce((items, level) => ({
42 | ...items,
43 | ...{
44 | [`Shift-Ctrl-${level}`]: setBlockType(type, { level }),
45 | },
46 | }), {})
47 | }
48 |
49 | inputRules({ type }) {
50 | return this.options.levels.map(level => textblockTypeInputRule(
51 | new RegExp(`^(#{1,${level}})\\s$`),
52 | type,
53 | () => ({ level }),
54 | ))
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/HorizontalRule.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { nodeInputRule } from 'tiptap-commands'
3 |
4 | export default class HorizontalRule extends Node {
5 | get name() {
6 | return 'horizontal_rule'
7 | }
8 |
9 | get schema() {
10 | return {
11 | group: 'block',
12 | parseDOM: [{ tag: 'hr' }],
13 | toDOM: () => ['hr'],
14 | }
15 | }
16 |
17 | commands({ type }) {
18 | return () => (state, dispatch) => dispatch(state.tr.replaceSelectionWith(type.create()))
19 | }
20 |
21 | inputRules({ type }) {
22 | return [
23 | nodeInputRule(/^(?:---|___\s|\*\*\*\s)$/, type),
24 | ]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/Image.js:
--------------------------------------------------------------------------------
1 | import { Node, Plugin } from '../../../tiptap-svelte/src/index.js'
2 | import { nodeInputRule } from 'tiptap-commands'
3 |
4 | /**
5 | * Matches following attributes in Markdown-typed image: [, alt, src, title]
6 | *
7 | * Example:
8 | *  -> [, "Lorem", "image.jpg"]
9 | *  -> [, "", "image.jpg", "Ipsum"]
10 | *  -> [, "Lorem", "image.jpg", "Ipsum"]
11 | */
12 | const IMAGE_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
13 |
14 | export default class Image extends Node {
15 |
16 | get name() {
17 | return 'image'
18 | }
19 |
20 | get schema() {
21 | return {
22 | inline: true,
23 | attrs: {
24 | src: {},
25 | alt: {
26 | default: null,
27 | },
28 | title: {
29 | default: null,
30 | },
31 | },
32 | group: 'inline',
33 | draggable: true,
34 | parseDOM: [
35 | {
36 | tag: 'img[src]',
37 | getAttrs: dom => ({
38 | src: dom.getAttribute('src'),
39 | title: dom.getAttribute('title'),
40 | alt: dom.getAttribute('alt'),
41 | }),
42 | },
43 | ],
44 | toDOM: node => ['img', node.attrs],
45 | }
46 | }
47 |
48 | commands({ type }) {
49 | return attrs => (state, dispatch) => {
50 | const { selection } = state
51 | const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
52 | const node = type.create(attrs)
53 | const transaction = state.tr.insert(position, node)
54 | dispatch(transaction)
55 | }
56 | }
57 |
58 | inputRules({ type }) {
59 | return [
60 | nodeInputRule(IMAGE_INPUT_REGEX, type, match => {
61 | const [, alt, src, title] = match
62 | return {
63 | src,
64 | alt,
65 | title,
66 | }
67 | }),
68 | ]
69 | }
70 |
71 | get plugins() {
72 | return [
73 | new Plugin({
74 | props: {
75 | handleDOMEvents: {
76 | drop(view, event) {
77 | const hasFiles = event.dataTransfer
78 | && event.dataTransfer.files
79 | && event.dataTransfer.files.length
80 |
81 | if (!hasFiles) {
82 | return
83 | }
84 |
85 | const images = Array
86 | .from(event.dataTransfer.files)
87 | .filter(file => (/image/i).test(file.type))
88 |
89 | if (images.length === 0) {
90 | return
91 | }
92 |
93 | event.preventDefault()
94 |
95 | const { schema } = view.state
96 | const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
97 |
98 | images.forEach(image => {
99 | const reader = new FileReader()
100 |
101 | reader.onload = readerEvent => {
102 | const node = schema.nodes.image.create({
103 | src: readerEvent.target.result,
104 | })
105 | const transaction = view.state.tr.insert(coordinates.pos, node)
106 | view.dispatch(transaction)
107 | }
108 | reader.readAsDataURL(image)
109 | })
110 | },
111 | },
112 | },
113 | }),
114 | ]
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/ListItem.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { splitListItem, liftListItem, sinkListItem } from 'tiptap-commands'
3 |
4 | export default class ListItem extends Node {
5 |
6 | get name() {
7 | return 'list_item'
8 | }
9 |
10 | get schema() {
11 | return {
12 | content: 'paragraph block*',
13 | defining: true,
14 | draggable: false,
15 | parseDOM: [
16 | { tag: 'li' },
17 | ],
18 | toDOM: () => ['li', 0],
19 | }
20 | }
21 |
22 | keys({ type }) {
23 | return {
24 | Enter: splitListItem(type),
25 | Tab: sinkListItem(type),
26 | 'Shift-Tab': liftListItem(type),
27 | }
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/Mention.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { replaceText } from 'tiptap-commands'
3 | import SuggestionsPlugin from '../plugins/Suggestions'
4 |
5 | export default class Mention extends Node {
6 |
7 | get name() {
8 | return 'mention'
9 | }
10 |
11 | get defaultOptions() {
12 | return {
13 | matcher: {
14 | char: '@',
15 | allowSpaces: false,
16 | startOfLine: false,
17 | },
18 | mentionClass: 'mention',
19 | suggestionClass: 'mention-suggestion',
20 | }
21 | }
22 |
23 | get schema() {
24 | return {
25 | attrs: {
26 | id: {},
27 | label: {},
28 | },
29 | group: 'inline',
30 | inline: true,
31 | selectable: false,
32 | atom: true,
33 | toDOM: node => [
34 | 'span',
35 | {
36 | class: this.options.mentionClass,
37 | 'data-mention-id': node.attrs.id,
38 | },
39 | `${this.options.matcher.char}${node.attrs.label}`,
40 | ],
41 | parseDOM: [
42 | {
43 | tag: 'span[data-mention-id]',
44 | getAttrs: dom => {
45 | const id = dom.getAttribute('data-mention-id')
46 | const label = dom.innerText.split(this.options.matcher.char).join('')
47 | return { id, label }
48 | },
49 | },
50 | ],
51 | }
52 | }
53 |
54 | commands({ schema }) {
55 | return attrs => replaceText(null, schema.nodes[this.name], attrs)
56 | }
57 |
58 | get plugins() {
59 | return [
60 | SuggestionsPlugin({
61 | command: ({ range, attrs, schema }) => replaceText(range, schema.nodes[this.name], attrs),
62 | appendText: ' ',
63 | matcher: this.options.matcher,
64 | items: this.options.items,
65 | onEnter: this.options.onEnter,
66 | onChange: this.options.onChange,
67 | onExit: this.options.onExit,
68 | onKeyDown: this.options.onKeyDown,
69 | onFilter: this.options.onFilter,
70 | suggestionClass: this.options.suggestionClass,
71 | }),
72 | ]
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/OrderedList.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { wrappingInputRule, toggleList } from 'tiptap-commands'
3 |
4 | export default class OrderedList extends Node {
5 |
6 | get name() {
7 | return 'ordered_list'
8 | }
9 |
10 | get schema() {
11 | return {
12 | attrs: {
13 | order: {
14 | default: 1,
15 | },
16 | },
17 | content: 'list_item+',
18 | group: 'block',
19 | parseDOM: [
20 | {
21 | tag: 'ol',
22 | getAttrs: dom => ({
23 | order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1,
24 | }),
25 | },
26 | ],
27 | toDOM: node => (node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]),
28 | }
29 | }
30 |
31 | commands({ type, schema }) {
32 | return () => toggleList(type, schema.nodes.list_item)
33 | }
34 |
35 | keys({ type, schema }) {
36 | return {
37 | 'Shift-Ctrl-9': toggleList(type, schema.nodes.list_item),
38 | }
39 | }
40 |
41 | inputRules({ type }) {
42 | return [
43 | wrappingInputRule(
44 | /^(\d+)\.\s$/,
45 | type,
46 | match => ({ order: +match[1] }),
47 | (match, node) => node.childCount + node.attrs.order === +match[1],
48 | ),
49 | ]
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/Table.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import {
3 | tableEditing,
4 | columnResizing,
5 | goToNextCell,
6 | addColumnBefore,
7 | addColumnAfter,
8 | deleteColumn,
9 | addRowBefore,
10 | addRowAfter,
11 | deleteRow,
12 | deleteTable,
13 | mergeCells,
14 | splitCell,
15 | toggleHeaderColumn,
16 | toggleHeaderRow,
17 | toggleHeaderCell,
18 | setCellAttr,
19 | fixTables,
20 | } from 'prosemirror-tables'
21 | import { createTable } from 'prosemirror-utils'
22 | import { TextSelection } from 'prosemirror-state'
23 | import TableNodes from './TableNodes'
24 |
25 | export default class Table extends Node {
26 |
27 | get name() {
28 | return 'table'
29 | }
30 |
31 | get defaultOptions() {
32 | return {
33 | resizable: false,
34 | }
35 | }
36 |
37 | get schema() {
38 | return TableNodes.table
39 | }
40 |
41 | commands({ schema }) {
42 | return {
43 | createTable: ({ rowsCount, colsCount, withHeaderRow }) => (
44 | (state, dispatch) => {
45 | const offset = state.tr.selection.anchor + 1
46 |
47 | const nodes = createTable(schema, rowsCount, colsCount, withHeaderRow)
48 | const tr = state.tr.replaceSelectionWith(nodes).scrollIntoView()
49 | const resolvedPos = tr.doc.resolve(offset)
50 |
51 | tr.setSelection(TextSelection.near(resolvedPos))
52 |
53 | dispatch(tr)
54 | }
55 | ),
56 | addColumnBefore: () => addColumnBefore,
57 | addColumnAfter: () => addColumnAfter,
58 | deleteColumn: () => deleteColumn,
59 | addRowBefore: () => addRowBefore,
60 | addRowAfter: () => addRowAfter,
61 | deleteRow: () => deleteRow,
62 | deleteTable: () => deleteTable,
63 | toggleCellMerge: () => (
64 | (state, dispatch) => {
65 | if (mergeCells(state, dispatch)) {
66 | return
67 | }
68 | splitCell(state, dispatch)
69 | }
70 | ),
71 | mergeCells: () => mergeCells,
72 | splitCell: () => splitCell,
73 | toggleHeaderColumn: () => toggleHeaderColumn,
74 | toggleHeaderRow: () => toggleHeaderRow,
75 | toggleHeaderCell: () => toggleHeaderCell,
76 | setCellAttr: () => setCellAttr,
77 | fixTables: () => fixTables,
78 | }
79 | }
80 |
81 | keys() {
82 | return {
83 | Tab: goToNextCell(1),
84 | 'Shift-Tab': goToNextCell(-1),
85 | }
86 | }
87 |
88 | get plugins() {
89 | return [
90 | ...(this.options.resizable ? [columnResizing()] : []),
91 | tableEditing(),
92 | ]
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/TableCell.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import TableNodes from './TableNodes'
3 |
4 | export default class TableCell extends Node {
5 |
6 | get name() {
7 | return 'table_cell'
8 | }
9 |
10 | get schema() {
11 | return TableNodes.table_cell
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/TableHeader.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import TableNodes from './TableNodes'
3 |
4 | export default class TableHeader extends Node {
5 |
6 | get name() {
7 | return 'table_header'
8 | }
9 |
10 | get schema() {
11 | return TableNodes.table_header
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/TableNodes.js:
--------------------------------------------------------------------------------
1 | import { tableNodes } from 'prosemirror-tables'
2 |
3 | export default tableNodes({
4 | tableGroup: 'block',
5 | cellContent: 'block+',
6 | cellAttributes: {
7 | background: {
8 | default: null,
9 | getFromDOM(dom) {
10 | return dom.style.backgroundColor || null
11 | },
12 | setDOMAttr(value, attrs) {
13 | if (value) {
14 | const style = { style: `${(attrs.style || '')}background-color: ${value};` }
15 | Object.assign(attrs, style)
16 | }
17 | },
18 | },
19 | },
20 | })
21 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/TableRow.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import TableNodes from './TableNodes'
3 |
4 | export default class TableRow extends Node {
5 |
6 | get name() {
7 | return 'table_row'
8 | }
9 |
10 | get schema() {
11 | return TableNodes.table_row
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/TodoItem.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { sinkListItem, splitToDefaultListItem, liftListItem } from 'tiptap-commands'
3 | import View from './TodoItem.svelte'
4 |
5 | export default class TodoItem extends Node {
6 |
7 | get name() {
8 | return 'todo_item'
9 | }
10 |
11 | get defaultOptions() {
12 | return {
13 | nested: false,
14 | }
15 | }
16 |
17 | get view() {
18 | return View;
19 | }
20 |
21 | get schema() {
22 | return {
23 | attrs: {
24 | done: {
25 | default: false,
26 | },
27 | },
28 | draggable: true,
29 | content: this.options.nested ? '(paragraph|todo_list)+' : 'paragraph+',
30 | toDOM: node => {
31 | const { done } = node.attrs
32 | return [
33 | 'li',
34 | {
35 | 'data-type': this.name,
36 | 'data-done': done.toString(),
37 | },
38 | ['span', { class: 'todo-checkbox', contenteditable: 'false' }],
39 | ['div', { class: 'todo-content' }, 0],
40 | ]
41 | },
42 | parseDOM: [{
43 | priority: 51,
44 | tag: `[data-type="${this.name}"]`,
45 | getAttrs: dom => ({
46 | done: dom.getAttribute('data-done') === 'true',
47 | }),
48 | }],
49 | }
50 | }
51 |
52 | keys({ type }) {
53 | return {
54 | Enter: splitToDefaultListItem(type),
55 | Tab: this.options.nested ? sinkListItem(type) : () => {},
56 | 'Shift-Tab': liftListItem(type),
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/TodoItem.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/nodes/TodoList.js:
--------------------------------------------------------------------------------
1 | import { Node } from '../../../tiptap-svelte/src/index.js'
2 | import { toggleList, wrappingInputRule } from 'tiptap-commands'
3 |
4 | export default class TodoList extends Node {
5 |
6 | get name() {
7 | return 'todo_list'
8 | }
9 |
10 | get schema() {
11 | return {
12 | group: 'block',
13 | content: 'todo_item+',
14 | toDOM: () => ['ul', { 'data-type': this.name }, 0],
15 | parseDOM: [{
16 | priority: 51,
17 | tag: `[data-type="${this.name}"]`,
18 | }],
19 | }
20 | }
21 |
22 | commands({ type, schema }) {
23 | return () => toggleList(type, schema.nodes.todo_item)
24 | }
25 |
26 | inputRules({ type }) {
27 | return [
28 | wrappingInputRule(/^\s*(\[ \])\s$/, type),
29 | ]
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/tiptap-svelte-extensions/src/plugins/Highlight.js:
--------------------------------------------------------------------------------
1 | import { Plugin, PluginKey } from '../../../tiptap-svelte/src/index.js'
2 | import { Decoration, DecorationSet } from 'prosemirror-view'
3 | import { findBlockNodes } from 'prosemirror-utils'
4 | import low from 'lowlight/lib/core'
5 |
6 | function getDecorations({ doc, name }) {
7 | const decorations = []
8 | const blocks = findBlockNodes(doc).filter(item => item.node.type.name === name)
9 | const flatten = list => list.reduce(
10 | (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
11 | )
12 |
13 | function parseNodes(nodes, className = []) {
14 | return nodes.map(node => {
15 |
16 | const classes = [
17 | ...className,
18 | ...node.properties ? node.properties.className : [],
19 | ]
20 |
21 | if (node.children) {
22 | return parseNodes(node.children, classes)
23 | }
24 |
25 | return {
26 | text: node.value,
27 | classes,
28 | }
29 | })
30 | }
31 |
32 | blocks.forEach(block => {
33 | let startPos = block.pos + 1
34 | const nodes = low.highlightAuto(block.node.textContent).value
35 |
36 | flatten(parseNodes(nodes))
37 | .map(node => {
38 | const from = startPos
39 | const to = from + node.text.length
40 |
41 | startPos = to
42 |
43 | return {
44 | ...node,
45 | from,
46 | to,
47 | }
48 | })
49 | .forEach(node => {
50 | const decoration = Decoration.inline(node.from, node.to, {
51 | class: node.classes.join(' '),
52 | })
53 | decorations.push(decoration)
54 | })
55 | })
56 |
57 | return DecorationSet.create(doc, decorations)
58 | }
59 |
60 | export default function HighlightPlugin({ name }) {
61 | return new Plugin({
62 | name: new PluginKey('highlight'),
63 | state: {
64 | init: (_, { doc }) => getDecorations({ doc, name }),
65 | apply: (transaction, decorationSet, oldState, state) => {
66 | // TODO: find way to cache decorations
67 | // see: https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
68 |
69 | const nodeName = state.selection.$head.parent.type.name
70 | const previousNodeName = oldState.selection.$head.parent.type.name
71 |
72 | if (transaction.docChanged && [nodeName, previousNodeName].includes(name)) {
73 | return getDecorations({ doc: transaction.doc, name })
74 | }
75 |
76 | return decorationSet.map(transaction.mapping, transaction.doc)
77 | },
78 | },
79 | props: {
80 | decorations(state) {
81 | return this.getState(state)
82 | },
83 | },
84 | })
85 | }
86 |
--------------------------------------------------------------------------------
/tiptap-svelte/README.md:
--------------------------------------------------------------------------------
1 | # tiptap
2 | This is the core package of [tiptap](https://www.npmjs.com/package/tiptap).
3 |
4 | [](https://www.npmjs.com/package/tiptap)
5 | [](https://npmcharts.com/compare/tiptap?minimal=true)
6 | [](https://www.npmjs.com/package/tiptap)
7 | [](https://www.npmjs.com/package/tiptap)
8 |
--------------------------------------------------------------------------------
/tiptap-svelte/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tiptap-svelte",
3 | "version": "0.0.0",
4 | "description": "A rich-text editor for Svelte",
5 | "homepage": "https://tiptap.scrumpy.io",
6 | "license": "MIT",
7 | "main": "dist/tiptap.common.js",
8 | "module": "dist/tiptap.esm.js",
9 | "unpkg": "dist/tiptap.js",
10 | "jsdelivr": "dist/tiptap.js",
11 | "files": [
12 | "src",
13 | "dist"
14 | ],
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/andrewjk/tiptap-svelte.git"
18 | },
19 | "bugs": {
20 | "url": "https://github.com/andrewjk/tiptap-svelte/issues"
21 | },
22 | "dependencies": {
23 | "prosemirror-commands": "^1.1.3",
24 | "prosemirror-dropcursor": "^1.3.2",
25 | "prosemirror-gapcursor": "^1.1.3",
26 | "prosemirror-inputrules": "^1.1.2",
27 | "prosemirror-keymap": "^1.1.3",
28 | "prosemirror-model": "^1.9.1",
29 | "prosemirror-state": "^1.3.2",
30 | "prosemirror-view": "^1.13.11",
31 | "tiptap-commands": "^1.12.5",
32 | "tiptap-utils": "^1.8.3"
33 | },
34 | "peerDependencies": {
35 | "svelte": "^3.18.1"
36 | }
37 | }
--------------------------------------------------------------------------------
/tiptap-svelte/src/Components/EditorContent.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Components/EditorFloatingMenu.svelte:
--------------------------------------------------------------------------------
1 |
41 |
42 | {#if editor}
43 |
44 |
51 |
52 | {/if}
53 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Components/EditorMenuBar.svelte:
--------------------------------------------------------------------------------
1 |
33 |
34 | {#if editor}
35 |
36 |
42 |
43 | {/if}
44 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Components/EditorMenuBubble.svelte:
--------------------------------------------------------------------------------
1 |
43 |
44 | {#if editor}
45 |
46 |
53 |
54 | {/if}
55 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Nodes/Doc.js:
--------------------------------------------------------------------------------
1 | import Node from '../Utils/Node'
2 |
3 | export default class Doc extends Node {
4 |
5 | get name() {
6 | return 'doc'
7 | }
8 |
9 | get schema() {
10 | return {
11 | content: 'block+',
12 | }
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Nodes/Paragraph.js:
--------------------------------------------------------------------------------
1 | import { setBlockType } from 'tiptap-commands'
2 | import Node from '../Utils/Node'
3 |
4 | export default class Paragraph extends Node {
5 |
6 | get name() {
7 | return 'paragraph'
8 | }
9 |
10 | get schema() {
11 | return {
12 | content: 'inline*',
13 | group: 'block',
14 | draggable: false,
15 | parseDOM: [{
16 | tag: 'p',
17 | }],
18 | toDOM: () => ['p', 0],
19 | }
20 | }
21 |
22 | commands({ type }) {
23 | return () => setBlockType(type)
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Nodes/Text.js:
--------------------------------------------------------------------------------
1 | import Node from '../Utils/Node'
2 |
3 | export default class Text extends Node {
4 |
5 | get name() {
6 | return 'text'
7 | }
8 |
9 | get schema() {
10 | return {
11 | group: 'inline',
12 | }
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Nodes/index.js:
--------------------------------------------------------------------------------
1 | export { default as Doc } from './Doc'
2 | export { default as Paragraph } from './Paragraph'
3 | export { default as Text } from './Text'
4 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Plugins/FloatingMenu.js:
--------------------------------------------------------------------------------
1 | import { Plugin, PluginKey } from 'prosemirror-state'
2 |
3 | class Menu {
4 |
5 | constructor({ options, editorView }) {
6 | this.options = {
7 | ...{
8 | resizeObserver: true,
9 | element: null,
10 | onUpdate: () => false,
11 | },
12 | ...options,
13 | }
14 | this.preventHide = false
15 | this.editorView = editorView
16 | this.isActive = false
17 | this.top = 0
18 |
19 | // the mousedown event is fired before blur so we can prevent it
20 | this.mousedownHandler = this.handleClick.bind(this)
21 | this.options.element.addEventListener('mousedown', this.mousedownHandler)
22 |
23 | this.options.editor.on('focus', ({ view }) => {
24 | this.update(view)
25 | })
26 |
27 | this.options.editor.on('blur', ({ event }) => {
28 | if (this.preventHide) {
29 | this.preventHide = false
30 | return
31 | }
32 |
33 | this.hide(event)
34 | })
35 |
36 | // sometimes we have to update the position
37 | // because of a loaded images for example
38 | if (this.options.resizeObserver && window.ResizeObserver) {
39 | this.resizeObserver = new ResizeObserver(() => {
40 | if (this.isActive) {
41 | this.update(this.editorView)
42 | }
43 | })
44 | this.resizeObserver.observe(this.editorView.dom)
45 | }
46 | }
47 |
48 | handleClick() {
49 | this.preventHide = true
50 | }
51 |
52 | update(view, lastState) {
53 | const { state } = view
54 |
55 | // Don't do anything if the document/selection didn't change
56 | if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
57 | return
58 | }
59 |
60 | if (!state.selection.empty) {
61 | this.hide()
62 | return
63 | }
64 |
65 | const currentDom = view.domAtPos(state.selection.anchor)
66 |
67 | const isActive = currentDom.node.innerHTML === ' '
68 | && currentDom.node.tagName === 'P'
69 | && currentDom.node.parentNode === view.dom
70 |
71 | if (!isActive) {
72 | this.hide()
73 | return
74 | }
75 |
76 | const parent = this.options.element.offsetParent
77 |
78 | if (!parent) {
79 | this.hide()
80 | return
81 | }
82 |
83 | const editorBoundings = parent.getBoundingClientRect()
84 | const cursorBoundings = view.coordsAtPos(state.selection.anchor)
85 | const top = cursorBoundings.top - editorBoundings.top
86 |
87 | this.isActive = true
88 | this.top = top
89 |
90 | this.sendUpdate()
91 | }
92 |
93 | sendUpdate() {
94 | this.options.onUpdate({
95 | isActive: this.isActive,
96 | top: this.top,
97 | })
98 | }
99 |
100 | hide(event) {
101 | if (event
102 | && event.relatedTarget
103 | && this.options.element.parentNode
104 | && this.options.element.parentNode.contains(event.relatedTarget)) {
105 | return
106 | }
107 |
108 | this.isActive = false
109 | this.sendUpdate()
110 | }
111 |
112 | destroy() {
113 | this.options.element.removeEventListener('mousedown', this.mousedownHandler)
114 |
115 | if (this.resizeObserver) {
116 | this.resizeObserver.unobserve(this.editorView.dom)
117 | }
118 | }
119 |
120 | }
121 |
122 | export default function (options) {
123 | return new Plugin({
124 | key: new PluginKey('floating_menu'),
125 | view(editorView) {
126 | return new Menu({ editorView, options })
127 | },
128 | })
129 | }
130 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Plugins/MenuBar.js:
--------------------------------------------------------------------------------
1 | import { Plugin, PluginKey } from 'prosemirror-state'
2 |
3 | class Menu {
4 |
5 | constructor({ options }) {
6 | this.options = options
7 | this.preventHide = false
8 |
9 | // the mousedown event is fired before blur so we can prevent it
10 | this.mousedownHandler = this.handleClick.bind(this)
11 | this.options.element.addEventListener('mousedown', this.mousedownHandler)
12 |
13 | this.options.editor.on('blur', () => {
14 | if (this.preventHide) {
15 | this.preventHide = false
16 | return
17 | }
18 |
19 | this.options.editor.emit('menubar:focusUpdate', false)
20 | })
21 | }
22 |
23 | handleClick() {
24 | this.preventHide = true
25 | }
26 |
27 | destroy() {
28 | this.options.element.removeEventListener('mousedown', this.mousedownHandler)
29 | }
30 |
31 | }
32 |
33 | export default function (options) {
34 | return new Plugin({
35 | key: new PluginKey('menu_bar'),
36 | view(editorView) {
37 | return new Menu({ editorView, options })
38 | },
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Plugins/MenuBubble.js:
--------------------------------------------------------------------------------
1 | import { Plugin, PluginKey } from 'prosemirror-state'
2 |
3 | function textRange(node, from, to) {
4 | const range = document.createRange()
5 | range.setEnd(node, to == null ? node.nodeValue.length : to)
6 | range.setStart(node, from || 0)
7 | return range
8 | }
9 |
10 | function singleRect(object, bias) {
11 | const rects = object.getClientRects()
12 | return !rects.length ? object.getBoundingClientRect() : rects[bias < 0 ? 0 : rects.length - 1]
13 | }
14 |
15 | function coordsAtPos(view, pos, end = false) {
16 | const { node, offset } = view.docView.domFromPos(pos)
17 | let side
18 | let rect
19 | if (node.nodeType === 3) {
20 | if (end && offset < node.nodeValue.length) {
21 | rect = singleRect(textRange(node, offset - 1, offset), -1)
22 | side = 'right'
23 | } else if (offset < node.nodeValue.length) {
24 | rect = singleRect(textRange(node, offset, offset + 1), -1)
25 | side = 'left'
26 | }
27 | } else if (node.firstChild) {
28 | if (offset < node.childNodes.length) {
29 | const child = node.childNodes[offset]
30 | rect = singleRect(child.nodeType === 3 ? textRange(child) : child, -1)
31 | side = 'left'
32 | }
33 | if ((!rect || rect.top === rect.bottom) && offset) {
34 | const child = node.childNodes[offset - 1]
35 | rect = singleRect(child.nodeType === 3 ? textRange(child) : child, 1)
36 | side = 'right'
37 | }
38 | } else {
39 | rect = node.getBoundingClientRect()
40 | side = 'left'
41 | }
42 |
43 | const x = rect[side]
44 |
45 | return {
46 | top: rect.top,
47 | bottom: rect.bottom,
48 | left: x,
49 | right: x,
50 | }
51 | }
52 |
53 |
54 | class Menu {
55 |
56 | constructor({ options, editorView }) {
57 | this.options = {
58 | ...{
59 | element: null,
60 | keepInBounds: true,
61 | onUpdate: () => false,
62 | },
63 | ...options,
64 | }
65 | this.editorView = editorView
66 | this.isActive = false
67 | this.left = 0
68 | this.bottom = 0
69 | this.top = 0
70 | this.preventHide = false
71 |
72 | // the mousedown event is fired before blur so we can prevent it
73 | this.mousedownHandler = this.handleClick.bind(this)
74 | this.options.element.addEventListener('mousedown', this.mousedownHandler)
75 |
76 | this.options.editor.on('focus', ({ view }) => {
77 | this.update(view)
78 | })
79 |
80 | this.options.editor.on('blur', ({ event }) => {
81 | if (this.preventHide) {
82 | this.preventHide = false
83 | return
84 | }
85 |
86 | this.hide(event)
87 | })
88 | }
89 |
90 | handleClick() {
91 | this.preventHide = true
92 | }
93 |
94 | update(view, lastState) {
95 | const { state } = view
96 |
97 | if (view.composing) {
98 | return
99 | }
100 |
101 | // Don't do anything if the document/selection didn't change
102 | if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
103 | return
104 | }
105 |
106 | // Hide the tooltip if the selection is empty
107 | if (state.selection.empty) {
108 | this.hide()
109 | return
110 | }
111 |
112 | // Otherwise, reposition it and update its content
113 | const { from, to } = state.selection
114 |
115 | // These are in screen coordinates
116 | // We can't use EditorView.cordsAtPos here because it can't handle linebreaks correctly
117 | // See: https://github.com/ProseMirror/prosemirror-view/pull/47
118 | const start = coordsAtPos(view, from)
119 | const end = coordsAtPos(view, to, true)
120 |
121 | // The box in which the tooltip is positioned, to use as base
122 | const parent = this.options.element.offsetParent
123 |
124 | if (!parent) {
125 | this.hide()
126 | return
127 | }
128 |
129 | const box = parent.getBoundingClientRect()
130 | const el = this.options.element.getBoundingClientRect()
131 |
132 | // Find a center-ish x position from the selection endpoints (when
133 | // crossing lines, end may be more to the left)
134 | const left = ((start.left + end.left) / 2) - box.left
135 |
136 | // Keep the menuBubble in the bounding box of the offsetParent i
137 | this.left = Math.round(this.options.keepInBounds
138 | ? Math.min(box.width - (el.width / 2), Math.max(left, el.width / 2)) : left)
139 | this.bottom = Math.round(box.bottom - start.top)
140 | this.top = Math.round(end.bottom - box.top)
141 | this.isActive = true
142 |
143 | this.sendUpdate()
144 | }
145 |
146 | sendUpdate() {
147 | this.options.onUpdate({
148 | isActive: this.isActive,
149 | left: this.left,
150 | bottom: this.bottom,
151 | top: this.top,
152 | })
153 | }
154 |
155 | hide(event) {
156 | if (event
157 | && event.relatedTarget
158 | && this.options.element.parentNode
159 | && this.options.element.parentNode.contains(event.relatedTarget)) {
160 | return
161 | }
162 |
163 | this.isActive = false
164 | this.sendUpdate()
165 | }
166 |
167 | destroy() {
168 | this.options.element.removeEventListener('mousedown', this.mousedownHandler)
169 | }
170 |
171 | }
172 |
173 | export default function (options) {
174 | return new Plugin({
175 | key: new PluginKey('menu_bubble'),
176 | view(editorView) {
177 | return new Menu({ editorView, options })
178 | },
179 | })
180 | }
181 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/ComponentView.js:
--------------------------------------------------------------------------------
1 | import { compile } from 'svelte/compiler'
2 |
3 | import { getMarkRange } from 'tiptap-utils'
4 |
5 | export default class ComponentView {
6 |
7 | constructor(component, {
8 | editor,
9 | extension,
10 | parent,
11 | node,
12 | view,
13 | decorations,
14 | getPos,
15 | }) {
16 | this.component = component
17 | this.editor = editor
18 | this.extension = extension
19 | this.parent = parent
20 | this.node = node
21 | this.view = view
22 | this.decorations = decorations
23 | this.isNode = !!this.node.marks
24 | this.isMark = !this.isNode
25 | this.getPos = this.isMark ? this.getMarkPos : getPos
26 | this.captureEvents = true
27 | this.dom = this.createDOM()
28 | // HACK: We're requiring extension nodes to have an element of class `content-xx` but there should be a better way
29 | // TODO: Is there a way to get the bound content element out of a component?
30 | this.contentDOM = this.dom.getElementsByClassName('content-xx')[0]
31 | }
32 |
33 | createDOM() {
34 | //const Component = compile(this.component.template);
35 | const Component = this.component;
36 | const props = {
37 | editor: this.editor,
38 | node: this.node,
39 | view: this.view,
40 | getPos: () => this.getPos(),
41 | decorations: this.decorations,
42 | selected: false,
43 | options: this.extension.options,
44 | updateAttrs: attrs => this.updateAttrs(attrs),
45 | }
46 |
47 | if (typeof this.extension.setSelection === 'function') {
48 | this.setSelection = this.extension.setSelection
49 | }
50 |
51 | // HACK: We're creating this component in a temporary container so that we can get its element
52 | // TODO: Is there a way to get the element out of the component directly?
53 | var container = document.createElement('div');
54 | container.id = Math.floor((Math.random() * 100000) + 1);
55 |
56 | // TODO: Fix the parent, which gets set initially in EditorContent.onMount
57 | this.vm = new Component({
58 | target: container, // this.parent
59 | props
60 | })
61 |
62 | return container; // this.vm;
63 | }
64 |
65 | update(node, decorations) {
66 | if (node.type !== this.node.type) {
67 | return false
68 | }
69 |
70 | if (node === this.node && this.decorations === decorations) {
71 | return true
72 | }
73 |
74 | this.node = node
75 | this.decorations = decorations
76 |
77 | this.updateComponentProps({
78 | node,
79 | decorations,
80 | })
81 |
82 | return true
83 | }
84 |
85 | updateComponentProps(props) {
86 | if (!this.vm.$set) {
87 | return
88 | }
89 |
90 | Object.entries(props).forEach(([key, value]) => {
91 | var opts = {}
92 | opts[key] = value
93 | this.vm.$set(opts)
94 | })
95 | }
96 |
97 | updateAttrs(attrs) {
98 | if (!this.view.editable) {
99 | return
100 | }
101 |
102 | const { state } = this.view
103 | const { type } = this.node
104 | const pos = this.getPos()
105 | const newAttrs = {
106 | ...this.node.attrs,
107 | ...attrs,
108 | }
109 | const transaction = this.isMark
110 | ? state.tr
111 | .removeMark(pos.from, pos.to, type)
112 | .addMark(pos.from, pos.to, type.create(newAttrs))
113 | : state.tr.setNodeMarkup(pos, null, newAttrs)
114 |
115 | this.view.dispatch(transaction)
116 | }
117 |
118 | // prevent a full re-render of the svelte component on update
119 | // we'll handle prop updates in `update()`
120 | ignoreMutation(mutation) {
121 | if (!this.contentDOM) {
122 | return true
123 | }
124 | return !this.contentDOM.contains(mutation.target)
125 | }
126 |
127 | // disable (almost) all prosemirror event listener for node views
128 | stopEvent(event) {
129 | if (typeof this.extension.stopEvent === 'function') {
130 | return this.extension.stopEvent(event)
131 | }
132 |
133 | const draggable = !!this.extension.schema.draggable
134 |
135 | // support a custom drag handle
136 | if (draggable && event.type === 'mousedown') {
137 | const dragHandle = event.target.closest
138 | && event.target.closest('[data-drag-handle]')
139 | const isValidDragHandle = dragHandle
140 | && (this.dom === dragHandle || this.dom.contains(dragHandle))
141 |
142 | if (isValidDragHandle) {
143 | this.captureEvents = false
144 | document.addEventListener('dragend', () => {
145 | this.captureEvents = true
146 | }, { once: true })
147 | }
148 | }
149 |
150 | const isCopy = event.type === 'copy'
151 | const isPaste = event.type === 'paste'
152 | const isCut = event.type === 'cut'
153 | const isDrag = event.type.startsWith('drag') || event.type === 'drop'
154 |
155 | if ((draggable && isDrag) || isCopy || isPaste || isCut) {
156 | return false
157 | }
158 |
159 | return this.captureEvents
160 | }
161 |
162 | selectNode() {
163 | this.updateComponentProps({
164 | selected: true,
165 | })
166 | }
167 |
168 | deselectNode() {
169 | this.updateComponentProps({
170 | selected: false,
171 | })
172 | }
173 |
174 | getMarkPos() {
175 | const pos = this.view.posAtDOM(this.dom)
176 | const resolvedPos = this.view.state.doc.resolve(pos)
177 | const range = getMarkRange(resolvedPos, this.node.type)
178 | return range
179 | }
180 |
181 | destroy() {
182 | // TODO: Maybe onDestroy??
183 | ////this.vm.$destroy()
184 | }
185 |
186 | }
187 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/Emitter.js:
--------------------------------------------------------------------------------
1 | export default class Emitter {
2 | // Add an event listener for given event
3 | on(event, fn) {
4 | this._callbacks = this._callbacks || {}
5 | // Create namespace for this event
6 | if (!this._callbacks[event]) {
7 | this._callbacks[event] = []
8 | }
9 | this._callbacks[event].push(fn)
10 | return this
11 | }
12 |
13 | emit(event, ...args) {
14 | this._callbacks = this._callbacks || {}
15 | const callbacks = this._callbacks[event]
16 |
17 | if (callbacks) {
18 | callbacks.forEach(callback => callback.apply(this, args))
19 | }
20 |
21 | return this
22 | }
23 |
24 | // Remove event listener for given event.
25 | // If fn is not provided, all event listeners for that event will be removed.
26 | // If neither is provided, all event listeners will be removed.
27 | off(event, fn) {
28 |
29 | if (!arguments.length) {
30 | this._callbacks = {}
31 | } else {
32 | // event listeners for the given event
33 | const callbacks = this._callbacks ? this._callbacks[event] : null
34 | if (callbacks) {
35 | if (fn) {
36 | this._callbacks[event] = callbacks.filter(cb => cb !== fn) // remove specific handler
37 | } else {
38 | delete this._callbacks[event] // remove all handlers
39 | }
40 | }
41 | }
42 |
43 | return this
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/Extension.js:
--------------------------------------------------------------------------------
1 | export default class Extension {
2 |
3 | constructor(options = {}) {
4 | this.options = {
5 | ...this.defaultOptions,
6 | ...options,
7 | }
8 | }
9 |
10 | init() {
11 | return null
12 | }
13 |
14 | bindEditor(editor = null) {
15 | this.editor = editor
16 | }
17 |
18 | get name() {
19 | return null
20 | }
21 |
22 | get type() {
23 | return 'extension'
24 | }
25 |
26 | get update() {
27 | return () => {}
28 | }
29 |
30 | get defaultOptions() {
31 | return {}
32 | }
33 |
34 | get plugins() {
35 | return []
36 | }
37 |
38 | inputRules() {
39 | return []
40 | }
41 |
42 | pasteRules() {
43 | return []
44 | }
45 |
46 | keys() {
47 | return {}
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/Mark.js:
--------------------------------------------------------------------------------
1 | import Extension from './Extension'
2 |
3 | export default class Mark extends Extension {
4 |
5 | constructor(options = {}) {
6 | super(options)
7 | }
8 |
9 | get type() {
10 | return 'mark'
11 | }
12 |
13 | get view() {
14 | return null
15 | }
16 |
17 | get schema() {
18 | return null
19 | }
20 |
21 | command() {
22 | return () => {}
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/Node.js:
--------------------------------------------------------------------------------
1 | import Extension from './Extension'
2 |
3 | export default class Node extends Extension {
4 |
5 | constructor(options = {}) {
6 | super(options)
7 | }
8 |
9 | get type() {
10 | return 'node'
11 | }
12 |
13 | get view() {
14 | return null
15 | }
16 |
17 | get schema() {
18 | return null
19 | }
20 |
21 | command() {
22 | return () => {}
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/camelCase.js:
--------------------------------------------------------------------------------
1 | export default function (str) {
2 | return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase())).replace(/\s+/g, '')
3 | }
4 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/index.js:
--------------------------------------------------------------------------------
1 | export { default as camelCase } from './camelCase'
2 | export { default as ComponentView } from './ComponentView'
3 | export { default as Emitter } from './Emitter'
4 | export { default as Extension } from './Extension'
5 | export { default as ExtensionManager } from './ExtensionManager'
6 | export { default as injectCSS } from './injectCSS'
7 | export { default as Mark } from './Mark'
8 | export { default as minMax } from './minMax'
9 | export { default as Node } from './Node'
10 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/injectCSS.js:
--------------------------------------------------------------------------------
1 | export default function (css) {
2 | if (process.env.NODE_ENV !== 'test') {
3 | const style = document.createElement('style')
4 | style.type = 'text/css'
5 | style.textContent = css
6 | const { head } = document
7 | const { firstChild } = head
8 |
9 | if (firstChild) {
10 | head.insertBefore(style, firstChild)
11 | } else {
12 | head.appendChild(style)
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/Utils/minMax.js:
--------------------------------------------------------------------------------
1 | export default function minMax(value = 0, min = 0, max = 0) {
2 | return Math.min(Math.max(parseInt(value, 10), min), max)
3 | }
4 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Editor } from './Editor'
2 | export { Extension, Node, Mark } from './Utils'
3 | export { Doc, Paragraph, Text } from './Nodes'
4 | export { default as EditorContent } from './Components/EditorContent'
5 | export { default as EditorMenuBar } from './Components/EditorMenuBar'
6 | export { default as EditorMenuBubble } from './Components/EditorMenuBubble'
7 | export { default as EditorFloatingMenu } from './Components/EditorFloatingMenu'
8 | export {
9 | Plugin,
10 | PluginKey,
11 | TextSelection,
12 | NodeSelection,
13 | } from 'prosemirror-state'
14 |
--------------------------------------------------------------------------------
/tiptap-svelte/src/style.css:
--------------------------------------------------------------------------------
1 | .ProseMirror {
2 | position: relative;
3 | }
4 |
5 | .ProseMirror {
6 | word-wrap: break-word;
7 | white-space: pre-wrap;
8 | -webkit-font-variant-ligatures: none;
9 | font-variant-ligatures: none;
10 | }
11 |
12 | .ProseMirror pre {
13 | white-space: pre-wrap;
14 | }
15 |
16 | .ProseMirror-gapcursor {
17 | display: none;
18 | pointer-events: none;
19 | position: absolute;
20 | }
21 |
22 | .ProseMirror-gapcursor:after {
23 | content: "";
24 | display: block;
25 | position: absolute;
26 | top: -2px;
27 | width: 20px;
28 | border-top: 1px solid black;
29 | animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
30 | }
31 |
32 | @keyframes ProseMirror-cursor-blink {
33 | to {
34 | visibility: hidden;
35 | }
36 | }
37 |
38 | .ProseMirror-hideselection *::selection {
39 | background: transparent;
40 | }
41 |
42 | .ProseMirror-hideselection *::-moz-selection {
43 | background: transparent;
44 | }
45 |
46 | .ProseMirror-hideselection * {
47 | caret-color: transparent;
48 | }
49 |
50 | .ProseMirror-focused .ProseMirror-gapcursor {
51 | display: block;
52 | }
53 |
--------------------------------------------------------------------------------