remove tiptap v1

This commit is contained in:
Hans Pagel 2021-04-21 15:04:23 +02:00
parent 2466d3398f
commit 2c20803d7e
201 changed files with 0 additions and 25010 deletions

View File

@ -1,14 +0,0 @@
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
insert_final_newline = true
max_line_length = 100
[*.{html,js,css,scss,vue}]
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

View File

@ -1 +0,0 @@
**/*.css

View File

@ -1,62 +0,0 @@
module.exports = {
plugins: ['html'],
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module',
},
env: {
es6: true,
node: true,
},
globals: {
document: false,
navigator: false,
window: false,
collect: false,
cy: false,
test: false,
expect: false,
it: false,
describe: false,
FileReader: false,
},
extends: [
'plugin:vue/base',
'airbnb-base',
],
rules: {
// required semicolons
'semi': ['error', 'never'],
// error handling
'no-console': ['error', { allow: ['warn', 'error'] }],
// indent
'no-tabs': 'off',
'indent': 'off',
// disable some import stuff
'import/extensions': 'off',
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': 'off',
'import/no-dynamic-require': 'off',
// disable for '__svg__'
'no-underscore-dangle': 'off',
'arrow-parens': ['error', 'as-needed'],
'padded-blocks': 'off',
'class-methods-use-this': 'off',
'global-require': 'off',
'func-names': ['error', 'never'],
}
}

View File

@ -1,37 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
url: https://github.com/ueberdosis/tiptap/issues/new
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to Reproduce / Codesandbox Example**
Steps to reproduce the behavior:
1. Go to '…'
2. Click on '…'
3. Scroll down to '…'
4. See error
Fork this or create a new Codesandbox replicating your error
https://codesandbox.io/s/vue-issue-template-h0g28
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- Mobile / Desktop: [eg. Desktop]
**Additional context**
Add any other context about the problem here.

View File

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Ask a Question
url: https://github.com/ueberdosis/tiptap/discussions/new
about: Ask the community for help

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,9 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "hanspagel"

View File

@ -1,55 +0,0 @@
name: ci
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2.3.4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.5
with:
node-version: ${{ matrix.node-version }}
- run: yarn install
- run: yarn build:packages
- run: yarn audit-ci
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2.3.4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.5
with:
node-version: ${{ matrix.node-version }}
- run: yarn install
- run: yarn build:packages
- run: yarn lint
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2.3.4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.5
with:
node-version: ${{ matrix.node-version }}
- run: yarn install
- run: yarn build:packages
- run: yarn test

23
.gitignore vendored
View File

@ -1,23 +0,0 @@
.history
.DS_Store
node_modules
dist/
coverage/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

View File

@ -1,10 +0,0 @@
tasks:
- init: yarn install
command: yarn run start
ports:
- port: 3000
onOpen: open-preview
vscode:
extensions:
- octref.vetur@0.23.0:0z6KpEz8h/vAvy4pYRDg3Q==

1
.nvmrc
View File

@ -1 +0,0 @@
v12.5.0

View File

@ -1,19 +0,0 @@
language: node_js
node_js:
- 10
cache: yarn
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s
- export PATH=$HOME/.yarn/bin:$PATH
install:
- yarn install
- yarn build:packages
script:
- yarn audit-ci
- yarn lint
- yarn test

View File

@ -1,49 +0,0 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
Please read and understand the contribution guide before creating an issue or pull request.
## Etiquette
This project is open source, and as such, the maintainers give their free time to build and maintain the source code
held within. They make the code freely available in the hope that it will be of use to other developers. It would be
extremely unfair for them to suffer abuse or anger for their hard work.
Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
world that developers are civilized and selfless people.
It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
## Viability
When requesting or submitting new features, first consider whether it might be useful to others. Open
source projects are used by many developers, who may have entirely different needs to your own. Think about
whether or not your feature is likely to be used by other users of the project.
## Procedure
Before filing an issue:
- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
- Check to make sure your feature suggestion isn't already present within the project.
- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
- Check the pull requests tab to ensure that the feature isn't already in progress.
Before submitting a pull request:
- Check the codebase to ensure that your feature doesn't already exist.
- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
## Requirements
If the project maintainer has any additional requirements, you will find them listed here.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
**Happy coding**!

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020, überdosis GbR
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

558
README.md
View File

@ -1,558 +0,0 @@
> Were working on [tiptap 2](https://blog.ueber.io/post/tiptap-2-0-beta/). Become a sponsor to get access immediately! [Sponsor 💖](https://github.com/sponsors/ueberdosis)
# tiptap
A renderless and extendable rich-text editor for [Vue.js](https://github.com/vuejs/vue)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/ueberdosis/tiptap)
[![Version](https://img.shields.io/npm/v/tiptap.svg?label=version)](https://www.npmjs.com/package/tiptap)
[![Downloads](https://img.shields.io/npm/dm/tiptap.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/tiptap.svg)](https://www.npmjs.com/package/tiptap)
[![Filesize](https://img.badgesize.io/https://unpkg.com/tiptap/dist/tiptap.min.js?compression=gzip&label=size&colorB=000000)](https://www.npmjs.com/package/tiptap)
[![Build Status](https://github.com/ueberdosis/tiptap/workflows/ci/badge.svg)](https://github.com/ueberdosis/tiptap/actions)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
## [FAQ] Should I start to integrate tiptap 1 or wait for tiptap 2?
Good question! [tiptap 2](https://blog.ueber.io/post/tiptap-2-0-beta/) is about to come in the next months and were going to provide an upgrade guide for you. Sure, there are a lot of things that will change, but you should be able to refactor everything in an hour so (depending on the size of your project).
The extension API will have a lot of breaking changes. So if youre up to write a lot of custom extensions, expect to rewrite them for tiptap 2. Youll likely reuse all the single parts (schema, inputRules, pasteRules, keyboard shortcuts …), but the API to register them will be different.
**For the braves:** [Sponsor us](https://github.com/sponsors/ueberdosis) to get access to tiptap 2 and start your project with a fresh breeze of air.
**For everyone else:** No need to wait for tiptap 2. Start your project, youll be able to upgrade in a reasonable amount of time.
## Why I built tiptap
I was looking for a text editor for [Vue.js](https://github.com/vuejs/vue) and found some solutions that didn't really satisfy me. The editor should be easy to extend and not based on old dependencies such as jQuery. For React there is already a great editor called [Slate.js](https://github.com/ianstormtaylor/slate), which impresses with its modularity. I came across [Prosemirror](https://github.com/prosemirror) and decided to build on it. Prosemirror is a toolkit for building rich-text editors that are already in use at many well-known companies such as *Atlassian* or *New York Times*.
### What does `renderless` mean?
With renderless components you'll have (almost) full control over markup and styling. I don't want to tell you what a menu should look like or where it should be rendered in the DOM. That's all up to you. There is also a [good article about renderless components](https://adamwathan.me/renderless-components-in-vuejs/) by Adam Wathan.
### How is the data stored under the hood?
You can save your data as a raw `HTML` string or can get a `JSON`-serializable representation of your document. And of course, you can pass these two types back to the editor.
## 💖 Sponsor the development
Are you using tiptap in production? We need your sponsorship to maintain, update and develop tiptap. [Become a Sponsor now!](https://github.com/sponsors/ueberdosis)
## Examples
To check out some live examples, visit [tiptap.dev](https://tiptap.dev/).
## Installation
```
npm install tiptap
```
or
```
yarn add tiptap
```
## Basic Setup
```vue
<template>
<editor-content :editor="editor" />
</template>
<script>
// Import the editor
import { Editor, EditorContent } from 'tiptap'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
content: '<p>This is just a boring paragraph</p>',
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
```
## Editor Properties
| **Property** | **Type** | **Default** | **Description** |
| ---------------------- | :--------------: | :---------: | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content` | `Object\|String` | `null` | The editor state object used by Prosemirror. You can also pass HTML to the `content` slot. When used both, the `content` slot will be ignored. |
| `editorProps` | `Object` | `{}` | A list of [Prosemirror editorProps](https://prosemirror.net/docs/ref/#view.EditorProps). |
| `editable` | `Boolean` | `true` | When set to `false` the editor is read-only. |
| `autoFocus` | `Boolean` | `false` | Focus the editor on init. |
| `extensions` | `Array` | `[]` | A list of extensions used, by the editor. This can be `Nodes`, `Marks` or `Plugins`. |
| `useBuiltInExtensions` | `Boolean` | `true` | By default tiptap adds a `Doc`, `Paragraph` and `Text` node to the Prosemirror schema. |
| `dropCursor` | `Object` | `{}` | Config for `prosemirror-dropcursor`. |
| `enableDropCursor` | `Boolean` | `true` | Option to enable / disable the dropCursor plugin. |
| `enableGapCursor` | `Boolean` | `true` | Option to enable / disable the gapCursor plugin. |
| `parseOptions` | `Object` | `{}` | A list of [Prosemirror parseOptions](https://prosemirror.net/docs/ref/#model.ParseOptions). |
| `onInit` | `Function` | `undefined` | This will return an Object with the current `state` and `view` of Prosemirror on init. |
| `onFocus` | `Function` | `undefined` | This will return an Object with the `event` and current `state` and `view` of Prosemirror on focus. |
| `onBlur` | `Function` | `undefined` | This will return an Object with the `event` and current `state` and `view` of Prosemirror on blur. |
| `onUpdate` | `Function` | `undefined` | This will return an Object with the current `state` of Prosemirror, a `getJSON()` and `getHTML()` function and the `transaction` on every change. |
## Editor Methods
| **Method** | **Arguments** | **Description** |
| ---------------- | :---------------------------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setContent` | `content, emitUpdate, parseOptions` | Replace the current content. You can pass an HTML string or a JSON document. `emitUpdate` defaults to `false`. `parseOptions` defaults to those provided in constructor. |
| `clearContent` | `emitUpdate` | Clears the current content. `emitUpdate` defaults to `false`. |
| `setOptions` | `options` | Overwrites the current editor properties. |
| `registerPlugin` | `plugin`, `handlePlugins` | Register a Prosemirror plugin. You can pass a function `handlePlugins` with parameters `(plugin, oldPlugins)` to define an order in which `newPlugins` will be called. `handlePlugins` defaults to pushing `plugin` to front of `oldPlugins`. |
| `getJSON` | | Get the current content as JSON. |
| `getHTML` | | Get the current content as HTML. |
| `focus` | | Focus the editor. |
| `blur` | | Blur the editor. |
| `destroy` | | Destroy the editor. |
## Components
| **Name** | **Description** |
| -------------------------- | -------------------------------------- |
| `<editor-content />` | Here the content will be rendered. |
| `<editor-menu-bar />` | Here a menu bar will be rendered. |
| `<editor-menu-bubble />` | Here a menu bubble will be rendered. |
| `<editor-floating-menu />` | Here a floating menu will be rendered. |
### EditorMenuBar
The `<editor-menu-bar />` component is renderless and will receive some properties through a scoped slot.
| **Property** | **Type** | **Description** |
| -------------- | :--------: | ------------------------------------------------------------------------------------------------------ |
| `commands` | `Array` | A list of all commands. |
| `isActive` | `Object` | An object of functions to check if your selected text is a node or mark. `isActive.{node|mark}(attrs)` |
| `getMarkAttrs` | `Function` | A function to get all mark attributes of your selection. |
| `getNodeAttrs` | `Function` | A function to get all node attributes of your selection. |
| `focused` | `Boolean` | Whether the editor is focused. |
| `focus` | `Function` | A function to focus the editor. |
#### Example
```vue
<template>
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div>
<button :class="{ 'is-active': isActive.bold() }" @click="commands.bold">
Bold
</button>
<button :class="{ 'is-active': isActive.heading({ level: 2 }) }" @click="commands.heading({ level: 2 })">
H2
</button>
</div>
</editor-menu-bar>
</template>
```
### EditorMenuBubble
The `<editor-menu-bubble />` component is renderless and will receive some properties through a scoped slot.
| **Property** | **Type** | **Description** |
| -------------- | :--------: | ------------------------------------------------------------------------------------------------------ |
| `commands` | `Array` | A list of all commands. |
| `isActive` | `Object` | An object of functions to check if your selected text is a node or mark. `isActive.{node|mark}(attrs)` |
| `getMarkAttrs` | `Function` | A function to get all mark attributes of your selection. |
| `getNodeAttrs` | `Function` | A function to get all node attributes of your selection. |
| `focused` | `Boolean` | Whether the editor is focused. |
| `focus` | `Function` | A function to focus the editor. |
| `menu` | `Object` | An object for positioning your menu. |
#### Example
```vue
<template>
<editor-menu-bubble :editor="editor" v-slot="{ commands, isActive, menu }">
<div
:class="{ 'is-active': menu.isActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
>
<button :class="{ 'is-active': isActive.bold() }" @click="commands.bold">
Bold
</button>
<button :class="{ 'is-active': isActive.heading({ level: 2 }) }" @click="commands.heading({ level: 2 })">
H2
</button>
</div>
</editor-menu-bubble>
</template>
```
### EditorFloatingMenu
The `<editor-floating-menu />` component is renderless and will receive some properties through a scoped slot.
| **Property** | **Type** | **Description** |
| -------------- | :--------: | ------------------------------------------------------------------------------------------------------ |
| `commands` | `Array` | A list of all commands. |
| `isActive` | `Object` | An object of functions to check if your selected text is a node or mark. `isActive.{node|mark}(attrs)` |
| `getMarkAttrs` | `Function` | A function to get all mark attributes of your selection. |
| `getNodeAttrs` | `Function` | A function to get all node attributes of your selection. |
| `focused` | `Boolean` | Whether the editor is focused. |
| `focus` | `Function` | A function to focus the editor. |
| `menu` | `Object` | An object for positioning your menu. |
#### Example
```vue
<template>
<editor-floating-menu :editor="editor" v-slot="{ commands, isActive, menu }">
<div
:class="{ 'is-active': menu.isActive }"
:style="`top: ${menu.top}px`"
>
<button :class="{ 'is-active': isActive.bold() }" @click="commands.bold">
Bold
</button>
<button :class="{ 'is-active': isActive.heading({ level: 2 }) }" @click="commands.heading({ level: 2 })">
H2
</button>
</div>
</editor-floating-menu>
</template>
```
## Extensions
By default, the editor will only support paragraphs. Other nodes and marks are available as **extensions**. There is a package called `tiptap-extensions` with the most basic nodes, marks, and plugins.
### Available Extensions
```vue
<template>
<div>
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<button :class="{ 'is-active': isActive.bold() }" @click="commands.bold">
Bold
</button>
</editor-menu-bar>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
Blockquote,
CodeBlock,
HardBreak,
Heading,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorMenuBar,
EditorContent,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new BulletList(),
new OrderedList(),
new ListItem(),
new TodoItem(),
new TodoList(),
new Bold(),
new Code(),
new Italic(),
new Link(),
new Strike(),
new Underline(),
new History(),
],
content: `
<h1>Yay Headlines!</h1>
<p>All these <strong>cool tags</strong> are working now.</p>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
```
### Create Custom Extensions
The most powerful feature of tiptap is that you can create your own extensions. There are 3 types of extensions.
| **Type** | **Description** |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `Extension` | The most basic type. It's useful to register some [Prosemirror plugins](https://prosemirror.net/docs/guide/) or some input rules. |
| `Node` | Add a custom node. Nodes are block elements like a headline or a paragraph. |
| `Mark` | Add a custom mark. Marks are used to add extra styling or other information to inline content like a strong tag or links. |
### Extension Class
| **Method** | **Type** | **Default** | **Description** |
| ----------------------------- | :--------: | :---------: | ---------------------------------------------------------------------------- |
| `get name()` | `String` | `null` | Define a name for your extension. |
| `get defaultOptions()` | `Object` | `{}` | Define some default options. The options are available as `this.$options`. |
| `get plugins()` | `Array` | `[]` | Define a list of [Prosemirror plugins](https://prosemirror.net/docs/guide/). |
| `keys({ schema })` | `Object` | `null` | Define some keybindings. |
| `commands({ schema, attrs })` | `Object` | `null` | Define a command. |
| `inputRules({ schema })` | `Array` | `[]` | Define a list of input rules. |
| `pasteRules({ schema })` | `Array` | `[]` | Define a list of paste rules. |
| `get update()` | `Function` | `undefined` | Called when options of extension are changed via `editor.extensions.options` |
### Node|Mark Class
| **Method** | **Type** | **Default** | **Description** |
| ----------------------------------- | :------: | :---------: | ------------------------------------------------------------------------------------- |
| `get name()` | `String` | `null` | Define a name for your node or mark. |
| `get defaultOptions()` | `Object` | `{}` | Define some default options. The options are available as `this.$options`. |
| `get schema()` | `Object` | `null` | Define a [schema](https://prosemirror.net/docs/guide/#schema). |
| `get view()` | `Object` | `null` | Define a node view as a vue component. |
| `keys({ type, schema })` | `Object` | `null` | Define some keybindings. |
| `commands({ type, schema, attrs })` | `Object` | `null` | Define a command. For example this is used for menus to convert to this node or mark. |
| `inputRules({ type, schema })` | `Array` | `[]` | Define a list of input rules. |
| `pasteRules({ type, schema })` | `Array` | `[]` | Define a list of paste rules. |
| `get plugins()` | `Array` | `[]` | Define a list of [Prosemirror plugins](https://prosemirror.net/docs/guide/). |
### Create a Node
Let's take a look at a real example. This is basically how the default `blockquote` node from [`tiptap-extensions`](https://www.npmjs.com/package/tiptap-extensions) looks like.
```js
import { Node } from 'tiptap'
import { wrappingInputRule, setBlockType, toggleWrap } from 'tiptap-commands'
export default class BlockquoteNode extends Node {
// choose a unique name
get name() {
return 'blockquote'
}
// the prosemirror schema object
// take a look at https://prosemirror.net/docs/guide/#schema for a detailed explanation
get schema() {
return {
content: 'block+',
group: 'block',
defining: true,
draggable: false,
// define how the editor will detect your node from pasted HTML
// every blockquote tag will be converted to this blockquote node
parseDOM: [
{ tag: 'blockquote' },
],
// this is how this node will be rendered
// in this case a blockquote tag with a class called `awesome-blockquote` will be rendered
// the '0' stands for its text content inside
toDOM: () => ['blockquote', { class: 'awesome-blockquote' }, 0],
}
}
// this command will be called from menus to add a blockquote
// `type` is the prosemirror schema object for this blockquote
// `schema` is a collection of all registered nodes and marks
commands({ type, schema }) {
return () => toggleWrap(type)
}
// here you can register some shortcuts
// in this case you can create a blockquote with `ctrl` + `>`
keys({ type }) {
return {
'Ctrl->': toggleWrap(type),
}
}
// a blockquote will be created when you are on a new line and type `>` followed by a space
inputRules({ type }) {
return [
wrappingInputRule(/^\s*>\s$/, type),
]
}
}
```
### Create a Node as a Vue Component
The real power of the nodes comes in combination with Vue components. Let us build an iframe node, where you can change its URL (this can also be found in our [examples](https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Embeds)).
```js
import { Node } from 'tiptap'
export default class IframeNode extends Node {
get name() {
return 'iframe'
}
get schema() {
return {
// here you have to specify all values that can be stored in this node
attrs: {
src: {
default: null,
},
},
group: 'block',
selectable: false,
// parseDOM and toDOM is still required to make copy and paste work
parseDOM: [{
tag: 'iframe',
getAttrs: dom => ({
src: dom.getAttribute('src'),
}),
}],
toDOM: node => ['iframe', {
src: node.attrs.src,
frameborder: 0,
allowfullscreen: 'true',
}],
}
}
// return a vue component
// this can be an object or an imported component
get view() {
return {
// there are some props available
// `node` is a Prosemirror Node Object
// `updateAttrs` is a function to update attributes defined in `schema`
// `view` is the ProseMirror view instance
// `options` is an array of your extension options
// `selected` is a boolean which is true when selected
// `editor` is a reference to the TipTap editor instance
// `getPos` is a function to retrieve the start position of the node
// `decorations` is an array of decorations around the node
props: ['node', 'updateAttrs', 'view'],
computed: {
src: {
get() {
return this.node.attrs.src
},
set(src) {
// we cannot update `src` itself because `this.node.attrs` is immutable
this.updateAttrs({
src,
})
},
},
},
template: `
<div class="iframe">
<iframe class="iframe__embed" :src="src"></iframe>
<input class="iframe__input" type="text" v-model="src" v-if="view.editable" />
</div>
`,
}
}
}
```
#### NodeView Prop Types
| **Prop** | **Type** | **Description** |
| ------------- | :--------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node` | `Object` | The Prosemirror node object. Common use case is to get `node.attrs` using a getter on a computed property. |
| `updateAttrs` | `Function` | A function to update `node.attrs` defined in `schema`. Common use case is as setter on a computed property. |
| `view` | `Object` | The Prosemirror editor view instance. |
| `options` | `Array` | An array of your extension options. |
| `getPos` | `Function` | A function that returns the anchored position of the node. |
| `selected` | `Boolean` | A boolean that is set when the node is or is not selected. Common use case is using `watch` to see when the view is selected/unselected to do something, such focus an `<input>` or refocus the editor. |
## Collaborative editing
Collaborative editing is a complex topic. Luckily, @naept wrote an article about [collaborative editing with tiptap](https://medium.com/@julien.aupart/easy-collaborative-editor-with-tiptap-and-prosemirror-baa3314636c6?sk=fd25b326cc148b43a0e0a46e584f40c2) and also published two helpful repositories:
* [tiptap-collab-server](https://github.com/naept/tiptap-collab-server) by @naept
* [tiptap-extension-collaboration](https://github.com/naept/tiptap-extension-collaboration) by @naept
## Browser Support
| ![Chrome](https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png) | ![Firefox](https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png) | ![IE](https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png) | ![Opera](https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png) | ![Safari](https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png) |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| Last 2 Versions ✔ | Last 2 Versions ✔ | Last 2 Versions ✔ | Last 2 Versions ✔ | Last 2 Versions ✔ |
## Development Setup
Currently, only Yarn is supported for development because of a feature called workspaces we are using here.
``` bash
# install dependencies
yarn install
# serve examples at localhost:3000
yarn start
# build dist files for packages
yarn build:packages
# build dist files for examples
yarn build:examples
```
## Contribute using the online one-click setup
You can use Gitpod(a free online VS Code-like IDE) for contributing. With a single click, it will launch a workspace and automatically:
- clone the `tiptap` repo.
- install the dependencies.
- run `yarn run start`.
So that anyone interested in contributing can start straight away.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/ueberdosis/tiptap/)
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
## Credits
- [Philipp Kühn](https://github.com/philippkuehn)
- [Hans Pagel](https://github.com/hanspagel)
- [Christoph Flathmann](https://github.com/Chrissi2812)
- [Erick Wilder](https://github.com/erickwilder)
- [Marius Tolzmann](https://github.com/mariux)
- [All Contributors](../../contributors)
## Related projects
- [html-to-prosemirror](https://github.com/ueberdosis/html-to-prosemirror) by @hanspagel
- [prosemirror-to-html](https://github.com/ueberdosis/prosemirror-to-html) by @hanspagel
- [tiptap-svelte](https://github.com/andrewjk/tiptap-svelte) by @andrewjk
- [Laravel Nova Tiptap Editor Field](https://github.com/manogi/nova-tiptap) by @manogi
- [WYSIWYG editor for Vuetify](https://github.com/iliyaZelenko/tiptap-vuetify) by @iliyaZelenko
- [Quasar Tiptap Demo](https://github.com/kfields/quasar-tiptap-demo) @kfields
- [Python Library that converts tiptap JSON](https://github.com/scrolltech/tiptapy) @scrolltech
- [WYSIWYG editor for Element UI](https://github.com/Leecason/element-tiptap) by @Leecason
- [WYSIWYG editor for Quasar Framework](https://github.com/donotebase/quasar-tiptap) by @mekery
## Love our work?
[Sponsor us](https://github.com/sponsors/ueberdosis) ❤️
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@ -1,10 +0,0 @@
module.exports = {
presets: [
[
'@babel/preset-env',
],
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
],
}

View File

@ -1,24 +0,0 @@
import ora from 'ora'
import webpack from 'webpack'
import config from './webpack.config'
const spinner = ora('Building …')
export default new Promise((resolve, reject) => {
spinner.start()
webpack(config, (error, stats) => {
if (error) {
return reject(error)
}
if (stats.hasErrors()) {
process.stdout.write(stats.toString() + "\n");
return reject(new Error('Build failed with errors.'))
}
return resolve('Build complete.')
})
})
.then(success => spinner.succeed(success))
.catch(error => spinner.fail(error))

View File

@ -1,6 +0,0 @@
import path from 'path'
export const rootPath = path.resolve(__dirname, '../')
export const srcPath = path.resolve(rootPath, '../examples')
export const buildPath = path.resolve(rootPath, '../dist')
export const sassImportPath = srcPath

View File

@ -1,54 +0,0 @@
import path from 'path'
import browserSync from 'browser-sync'
import webpack from 'webpack'
import httpProxyMiddleware from 'http-proxy-middleware'
import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import historyApiFallbackMiddleware from 'connect-history-api-fallback'
import config from './webpack.config'
import { sassImport } from './utilities'
import { srcPath, sassImportPath } from './paths'
const bundler = webpack(config)
const middlewares = []
middlewares.push(historyApiFallbackMiddleware())
// add webpack stuff
middlewares.push(webpackDevMiddleware(bundler, {
publicPath: config.output.publicPath,
stats: {
colors: true,
chunks: false,
},
}))
// add hot reloading
middlewares.push(webpackHotMiddleware(bundler))
// start browsersync
const url = 'http://localhost'
const bs = browserSync.create()
const server = bs.init({
server: {
baseDir: `${srcPath}/`,
middleware: middlewares,
},
files: [],
logLevel: 'silent',
open: false,
notify: false,
injectChanges: false,
ghostMode: {
clicks: false,
forms: false,
scroll: false,
},
})
console.log(`${url}:${server.options.get('port')}`)
// sass import
bs.watch(path.join(sassImportPath, '**/!(index|index_sub).scss'), { ignoreInitial: true }, () => {
sassImport(sassImportPath)
})

