├── .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 | 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 | 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 | 90 | 96 | 97 | 103 | 104 | 110 | 111 | 117 | 118 | 124 | 125 | 131 | 132 | 138 | 139 | 145 | 146 | 152 | 153 | 159 | 160 | 166 | 167 | 173 | 174 | 180 | 181 | 184 | 185 | 188 | 189 | 192 | 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 |
20 |
21 |
22 |
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 | 38 | ` 39 | }); 40 | }); 41 | 42 | onDestroy(() => { 43 | if (editor) { 44 | editor.destroy(); 45 | } 46 | }); 47 | 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 |
96 | 97 | 103 | 104 | 110 | 111 | 117 | 118 | 124 | 125 | 131 | 132 | 138 | 139 | 145 | 146 |
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 | 54 | 55 | 58 | 59 | 62 | 63 | 64 | 65 | 66 |
67 | {/if} 68 | -------------------------------------------------------------------------------- /tiptap-svelte-examples/src/routes/images/index.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 | {#if editor} 55 |
56 | 57 | 62 | 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 | 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 | 127 | 128 | 134 | 135 | 141 | 142 | 148 | 149 | 155 | 156 | 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://img.shields.io/npm/v/tiptap-extensions.svg?label=version)](https://www.npmjs.com/package/tiptap-extensions) 5 | [![](https://img.shields.io/npm/dm/tiptap-extensions.svg)](https://npmcharts.com/compare/tiptap-extensions?minimal=true) 6 | [![](https://img.shields.io/npm/l/tiptap-extensions.svg)](https://www.npmjs.com/package/tiptap-extensions) 7 | [![](http://img.badgesize.io/https://unpkg.com/tiptap-extensions/dist/extensions.min.js?compression=gzip&label=size&colorB=000000)](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) -> [, "Lorem", "image.jpg"] 9 | * ![](image.jpg "Ipsum") -> [, "", "image.jpg", "Ipsum"] 10 | * ![Lorem](image.jpg "Ipsum") -> [, "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://img.shields.io/npm/v/tiptap.svg?label=version)](https://www.npmjs.com/package/tiptap) 5 | [![](https://img.shields.io/npm/dm/tiptap.svg)](https://npmcharts.com/compare/tiptap?minimal=true) 6 | [![](https://img.shields.io/npm/l/tiptap.svg)](https://www.npmjs.com/package/tiptap) 7 | [![](http://img.badgesize.io/https://unpkg.com/tiptap/dist/tiptap.min.js?compression=gzip&label=size&colorB=000000)](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 | --------------------------------------------------------------------------------