View File

@ -1,47 +0,0 @@
import fs from 'fs'
import path from 'path'
import glob from 'glob'
import minimist from 'minimist'
let argv = minimist(process.argv.slice(2))
export function removeEmpty(array) {
return array.filter(entry => !!entry)
}
export function ifElse(condition) {
return (then, otherwise) => (condition ? then : otherwise)
}
export const env = argv.env || 'development'
export const use = argv.use || null
export const isDev = use ? use === 'development' : env === 'development'
export const isProd = use ? use === 'production' : env === 'production'
export const isTest = env === 'testing'
export const ifDev = ifElse(isDev)
export const ifProd = ifElse(isProd)
export const ifTest = ifElse(isTest)
export function sassImport(basePath) {
const indexFileName = 'index.scss'
glob.sync(`${basePath}/**/${indexFileName}`).forEach(sourceFile => {
fs.writeFileSync(sourceFile, '// This is a dynamically generated file \n\n')
glob.sync(`${path.dirname(sourceFile)}/*.scss`).forEach(file => {
if (path.basename(file) !== indexFileName) {
fs.appendFileSync(sourceFile, `@import "${path.basename(file)}";\n`)
}
})
})
const indexSubFileName = 'index_sub.scss'
glob.sync(`${basePath}/**/${indexSubFileName}`).forEach(sourceFile => {
fs.writeFileSync(sourceFile, '// This is a dynamically generated file \n\n')
glob.sync(`${path.dirname(sourceFile)}/**/*.scss`).forEach(file => {
if (path.basename(file) !== indexSubFileName) {
let importPath = (path.dirname(sourceFile) === path.dirname(file)) ? path.basename(file) : file
importPath = importPath.replace(`${path.dirname(sourceFile)}/`, '')
fs.appendFileSync(sourceFile, `@import "${importPath}";\n`)
}
})
})
}

View File

@ -1,232 +0,0 @@
import path from 'path'
import webpack from 'webpack'
import DartSass from 'dart-sass'
import { VueLoaderPlugin } from 'vue-loader'
import SvgStore from 'webpack-svgstore-plugin'
import CopyWebpackPlugin from 'copy-webpack-plugin'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import ManifestPlugin from 'webpack-manifest-plugin'
import ImageminWebpackPlugin from 'imagemin-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin'
import { ifDev, ifProd, removeEmpty } from './utilities'
import { rootPath, srcPath, buildPath } from './paths'
export default {
mode: ifDev('development', 'production'),
entry: {
app: removeEmpty([
ifDev('webpack-hot-middleware/client?reload=true'),
`${srcPath}/assets/sass/main.scss`,
`${srcPath}/main.js`,
]),
},
output: {
path: `${buildPath}/`,
filename: `assets/js/[name]${ifProd('.[hash]', '')}.js`,
chunkFilename: `assets/js/[name]${ifProd('.[chunkhash]', '')}.js`,
publicPath: '/',
},
resolve: {
extensions: ['.js', '.scss', '.vue'],
alias: {
vue$: 'vue/dist/vue.esm.js',
modules: path.resolve(rootPath, '../node_modules'),
images: `${srcPath}/assets/images`,
fonts: `${srcPath}/assets/fonts`,
variables: `${srcPath}/assets/sass/variables`,
tiptap: path.resolve(rootPath, '../packages/tiptap/src'),
'tiptap-commands': path.resolve(rootPath, '../packages/tiptap-commands/src'),
'tiptap-utils': path.resolve(rootPath, '../packages/tiptap-utils/src'),
'tiptap-models': path.resolve(rootPath, '../packages/tiptap-models/src'),
'tiptap-extensions': path.resolve(rootPath, '../packages/tiptap-extensions/src'),
},
modules: [
srcPath,
path.resolve(rootPath, '../node_modules'),
],
},
devtool: ifDev('eval-source-map', 'source-map'),
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.js$/,
exclude: [/node_modules/],
use: {
loader: ifDev('babel-loader?cacheDirectory=true', 'babel-loader'),
options: {
presets: [
'@babel/preset-env',
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
],
},
},
},
{
test: /\.css$/,
use: removeEmpty([
ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
'css-loader',
'postcss-loader',
]),
},
{
test: /\.scss$/,
use: removeEmpty([
ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
'css-loader',
'postcss-loader',
{
loader: 'sass-loader',
options: {
implementation: DartSass,
},
},
]),
},
{
test: /\.(png|jpe?g|gif|svg|ico)(\?.*)?$/,
use: {
loader: 'file-loader',
options: {
name: `assets/images/[name]${ifProd('.[hash]', '')}.[ext]`,
},
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'file-loader',
options: {
name: `assets/fonts/[name]${ifProd('.[hash]', '')}.[ext]`,
},
},
},
],
},
// splitting out the vendor
optimization: {
namedModules: true,
splitChunks: {
name: 'vendor',
minChunks: 2,
},
noEmitOnErrors: true,
// concatenateModules: true,
},
plugins: removeEmpty([
// create manifest file for server-side asset manipulation
new ManifestPlugin({
fileName: 'assets/manifest.json',
writeToFileEmit: true,
}),
// define env
// new webpack.DefinePlugin({
// 'process.env': {},
// }),
// copy static files
new CopyWebpackPlugin([
{
context: `${srcPath}/assets/static`,
from: { glob: '**/*', dot: false },
to: `${buildPath}/assets`,
},
{
context: `${srcPath}/assets/static`,
from: { glob: '**/*', dot: false },
to: `${buildPath}/assets/[path][name].[hash].[ext]`,
},
]),
// enable hot reloading
ifDev(new webpack.HotModuleReplacementPlugin()),
// html
new HtmlWebpackPlugin({
filename: 'index.html',
template: `${srcPath}/index.html`,
inject: true,
minify: ifProd({
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
}),
buildVersion: new Date().valueOf(),
chunksSortMode: 'none',
}),
new VueLoaderPlugin(),
// create css files
ifProd(new MiniCssExtractPlugin({
filename: `assets/css/[name]${ifProd('.[hash]', '')}.css`,
chunkFilename: `assets/css/[name]${ifProd('.[hash]', '')}.css`,
})),
// minify css files
ifProd(new OptimizeCssAssetsPlugin({
cssProcessorOptions: {
reduceIdents: false,
autoprefixer: false,
zindex: false,
discardComments: {
removeAll: true,
},
},
})),
// svg icons
new SvgStore({
prefix: 'icon--',
svgoOptions: {
plugins: [
{ cleanupIDs: false },
{ collapseGroups: false },
{ removeTitle: true },
],
},
}),
// image optimization
new ImageminWebpackPlugin({
optipng: ifDev(null, {
optimizationLevel: 3,
}),
jpegtran: ifDev(null, {
progressive: true,
quality: 80,
}),
svgo: ifDev(null, {
plugins: [
{ cleanupIDs: false },
{ removeViewBox: false },
{ removeUselessStrokeAndFill: false },
{ removeEmptyAttrs: false },
],
}),
}),
]),
node: {
fs: 'empty',
},
}

View File

@ -1,80 +0,0 @@
import fs from 'fs'
import path from 'path'
import zlib from 'zlib'
import Terser from 'terser'
import { rollup } from 'rollup'
import config from './config'
function getSize(code) {
return `${(code.length / 1024).toFixed(2)}kb`
}
function logError(e) {
console.log(e)
}
function blue(str) {
return `\x1b[1m\x1b[34m${str}\x1b[39m\x1b[22m`
}
function write(dest, code, zip) {
return new Promise((resolve, reject) => {
function report(extra) {
console.log(`${blue(path.relative(process.cwd(), dest))} ${getSize(code)}${extra || ''}`)
resolve()
}
fs.writeFile(dest, code, error => {
if (error) return reject(error)
if (zip) {
zlib.gzip(code, (err, zipped) => {
if (err) return reject(err)
report(` (gzipped: ${getSize(zipped)})`)
})
} else {
report()
}
})
})
}
function buildEntry({ input, output }) {
const isProd = /min\.js$/.test(output.file)
return rollup(input)
.then(bundle => bundle.generate(output))
.then(response => {
if (isProd) {
const minified = Terser.minify(response.output[0].code, {
output: {
preamble: output.banner,
ascii_only: true,
},
}).code
return write(output.file, minified, true)
}
return write(output.file, response.output[0].code)
})
}
function build(builds) {
let built = 0
const total = builds.length
const next = () => {
const distPath = path.dirname(builds[built].output.file)
if (!fs.existsSync(distPath)) {
fs.mkdirSync(distPath)
}
buildEntry(builds[built]).then(() => {
built++
if (built < total) {
next()
}
}).catch(logError)
}
next()
}
build(config)

View File

@ -1,119 +0,0 @@
import path from 'path'
import vue from 'rollup-plugin-vue'
import babel from 'rollup-plugin-babel'
import flow from 'rollup-plugin-flow-no-whitespace'
import cjs from 'rollup-plugin-commonjs'
import node from 'rollup-plugin-node-resolve'
import replace from 'rollup-plugin-replace'
import cssOnly from 'rollup-plugin-css-only'
const resolve = _path => path.resolve(__dirname, '../../', _path)
function genConfig(opts) {
const { version } = require(opts.package)
const banner = `
/*!
* ${opts.name} v${version}
* (c) ${new Date().getFullYear()} überdosis GbR (limited liability)
* @license MIT
*/
`
const config = {
input: {
input: opts.input,
plugins: [
cssOnly({ output: false }),
flow(),
node(),
cjs(),
vue({
css: true,
compileTemplate: true,
}),
replace({
__VERSION__: version,
}),
babel({
exclude: 'node_modules/**',
}),
],
external(id) { return !/^[\.\/]/.test(id) },
},
output: {
file: opts.file,
format: opts.format,
banner,
name: opts.outputName,
},
}
if (opts.env) {
config.input.plugins.unshift(replace({
'process.env.NODE_ENV': JSON.stringify(opts.env),
}))
}
return config
}
export default [
{
package: 'tiptap',
outputName: 'tiptap',
outputFileName: 'tiptap',
},
{
package: 'tiptap-commands',
outputName: 'tiptapCommands',
outputFileName: 'commands',
},
{
package: 'tiptap-utils',
outputName: 'tiptapUtils',
outputFileName: 'utils',
},
{
package: 'tiptap-extensions',
outputName: 'tiptapExtensions',
outputFileName: 'extensions',
},
].map(item => [
{
name: item.package,
outputName: item.outputName,
package: resolve(`packages/${item.package}/package.json`),
input: resolve(`packages/${item.package}/src/index.js`),
file: resolve(`packages/${item.package}/dist/${item.outputFileName}.js`),
format: 'umd',
env: 'development',
},
{
name: item.package,
outputName: item.outputName,
package: resolve(`packages/${item.package}/package.json`),
input: resolve(`packages/${item.package}/src/index.js`),
file: resolve(`packages/${item.package}/dist/${item.outputFileName}.min.js`),
format: 'umd',
env: 'production',
},
{
name: item.package,
outputName: item.outputName,
package: resolve(`packages/${item.package}/package.json`),
input: resolve(`packages/${item.package}/src/index.js`),
file: resolve(`packages/${item.package}/dist/${item.outputFileName}.common.js`),
format: 'cjs',
},
{
name: item.package,
outputName: item.outputName,
package: resolve(`packages/${item.package}/package.json`),
input: resolve(`packages/${item.package}/src/index.js`),
file: resolve(`packages/${item.package}/dist/${item.outputFileName}.esm.js`),
format: 'es',
}])
.reduce((allConfigs, configs) => ([
...allConfigs,
...configs,
]), [])
.map(genConfig)

View File

@ -1,46 +0,0 @@
<template>
<div class="page" spellcheck="false">
<banner />
<navigation />
<hero />
<subnavigation />
<div class="page__content">
<router-view />
</div>
<div class="page__footer">
<a class="page__source-link" :href="$route.meta.githubUrl" target="_blank">
<icon name="code" />
<span>
Show Code
</span>
</a>
</div>
</div>
</template>
<script>
import Banner from 'Components/Banner'
import Navigation from 'Components/Navigation'
import Hero from 'Components/Hero'
import Subnavigation from 'Components/Subnavigation'
import Icon from 'Components/Icon'
export default {
components: {
Banner,
Navigation,
Hero,
Subnavigation,
Icon,
},
}
</script>
<style lang="scss" src="./style.scss"></style>

View File

@ -1,26 +0,0 @@
@import "~variables";
.page {
&__content {
padding: 4rem 1rem;
}
&__footer {
text-align: center;
margin-bottom: 2rem;
}
&__source-link {
display: inline-block;
text-decoration: none;
text-transform: uppercase;
font-weight: bold;
font-size: 0.8rem;
background-color: rgba($color-black, 0.1);
color: $color-black;
border-radius: 5px;
padding: 0.2rem 0.5rem;
}
}

View File

@ -1,12 +0,0 @@
<template>
<div class="banner">
<span class="banner__message">
Were working on <a href="https://blog.ueber.io/post/tiptap-2-0-beta/">tiptap 2</a>. Become a sponsor to get access immediately!
</span>
<a href="https://github.com/sponsors/ueberdosis" class="banner__action">
Sponsor 💖
</a>
</div>
</template>
<style lang="scss" src="./style.scss"></style>

View File

@ -1,34 +0,0 @@
@import "~variables";
.banner {
font-size: 0.9rem;
background-color: $color-white;
color: $color-black;
border-bottom: 1px solid $color-grey;
text-align: center;
padding: 0.5rem 1rem;
&__action {
display: inline-block;
border: 3px solid $color-grey;
font-weight: bold;
transition: 0.1s ease-in-out all;
text-decoration: none;
padding: 0.4rem 0.7rem;
margin: 0.5rem 0.5rem 0.25rem;
border-radius: 5px;
white-space: nowrap;
&:hover {
border-color: $color-black;
}
}
&__message {
@media all and (max-width: 600px) {
display: block;
}
}
}

View File

@ -1,17 +0,0 @@
<template>
<div class="hero">
<div class="hero__inner">
<svg class="hero__logo" xmlns="http://www.w3.org/2000/svg" width="150" height="147" viewBox="0 0 150 147">
<path fill-rule="evenodd" d="M26.078305,62.3613338 C25.7098682,65.5071869 26.7651499,68.7890473 29.2305323,71.1698395 C31.8780402,73.7265081 35.5095868,74.6264505 38.835659,73.8909922 C40.1685862,69.1852287 42.5994079,65.0738098 46.1185082,61.5792271 C46.7063387,60.995492 47.6560804,60.9988119 48.2398155,61.5866425 C48.8235507,62.1744731 48.8202307,63.1242147 48.2324001,63.7079499 C45.1597213,66.7592214 43.0224884,70.3159362 41.8118122,74.3988864 C42.3036563,76.1343979 43.2448226,77.7695377 44.6376666,79.1145915 C48.8244385,83.1577101 55.4712308,83.06028 59.4841755,78.9047541 C61.5413401,76.7744978 62.5014774,73.9953585 62.3942536,71.2471292 C64.9641607,68.2381852 67.396202,63.4176619 68.4179079,58.0024937 C80.3874868,49.1562654 93.8426151,32.568523 104.371445,5.59032072 C93.9174954,41.5317695 86.0252453,64.5167382 80.6946894,74.5452392 C78.8496917,78.0162771 77.2336561,81.3879265 75.8950435,84.6284349 C70.1999494,87.1232884 65.6357246,91.845809 62.9452121,95.9367699 C59.846433,96.4572059 57.0236331,98.34297 55.4397322,101.321854 C52.7260669,106.425517 54.6851579,112.773997 59.824176,115.506462 C61.0978472,116.183685 62.4470058,116.571386 63.7975953,116.696179 C64.912262,112.255705 67.1815584,108.483174 70.5954127,105.413494 C71.2114278,104.859583 72.15984,104.909929 72.7137505,105.525944 C73.267661,106.141959 73.2173152,107.090371 72.6013001,107.644282 C68.8800506,110.990367 66.7377922,115.262277 66.1547346,120.528599 C66.6676865,123.65414 68.5866912,126.517989 71.6128022,128.127001 C74.862469,129.854879 78.6014006,129.718966 81.5959091,128.095201 C81.5801274,123.204349 82.7835226,118.582195 85.2030509,114.25301 C85.6072107,113.52986 86.5210761,113.271267 87.2442264,113.675427 C87.9673767,114.079586 88.2259696,114.993452 87.8218099,115.716602 C85.7092106,119.496609 84.635134,123.504634 84.5967664,127.763114 C85.5479283,129.295814 86.9033354,130.608181 88.6129606,131.517205 C93.7519787,134.249669 100.11443,132.323909 102.826502,127.223244 C104.216797,124.608478 104.373706,121.672349 103.513121,119.060136 C104.520365,116.84997 105.289548,113.998031 105.615371,110.855954 C119.212334,100.723957 134.689881,80.6105128 145.607087,46.648385 C148.450672,54.5670114 150,63.1025732 150,72 C150,113.421356 116.421356,147 75,147 C33.5786438,147 0,113.421356 0,72 C0,38.4830506 21.9858616,10.1011743 52.3224198,0.48953606 C48.9402683,10.0685574 46.0643582,17.0871252 43.6946894,21.5452392 C41.7000425,25.2978155 39.9730067,28.9342266 38.5748172,32.4143498 C35.1216793,33.8554208 32.0687555,35.8637935 29.77154,37.837492 C26.6493508,37.4836279 23.4161144,38.5182716 21.0724792,40.9451768 C17.0571768,45.1031442 17.1904975,51.7456943 21.3772695,55.7888129 C22.4149324,56.7908723 23.6049613,57.5354331 24.8688331,58.0276644 C27.16428,54.0664499 30.3855184,51.0655629 34.5132451,49.0557819 C35.2580753,48.6931257 36.1558704,49.0029389 36.5185265,49.7477691 C36.8811827,50.4925993 36.5713695,51.3903944 35.8265393,51.7530505 C31.3271397,53.9437983 28.0903713,57.4597339 26.078305,62.3613338 Z"/>
</svg>
<h1>
tiptap a renderless rich-text editor for Vue.js
</h1>
<p>
This editor is based on <a href="https://prosemirror.net">Prosemirror</a>, <em>fully extendable</em> and renderless. You can easily add custom nodes as <strong>Vue components</strong>.
</p>
</div>
</div>
</template>
<style lang="scss" src="./style.scss" scoped></style>

View File

@ -1,24 +0,0 @@
@import "~variables";
.hero {
background-color: $color-black;
color: $color-white;
text-align: center;
padding: 3rem 1rem;
&__inner {
margin: 0 auto;
max-width: 30rem;
}
&__logo {
width: 4rem;
height: 4rem;
path {
fill: $color-white;
}
}
}

View File

@ -1,74 +0,0 @@
<template>
<div class="icon" :class="[`icon--${name}`, `icon--${size}`, { 'has-align-fix': fixAlign }]">
<svg class="icon__svg">
<use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="'#icon--' + name"></use>
</svg>
</div>
</template>
<script>
export default {
props: {
name: {},
size: {
default: 'normal',
},
modifier: {
default: null,
},
fixAlign: {
default: true,
},
},
}
</script>
<style lang="scss" scoped>
.icon {
position: relative;
display: inline-block;
vertical-align: middle;
width: 0.8rem;
height: 0.8rem;
margin: 0 .3rem;
top: -.05rem;
fill: currentColor;
// &.has-align-fix {
// top: -.1rem;
// }
&__svg {
display: inline-block;
vertical-align: top;
width: 100%;
height: 100%;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
// svg sprite
body > svg,
.icon use > svg,
symbol {
path,
rect,
circle,
g {
fill: currentColor;
stroke: none;
}
*[d="M0 0h24v24H0z"] {
display: none;
}
}
</style>

View File

@ -1,40 +0,0 @@
<template>
<div class="navigation">
<div>
<h1 class="navigation__logo">
tiptap
</h1>
<github-button
class="navigation__count"
href="https://github.com/ueberdosis/tiptap"
data-show-count="true"
aria-label="Star ueberdosis/tiptap on GitHub"
/>
</div>
<div>
<a class="navigation__link" href="https://tiptap.dev/docs" target="_blank">
Documentation
</a>
<a class="navigation__github-link" href="https://github.com/ueberdosis/tiptap" target="_blank">
<icon class="navigation__icon" name="github" />
</a>
</div>
</div>
</template>
<script>
import Icon from 'Components/Icon'
import GithubButton from 'vue-github-button'
export default {
components: {
Icon,
GithubButton,
},
}
</script>
<style lang="scss" src="./style.scss" scoped></style>

View File

@ -1,52 +0,0 @@
@import "~variables";
.navigation {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background-color: $color-black;
color: $color-white;
flex-wrap: wrap;
&__logo {
display: inline-block;
vertical-align: middle;
font-size: 1.1rem;
font-weight: bold;
margin: 0;
margin-right: 0.5rem;
}
&__icon {
width: 1.5rem;
height: 1.5rem;
}
&__count {
display: inline-block;
vertical-align: middle;
margin-top: 0.3rem;
}
&__link {
display: inline-block;
color: rgba($color-white, 0.5);
text-decoration: none;
font-weight: bold;
font-size: 0.9rem;
padding: 0.1rem 0.5rem;
border-radius: 3px;
&:hover {
color: $color-white;
background-color: rgba($color-white, 0.1);
}
}
&__github-link {
margin-left: 0.5rem;
}
}

View File

@ -1,218 +0,0 @@
<template>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar">
<button
class="menubar__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
>
<icon name="bold" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
>
<icon name="italic" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.strike() }"
@click="commands.strike"
>
<icon name="strike" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.underline() }"
@click="commands.underline"
>
<icon name="underline" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code"
>
<icon name="code" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.paragraph() }"
@click="commands.paragraph"
>
<icon name="paragraph" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
>
H1
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
@click="commands.heading({ level: 2 })"
>
H2
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 3 }) }"
@click="commands.heading({ level: 3 })"
>
H3
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list"
>
<icon name="ul" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list"
>
<icon name="ol" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.blockquote() }"
@click="commands.blockquote"
>
<icon name="quote" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code_block() }"
@click="commands.code_block"
>
<icon name="code" />
</button>
<button
class="menubar__button"
@click="commands.horizontal_rule"
>
<icon name="hr" />
</button>
<button
class="menubar__button"
@click="commands.undo"
>
<icon name="undo" />
</button>
<button
class="menubar__button"
@click="commands.redo"
>
<icon name="redo" />
</button>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
Blockquote,
CodeBlock,
HardBreak,
Heading,
HorizontalRule,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new HorizontalRule(),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
],
content: `
<h2>
Hi there,
</h2>
<p>
this is a very <em>basic</em> example of tiptap.
</p>
<pre><code>body { display: none; }</code></pre>
<ul>
<li>
A regular list
</li>
<li>
With regular items
</li>
</ul>
<blockquote>
It's amazing 👏
<br />
mom
</blockquote>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@ -1,54 +0,0 @@
export const JavaScriptExample = `function $initHighlight(block, flags) {
try {
if (block.className.search(/\bno\-highlight\b/) != -1)
return processBlock(block, true, 0x0F) + ' class=""';
} catch (e) {
/* handle exception */
}
for (var i = 0 / 2; i < classes.length; i++) { // "0 / 2" should not be parsed as regexp
if (checkCondition(classes[i]) === undefined)
return /\d+/g;
}
}`
export const CSSExample = `@font-face {
font-family: Chunkfive; src: url('Chunkfive.otf');
}
body, .usertext {
color: #F0F0F0; background: #600;
font-family: Chunkfive, sans;
}
@import url(print.css);
@media print {
a[href^=http]::after {
content: attr(href)
}
}`
export const ExplicitImportExample = `import javascript from 'highlight.js/lib/languages/javascript'
import css from 'highlight.js/lib/languages/css'
import { Editor } from 'tiptap'
import {
CodeBlockHighlight,
} from 'tiptap-extensions'
export default {
components: {
Editor,
},
data() {
return {
extensions: [
new CodeBlockHighlight({
languages: {
javascript,
css,
},
})
]
}
}
}`;

View File

@ -1,136 +0,0 @@
<template>
<div class="editor">
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from 'tiptap'
import {
CodeBlockHighlight,
HardBreak,
Heading,
Bold,
Code,
Italic,
} from 'tiptap-extensions'
import javascript from 'highlight.js/lib/languages/javascript'
import css from 'highlight.js/lib/languages/css'
import {
JavaScriptExample,
CSSExample,
ExplicitImportExample,
} from './examples'
export default {
components: {
EditorContent,
},
data() {
return {
editor: new Editor({
extensions: [
new CodeBlockHighlight({
languages: {
javascript,
css,
},
}),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new Bold(),
new Code(),
new Italic(),
],
content: `
<h2>
Code Highlighting
</h2>
<p>
These are code blocks with <strong>automatic syntax highlighting</strong> based on highlight.js.
</p>
<pre><code>${JavaScriptExample}</code></pre>
<pre><code>${CSSExample}</code></pre>
<p>
Note: tiptap doesn't import syntax highlighting language definitions from highlight.js. You
<strong>must</strong> import them and initialize the extension with all languages you want to support:
</p>
<pre><code>${ExplicitImportExample}</code></pre>
`,
}),
}
},
}
</script>
<style lang="scss">
pre {
&::before {
content: attr(data-language);
text-transform: uppercase;
display: block;
text-align: right;
font-weight: bold;
font-size: 0.6rem;
}
code {
.hljs-comment,
.hljs-quote {
color: #999999;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f2777a;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #f99157;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #99cc99;
}
.hljs-title,
.hljs-section {
color: #ffcc66;
}
.hljs-keyword,
.hljs-selector-tag {
color: #6699cc;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
}
</style>

View File

@ -1,127 +0,0 @@
<template>
<div class="editor">
<h2>
Collaborative Editing
</h2>
<div class="message">
With the Collaboration Extension it's possible for several users to work on a document at the same time. To make this possible, client-side and server-side code is required. This example shows this using a <a href="https://glitch.com/edit/#!/tiptap-sockets" target="_blank">socket server on glitch.com</a>. To keep the demo code clean, only a few nodes and marks are activated. There is also set a 250ms debounce for all changes. Try it out below:
</div>
<template v-if="editor && !loading">
<div class="count">
{{ count }} {{ count === 1 ? 'user' : 'users' }} connected
</div>
<editor-content class="editor__content" :editor="editor" />
</template>
<em v-else>
Connecting to socket server
</em>
</div>
</template>
<script>
import io from 'socket.io-client'
import { Editor, EditorContent } from 'tiptap'
import {
HardBreak,
Heading,
Bold,
Code,
Italic,
History,
Collaboration,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
},
data() {
return {
loading: true,
editor: null,
socket: null,
count: 0,
}
},
methods: {
onInit({ doc, version }) {
this.loading = false
if (this.editor) {
this.editor.destroy()
}
this.editor = new Editor({
content: doc,
extensions: [
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new Bold(),
new Code(),
new Italic(),
new History(),
new Collaboration({
// the initial version we start with
// version is an integer which is incremented with every change
version,
// debounce changes so we can save some requests
debounce: 250,
// onSendable is called whenever there are changed we have to send to our server
onSendable: ({ sendable }) => {
this.socket.emit('update', sendable)
},
}),
],
})
},
setCount(count) {
this.count = count
},
},
mounted() {
// server implementation: https://glitch.com/edit/#!/tiptap-sockets
this.socket = io('wss://tiptap-sockets.glitch.me')
// get the current document and its version
.on('init', data => this.onInit(data))
// send all updates to the collaboration extension
.on('update', data => this.editor.extensions.options.collaboration.update(data))
// get count of connected users
.on('getCount', count => this.setCount(count))
},
beforeDestroy() {
this.editor.destroy()
this.socket.destroy()
},
}
</script>
<style lang="scss">
@import "~variables";
.count {
display: flex;
align-items: center;
font-weight: bold;
color: rgba($color-black, 0.5);
color: #27b127;
margin-bottom: 1rem;
text-transform: uppercase;
font-size: 0.7rem;
line-height: 1;
&:before {
content: '';
display: inline-flex;
background-color: #27b127;
width: 0.4rem;
height: 0.4rem;
border-radius: 50%;
margin-right: 0.3rem;
}
}
</style>

View File

@ -1,32 +0,0 @@
import { Node } from 'tiptap'
export default class DragItem extends Node {
get name() {
return 'drag_item'
}
get schema() {
return {
group: 'block',
draggable: true,
content: 'paragraph+',
toDOM: () => ['div', { 'data-type': this.name }, 0],
parseDOM: [{
tag: `[data-type="${this.name}"]`,
}],
}
}
// Attention! For the data-drag-handle to work, the component must contain another element with `ref="content"` somewhere (it can be invisible).
get view() {
return {
template: `
<div data-type="drag_item" contenteditable="false">
<div ref="content" contenteditable="true"></div>
<div data-drag-handle></div>
</div>
`,
}
}
}

View File

@ -1,70 +0,0 @@
<template>
<div class="editor">
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent } from 'tiptap'
import { Heading, Code } from 'tiptap-extensions'
import DragItem from './DragItem'
export default {
components: {
EditorContent,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Heading(),
new Code(),
new DragItem(),
],
content: `
<h2>
Drag Handle
</h2>
<p>
Add <code>data-drag-handle</code> to a DOM element within your node view to define your custom drag handle.
</p>
<div data-type="drag_item">
Drag me!
</div>
<div data-type="drag_item">
Try it!
</div>
<div data-type="drag_item">
It works!
</div>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
[data-type="drag_item"] {
display: flex;
padding: 0.5rem;
background-color: rgba(black, 0.05);
margin-bottom: 0.5rem;
border-radius: 6px;
> :first-child {
flex: 1 1 auto;
}
> :last-child {
flex: 0 0 auto;
margin-left: auto;
cursor: move;
}
}
</style>

View File

@ -1,56 +0,0 @@
import { Node } from 'tiptap'
export default class Iframe extends Node {
get name() {
return 'iframe'
}
get schema() {
return {
attrs: {
src: {
default: null,
},
},
group: 'block',
selectable: false,
parseDOM: [{
tag: 'iframe',
getAttrs: dom => ({
src: dom.getAttribute('src'),
}),
}],
toDOM: node => ['iframe', {
src: node.attrs.src,
frameborder: 0,
allowfullscreen: 'true',
}],
}
}
get view() {
return {
props: ['node', 'updateAttrs', 'view'],
computed: {
src: {
get() {
return this.node.attrs.src
},
set(src) {
this.updateAttrs({
src,
})
},
},
},
template: `
<div class="iframe">
<iframe class="iframe__embed" :src="src"></iframe>
<input class="iframe__input" @paste.stop type="text" v-model="src" v-if="view.editable" />
</div>
`,
}
}
}

View File

@ -1,74 +0,0 @@
<template>
<div class="editor">
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from 'tiptap'
import {
HardBreak,
Heading,
Bold,
Italic,
History,
TrailingNode,
} from 'tiptap-extensions'
import Iframe from './Iframe.js'
export default {
components: {
EditorContent,
},
data() {
return {
editor: new Editor({
extensions: [
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new Bold(),
new Italic(),
new History(),
new TrailingNode(),
// custom extension
new Iframe(),
],
content: `
<h2>
Embeds
</h2>
<p>
This is an example of a custom iframe node. This iframe is rendered as a <strong>vue component</strong>. This makes it possible to render the input below to change its source.
</p>
<iframe src="https://www.youtube.com/embed/XIMLoLxmTDw" frameborder="0" allowfullscreen></iframe>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
@import "~variables";
.iframe {
&__embed {
width: 100%;
height: 15rem;
border: 0;
}
&__input {
display: block;
width: 100%;
font: inherit;
border: 0;
border-radius: 5px;
background-color: rgba($color-black, 0.1);
padding: 0.3rem 0.5rem;
}
}
</style>

View File

@ -1,282 +0,0 @@
<template>
<div>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar">
<button
class="menubar__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
>
<icon name="bold" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
>
<icon name="italic" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.strike() }"
@click="commands.strike"
>
<icon name="strike" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.underline() }"
@click="commands.underline"
>
<icon name="underline" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code"
>
<icon name="code" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.paragraph() }"
@click="commands.paragraph"
>
<icon name="paragraph" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
>
H1
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
@click="commands.heading({ level: 2 })"
>
H2
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 3 }) }"
@click="commands.heading({ level: 3 })"
>
H3
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list"
>
<icon name="ul" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list"
>
<icon name="ol" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.blockquote() }"
@click="commands.blockquote"
>
<icon name="quote" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code_block() }"
@click="commands.code_block"
>
<icon name="code" />
</button>
<button
class="menubar__button"
@click="commands.horizontal_rule"
>
<icon name="hr" />
</button>
<button
class="menubar__button"
@click="commands.undo"
>
<icon name="undo" />
</button>
<button
class="menubar__button"
@click="commands.redo"
>
<icon name="redo" />
</button>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
<div class="actions">
<button class="button" @click="clearContent">
Clear Content
</button>
<button class="button" @click="setContent">
Set Content
</button>
</div>
<div class="export">
<h3>JSON</h3>
<pre><code v-html="json"></code></pre>
<h3>HTML</h3>
<pre><code>{{ html }}</code></pre>
</div>
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
Blockquote,
CodeBlock,
HardBreak,
Heading,
HorizontalRule,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new HorizontalRule(),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
],
content: `
<h2>
Export HTML or JSON
</h2>
<p>
You are able to export your data as <code>HTML</code> or <code>JSON</code>.
</p>
`,
onUpdate: ({ getJSON, getHTML }) => {
this.json = getJSON()
this.html = getHTML()
},
}),
json: 'Update content to see changes',
html: 'Update content to see changes',
}
},
methods: {
clearContent() {
this.editor.clearContent(true)
this.editor.focus()
},
setContent() {
// you can pass a json document
this.editor.setContent({
type: 'doc',
content: [{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This is some inserted text. 👋',
},
],
}],
}, true)
// HTML string is also supported
// this.editor.setContent('<p>This is some inserted text. 👋</p>')
this.editor.focus()
},
},
}
</script>
<style lang="scss" scoped>
@import "~variables";
.actions {
max-width: 30rem;
margin: 0 auto 2rem auto;
}
.export {
max-width: 30rem;
margin: 0 auto 2rem auto;
pre {
padding: 1rem;
border-radius: 5px;
font-size: 0.8rem;
font-weight: bold;
background: rgba($color-black, 0.05);
color: rgba($color-black, 0.8);
}
code {
display: block;
white-space: pre-wrap;
}
}
</style>

View File

@ -1,154 +0,0 @@
<template>
<div class="editor">
<editor-floating-menu :editor="editor" v-slot="{ commands, isActive, menu }">
<div
class="editor__floating-menu"
:class="{ 'is-active': menu.isActive }"
:style="`top: ${menu.top}px`"
>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
>
H1
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
@click="commands.heading({ level: 2 })"
>
H2
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 3 }) }"
@click="commands.heading({ level: 3 })"
>
H3
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list"
>
<icon name="ul" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list"
>
<icon name="ol" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.blockquote() }"
@click="commands.blockquote"
>
<icon name="quote" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code_block() }"
@click="commands.code_block"
>
<icon name="code" />
</button>
</div>
</editor-floating-menu>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorFloatingMenu } from 'tiptap'
import {
Blockquote,
BulletList,
CodeBlock,
HardBreak,
Heading,
ListItem,
OrderedList,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorFloatingMenu,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new History(),
],
content: `
<h2>
Floating Menu
</h2>
<p>
This is an example of a medium-like editor. Enter a new line and some buttons will appear.
</p>
`,
}),
}
},
}
</script>
<style lang="scss">
@import "~variables";
.editor {
position: relative;
&__floating-menu {
position: absolute;
z-index: 1;
margin-top: -0.25rem;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
&.is-active {
opacity: 1;
visibility: visible;
}
}
}
</style>

View File

@ -1,97 +0,0 @@
<template>
<div class="editor">
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
Blockquote,
CodeBlock,
HardBreak,
Heading,
HorizontalRule,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
Focus,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new HorizontalRule(),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
new Focus({
className: 'has-focus',
nested: true,
}),
],
autoFocus: true,
content: `
<h2>
Focus classes
</h2>
<p>
With the focus extension you can add custom classes to focused nodes. Default options:
</p>
<pre><code>{\n className: 'has-focus',\n nested: true,\n}</code></pre>
<ul>
<li>
When set <code>nested</code> to <code>true</code> also nested elements like this list item will be captured.
</li>
<li>
Otherwise only the wrapping list will get this class.
</li>
</ul>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
@import "~variables";
.has-focus {
border-radius: 3px;
box-shadow: 0 0 0 3px #3ea4ffe6;
}
</style>

View File

@ -1,184 +0,0 @@
<template>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }">
<div
class="menubar is-hidden"
:class="{ 'is-focused': focused }"
>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
>
<icon name="bold" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
>
<icon name="italic" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.strike() }"
@click="commands.strike"
>
<icon name="strike" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.underline() }"
@click="commands.underline"
>
<icon name="underline" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code"
>
<icon name="code" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.paragraph() }"
@click="commands.paragraph"
>
<icon name="paragraph" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
>
H1
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
@click="commands.heading({ level: 2 })"
>
H2
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 3 }) }"
@click="commands.heading({ level: 3 })"
>
H3
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list"
>
<icon name="ul" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list"
>
<icon name="ol" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.blockquote() }"
@click="commands.blockquote"
>
<icon name="quote" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code_block() }"
@click="commands.code_block"
>
<icon name="code" />
</button>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
Blockquote,
BulletList,
CodeBlock,
HardBreak,
Heading,
ListItem,
OrderedList,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
],
content: `
<h2>
Hiding Menu Bar
</h2>
<p>
Click into this text to see the menu. Click outside and the menu will disappear. It's like magic.
</p>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@ -1,71 +0,0 @@
<template>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar">
<button
class="menubar__button"
@click="commands.undo"
>
<icon name="undo" />
</button>
<button
class="menubar__button"
@click="commands.redo"
>
<icon name="redo" />
</button>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
HardBreak,
Heading,
Bold,
Code,
Italic,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new Bold(),
new Code(),
new Italic(),
new History(),
],
content: `
<h2>
History
</h2>
<p>
Try to change some content here. With the <code>History</code> extension you are able to undo and redo your changes. You can also use keyboard shortcuts for this (<code>cmd+z</code> and <code>cmd+shift+z</code>).
</p>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@ -1,68 +0,0 @@
<template>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands }">
<div class="menubar">
<button
class="menubar__button"
@click="showImagePrompt(commands.image)"
>
<icon name="image" />
</button>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
HardBreak,
Heading,
Image,
Bold,
Code,
Italic,
} from 'tiptap-extensions'
export default {
components: {
Icon,
EditorContent,
EditorMenuBar,
},
data() {
return {
editor: new Editor({
extensions: [
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new Image(),
new Bold(),
new Code(),
new Italic(),
],
content: `
<h2>
Images
</h2>
<p>
This is basic example of implementing images. Try to drop new images here. Reordering also works.
</p>
<img src="https://66.media.tumblr.com/dcd3d24b79d78a3ee0f9192246e727f1/tumblr_o00xgqMhPM1qak053o1_400.gif" />
`,
}),
}
},
methods: {
showImagePrompt(command) {
const src = prompt('Enter the url of your image here')
if (src !== null) {
command({ src })
}
},
},
}
</script>

View File

@ -1,111 +0,0 @@
<template>
<div class="editor">
<editor-menu-bubble class="menububble" :editor="editor" @hide="hideLinkMenu" v-slot="{ commands, isActive, getMarkAttrs, menu }">
<div
class="menububble"
:class="{ 'is-active': menu.isActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
>
<form class="menububble__form" v-if="linkMenuIsActive" @submit.prevent="setLinkUrl(commands.link, linkUrl)">
<input class="menububble__input" type="text" v-model="linkUrl" placeholder="https://" ref="linkInput" @keydown.esc="hideLinkMenu"/>
<button class="menububble__button" @click="setLinkUrl(commands.link, null)" type="button">
<icon name="remove" />
</button>
</form>
<template v-else>
<button
class="menububble__button"
@click="showLinkMenu(getMarkAttrs('link'))"
:class="{ 'is-active': isActive.link() }"
>
<span>{{ isActive.link() ? 'Update Link' : 'Add Link'}}</span>
<icon name="link" />
</button>
</template>
</div>
</editor-menu-bubble>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBubble } from 'tiptap'
import {
Blockquote,
BulletList,
CodeBlock,
HardBreak,
Heading,
ListItem,
OrderedList,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBubble,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new History(),
],
content: `
<h2>
Links
</h2>
<p>
Try to add some links to the <a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a>. By default every link will get a <code>rel="noopener noreferrer nofollow"</code> attribute.
</p>
`,
}),
linkUrl: null,
linkMenuIsActive: false,
}
},
methods: {
showLinkMenu(attrs) {
this.linkUrl = attrs.href
this.linkMenuIsActive = true
this.$nextTick(() => {
this.$refs.linkInput.focus()
})
},
hideLinkMenu() {
this.linkUrl = null
this.linkMenuIsActive = false
},
setLinkUrl(command, url) {
command({ href: url })
this.hideLinkMenu()
},
},
}
</script>

View File

@ -1,69 +0,0 @@
<template>
<div class="editor">
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent } from 'tiptap'
import {
Blockquote,
CodeBlock,
HardBreak,
Heading,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Bold(),
new Code(),
new Italic(),
new Link(),
new History(),
],
content: `
<h2>
Markdown Shortcuts
</h2>
<p>
Start a new line and type <code>#</code> followed by a <code>space</code> and you will get an H1 headline.
</p>
<p>
This feature is called <strong>input rules</strong>. There are some of these shortcuts for the most basic nodes enabled by default. Try <code>#, ##, ###, </code> for headlines, <code>></code> for blockquotes, <code>- or +</code> for bullet lists. And of course you can add your own input rules.
</p>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@ -1,106 +0,0 @@
<template>
<div class="editor">
<editor-menu-bubble :editor="editor" :keep-in-bounds="keepInBounds" v-slot="{ commands, isActive, menu }">
<div
class="menububble"
:class="{ 'is-active': menu.isActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
>
<button
class="menububble__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
>
<icon name="bold" />
</button>
<button
class="menububble__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
>
<icon name="italic" />
</button>
<button
class="menububble__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code"
>
<icon name="code" />
</button>
</div>
</editor-menu-bubble>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBubble } from 'tiptap'
import {
Blockquote,
BulletList,
CodeBlock,
HardBreak,
Heading,
ListItem,
OrderedList,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBubble,
Icon,
},
data() {
return {
keepInBounds: true,
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
],
content: `
<h2>
Menu Bubble
</h2>
<p>
Hey, try to select some text here. There will popup a menu for selecting some inline styles. <em>Remember:</em> you have full control about content and styling of this menu.
</p>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@ -1,52 +0,0 @@
<template>
<div class="editor">
<input type="text" v-model="editor.extensions.options.placeholder.emptyNodeText">
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from 'tiptap'
import {
BulletList,
ListItem,
Placeholder,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
},
data() {
return {
editor: new Editor({
extensions: [
new BulletList(),
new ListItem(),
new Placeholder({
emptyEditorClass: 'is-editor-empty',
emptyNodeClass: 'is-empty',
emptyNodeText: 'Write something …',
showOnlyWhenEditable: true,
showOnlyCurrent: true,
}),
],
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
.editor p.is-editor-empty:first-child::before {
content: attr(data-empty-text);
float: left;
color: #aaa;
pointer-events: none;
height: 0;
font-style: italic;
}
</style>

View File

@ -1,68 +0,0 @@
<template>
<div class="editor">
<div class="checkbox">
<input type="checkbox" id="editable" v-model="editable" />
<label for="editable">editable</label>
</div>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from 'tiptap'
import {
HardBreak,
Heading,
Bold,
Code,
Italic,
Link,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
},
data() {
return {
editor: new Editor({
editable: false,
extensions: [
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new Link(),
new Bold(),
new Code(),
new Italic(),
],
content: `
<h2>
Read-Only
</h2>
<p>
This text is <strong>read-only</strong>. You are not able to edit something. <a href="https://ueber.io/">Links to fancy websites</a> are still working.
</p>
`,
}),
editable: false,
}
},
watch: {
editable() {
this.editor.setOptions({
editable: this.editable,
})
},
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
.checkbox {
margin-bottom: 1rem;
}
</style>

View File

@ -1,247 +0,0 @@
<template>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar">
<button
class="menubar__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
>
<icon name="bold" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
>
<icon name="italic" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.strike() }"
@click="commands.strike"
>
<icon name="strike" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.underline() }"
@click="commands.underline"
>
<icon name="underline" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code"
>
<icon name="code" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.paragraph() }"
@click="commands.paragraph"
>
<icon name="paragraph" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
>
H1
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list"
>
<icon name="ul" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list"
>
<icon name="ol" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.blockquote() }"
@click="commands.blockquote"
>
<icon name="quote" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code_block() }"
@click="commands.code_block"
>
<icon name="code" />
</button>
<button
class="menubar__button"
@click="commands.undo"
>
<icon name="undo" />
</button>
<button
class="menubar__button"
@click="commands.redo"
>
<icon name="redo" />
</button>
</div>
</editor-menu-bar>
<div class="search">
<input
ref="search"
@keydown.enter.prevent="editor.commands.find(searchTerm)"
placeholder="Search …"
type="text"
v-model="searchTerm"
/>
<input
@keydown.enter.prevent="editor.commands.replace(replaceWith)"
placeholder="Replace …"
type="text"
v-model="replaceWith"
/>
<button class="button" @click="editor.commands.find(searchTerm)">
Find
</button>
<button class="button" @click="editor.commands.clearSearch()">
Clear
</button>
<button class="button" @click="editor.commands.replace(replaceWith)">
Replace
</button>
<button class="button" @click="editor.commands.replaceAll(replaceWith)">
Replace All
</button>
</div>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
Blockquote,
CodeBlock,
HardBreak,
Heading,
HorizontalRule,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
Search,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
// searching: false,
searchTerm: null,
replaceWith: null,
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new HorizontalRule(),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
new Search({
disableRegex: false,
}),
],
content: `
<h2>
Search and Replace
</h2>
<p>
Search something. 🔍 Replace something. Or replace all the things! 💥 That's it. That's how a search works. Good luck.
</p>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
@import "~variables";
.search {
display: flex;
flex-wrap: wrap;
background-color: rgba($color-black, 0.1);
padding: 0.5rem;
border-radius: 5px;
margin: 1rem 0;
input {
padding: 0.25rem;
border: 0;
border-radius: 3px;
margin-right: 0.2rem;
font: inherit;
font-size: 0.8rem;
width: 20%;
flex: 1;
}
button {
}
}
.find {
background: rgba(255, 213, 0, 0.5);
}
</style>

View File

@ -1,325 +0,0 @@
<template>
<div>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands }">
<div class="menubar">
<button class="menubar__button" @click="commands.mention({ id: 1, label: 'Philipp Kühn' })">
<icon name="mention" />
<span>Insert Mention</span>
</button>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
<div class="suggestion-list" v-show="showSuggestions" ref="suggestions">
<template v-if="hasResults">
<div
v-for="(user, index) in filteredUsers"
:key="user.id"
class="suggestion-list__item"
:class="{ 'is-selected': navigatedUserIndex === index }"
@click="selectUser(user)"
>
{{ user.name }}
</div>
</template>
<div v-else class="suggestion-list__item is-empty">
No users found
</div>
</div>
</div>
</template>
<script>
import Fuse from 'fuse.js'
import tippy, { sticky } from 'tippy.js'
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
HardBreak,
Heading,
Mention,
Code,
Bold,
Italic,
} from 'tiptap-extensions'
export default {
components: {
Icon,
EditorContent,
EditorMenuBar,
},
data() {
return {
editor: new Editor({
extensions: [
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new Mention({
// a list of all suggested items
items: async () => {
await new Promise(resolve => {
setTimeout(resolve, 500)
})
return [
{ id: 1, name: 'Sven Adlung' },
{ id: 2, name: 'Patrick Baber' },
{ id: 3, name: 'Nick Hirche' },
{ id: 4, name: 'Philip Isik' },
{ id: 5, name: 'Timo Isik' },
{ id: 6, name: 'Philipp Kühn' },
{ id: 7, name: 'Hans Pagel' },
{ id: 8, name: 'Sebastian Schrama' },
]
},
// is called when a suggestion starts
onEnter: ({
items, query, range, command, virtualNode,
}) => {
this.query = query
this.filteredUsers = items
this.suggestionRange = range
this.renderPopup(virtualNode)
// we save the command for inserting a selected mention
// this allows us to call it inside of our custom popup
// via keyboard navigation and on click
this.insertMention = command
},
// is called when a suggestion has changed
onChange: ({
items, query, range, virtualNode,
}) => {
this.query = query
this.filteredUsers = items
this.suggestionRange = range
this.navigatedUserIndex = 0
this.renderPopup(virtualNode)
},
// is called when a suggestion is cancelled
onExit: () => {
// reset all saved values
this.query = null
this.filteredUsers = []
this.suggestionRange = null
this.navigatedUserIndex = 0
this.destroyPopup()
},
// is called on every keyDown event while a suggestion is active
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
this.upHandler()
return true
}
if (event.key === 'ArrowDown') {
this.downHandler()
return true
}
if (event.key === 'Enter') {
this.enterHandler()
return true
}
return false
},
// is called when a suggestion has changed
// this function is optional because there is basic filtering built-in
// you can overwrite it if you prefer your own filtering
// in this example we use fuse.js with support for fuzzy search
onFilter: async (items, query) => {
if (!query) {
return items
}
await new Promise(resolve => {
setTimeout(resolve, 500)
})
const fuse = new Fuse(items, {
threshold: 0.2,
keys: ['name'],
})
return fuse.search(query).map(item => item.item)
},
}),
new Code(),
new Bold(),
new Italic(),
],
content: `
<h2>
Suggestions
</h2>
<p>
Sometimes it's useful to <strong>mention</strong> someone. That's a feature we're very used to. Under the hood this technique can also be used for other features likes <strong>hashtags</strong> and <strong>commands</strong> lets call it <em>suggestions</em>.
</p>
<p>
This is an example how to mention some users like <span data-mention-id="1">Philipp Kühn</span> or <span data-mention-id="2">Hans Pagel</span>. Try to type <code>@</code> and a popup (rendered with tippy.js) will appear. You can navigate with arrow keys through a list of suggestions.
</p>
`,
}),
query: null,
suggestionRange: null,
filteredUsers: [],
navigatedUserIndex: 0,
insertMention: () => {},
}
},
computed: {
hasResults() {
return this.filteredUsers.length
},
showSuggestions() {
return this.query || this.hasResults
},
},
methods: {
// navigate to the previous item
// if it's the first item, navigate to the last one
upHandler() {
this.navigatedUserIndex = ((this.navigatedUserIndex + this.filteredUsers.length) - 1) % this.filteredUsers.length
},
// navigate to the next item
// if it's the last item, navigate to the first one
downHandler() {
this.navigatedUserIndex = (this.navigatedUserIndex + 1) % this.filteredUsers.length
},
enterHandler() {
const user = this.filteredUsers[this.navigatedUserIndex]
if (user) {
this.selectUser(user)
}
},
// we have to replace our suggestion text with a mention
// so it's important to pass also the position of your suggestion text
selectUser(user) {
this.insertMention({
range: this.suggestionRange,
attrs: {
id: user.id,
label: user.name,
},
})
this.editor.focus()
},
// renders a popup with suggestions
// tiptap provides a virtualNode object for using popper.js (or tippy.js) for popups
renderPopup(node) {
const { x, y } = node.getBoundingClientRect();
if (x === 0 && y === 0) {
return
}
if (this.popup) {
return
}
// ref: https://atomiks.github.io/tippyjs/v6/all-props/
this.popup = tippy('.page', {
getReferenceClientRect: () => node.getBoundingClientRect(),
appendTo: () => document.body,
interactive: true,
sticky: true, // make sure position of tippy is updated when content changes
plugins: [sticky],
content: this.$refs.suggestions,
trigger: 'mouseenter', // manual
showOnCreate: true,
theme: 'dark',
placement: 'top-start',
inertia: true,
duration: [400, 200],
})
},
destroyPopup() {
if (this.popup) {
this.popup[0].destroy()
this.popup = null
}
},
},
beforeDestroy() {
this.destroyPopup()
},
}
</script>
<style lang="scss">
@import "~variables";
.mention {
background: rgba($color-black, 0.1);
color: rgba($color-black, 0.6);
font-size: 0.8rem;
font-weight: bold;
border-radius: 5px;
padding: 0.2rem 0.5rem;
white-space: nowrap;
}
.mention-suggestion {
color: rgba($color-black, 0.6);
}
.suggestion-list {
padding: 0.2rem;
border: 2px solid rgba($color-black, 0.1);
font-size: 0.8rem;
font-weight: bold;
&__no-results {
padding: 0.2rem 0.5rem;
}
&__item {
border-radius: 5px;
padding: 0.2rem 0.5rem;
margin-bottom: 0.2rem;
cursor: pointer;
&:last-child {
margin-bottom: 0;
}
&.is-selected,
&:hover {
background-color: rgba($color-white, 0.2);
}
&.is-empty {
opacity: 0.5;
}
}
}
.tippy-box[data-theme~=dark] {
background-color: $color-black;
padding: 0;
font-size: 1rem;
text-align: inherit;
color: $color-white;
border-radius: 5px;
}
</style>

View File

@ -1,278 +0,0 @@
<template>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar">
<div class="toolbar">
<button
class="menubar__button"
@click="commands.undo"
>
<icon name="undo" />
</button>
<button
class="menubar__button"
@click="commands.redo"
>
<icon name="redo" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
>
<icon name="bold" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
>
<icon name="italic" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.strike() }"
@click="commands.strike"
>
<icon name="strike" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.underline() }"
@click="commands.underline"
>
<icon name="underline" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code"
>
<icon name="code" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.paragraph() }"
@click="commands.paragraph"
>
<icon name="paragraph" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
>
H1
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
@click="commands.heading({ level: 2 })"
>
H2
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 3 }) }"
@click="commands.heading({ level: 3 })"
>
H3
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list"
>
<icon name="ul" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list"
>
<icon name="ol" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.blockquote() }"
@click="commands.blockquote"
>
<icon name="quote" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code_block() }"
@click="commands.code_block"
>
<icon name="code" />
</button>
<button
class="menubar__button"
@click="commands.createTable({rowsCount: 3, colsCount: 3, withHeaderRow: false })"
>
<icon name="table" />
</button>
<span v-if="isActive.table()">
<button
class="menubar__button"
@click="commands.deleteTable"
>
<icon name="delete_table" />
</button>
<button
class="menubar__button"
@click="commands.addColumnBefore"
>
<icon name="add_col_before" />
</button>
<button
class="menubar__button"
@click="commands.addColumnAfter"
>
<icon name="add_col_after" />
</button>
<button
class="menubar__button"
@click="commands.deleteColumn"
>
<icon name="delete_col" />
</button>
<button
class="menubar__button"
@click="commands.addRowBefore"
>
<icon name="add_row_before" />
</button>
<button
class="menubar__button"
@click="commands.addRowAfter"
>
<icon name="add_row_after" />
</button>
<button
class="menubar__button"
@click="commands.deleteRow"
>
<icon name="delete_row" />
</button>
<button
class="menubar__button"
@click="commands.toggleCellMerge"
>
<icon name="combine_cells" />
</button>
</span>
</div>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
Blockquote,
CodeBlock,
HardBreak,
Heading,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Table,
TableHeader,
TableCell,
TableRow,
Strike,
Underline,
History,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
new Table({
resizable: true,
}),
new TableHeader(),
new TableCell(),
new TableRow(),
],
content: `
<h2>
Tables
</h2>
<p>
Tables come with some useful commands like adding, removing or merging rows and columns. Navigate with <code>tab</code> or arrow keys. Resizing is also supported.
</p>
<table>
<tr>
<th colspan="3" data-colwidth="100,0,0">Wide header</th>
</tr>
<tr>
<td>One</td>
<td>Two</td>
<td>Three</td>
</tr>
<tr>
<td>Four</td>
<td>Five</td>
<td>Six</td>
</tr>
</table>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@ -1,11 +0,0 @@
import { Doc } from 'tiptap'
export default class CustomDoc extends Doc {
get schema() {
return {
content: 'title block+',
}
}
}

View File

@ -1,19 +0,0 @@
import { Node } from 'tiptap'
export default class Title extends Node {
get name() {
return 'title'
}
get schema() {
return {
content: 'inline*',
parseDOM: [{
tag: 'h1',
}],
toDOM: () => ['h1', 0],
}
}
}

View File

@ -1,54 +0,0 @@
<template>
<div class="editor">
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from 'tiptap'
import { Placeholder } from 'tiptap-extensions'
import Doc from './Doc'
import Title from './Title'
export default {
components: {
EditorContent,
},
data() {
return {
editor: new Editor({
autoFocus: true,
extensions: [
new Doc(),
new Title(),
new Placeholder({
showOnlyCurrent: false,
emptyNodeText: node => {
if (node.type.name === 'title') {
return 'Give me a name'
}
return 'Write something'
},
}),
],
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
.editor *.is-empty:nth-child(1)::before,
.editor *.is-empty:nth-child(2)::before {
content: attr(data-empty-text);
float: left;
color: #aaa;
pointer-events: none;
height: 0;
font-style: italic;
}
</style>

View File

@ -1,162 +0,0 @@
<template>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar">
<button
class="menubar__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
>
<icon name="bold" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
>
<icon name="italic" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code"
>
<icon name="code" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.todo_list() }"
@click="commands.todo_list"
>
<icon name="checklist" />
</button>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
CodeBlock,
HardBreak,
Heading,
TodoItem,
TodoList,
Bold,
Code,
Italic,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new TodoItem({
nested: true,
}),
new TodoList(),
new Bold(),
new Code(),
new Italic(),
],
content: `
<h2>
Todo List
</h2>
<p>
There is always something to do. Thankfully, there are checklists for that. Don't forget to call mom.
</p>
<ul data-type="todo_list">
<li data-type="todo_item" data-done="true">
Buy beer
</li>
<li data-type="todo_item" data-done="true">
Buy meat
</li>
<li data-type="todo_item" data-done="true">
Buy milk
</li>
<li data-type="todo_item" data-done="false">
Call mom
</li>
</ul>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
@import "~variables";
ul[data-type="todo_list"] {
padding-left: 0;
}
li[data-type="todo_item"] {
display: flex;
flex-direction: row;
}
.todo-checkbox {
border: 2px solid $color-black;
height: 0.9em;
width: 0.9em;
box-sizing: border-box;
margin-right: 10px;
margin-top: 0.3rem;
user-select: none;
-webkit-user-select: none;
cursor: pointer;
border-radius: 0.2em;
background-color: transparent;
transition: 0.4s background;
}
.todo-content {
flex: 1;
> p:last-of-type {
margin-bottom: 0;
}
> ul[data-type="todo_list"] {
margin: .5rem 0;
}
}
li[data-done="true"] {
> .todo-content {
> p {
text-decoration: line-through;
}
}
> .todo-checkbox {
background-color: $color-black;
}
}
li[data-done="false"] {
text-decoration: none;
}
</style>

View File

@ -1,210 +0,0 @@
<template>
<div class="editor">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar">
<button
class="menubar__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
>
<icon name="bold" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
>
<icon name="italic" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.strike() }"
@click="commands.strike"
>
<icon name="strike" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.underline() }"
@click="commands.underline"
>
<icon name="underline" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code"
>
<icon name="code" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.paragraph() }"
@click="commands.paragraph"
>
<icon name="paragraph" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
>
H1
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
@click="commands.heading({ level: 2 })"
>
H2
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 3 }) }"
@click="commands.heading({ level: 3 })"
>
H3
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list"
>
<icon name="ul" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list"
>
<icon name="ol" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.blockquote() }"
@click="commands.blockquote"
>
<icon name="quote" />
</button>
<button
class="menubar__button"
:class="{ 'is-active': isActive.code_block() }"
@click="commands.code_block"
>
<icon name="code" />
</button>
<button
class="menubar__button"
@click="commands.horizontal_rule"
>
<icon name="hr" />
</button>
<button
class="menubar__button"
@click="commands.undo"
>
<icon name="undo" />
</button>
<button
class="menubar__button"
@click="commands.redo"
>
<icon name="redo" />
</button>
</div>
</editor-menu-bar>
<editor-content class="editor__content" :editor="editor" />
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
Blockquote,
CodeBlock,
HardBreak,
Heading,
HorizontalRule,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
TrailingNode,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
EditorMenuBar,
Icon,
},
data() {
return {
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new HorizontalRule(),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
new TrailingNode({
node: 'paragraph',
notAfter: ['paragraph'],
}),
],
content: `
<h2>
Trailing Paragraph
</h2>
<p>
In this demo we force to render a paragraph at the end of the document. This can be useful in some situations, for example after adding images.
</p>
<pre><code>Also, sometimes it's hard to remember to type \`cmd/ctrl + enter\` to leave a code block.</code></pre>
`,
}),
}
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@ -1,72 +0,0 @@
<template>
<div class="subnavigation">
<router-link class="subnavigation__link" to="/">
Basic
</router-link>
<router-link class="subnavigation__link" to="/menu-bubble">
Menu Bubble
</router-link>
<router-link class="subnavigation__link" to="/floating-menu">
Floating Menu
</router-link>
<router-link class="subnavigation__link" to="/links">
Links
</router-link>
<router-link class="subnavigation__link" to="/images">
Images
</router-link>
<router-link class="subnavigation__link" to="/hiding-menu-bar">
Hiding Menu Bar
</router-link>
<router-link class="subnavigation__link" to="/todo-list">
Todo List
</router-link>
<router-link class="subnavigation__link" to="/tables">
Tables
</router-link>
<router-link class="subnavigation__link" to="/search-and-replace">
Search and Replace
</router-link>
<router-link class="subnavigation__link" to="/suggestions">
Suggestions
</router-link>
<router-link class="subnavigation__link" to="/markdown-shortcuts">
Markdown Shortcuts
</router-link>
<router-link class="subnavigation__link" to="/code-highlighting">
Code Highlighting
</router-link>
<router-link class="subnavigation__link" to="/history">
History
</router-link>
<router-link class="subnavigation__link" to="/read-only">
Read-Only
</router-link>
<router-link class="subnavigation__link" to="/embeds">
Embeds
</router-link>
<router-link class="subnavigation__link" to="/placeholder">
Placeholder
</router-link>
<router-link class="subnavigation__link" to="/focus">
Focus
</router-link>
<router-link class="subnavigation__link" to="/collaboration">
Collaboration
</router-link>
<router-link class="subnavigation__link" to="/title">
Title
</router-link>
<router-link class="subnavigation__link" to="/trailing-paragraph">
Trailing Paragraph
</router-link>
<router-link class="subnavigation__link" to="/drag-handle">
Drag Handle
</router-link>
<router-link class="subnavigation__link" to="/export">
Export HTML or JSON
</router-link>
</div>
</template>
<style lang="scss" src="./style.scss" scoped></style>

View File

@ -1,36 +0,0 @@
@import "~variables";
.subnavigation {
padding: 0.5rem;
background-color: rgba($color-black, 0.9);
color: $color-white;
text-align: center;
@media (min-width: 600px) {
position: sticky;
top: 0;
z-index: 1000;
}
&__link {
display: inline-block;
color: rgba($color-white, 0.5);
text-decoration: none;
font-weight: bold;
font-size: 0.9rem;
padding: 0.1rem 0.5rem;
border-radius: 3px;
&:hover {
color: $color-white;
background-color: rgba($color-white, 0.1);
}
&.is-exact-active {
color: $color-white;
background-color: rgba($color-white, 0.2);
}
}
}

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M5,14 C7.76005315,14.0033061 9.99669388,16.2399468 10,19 C10,21.7614237 7.76142375,24 5,24 C2.23857625,24 1.77635684e-15,21.7614237 1.77635684e-15,19 C1.77635684e-15,16.2385763 2.23857625,14 5,14 Z M7.5,19.9375 C8.01776695,19.9375 8.4375,19.517767 8.4375,19 C8.4375,18.482233 8.01776695,18.0625 7.5,18.0625 L6.25,18.0625 C6.07741102,18.0625 5.9375,17.922589 5.9375,17.75 L5.9375,16.5 C5.9375,15.982233 5.51776695,15.5625 5,15.5625 C4.48223305,15.5625 4.0625,15.982233 4.0625,16.5 L4.0625,17.75 C4.0625,17.922589 3.92258898,18.0625 3.75,18.0625 L2.5,18.0625 C1.98223305,18.0625 1.5625,18.482233 1.5625,19 C1.5625,19.517767 1.98223305,19.9375 2.5,19.9375 L3.75,19.9375 C3.92258898,19.9375 4.0625,20.077411 4.0625,20.25 L4.0625,21.5 C4.0625,22.017767 4.48223305,22.4375 5,22.4375 C5.51776695,22.4375 5.9375,22.017767 5.9375,21.5 L5.9375,20.25 C5.9375,20.077411 6.07741102,19.9375 6.25,19.9375 L7.5,19.9375 Z M16,19 C16,20.6568542 17.3431458,22 19,22 C20.6568542,22 22,20.6568542 22,19 L22,5 C22,3.34314575 20.6568542,2 19,2 C17.3431458,2 16,3.34314575 16,5 L16,19 Z M14,19 L14,5 C14,2.23857625 16.2385763,0 19,0 C21.7614237,0 24,2.23857625 24,5 L24,19 C24,21.7614237 21.7614237,24 19,24 C16.2385763,24 14,21.7614237 14,19 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M19,14 C21.7600532,14.0033061 23.9966939,16.2399468 24,19 C24,21.7614237 21.7614237,24 19,24 C16.2385763,24 14,21.7614237 14,19 C14,16.2385763 16.2385763,14 19,14 Z M21.5,19.9375 C22.017767,19.9375 22.4375,19.517767 22.4375,19 C22.4375,18.482233 22.017767,18.0625 21.5,18.0625 L20.25,18.0625 C20.077411,18.0625 19.9375,17.922589 19.9375,17.75 L19.9375,16.5 C19.9375,15.982233 19.517767,15.5625 19,15.5625 C18.482233,15.5625 18.0625,15.982233 18.0625,16.5 L18.0625,17.75 C18.0625,17.922589 17.922589,18.0625 17.75,18.0625 L16.5,18.0625 C15.982233,18.0625 15.5625,18.482233 15.5625,19 C15.5625,19.517767 15.982233,19.9375 16.5,19.9375 L17.75,19.9375 C17.922589,19.9375 18.0625,20.077411 18.0625,20.25 L18.0625,21.5 C18.0625,22.017767 18.482233,22.4375 19,22.4375 C19.517767,22.4375 19.9375,22.017767 19.9375,21.5 L19.9375,20.25 C19.9375,20.077411 20.077411,19.9375 20.25,19.9375 L21.5,19.9375 Z M2,19 C2,20.6568542 3.34314575,22 5,22 C6.65685425,22 8,20.6568542 8,19 L8,5 C8,3.34314575 6.65685425,2 5,2 C3.34314575,2 2,3.34314575 2,5 L2,19 Z M-2.7585502e-16,19 L5.81397739e-16,5 C-1.37692243e-16,2.23857625 2.23857625,0 5,0 C7.76142375,0 10,2.23857625 10,5 L10,19 C10,21.7614237 7.76142375,24 5,24 C2.23857625,24 4.43234962e-16,21.7614237 -2.7585502e-16,19 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M19,0 C21.7600532,0.00330611633 23.9966939,2.23994685 24,5 C24,7.76142375 21.7614237,10 19,10 C16.2385763,10 14,7.76142375 14,5 C14,2.23857625 16.2385763,0 19,0 Z M21.5,5.9375 C22.017767,5.9375 22.4375,5.51776695 22.4375,5 C22.4375,4.48223305 22.017767,4.0625 21.5,4.0625 L20.25,4.0625 C20.077411,4.0625 19.9375,3.92258898 19.9375,3.75 L19.9375,2.5 C19.9375,1.98223305 19.517767,1.5625 19,1.5625 C18.482233,1.5625 18.0625,1.98223305 18.0625,2.5 L18.0625,3.75 C18.0625,3.92258898 17.922589,4.0625 17.75,4.0625 L16.5,4.0625 C15.982233,4.0625 15.5625,4.48223305 15.5625,5 C15.5625,5.51776695 15.982233,5.9375 16.5,5.9375 L17.75,5.9375 C17.922589,5.9375 18.0625,6.07741102 18.0625,6.25 L18.0625,7.5 C18.0625,8.01776695 18.482233,8.4375 19,8.4375 C19.517767,8.4375 19.9375,8.01776695 19.9375,7.5 L19.9375,6.25 C19.9375,6.07741102 20.077411,5.9375 20.25,5.9375 L21.5,5.9375 Z M5,16 C3.34314575,16 2,17.3431458 2,19 C2,20.6568542 3.34314575,22 5,22 L19,22 C20.6568542,22 22,20.6568542 22,19 C22,17.3431458 20.6568542,16 19,16 L5,16 Z M5,14 L19,14 C21.7614237,14 24,16.2385763 24,19 C24,21.7614237 21.7614237,24 19,24 L5,24 C2.23857625,24 3.38176876e-16,21.7614237 0,19 C-1.2263553e-15,16.2385763 2.23857625,14 5,14 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M19,14 C21.7600532,14.0033061 23.9966939,16.2399468 24,19 C24,21.7614237 21.7614237,24 19,24 C16.2385763,24 14,21.7614237 14,19 C14,16.2385763 16.2385763,14 19,14 Z M21.5,19.9375 C22.017767,19.9375 22.4375,19.517767 22.4375,19 C22.4375,18.482233 22.017767,18.0625 21.5,18.0625 L20.25,18.0625 C20.077411,18.0625 19.9375,17.922589 19.9375,17.75 L19.9375,16.5 C19.9375,15.982233 19.517767,15.5625 19,15.5625 C18.482233,15.5625 18.0625,15.982233 18.0625,16.5 L18.0625,17.75 C18.0625,17.922589 17.922589,18.0625 17.75,18.0625 L16.5,18.0625 C15.982233,18.0625 15.5625,18.482233 15.5625,19 C15.5625,19.517767 15.982233,19.9375 16.5,19.9375 L17.75,19.9375 C17.922589,19.9375 18.0625,20.077411 18.0625,20.25 L18.0625,21.5 C18.0625,22.017767 18.482233,22.4375 19,22.4375 C19.517767,22.4375 19.9375,22.017767 19.9375,21.5 L19.9375,20.25 C19.9375,20.077411 20.077411,19.9375 20.25,19.9375 L21.5,19.9375 Z M5,2 C3.34314575,2 2,3.34314575 2,5 C2,6.65685425 3.34314575,8 5,8 L19,8 C20.6568542,8 22,6.65685425 22,5 C22,3.34314575 20.6568542,2 19,2 L5,2 Z M5,0 L19,0 C21.7614237,-5.07265313e-16 24,2.23857625 24,5 C24,7.76142375 21.7614237,10 19,10 L5,10 C2.23857625,10 3.38176876e-16,7.76142375 0,5 C-1.2263553e-15,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>text-bold</title><path d="M17.194,10.962A6.271,6.271,0,0,0,12.844.248H4.3a1.25,1.25,0,0,0,0,2.5H5.313a.25.25,0,0,1,.25.25V21a.25.25,0,0,1-.25.25H4.3a1.25,1.25,0,1,0,0,2.5h9.963a6.742,6.742,0,0,0,2.93-12.786Zm-4.35-8.214a3.762,3.762,0,0,1,0,7.523H8.313a.25.25,0,0,1-.25-.25V3a.25.25,0,0,1,.25-.25Zm1.42,18.5H8.313a.25.25,0,0,1-.25-.25V13.021a.25.25,0,0,1,.25-.25h4.531c.017,0,.033,0,.049,0l.013,0h1.358a4.239,4.239,0,0,1,0,8.477Z"/></svg>

Before

Width:  |  Height:  |  Size: 505 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>checklist-alternate</title><path d="M21,0H3A3,3,0,0,0,0,3V21a3,3,0,0,0,3,3H21a3,3,0,0,0,3-3V3A3,3,0,0,0,21,0Zm1,21a1,1,0,0,1-1,1H3a1,1,0,0,1-1-1V3A1,1,0,0,1,3,2H21a1,1,0,0,1,1,1Z"/><path d="M11.249,4.5a1.251,1.251,0,0,0-1.75.25L7.365,7.6l-.482-.481A1.25,1.25,0,0,0,5.116,8.883l1.5,1.5A1.262,1.262,0,0,0,8.5,10.249l3-4A1.25,1.25,0,0,0,11.249,4.5Z"/><path d="M11.249,13.5a1.251,1.251,0,0,0-1.75.25L7.365,16.6l-.482-.481a1.25,1.25,0,1,0-1.767,1.768l1.5,1.5A1.265,1.265,0,0,0,8.5,19.249l3-4A1.25,1.25,0,0,0,11.249,13.5Z"/><path d="M18.5,7.749H14a1.25,1.25,0,0,0,0,2.5h4.5a1.25,1.25,0,0,0,0-2.5Z"/><path d="M18.5,15.749H14a1.25,1.25,0,0,0,0,2.5h4.5a1.25,1.25,0,1,0,0-2.5Z"/></svg>

Before

Width:  |  Height:  |  Size: 743 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>angle-brackets</title><path d="M9.147,21.552a1.244,1.244,0,0,1-.895-.378L.84,13.561a2.257,2.257,0,0,1,0-3.125L8.252,2.823a1.25,1.25,0,0,1,1.791,1.744l-6.9,7.083a.5.5,0,0,0,0,.7l6.9,7.082a1.25,1.25,0,0,1-.9,2.122Z"/><path d="M14.854,21.552a1.25,1.25,0,0,1-.9-2.122l6.9-7.083a.5.5,0,0,0,0-.7l-6.9-7.082a1.25,1.25,0,0,1,1.791-1.744l7.411,7.612a2.257,2.257,0,0,1,0,3.125l-7.412,7.614A1.244,1.244,0,0,1,14.854,21.552Zm6.514-9.373h0Z"/></svg>

Before

Width:  |  Height:  |  Size: 504 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M2,19 C2,20.6568542 3.34314575,22 5,22 L19,22 C20.6568542,22 22,20.6568542 22,19 L22,5 C22,3.34314575 20.6568542,2 19,2 L5,2 C3.34314575,2 2,3.34314575 2,5 L2,19 Z M-1.16403344e-15,19 L-3.0678068e-16,5 C-6.44957556e-16,2.23857625 2.23857625,0 5,0 L19,0 C21.7614237,0 24,2.23857625 24,5 L24,19 C24,21.7614237 21.7614237,24 19,24 L5,24 C2.23857625,24 9.50500275e-16,21.7614237 -1.16403344e-15,19 Z M12,10 C12.5522847,10 13,10.4477153 13,11 L13,13 C13,13.5522847 12.5522847,14 12,14 C11.4477153,14 11,13.5522847 11,13 L11,11 C11,10.4477153 11.4477153,10 12,10 Z M12,16 C12.5522847,16 13,16.4477153 13,17 L13,20 C13,20.5522847 12.5522847,21 12,21 C11.4477153,21 11,20.5522847 11,20 L11,17 C11,16.4477153 11.4477153,16 12,16 Z M12,3 C12.5522847,3 13,3.44771525 13,4 L13,7 C13,7.55228475 12.5522847,8 12,8 C11.4477153,8 11,7.55228475 11,7 L11,4 C11,3.44771525 11.4477153,3 12,3 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M12.6414391,21.9312708 C12.9358807,22.5689168 13.3234155,23.1547532 13.7866134,23.6713497 C13.2317936,23.8836754 12.6294813,24 12,24 C9.23857625,24 7,21.7614237 7,19 L7,5 C7,2.23857625 9.23857625,0 12,0 C14.7614237,0 17,2.23857625 17,5 L17,12.2898787 C16.2775651,12.5048858 15.6040072,12.8333806 15,13.2546893 L15,5 C15,3.34314575 13.6568542,2 12,2 C10.3431458,2 9,3.34314575 9,5 L9,19 C9,20.6568542 10.3431458,22 12,22 C12.220157,22 12.4347751,21.9762852 12.6414391,21.9312708 Z M19,14 C21.7600532,14.0033061 23.9966939,16.2399468 24,19 C24,21.7614237 21.7614237,24 19,24 C16.2385763,24 14,21.7614237 14,19 C14,16.2385763 16.2385763,14 19,14 Z M16.5,19.9375 L21.5,19.9375 C22.017767,19.9375 22.4375,19.517767 22.4375,19 C22.4375,18.482233 22.017767,18.0625 21.5,18.0625 L16.5,18.0625 C15.982233,18.0625 15.5625,18.482233 15.5625,19 C15.5625,19.517767 15.982233,19.9375 16.5,19.9375 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 990 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M13.2546893,15 C12.8333806,15.6040072 12.5048858,16.2775651 12.2898787,17 L5,17 C2.23857625,17 3.38176876e-16,14.7614237 0,12 C-1.2263553e-15,9.23857625 2.23857625,7 5,7 L19,7 C21.7614237,7 24,9.23857625 24,12 C24,12.6294813 23.8836754,13.2317936 23.6713497,13.7866134 C23.1547532,13.3234155 22.5689168,12.9358807 21.9312708,12.6414391 C21.9762852,12.4347751 22,12.220157 22,12 C22,10.3431458 20.6568542,9 19,9 L5,9 C3.34314575,9 2,10.3431458 2,12 C2,13.6568542 3.34314575,15 5,15 L13.2546893,15 Z M19,14 C21.7600532,14.0033061 23.9966939,16.2399468 24,19 C24,21.7614237 21.7614237,24 19,24 C16.2385763,24 14,21.7614237 14,19 C14,16.2385763 16.2385763,14 19,14 Z M16.5,19.9375 L21.5,19.9375 C22.017767,19.9375 22.4375,19.517767 22.4375,19 C22.4375,18.482233 22.017767,18.0625 21.5,18.0625 L16.5,18.0625 C15.982233,18.0625 15.5625,18.482233 15.5625,19 C15.5625,19.517767 15.982233,19.9375 16.5,19.9375 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1008 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M19,14 C21.7600532,14.0033061 23.9966939,16.2399468 24,19 C24,21.7614237 21.7614237,24 19,24 C16.2385763,24 14,21.7614237 14,19 C14,16.2385763 16.2385763,14 19,14 Z M16.5,19.9375 L21.5,19.9375 C22.017767,19.9375 22.4375,19.517767 22.4375,19 C22.4375,18.482233 22.017767,18.0625 21.5,18.0625 L16.5,18.0625 C15.982233,18.0625 15.5625,18.482233 15.5625,19 C15.5625,19.517767 15.982233,19.9375 16.5,19.9375 Z M12.2898787,17 L9,17 L9,22 L12.6736312,22 C13.0297295,22.7496048 13.515133,23.4258795 14.1010173,24 L5,24 C2.23857625,24 -1.43817996e-15,21.7614237 -1.77635684e-15,19 L-3.55271368e-15,5 C-3.89089055e-15,2.23857625 2.23857625,5.07265313e-16 5,-1.77635684e-15 L19,-1.77635684e-15 C21.7614237,-2.28362215e-15 24,2.23857625 24,5 L24,7.82313285 C24.0122947,7.88054124 24.0187107,7.93964623 24.0187107,8 C24.0187107,8.06035377 24.0122947,8.11945876 24,8.17686715 L24,14.1010173 C23.4258795,13.515133 22.7496048,13.0297295 22,12.6736312 L22,9 L17,9 L17,12.2898787 C16.2775651,12.5048858 15.6040072,12.8333806 15,13.2546893 L15,9 L9,9 L9,15 L13.2546893,15 C12.8333806,15.6040072 12.5048858,16.2775651 12.2898787,17 Z M17,7 L22,7 L22,5 C22,3.34314575 20.6568542,2 19,2 L17,2 L17,7 Z M15,7 L15,2 L9,2 L9,7 L15,7 Z M7,2 L5,2 C3.34314575,2 2,3.34314575 2,5 L2,7 L7,7 L7,2 Z M2,9 L2,15 L7,15 L7,9 L2,9 Z M2,17 L2,19 C2,20.6568542 3.34314575,22 5,22 L7,22 L7,17 L2,17 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M11.999,0.5 C5.649,0.5 0.5,5.648 0.5,12 C0.5,17.082 3.794,21.392 8.365,22.914 C8.939,23.017 9.121,22.678 9.121,22.373 C9.121,22.099 9.127,21.336 9.121,20.376 C5.923,21.07 5.26,18.861 5.26,18.861 C4.737,17.532 3.985,17.179 3.985,17.179 C2.94,16.465 4.062,16.48 4.062,16.48 C5.215,16.56 5.824,17.664 5.824,17.664 C6.85,19.422 8.515,18.914 9.17,18.62 C9.276,17.878 9.572,17.369 9.901,17.084 C7.347,16.792 4.663,15.807 4.663,11.398 C4.663,10.143 5.111,9.117 5.847,8.312 C5.729,8.023 5.333,6.852 5.959,5.269 C5.959,5.269 6.926,4.96 9.121,6.449 C10.039,6.193 11.023,6.066 12.001,6.061 C12.977,6.066 13.961,6.193 14.881,6.449 C17.076,4.961 18.04,5.269 18.04,5.269 C18.667,6.852 18.272,8.023 18.154,8.312 C18.89,9.117 19.337,10.143 19.337,11.398 C19.337,15.818 16.648,16.789 14.086,17.072 C14.498,17.429 14.873,18.119 14.873,19.192 C14.873,20.63 14.873,21.998 14.873,22.376 C14.873,22.684 15.059,23.023 15.643,22.912 C20.209,21.389 23.5,17.08 23.5,12 C23.5,5.648 18.352,0.5 11.999,0.5 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M5,13 C4.44771525,13 4,12.5522847 4,12 C4,11.4477153 4.44771525,11 5,11 L19,11 C19.5522847,11 20,11.4477153 20,12 C20,12.5522847 19.5522847,13 19,13 L5,13 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 262 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>paginate-filter-picture-alternate</title><circle cx="9.75" cy="6.247" r="2.25"/><path d="M16.916,8.71A1.027,1.027,0,0,0,16,8.158a1.007,1.007,0,0,0-.892.586L13.55,12.178a.249.249,0,0,1-.422.053l-.82-1.024a1,1,0,0,0-.813-.376,1.007,1.007,0,0,0-.787.426L7.59,15.71A.5.5,0,0,0,8,16.5H20a.5.5,0,0,0,.425-.237.5.5,0,0,0,.022-.486Z"/><path d="M22,0H5.5a2,2,0,0,0-2,2V18.5a2,2,0,0,0,2,2H22a2,2,0,0,0,2-2V2A2,2,0,0,0,22,0Zm-.145,18.354a.5.5,0,0,1-.354.146H6a.5.5,0,0,1-.5-.5V2.5A.5.5,0,0,1,6,2H21.5a.5.5,0,0,1,.5.5V18A.5.5,0,0,1,21.855,18.351Z"/><path d="M19.5,22H2.5a.5.5,0,0,1-.5-.5V4.5a1,1,0,0,0-2,0V22a2,2,0,0,0,2,2H19.5a1,1,0,0,0,0-2Z"/></svg>

Before

Width:  |  Height:  |  Size: 707 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>text-italic</title><path d="M22.5.248H14.863a1.25,1.25,0,0,0,0,2.5h1.086a.25.25,0,0,1,.211.384L4.78,21.017a.5.5,0,0,1-.422.231H1.5a1.25,1.25,0,0,0,0,2.5H9.137a1.25,1.25,0,0,0,0-2.5H8.051a.25.25,0,0,1-.211-.384L19.22,2.98a.5.5,0,0,1,.422-.232H22.5a1.25,1.25,0,0,0,0-2.5Z"/></svg>

Before

Width:  |  Height:  |  Size: 346 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>hyperlink-2</title><path d="M12.406,14.905a1,1,0,0,0-.543,1.307,1,1,0,0,1-.217,1.09L8.818,20.131a2,2,0,0,1-2.828,0L3.868,18.01a2,2,0,0,1,0-2.829L6.7,12.353a1.013,1.013,0,0,1,1.091-.217,1,1,0,0,0,.763-1.849,3.034,3.034,0,0,0-3.268.652L2.454,13.767a4.006,4.006,0,0,0,0,5.657l2.122,2.121a4,4,0,0,0,5.656,0l2.829-2.828a3.008,3.008,0,0,0,.651-3.27A1,1,0,0,0,12.406,14.905Z"/><path d="M7.757,16.241a1.011,1.011,0,0,0,1.414,0L16.95,8.463a1,1,0,0,0-1.414-1.414L7.757,14.827A1,1,0,0,0,7.757,16.241Z"/><path d="M21.546,4.574,19.425,2.453a4.006,4.006,0,0,0-5.657,0L10.939,5.281a3.006,3.006,0,0,0-.651,3.269,1,1,0,1,0,1.849-.764A1,1,0,0,1,12.354,6.7l2.828-2.828a2,2,0,0,1,2.829,0l2.121,2.121a2,2,0,0,1,0,2.829L17.3,11.645a1.015,1.015,0,0,1-1.091.217,1,1,0,0,0-.765,1.849,3.026,3.026,0,0,0,3.27-.651l2.828-2.828A4.007,4.007,0,0,0,21.546,4.574Z"/></svg>

Before

Width:  |  Height:  |  Size: 907 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>read-email-at-alternate</title><path d="M12,.5A11.634,11.634,0,0,0,.262,12,11.634,11.634,0,0,0,12,23.5a11.836,11.836,0,0,0,6.624-2,1.25,1.25,0,1,0-1.393-2.076A9.34,9.34,0,0,1,12,21a9.132,9.132,0,0,1-9.238-9A9.132,9.132,0,0,1,12,3a9.132,9.132,0,0,1,9.238,9v.891a1.943,1.943,0,0,1-3.884,0V12A5.355,5.355,0,1,0,12,17.261a5.376,5.376,0,0,0,3.861-1.634,4.438,4.438,0,0,0,7.877-2.736V12A11.634,11.634,0,0,0,12,.5Zm0,14.261A2.763,2.763,0,1,1,14.854,12,2.812,2.812,0,0,1,12,14.761Z"/></svg>

Before

Width:  |  Height:  |  Size: 549 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>list-numbers</title><path d="M7.75,4.5h15a1,1,0,0,0,0-2h-15a1,1,0,0,0,0,2Z"/><path d="M22.75,11h-15a1,1,0,1,0,0,2h15a1,1,0,0,0,0-2Z"/><path d="M22.75,19.5h-15a1,1,0,0,0,0,2h15a1,1,0,0,0,0-2Z"/><path d="M2.212,17.248A2,2,0,0,0,.279,18.732a.75.75,0,1,0,1.45.386.5.5,0,1,1,.483.63.75.75,0,1,0,0,1.5.5.5,0,1,1-.482.635.75.75,0,1,0-1.445.4,2,2,0,1,0,3.589-1.648.251.251,0,0,1,0-.278,2,2,0,0,0-1.662-3.111Z"/><path d="M4.25,10.748a2,2,0,0,0-4,0,.75.75,0,0,0,1.5,0,.5.5,0,0,1,1,0,1.031,1.031,0,0,1-.227.645L.414,14.029A.75.75,0,0,0,1,15.248H3.5a.75.75,0,0,0,0-1.5H3.081a.249.249,0,0,1-.195-.406L3.7,12.33A2.544,2.544,0,0,0,4.25,10.748Z"/><path d="M4,5.248H3.75A.25.25,0,0,1,3.5,5V1.623A1.377,1.377,0,0,0,2.125.248H1.5a.75.75,0,0,0,0,1.5h.25A.25.25,0,0,1,2,2V5a.25.25,0,0,1-.25.25H1.5a.75.75,0,0,0,0,1.5H4a.75.75,0,0,0,0-1.5Z"/></svg>

Before

Width:  |  Height:  |  Size: 894 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>paragraph</title><path d="M22.5.248H7.228a6.977,6.977,0,1,0,0,13.954H9.546a.25.25,0,0,1,.25.25V22.5a1.25,1.25,0,0,0,2.5,0V3a.25.25,0,0,1,.25-.25h3.682a.25.25,0,0,1,.25.25V22.5a1.25,1.25,0,0,0,2.5,0V3a.249.249,0,0,1,.25-.25H22.5a1.25,1.25,0,0,0,0-2.5ZM9.8,11.452a.25.25,0,0,1-.25.25H7.228a4.477,4.477,0,1,1,0-8.954H9.546A.25.25,0,0,1,9.8,3Z"/></svg>

Before

Width:  |  Height:  |  Size: 416 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>close-quote</title><path d="M18.559,3.932a4.942,4.942,0,1,0,0,9.883,4.609,4.609,0,0,0,1.115-.141.25.25,0,0,1,.276.368,6.83,6.83,0,0,1-5.878,3.523,1.25,1.25,0,0,0,0,2.5,9.71,9.71,0,0,0,9.428-9.95V8.873A4.947,4.947,0,0,0,18.559,3.932Z"/><path d="M6.236,3.932a4.942,4.942,0,0,0,0,9.883,4.6,4.6,0,0,0,1.115-.141.25.25,0,0,1,.277.368A6.83,6.83,0,0,1,1.75,17.565a1.25,1.25,0,0,0,0,2.5,9.711,9.711,0,0,0,9.428-9.95V8.873A4.947,4.947,0,0,0,6.236,3.932Z"/></svg>

Before

Width:  |  Height:  |  Size: 521 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>redo</title><path d="M22.608.161a.5.5,0,0,0-.545.108L19.472,2.86a.25.25,0,0,1-.292.045A12.537,12.537,0,0,0,6.214,3.77,12.259,12.259,0,0,0,6.1,23.632a1.25,1.25,0,0,0,1.476-2.018A9.759,9.759,0,0,1,7.667,5.805a10,10,0,0,1,9.466-1.1.25.25,0,0,1,.084.409l-1.85,1.85a.5.5,0,0,0,.354.853h6.7a.5.5,0,0,0,.5-.5V.623A.5.5,0,0,0,22.608.161Z"/></svg>

Before

Width:  |  Height:  |  Size: 406 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>delete-2-alternate</title><path d="M20.485,3.511A12.01,12.01,0,1,0,24,12,12.009,12.009,0,0,0,20.485,3.511Zm-1.767,15.21A9.51,9.51,0,1,1,21.5,12,9.508,9.508,0,0,1,18.718,18.721Z"/><path d="M16.987,7.01a1.275,1.275,0,0,0-1.8,0l-3.177,3.177L8.829,7.01A1.277,1.277,0,0,0,7.024,8.816L10.2,11.993,7.024,15.171a1.277,1.277,0,0,0,1.805,1.806L12.005,13.8l3.177,3.178a1.277,1.277,0,0,0,1.8-1.806l-3.176-3.178,3.176-3.177A1.278,1.278,0,0,0,16.987,7.01Z"/></svg>

Before

Width:  |  Height:  |  Size: 518 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>text-strike-through</title><path d="M23.75,12.952A1.25,1.25,0,0,0,22.5,11.7H13.564a.492.492,0,0,1-.282-.09c-.722-.513-1.482-.981-2.218-1.432-2.8-1.715-4.5-2.9-4.5-4.863,0-2.235,2.207-2.569,3.523-2.569a4.54,4.54,0,0,1,3.081.764A2.662,2.662,0,0,1,13.615,5.5l0,.3a1.25,1.25,0,1,0,2.5,0l0-.268A4.887,4.887,0,0,0,14.95,1.755C13.949.741,12.359.248,10.091.248c-3.658,0-6.023,1.989-6.023,5.069,0,2.773,1.892,4.512,4,5.927a.25.25,0,0,1-.139.458H1.5a1.25,1.25,0,0,0,0,2.5H12.477a.251.251,0,0,1,.159.058,4.339,4.339,0,0,1,1.932,3.466c0,3.268-3.426,3.522-4.477,3.522-1.814,0-3.139-.405-3.834-1.173a3.394,3.394,0,0,1-.65-2.7,1.25,1.25,0,0,0-2.488-.246A5.76,5.76,0,0,0,4.4,21.753c1.2,1.324,3.114,2,5.688,2,4.174,0,6.977-2.42,6.977-6.022a6.059,6.059,0,0,0-.849-3.147.25.25,0,0,1,.216-.377H22.5A1.25,1.25,0,0,0,23.75,12.952Z"/></svg>

Before

Width:  |  Height:  |  Size: 885 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M17,17 L17,22 L19,22 C20.6568542,22 22,20.6568542 22,19 L22,17 L17,17 Z M15,17 L9,17 L9,22 L15,22 L15,17 Z M17,15 L22,15 L22,9 L17,9 L17,15 Z M15,15 L15,9 L9,9 L9,15 L15,15 Z M17,7 L22,7 L22,5 C22,3.34314575 20.6568542,2 19,2 L17,2 L17,7 Z M15,7 L15,2 L9,2 L9,7 L15,7 Z M24,16.1768671 L24,19 C24,21.7614237 21.7614237,24 19,24 L5,24 C2.23857625,24 2.11453371e-15,21.7614237 1.77635684e-15,19 L0,5 C-3.38176876e-16,2.23857625 2.23857625,2.28362215e-15 5,0 L19,0 C21.7614237,-5.07265313e-16 24,2.23857625 24,5 L24,7.82313285 C24.0122947,7.88054124 24.0187107,7.93964623 24.0187107,8 C24.0187107,8.06035377 24.0122947,8.11945876 24,8.17686715 L24,15.8231329 C24.0122947,15.8805412 24.0187107,15.9396462 24.0187107,16 C24.0187107,16.0603538 24.0122947,16.1194588 24,16.1768671 Z M7,2 L5,2 C3.34314575,2 2,3.34314575 2,5 L2,7 L7,7 L7,2 Z M2,9 L2,15 L7,15 L7,9 L2,9 Z M2,17 L2,19 C2,20.6568542 3.34314575,22 5,22 L7,22 L7,17 L2,17 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>list-bullets</title><circle cx="2.5" cy="3.998" r="2.5"/><path d="M8.5,5H23a1,1,0,0,0,0-2H8.5a1,1,0,0,0,0,2Z"/><circle cx="2.5" cy="11.998" r="2.5"/><path d="M23,11H8.5a1,1,0,0,0,0,2H23a1,1,0,0,0,0-2Z"/><circle cx="2.5" cy="19.998" r="2.5"/><path d="M23,19H8.5a1,1,0,0,0,0,2H23a1,1,0,0,0,0-2Z"/></svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>text-underline</title><path d="M22.5,21.248H1.5a1.25,1.25,0,0,0,0,2.5h21a1.25,1.25,0,0,0,0-2.5Z"/><path d="M1.978,2.748H3.341a.25.25,0,0,1,.25.25v8.523a8.409,8.409,0,0,0,16.818,0V3a.25.25,0,0,1,.25-.25h1.363a1.25,1.25,0,0,0,0-2.5H16.3a1.25,1.25,0,0,0,0,2.5h1.363a.25.25,0,0,1,.25.25v8.523a5.909,5.909,0,0,1-11.818,0V3a.25.25,0,0,1,.25-.25H7.7a1.25,1.25,0,1,0,0-2.5H1.978a1.25,1.25,0,0,0,0,2.5Z"/></svg>

Before

Width:  |  Height:  |  Size: 470 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>undo</title><path d="M17.786,3.77A12.542,12.542,0,0,0,4.821,2.905a.249.249,0,0,1-.292-.045L1.937.269A.507.507,0,0,0,1.392.16a.5.5,0,0,0-.308.462v6.7a.5.5,0,0,0,.5.5h6.7a.5.5,0,0,0,.354-.854L6.783,5.115a.253.253,0,0,1-.068-.228.249.249,0,0,1,.152-.181,10,10,0,0,1,9.466,1.1,9.759,9.759,0,0,1,.094,15.809A1.25,1.25,0,0,0,17.9,23.631a12.122,12.122,0,0,0,5.013-9.961A12.125,12.125,0,0,0,17.786,3.77Z"/></svg>

Before

Width:  |  Height:  |  Size: 472 B

View File

@ -1,123 +0,0 @@
.editor {
position: relative;
max-width: 30rem;
margin: 0 auto 5rem auto;
&__content {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
* {
caret-color: currentColor;
}
pre {
padding: 0.7rem 1rem;
border-radius: 5px;
background: $color-black;
color: $color-white;
font-size: 0.8rem;
overflow-x: auto;
code {
display: block;
}
}
p code {
padding: 0.2rem 0.4rem;
border-radius: 5px;
font-size: 0.8rem;
font-weight: bold;
background: rgba($color-black, 0.1);
color: rgba($color-black, 0.8);
}
ul,
ol {
padding-left: 1rem;
}
li > p,
li > ol,
li > ul {
margin: 0;
}
a {
color: inherit;
}
blockquote {
border-left: 3px solid rgba($color-black, 0.1);
color: rgba($color-black, 0.8);
padding-left: 0.8rem;
font-style: italic;
p {
margin: 0;
}
}
img {
max-width: 100%;
border-radius: 3px;
}
table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
td, th {
min-width: 1em;
border: 2px solid $color-grey;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box;
position: relative;
> * {
margin-bottom: 0;
}
}
th {
font-weight: bold;
text-align: left;
}
.selectedCell:after {
z-index: 2;
position: absolute;
content: "";
left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none;
}
.column-resize-handle {
position: absolute;
right: -2px; top: 0; bottom: 0;
width: 4px;
z-index: 20;
background-color: #adf;
pointer-events: none;
}
}
.tableWrapper {
margin: 1em 0;
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
}
}

View File

@ -1,92 +0,0 @@
@import "~variables";
* {
margin: 0;
padding: 0;
box-sizing: border-box;
text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-touch-callout: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
&:focus {
outline: none;
}
}
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: 'Inter';
font-size: 18px;
color: $color-black;
line-height: 1.5;
}
body {
margin: 0;
}
a {
color: inherit;
}
h1,
h2,
h3,
p,
ul,
ol,
pre,
blockquote {
margin: 1rem 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
h1,
h2,
h3 {
line-height: 1.3;
}
.button {
font-weight: bold;
display: inline-flex;
background: transparent;
border: 0;
color: $color-black;
padding: 0.2rem 0.5rem;
margin-right: 0.2rem;
border-radius: 3px;
cursor: pointer;
background-color: rgba($color-black, 0.1);
&:hover {
background-color: rgba($color-black, 0.15);
}
}
.message {
background-color: rgba($color-black, 0.05);
color: rgba($color-black, 0.7);
padding: 1rem;
border-radius: 6px;
margin-bottom: 1.5rem;
font-style: italic;
}
@import "./editor";
@import "./menubar";
@import "./menububble";

View File

@ -1,40 +0,0 @@
.menubar {
margin-bottom: 1rem;
transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
&.is-hidden {
visibility: hidden;
opacity: 0;
}
&.is-focused {
visibility: visible;
opacity: 1;
transition: visibility 0.2s, opacity 0.2s;
}
&__button {
font-weight: bold;
display: inline-flex;
background: transparent;
border: 0;
color: $color-black;
padding: 0.2rem 0.5rem;
margin-right: 0.2rem;
border-radius: 3px;
cursor: pointer;
&:hover {
background-color: rgba($color-black, 0.05);
}
&.is-active {
background-color: rgba($color-black, 0.1);
}
}
span#{&}__button {
font-size: 13.3333px;
}
}

View File

@ -1,53 +0,0 @@
.menububble {
position: absolute;
display: flex;
z-index: 20;
background: $color-black;
border-radius: 5px;
padding: 0.3rem;
margin-bottom: 0.5rem;
transform: translateX(-50%);
visibility: hidden;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
&.is-active {
opacity: 1;
visibility: visible;
}
&__button {
display: inline-flex;
background: transparent;
border: 0;
color: $color-white;
padding: 0.2rem 0.5rem;
margin-right: 0.2rem;
border-radius: 3px;
cursor: pointer;
&:last-child {
margin-right: 0;
}
&:hover {
background-color: rgba($color-white, 0.1);
}
&.is-active {
background-color: rgba($color-white, 0.2);
}
}
&__form {
display: flex;
align-items: center;
}
&__input {
font: inherit;
border: none;
background: transparent;
color: $color-white;
}
}

View File

@ -1,3 +0,0 @@
$color-black: #000000;
$color-white: #ffffff;
$color-grey: #dddddd;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,73 +0,0 @@
'use strict';
var isSvg = document.createElementNS && document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ).createSVGRect;
var localStorage = 'localStorage' in window && window['localStorage'] !== null ? window.localStorage : false;
function svgSpriteInjector(source, opts) {
var file;
opts = opts || {};
if (source instanceof Node) {
file = source.getAttribute('data-svg-sprite');
opts.revision = source.getAttribute('data-svg-sprite-revision') || opts.revision;
} else if (typeof source === 'string') {
file = source;
}
if (isSvg) {
if (file) {
injector(file, opts);
} else {
console.error('svg-sprite-injector: undefined sprite filename!');
}
} else {
console.error('svg-sprite-injector require ie9 or greater!');
}
};
function injector(filepath, opts) {
var name = 'injectedSVGSprite' + filepath,
revision = opts.revision,
request;
// localStorage cache
if (revision !== undefined && localStorage && localStorage[name + 'Rev'] == revision) {
return injectOnLoad(localStorage[name]);
}
// Async load
request = new XMLHttpRequest();
request.open('GET', filepath, true);
request.onreadystatechange = function (e) {
var data;
if (request.readyState === 4 && request.status >= 200 && request.status < 400) {
injectOnLoad(data = request.responseText);
if (revision !== undefined && localStorage) {
localStorage[name] = data;
localStorage[name + 'Rev'] = revision;
}
}
};
request.send();
}
function injectOnLoad(data) {
if (data) {
if (document.body) {
injectData(data);
} else {
document.addEventListener('DOMContentLoaded', injectData.bind(null, data));
}
}
}
function injectData(data) {
var body = document.body;
body.insertAdjacentHTML('afterbegin', data);
if (body.firstChild.tagName === 'svg') {
body.firstChild.style.display = 'none';
}
}
export default svgSpriteInjector;

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>tiptap</title>
<meta name="description" content="A renderless & extendable rich-text editor for Vue.js">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<meta property="og:image" content="/assets/images/open-graph.png">
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<script async defer data-domain="tiptap.dev" src="https://plausible.io/js/plausible.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -1,180 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import svgSpriteLoader from 'helpers/svg-sprite-loader'
import App from 'Components/App'
const __svg__ = { path: './assets/images/icons/*.svg', name: 'assets/images/[hash].sprite.svg' }
svgSpriteLoader(__svg__.filename)
Vue.config.productionTip = false
Vue.use(VueRouter)
const routes = [
{
path: '/',
component: () => import('Components/Routes/Basic'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Basic',
},
},
{
path: '/menu-bubble',
component: () => import('Components/Routes/MenuBubble'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/MenuBubble',
},
},
{
path: '/floating-menu',
component: () => import('Components/Routes/FloatingMenu'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/FloatingMenu',
},
},
{
path: '/links',
component: () => import('Components/Routes/Links'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Links',
},
},
{
path: '/images',
component: () => import('Components/Routes/Images'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Images',
},
},
{
path: '/hiding-menu-bar',
component: () => import('Components/Routes/HidingMenuBar'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/HidingMenuBar',
},
},
{
path: '/tables',
component: () => import('Components/Routes/Tables'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Tables',
},
},
{
path: '/todo-list',
component: () => import('Components/Routes/TodoList'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/TodoList',
},
},
{
path: '/search-and-replace',
component: () => import('Components/Routes/SearchAndReplace'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/SearchAndReplace',
},
},
{
path: '/suggestions',
component: () => import('Components/Routes/Suggestions'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Suggestions',
},
},
{
path: '/markdown-shortcuts',
component: () => import('Components/Routes/MarkdownShortcuts'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/MarkdownShortcuts',
},
},
{
path: '/code-highlighting',
component: () => import('Components/Routes/CodeHighlighting'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/CodeHighlighting',
},
},
{
path: '/history',
component: () => import('Components/Routes/History'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/History',
},
},
{
path: '/read-only',
component: () => import('Components/Routes/ReadOnly'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/ReadOnly',
},
},
{
path: '/embeds',
component: () => import('Components/Routes/Embeds'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Embeds',
},
},
{
path: '/placeholder',
component: () => import('Components/Routes/Placeholder'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Placeholder',
},
},
{
path: '/focus',
component: () => import('Components/Routes/Focus'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Focus',
},
},
{
path: '/collaboration',
component: () => import('Components/Routes/Collaboration'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Collaboration',
},
},
{
path: '/title',
component: () => import('Components/Routes/Title'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Title',
},
},
{
path: '/trailing-paragraph',
component: () => import('Components/Routes/TrailingParagraph'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/TrailingParagraph',
},
},
{
path: '/drag-handle',
component: () => import('Components/Routes/DragHandle'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/DragHandle',
},
},
{
path: '/export',
component: () => import('Components/Routes/Export'),
meta: {
githubUrl: 'https://github.com/ueberdosis/tiptap/tree/main/examples/Components/Routes/Export',
},
},
]
const router = new VueRouter({
routes,
mode: 'history',
linkActiveClass: 'is-active',
linkExactActiveClass: 'is-exact-active',
})
new Vue({
router,
render: h => h(App),
}).$mount('#app')

View File

@ -1,9 +0,0 @@
module.exports = {
verbose: true,
roots: [
'<rootDir>/packages/',
],
moduleNameMapper: {
'\\.(css)$': 'identity-obj-proxy',
},
}

View File

@ -1,8 +0,0 @@
{
"packages": [
"packages/*"
],
"npmClient": "yarn",
"version": "independent",
"useWorkspaces": true
}

Some files were not shown because too many files have changed in this diff Show More