Merge branch 'main' into feature/new-highlight-extension
# Conflicts: # packages/core/src/commands/toggleMark.ts
This commit is contained in:
@@ -56,6 +56,8 @@ module.exports = {
|
|||||||
'no-param-reassign': 'off',
|
'no-param-reassign': 'off',
|
||||||
'import/prefer-default-export': 'off',
|
'import/prefer-default-export': 'off',
|
||||||
'consistent-return': 'off',
|
'consistent-return': 'off',
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'@typescript-eslint/no-redeclare': ['error'],
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': ['error'],
|
'@typescript-eslint/no-unused-vars': ['error'],
|
||||||
'no-use-before-define': 'off',
|
'no-use-before-define': 'off',
|
||||||
|
|||||||
2
.github/workflows/clean-up.yml
vendored
2
.github/workflows/clean-up.yml
vendored
@@ -17,4 +17,4 @@ jobs:
|
|||||||
- name: Remove old artifacts
|
- name: Remove old artifacts
|
||||||
uses: c-hive/gha-remove-artifacts@v1
|
uses: c-hive/gha-remove-artifacts@v1
|
||||||
with:
|
with:
|
||||||
age: '1 week'
|
age: '1 hour'
|
||||||
|
|||||||
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2.3.3
|
- uses: actions/checkout@v2.3.3
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2.1.1
|
uses: actions/setup-node@v2.1.2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2.3.3
|
- uses: actions/checkout@v2.3.3
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2.1.1
|
uses: actions/setup-node@v2.1.2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
@@ -87,18 +87,20 @@ jobs:
|
|||||||
browser: chrome
|
browser: chrome
|
||||||
|
|
||||||
- name: Export screenshots (on failure only)
|
- name: Export screenshots (on failure only)
|
||||||
uses: actions/upload-artifact@v2.1.4
|
uses: actions/upload-artifact@v2.2.0
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: cypress-screenshots
|
name: cypress-screenshots
|
||||||
path: tests/cypress/screenshots
|
path: tests/cypress/screenshots
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
- name: Export screen recordings (on failure only)
|
- name: Export screen recordings (on failure only)
|
||||||
uses: actions/upload-artifact@v2.1.4
|
uses: actions/upload-artifact@v2.2.0
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: cypress-videos
|
name: cypress-videos
|
||||||
path: tests/cypress/videos
|
path: tests/cypress/videos
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
- name: Send Slack notifications
|
- name: Send Slack notifications
|
||||||
uses: act10ns/slack@v1
|
uses: act10ns/slack@v1
|
||||||
@@ -125,7 +127,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2.3.3
|
- uses: actions/checkout@v2.3.3
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2.1.1
|
uses: actions/setup-node@v2.1.2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ A renderless and extendable rich-text editor for [Vue.js](https://github.com/vue
|
|||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
- [x] Proof of concept
|
- [x] Proof of concept
|
||||||
- [ ] Building out the editor
|
- [x] Building out the editor
|
||||||
- [x] Adding a bunch of extensions
|
- [x] Adding a bunch of extensions
|
||||||
- [ ] Creating a few examples
|
- [x] Creating a few examples
|
||||||
- [ ] Giving sponsors access to gather feedback
|
- [ ] Giving sponsors access to gather feedback
|
||||||
- [ ] Incorporate feedback
|
- [ ] Incorporate feedback
|
||||||
- [ ] Publicly release version 2.0-beta
|
- [ ] Publicly release version 2.0-beta
|
||||||
|
|||||||
15
docs/gridsome.client.js
Normal file
15
docs/gridsome.client.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default function (Vue, options, context) {
|
||||||
|
|
||||||
|
context.router.afterEach(to => {
|
||||||
|
if (to.hash) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const element = document.getElementById(to.hash.substr(1))
|
||||||
|
const top = element.offsetTop
|
||||||
|
const offset = parseFloat(getComputedStyle(element).scrollMarginTop)
|
||||||
|
|
||||||
|
window.scrollTo(0, top - offset)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ function addStyleResource(rule) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
siteName: 'tiptap 2',
|
siteName: 'tiptap',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
port: 3000,
|
port: 3000,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@@ -16,26 +16,27 @@
|
|||||||
"globby": "^11.0.0",
|
"globby": "^11.0.0",
|
||||||
"gridsome": "0.7.21",
|
"gridsome": "0.7.21",
|
||||||
"gridsome-plugin-simple-analytics": "^1.1.0",
|
"gridsome-plugin-simple-analytics": "^1.1.0",
|
||||||
"raw-loader": "^4.0.0",
|
"portal-vue": "^2.1.7",
|
||||||
"react": "^16.13.1",
|
"raw-loader": "^4.0.2",
|
||||||
"react-dom": "^16.13.1",
|
"react": "^17.0.1",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
"remark-container": "^0.1.2",
|
"remark-container": "^0.1.2",
|
||||||
"remark-toc": "^7.0.0",
|
"remark-toc": "^7.0.0",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.5",
|
||||||
"vue-github-button": "^1.1.2",
|
"vue-github-button": "^1.1.2",
|
||||||
"vue-live": "^1.14.0",
|
"vue-live": "^1.15.1",
|
||||||
"y-indexeddb": "^9.0.5",
|
"y-indexeddb": "^9.0.5",
|
||||||
"y-webrtc": "^10.1.6",
|
"y-webrtc": "^10.1.6",
|
||||||
"yjs": "^13.4.0"
|
"yjs": "^13.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
|
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
|
||||||
"@babel/preset-env": "^7.11.5",
|
"@babel/preset-env": "^7.11.5",
|
||||||
"@babel/preset-react": "^7.10.4",
|
"@babel/preset-react": "^7.10.4",
|
||||||
"html-loader": "^1.3.1",
|
"html-loader": "^1.3.2",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"sass-loader": "^10.0.2",
|
"sass-loader": "^10.0.3",
|
||||||
"style-resources-loader": "^1.3.3",
|
"style-resources-loader": "^1.3.3",
|
||||||
"ts-loader": "^8.0.4"
|
"ts-loader": "^8.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
.demo {
|
.demo {
|
||||||
background-color: $colorWhite;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
&__preview {
|
&__preview {
|
||||||
padding: 1.5rem;
|
padding: 1.25rem;
|
||||||
border: 1px solid rgba($colorBlack, 0.1);
|
|
||||||
border-top-left-radius: inherit;
|
border-top-left-radius: inherit;
|
||||||
border-top-right-radius: inherit;
|
border-top-right-radius: inherit;
|
||||||
border-bottom-width: 0;
|
border-bottom-width: 0;
|
||||||
|
color: $colorBlack;
|
||||||
|
background-color: $colorWhite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__source {
|
&__source {
|
||||||
// background-color: $colorBlack;
|
border: 1px solid rgba($colorWhite, 0.1);
|
||||||
|
background-color: $colorBlack;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__tabs {
|
&__tabs {
|
||||||
padding: 1rem 1.5rem 0 1.5rem;
|
padding: 1rem 1.25rem 0 1.25rem;
|
||||||
background-color: rgba($colorBlack, 0.9);
|
background-color: rgba($colorBlack, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,18 +54,29 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 0.5rem 1.25rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
border: 1px solid rgba($colorWhite, 0.1);
|
border: 1px solid rgba($colorWhite, 0.1);
|
||||||
border: 1px solid rgba($colorBlack, 0.1);
|
|
||||||
border-bottom-left-radius: inherit;
|
border-bottom-left-radius: inherit;
|
||||||
border-bottom-right-radius: inherit;
|
border-bottom-right-radius: inherit;
|
||||||
border-top-width: 0;
|
border-top-width: 0;
|
||||||
background-color: rgba($colorBlack, 0.9);
|
background-color: $colorBlack;
|
||||||
color: $colorWhite;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
// text-align: right;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__error {
|
&__error {
|
||||||
|
|||||||
@@ -46,27 +46,23 @@ export default {
|
|||||||
border-radius: 0.5rem 0.5rem 0 0;
|
border-radius: 0.5rem 0.5rem 0 0;
|
||||||
|
|
||||||
&__preview {
|
&__preview {
|
||||||
padding: 1.5rem;
|
padding: 1.25rem;
|
||||||
border: 1px solid rgba($colorBlack, 0.1);
|
|
||||||
border-top-left-radius: inherit;
|
border-top-left-radius: inherit;
|
||||||
border-top-right-radius: inherit;
|
border-top-right-radius: inherit;
|
||||||
border-bottom-width: 0;
|
border-bottom-width: 0;
|
||||||
|
color: $colorBlack;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__editor {
|
&__editor {
|
||||||
background-color: rgba($colorBlack, 0.9);
|
border: 1px solid rgba($colorWhite, 0.1);
|
||||||
color: rgba($colorWhite, 0.7);
|
background-color: $colorBlack;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__editor ::v-deep {
|
&__editor ::v-deep {
|
||||||
.prism-editor-wrapper {
|
.prism-editor-wrapper {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: unquote("max(300px, 60vh)");
|
max-height: unquote("max(300px, 60vh)");
|
||||||
padding: 1.5rem;
|
padding: 1.25rem;
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba($colorWhite, 0.25);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prism-editor__container {
|
.prism-editor__container {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ query {
|
|||||||
</static-query>
|
</static-query>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import collect from 'collect.js'
|
// import collect from 'collect.js'
|
||||||
import { VueLive } from 'vue-live'
|
import { VueLive } from 'vue-live'
|
||||||
import CustomLayout from './CustomLayout'
|
import CustomLayout from './CustomLayout'
|
||||||
|
|
||||||
@@ -66,18 +66,19 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
requires() {
|
requires() {
|
||||||
const names = this.$static.packages.edges
|
// const names = this.$static.packages.edges
|
||||||
.map(item => item.node.name)
|
// .map(item => item.node.name)
|
||||||
.filter(name => name !== 'html')
|
// .filter(name => name !== 'html')
|
||||||
|
|
||||||
const packages = Object.fromEntries(names.map(name => {
|
// const packages = Object.fromEntries(names.map(name => {
|
||||||
const module = require(`~/../../packages/${name}/index.ts`)
|
// const module = require(`~/../../packages/${name}/index.ts`)
|
||||||
const onlyDefault = module.default && Object.keys(module).length === 1
|
// const onlyDefault = module.default && Object.keys(module).length === 1
|
||||||
|
|
||||||
return [`@tiptap/${name}`, onlyDefault ? module.default : module]
|
// return [`@tiptap/${name}`, onlyDefault ? module.default : module]
|
||||||
}))
|
// }))
|
||||||
|
|
||||||
return packages
|
// return packages
|
||||||
|
return {}
|
||||||
},
|
},
|
||||||
|
|
||||||
file() {
|
file() {
|
||||||
@@ -90,17 +91,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.files = collect(require.context('~/demos/', true, /.+\..+$/).keys())
|
// this.files = collect(require.context('~/demos/', true, /.+\..+$/).keys())
|
||||||
.filter(path => path.startsWith(`./${this.name}/index.vue`))
|
// .filter(path => path.startsWith(`./${this.name}/index.vue`))
|
||||||
.map(path => path.replace('./', ''))
|
// .map(path => path.replace('./', ''))
|
||||||
.map(path => {
|
// .map(path => {
|
||||||
return {
|
// return {
|
||||||
path,
|
// path,
|
||||||
name: path.replace(`${this.name}/`, ''),
|
// name: path.replace(`${this.name}/`, ''),
|
||||||
content: require(`!!raw-loader!~/demos/${path}`).default,
|
// content: require(`!!raw-loader!~/demos/${path}`).default,
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
.toArray()
|
// .toArray()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,15 +9,6 @@
|
|||||||
border-bottom-width: 0;
|
border-bottom-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__source {
|
|
||||||
// background-color: $colorBlack;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__tabs {
|
|
||||||
padding: 1rem 1.5rem 0 1.5rem;
|
|
||||||
background-color: rgba($colorBlack, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__tab {
|
&__tab {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -51,22 +42,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 0.5rem 1.25rem;
|
||||||
border: 1px solid rgba($colorWhite, 0.1);
|
border: 1px solid rgba($colorWhite, 0.1);
|
||||||
border: 1px solid rgba($colorBlack, 0.1);
|
|
||||||
border-bottom-left-radius: inherit;
|
border-bottom-left-radius: inherit;
|
||||||
border-bottom-right-radius: inherit;
|
border-bottom-right-radius: inherit;
|
||||||
border-top-width: 0;
|
border-top-width: 0;
|
||||||
background-color: rgba($colorBlack, 0.9);
|
background-color: $colorBlack;
|
||||||
color: $colorWhite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link {
|
|
||||||
// text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__error {
|
&__error {
|
||||||
padding: 1rem 1.5rem;
|
padding: 1.25rem;
|
||||||
color: $colorRed;
|
color: $colorRed;
|
||||||
background-color: rgba($colorRed, 0.1);
|
background-color: rgba($colorRed, 0.1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
.page-navigation {
|
.page-navigation {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 3rem 0;
|
padding: 1.5rem 0;
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
font-weight: 500;
|
color: rgba($colorWhite, 0.6);
|
||||||
color: rgba($colorBlack, 0.6);
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $colorBlack;
|
color: $colorWhite;
|
||||||
|
background-color: rgba($colorWhite, 0.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
BUG: Headings can’t be transformed to a bullet or ordered list.
|
|
||||||
<div v-if="editor">
|
<div v-if="editor">
|
||||||
<button @click="editor.chain().focus().bold().run()" :class="{ 'is-active': editor.isActive('bold') }">
|
<button @click="editor.chain().focus().bold().run()" :class="{ 'is-active': editor.isActive('bold') }">
|
||||||
bold
|
bold
|
||||||
@@ -89,25 +88,24 @@ export default {
|
|||||||
Hi there,
|
Hi there,
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
this is a basic <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the bullet lists:
|
this is a basic <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
With one …
|
That’s a bullet list with one …
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
… or two list items.
|
… or two list items.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
And yes, even more.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
Isn’t that great? But wait, there’s more. Let’s try a code block:
|
Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
|
||||||
</p>
|
</p>
|
||||||
<pre><code class="language-css">body { display: none; }</code></pre>
|
<pre><code class="language-css">body {
|
||||||
|
display: none;
|
||||||
|
}</code></pre>
|
||||||
<p>
|
<p>
|
||||||
I know, I know, it’s impressive. Give it a try and click a little bit around. But don’t forget to check the other examples too.
|
I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.
|
||||||
</p>
|
</p>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
Wow, that’s amazing. Good work, boy! 👏
|
Wow, that’s amazing. Good work, boy! 👏
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ context('/examples/export-html-or-json', () => {
|
|||||||
|
|
||||||
it('should return json', () => {
|
it('should return json', () => {
|
||||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
const json = editor.json()
|
const json = editor.getJSON()
|
||||||
|
|
||||||
expect(json).to.deep.equal({
|
expect(json).to.deep.equal({
|
||||||
type: 'document',
|
type: 'document',
|
||||||
@@ -32,7 +32,7 @@ context('/examples/export-html-or-json', () => {
|
|||||||
|
|
||||||
it('should return html', () => {
|
it('should return html', () => {
|
||||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
const html = editor.html()
|
const html = editor.getHTML()
|
||||||
|
|
||||||
expect(html).to.equal('<p>Example Text</p>')
|
expect(html).to.equal('<p>Example Text</p>')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="button" @click="clearContent">
|
|
||||||
Clear Content
|
|
||||||
</button>
|
|
||||||
<button class="button" @click="setContent">
|
<button class="button" @click="setContent">
|
||||||
Set Content
|
Set Content
|
||||||
</button>
|
</button>
|
||||||
|
<button class="button" @click="clearContent">
|
||||||
|
Clear Content
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<editor-content :editor="editor" />
|
<editor-content :editor="editor" />
|
||||||
@@ -50,22 +50,17 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Get the initial content …
|
// Get the initial content …
|
||||||
this.json = this.editor.json()
|
this.json = this.editor.getJSON()
|
||||||
this.html = this.editor.html()
|
this.html = this.editor.getHTML()
|
||||||
|
|
||||||
// … and get the content after every change.
|
// … and get the content after every change.
|
||||||
this.editor.on('update', () => {
|
this.editor.on('update', () => {
|
||||||
this.json = this.editor.json()
|
this.json = this.editor.getJSON()
|
||||||
this.html = this.editor.html()
|
this.html = this.editor.getHTML()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
clearContent() {
|
|
||||||
this.editor.clearContent(true)
|
|
||||||
this.editor.focus()
|
|
||||||
},
|
|
||||||
|
|
||||||
setContent() {
|
setContent() {
|
||||||
// You can pass a JSON document …
|
// You can pass a JSON document …
|
||||||
this.editor.setContent({
|
this.editor.setContent({
|
||||||
@@ -87,6 +82,11 @@ export default {
|
|||||||
// It’s likely that you’d like to focus the Editor after most commands.
|
// It’s likely that you’d like to focus the Editor after most commands.
|
||||||
this.editor.focus()
|
this.editor.focus()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearContent() {
|
||||||
|
this.editor.clearContent(true)
|
||||||
|
this.editor.focus()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ context('/examples/focus', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should have class', () => {
|
it('should have class', () => {
|
||||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
|
||||||
editor.focus('start')
|
|
||||||
|
|
||||||
cy.get('.ProseMirror p:first').should('have.class', 'has-focus')
|
cy.get('.ProseMirror p:first').should('have.class', 'has-focus')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div>
|
||||||
<editor-content :editor="editor" />
|
<editor-content :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -10,6 +10,8 @@ import Document from '@tiptap/extension-document'
|
|||||||
import Paragraph from '@tiptap/extension-paragraph'
|
import Paragraph from '@tiptap/extension-paragraph'
|
||||||
import Text from '@tiptap/extension-text'
|
import Text from '@tiptap/extension-text'
|
||||||
import Code from '@tiptap/extension-code'
|
import Code from '@tiptap/extension-code'
|
||||||
|
import BulletList from '@tiptap/extension-bullet-list'
|
||||||
|
import ListItem from '@tiptap/extension-list-item'
|
||||||
import Focus from '@tiptap/extension-focus'
|
import Focus from '@tiptap/extension-focus'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -30,6 +32,8 @@ export default {
|
|||||||
Paragraph(),
|
Paragraph(),
|
||||||
Text(),
|
Text(),
|
||||||
Code(),
|
Code(),
|
||||||
|
BulletList(),
|
||||||
|
ListItem(),
|
||||||
Focus({
|
Focus({
|
||||||
className: 'has-focus',
|
className: 'has-focus',
|
||||||
nested: true,
|
nested: true,
|
||||||
@@ -38,12 +42,11 @@ export default {
|
|||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
content: `
|
content: `
|
||||||
<p>
|
<p>
|
||||||
The focus extension adds custom classes to focused nodes. By default, it’ll add a <code>has-focus</code> class, even to nested nodes:
|
The focus extension adds a class to the focused node only. That enables you to add a custom styling to just that node. By default, it’ll add <code>.has-focus</code>, even to nested nodes.
|
||||||
</p>
|
</p>
|
||||||
<pre><code>{ className: 'has-focus', nested: true }</code></pre>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>With <code>nested: true</code> nested elements like this list item will be focused.</li>
|
<li>Nested elements (like this list item) will be focused with the default setting of <code>nested: true</code>.</li>
|
||||||
<li>Otherwise the whole list will get the focus class, even if only a single list item is selected.</li>
|
<li>Otherwise the whole list will get the focus class, even when just a single list item is selected.</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,10 +39,13 @@ export default {
|
|||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<p>
|
<p>
|
||||||
Wow, this editor has support for links to the whole <a href="https://en.wikipedia.org/wiki/World_Wide_Web" target="_self">world wide web</a>. We tested a lot of URLs and I think you can add *every URL* you want. Isn’t that cool? Let’s try <a href="https://statamic.com/" target="_self">another one!</a> Yep, seems to work.
|
Wow, this editor has support for links to the whole <a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a>. We tested a lot of URLs and I think you can add *every URL* you want. Isn’t that cool? Let’s try <a href="https://statamic.com/">another one!</a> Yep, seems to work.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
By default every link will get a \`rel="noopener noreferrer nofollow"\` attribute. It’s configurable though.
|
By default every link will get a [rel="noopener noreferrer nofollow"] attribute <a href="https://web.dev/external-anchors-use-rel-noopener/">for security reasons</a>. It’s configurable though.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Ah, and links open in a new tab by default, but that’s also - yes, you’ve guessed it - configurable.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ export default {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.editor = new Editor({
|
this.editor = new Editor({
|
||||||
content: `
|
|
||||||
<p>
|
|
||||||
This is a radically reduced version of tiptap. It has only support for a document, paragraphs and text. That’s it. It’s probably too much for real minimalists though.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The paragraph extension is not literally required, but you need at least one node. That node can be something different, for example to render a task list and only that task list.
|
|
||||||
</p>
|
|
||||||
`,
|
|
||||||
extensions: [
|
extensions: [
|
||||||
Document(),
|
Document(),
|
||||||
Paragraph(),
|
Paragraph(),
|
||||||
Text(),
|
Text(),
|
||||||
],
|
],
|
||||||
|
content: `
|
||||||
|
<p>
|
||||||
|
This is a radically reduced version of tiptap. It has only support for a document, paragraphs and text. That’s it. It’s probably too much for real minimalists though.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different. You’ll mostly likely want to add a paragraph though.
|
||||||
|
</p>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ export default {
|
|||||||
editable: this.editable,
|
editable: this.editable,
|
||||||
content: `
|
content: `
|
||||||
<p>
|
<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.
|
This text is <strong>read-only</strong>. No matter what you try, you are not able to edit something. Okay, if you toggle the checkbox above you’ll be able to edit the text.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you want to check the state, you can call <code>editor.isEditable()</code>.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
extensions: defaultExtensions(),
|
extensions: defaultExtensions(),
|
||||||
@@ -60,5 +63,6 @@ export default {
|
|||||||
|
|
||||||
[contenteditable=false] {
|
[contenteditable=false] {
|
||||||
color: #999;
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ context('/api/extensions/blockquote', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse blockquote tags correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<blockquote><p>Example Text</p></blockquote>')
|
||||||
|
expect(editor.getHTML()).to.eq('<blockquote><p>Example Text</p></blockquote>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse blockquote tags without paragraphs correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<blockquote>Example Text</blockquote>')
|
||||||
|
expect(editor.getHTML()).to.eq('<blockquote><p>Example Text</p></blockquote>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should make the selected line a blockquote', () => {
|
it('the button should make the selected line a blockquote', () => {
|
||||||
cy.get('.ProseMirror blockquote')
|
cy.get('.ProseMirror blockquote')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
|||||||
@@ -10,6 +10,36 @@ context('/api/extensions/bold', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should transform b tags to strong tags', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><b>Example Text</b></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><strong>Example Text</strong></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sould omit b tags with normal font weight inline style', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><b style="font-weight: normal">Example Text</b></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should transform any tag with bold inline style to strong tags', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><span style="font-weight: bold">Example Text</span></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><strong>Example Text</strong></p>')
|
||||||
|
|
||||||
|
editor.setContent('<p><span style="font-weight: bolder">Example Text</span></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><strong>Example Text</strong></p>')
|
||||||
|
|
||||||
|
editor.setContent('<p><span style="font-weight: 500">Example Text</span></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><strong>Example Text</strong></p>')
|
||||||
|
|
||||||
|
editor.setContent('<p><span style="font-weight: 900">Example Text</span></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><strong>Example Text</strong></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should make the selected text bold', () => {
|
it('the button should make the selected text bold', () => {
|
||||||
cy.get('.demo__preview button:first')
|
cy.get('.demo__preview button:first')
|
||||||
.click()
|
.click()
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ context('/api/extensions/bullet-list', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse unordered lists correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<ul><li><p>Example Text</p></li></ul>')
|
||||||
|
expect(editor.getHTML()).to.eq('<ul><li><p>Example Text</p></li></ul>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse unordered lists without paragraphs correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<ul><li>Example Text</li></ul>')
|
||||||
|
expect(editor.getHTML()).to.eq('<ul><li><p>Example Text</p></li></ul>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should make the selected line a bullet list item', () => {
|
it('the button should make the selected line a bullet list item', () => {
|
||||||
cy.get('.ProseMirror ul')
|
cy.get('.ProseMirror ul')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ context('/api/extensions/code', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse code tags correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><code>Example Text</code></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><code>Example Text</code></p>')
|
||||||
|
|
||||||
|
editor.setContent('<code>Example Text</code>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><code>Example Text</code></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should mark the selected text as inline code', () => {
|
it('should mark the selected text as inline code', () => {
|
||||||
cy.get('.demo__preview button:first')
|
cy.get('.demo__preview button:first')
|
||||||
.click()
|
.click()
|
||||||
@@ -32,4 +42,11 @@ context('/api/extensions/code', () => {
|
|||||||
cy.get('.ProseMirror code')
|
cy.get('.ProseMirror code')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should make inline code from the markdown shortcut', () => {
|
||||||
|
cy.get('.ProseMirror')
|
||||||
|
.type('`Example`')
|
||||||
|
.find('code')
|
||||||
|
.should('contain', 'Example')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ context('/api/extensions/code-block', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse code blocks correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<pre><code>Example Text</code></pre>')
|
||||||
|
expect(editor.getHTML()).to.eq('<pre><code>Example Text</code></pre>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse code blocks with language correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<pre><code class="language-css">Example Text</code></pre>')
|
||||||
|
expect(editor.getHTML()).to.eq('<pre><code class="language-css">Example Text</code></pre>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should make the selected line a code block', () => {
|
it('the button should make the selected line a code block', () => {
|
||||||
cy.get('.demo__preview button:first')
|
cy.get('.demo__preview button:first')
|
||||||
.click()
|
.click()
|
||||||
@@ -39,36 +53,25 @@ context('/api/extensions/code-block', () => {
|
|||||||
|
|
||||||
it('the keyboard shortcut should make the selected line a code block', () => {
|
it('the keyboard shortcut should make the selected line a code block', () => {
|
||||||
cy.get('.ProseMirror')
|
cy.get('.ProseMirror')
|
||||||
.trigger('keydown', { shiftKey: true, ctrlKey: true, key: '\\' })
|
.trigger('keydown', { shiftKey: true, modKey: true, key: 'c' })
|
||||||
.find('pre')
|
.find('pre')
|
||||||
.should('contain', 'Example Text')
|
.should('contain', 'Example Text')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('the keyboard shortcut should toggle the code block', () => {
|
it('the keyboard shortcut should toggle the code block', () => {
|
||||||
cy.get('.ProseMirror')
|
cy.get('.ProseMirror')
|
||||||
.trigger('keydown', { shiftKey: true, ctrlKey: true, key: '\\' })
|
.trigger('keydown', { shiftKey: true, modKey: true, key: 'c' })
|
||||||
.find('pre')
|
.find('pre')
|
||||||
.should('contain', 'Example Text')
|
.should('contain', 'Example Text')
|
||||||
|
|
||||||
cy.get('.ProseMirror')
|
cy.get('.ProseMirror')
|
||||||
.type('{selectall}')
|
.type('{selectall}')
|
||||||
.trigger('keydown', { shiftKey: true, ctrlKey: true, key: '\\' })
|
.trigger('keydown', { shiftKey: true, modKey: true, key: 'c' })
|
||||||
|
|
||||||
cy.get('.ProseMirror pre')
|
cy.get('.ProseMirror pre')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should make a code block from markdown shortcuts', () => {
|
|
||||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
|
||||||
editor.clearContent()
|
|
||||||
|
|
||||||
cy.get('.ProseMirror')
|
|
||||||
.type('``` Code')
|
|
||||||
.find('pre>code')
|
|
||||||
.should('contain', 'Code')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse the language from a HTML code block', () => {
|
it('should parse the language from a HTML code block', () => {
|
||||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
editor.setContent('<pre><code class="language-css">body { display: none; }</code></pre>')
|
editor.setContent('<pre><code class="language-css">body { display: none; }</code></pre>')
|
||||||
@@ -79,7 +82,29 @@ context('/api/extensions/code-block', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should make a code block for js', () => {
|
it('should make a code block from backtick markdown shortcuts', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.clearContent()
|
||||||
|
|
||||||
|
cy.get('.ProseMirror')
|
||||||
|
.type('``` Code')
|
||||||
|
.find('pre>code')
|
||||||
|
.should('contain', 'Code')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should make a code block from tilde markdown shortcuts', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.clearContent()
|
||||||
|
|
||||||
|
cy.get('.ProseMirror')
|
||||||
|
.type('~~~ Code')
|
||||||
|
.find('pre>code')
|
||||||
|
.should('contain', 'Code')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should make a code block for js with backticks', () => {
|
||||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
editor.clearContent()
|
editor.clearContent()
|
||||||
|
|
||||||
@@ -89,4 +114,15 @@ context('/api/extensions/code-block', () => {
|
|||||||
.should('contain', 'Code')
|
.should('contain', 'Code')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should make a code block for js with tildes', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.clearContent()
|
||||||
|
|
||||||
|
cy.get('.ProseMirror')
|
||||||
|
.type('~~~js Code')
|
||||||
|
.find('pre>code.language-js')
|
||||||
|
.should('contain', 'Code')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,4 +2,25 @@ context('/api/extensions/document', () => {
|
|||||||
before(() => {
|
before(() => {
|
||||||
cy.visit('/api/extensions/document')
|
cy.visit('/api/extensions/document')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the document in as json', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
const json = editor.getJSON()
|
||||||
|
|
||||||
|
expect(json).to.deep.equal({
|
||||||
|
type: 'document',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ context('/api/extensions/hard-break', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse hard breaks correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p>Example<br>Text</p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example<br>Text</p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse hard breaks with self-closing tag correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p>Example<br />Text</p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example<br>Text</p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should add a line break', () => {
|
it('the button should add a line break', () => {
|
||||||
cy.get('.ProseMirror br')
|
cy.get('.ProseMirror br')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
@@ -20,7 +34,7 @@ context('/api/extensions/hard-break', () => {
|
|||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('the default keyboard shortcut should add a line break', () => {
|
it.skip('the default keyboard shortcut should add a line break', () => {
|
||||||
cy.get('.ProseMirror br')
|
cy.get('.ProseMirror br')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,28 @@ context('/api/extensions/heading', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const headings = [
|
||||||
|
'<h1>Example Text</h1>',
|
||||||
|
'<h2>Example Text</h2>',
|
||||||
|
'<h3>Example Text</h3>',
|
||||||
|
]
|
||||||
|
|
||||||
|
headings.forEach(html => {
|
||||||
|
it(`should parse headings correctly (${html})`, () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent(html)
|
||||||
|
expect(editor.getHTML()).to.eq(html)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should omit disabled heading levels', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<h4>Example Text</h4>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should make the selected line a h1', () => {
|
it('the button should make the selected line a h1', () => {
|
||||||
cy.get('.ProseMirror h1')
|
cy.get('.ProseMirror h1')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ context('/api/extensions/horizontal-rule', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse horizontal rules correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p>Example Text</p><hr>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example Text</p><hr>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse horizontal rules with self-closing tag correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p>Example Text</p><hr />')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example Text</p><hr>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should add a horizontal rule', () => {
|
it('the button should add a horizontal rule', () => {
|
||||||
cy.get('.ProseMirror hr')
|
cy.get('.ProseMirror hr')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
|||||||
45
docs/src/demos/Extensions/Image/index.vue
Normal file
45
docs/src/demos/Extensions/Image/index.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="editor">
|
||||||
|
<editor-content :editor="editor" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import { EditorContent } from '@tiptap/vue'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import Paragraph from '@tiptap/extension-paragraph'
|
||||||
|
import Text from '@tiptap/extension-text'
|
||||||
|
import Image from '@tiptap/extension-image'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
EditorContent,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editor: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.editor = new Editor({
|
||||||
|
extensions: [
|
||||||
|
Document(),
|
||||||
|
Paragraph(),
|
||||||
|
Text(),
|
||||||
|
Image(),
|
||||||
|
],
|
||||||
|
content: `
|
||||||
|
<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" />
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.editor.destroy()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -10,6 +10,27 @@ context('/api/extensions/italic', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('i tags should be transformed to em tags', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><i>Example Text</i></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><em>Example Text</em></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('i tags with normal font style should be omitted', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><i style="font-style: normal">Example Text</i></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('generic tags with italic style should be transformed to strong tags', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><span style="font-style: italic">Example Text</span></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><em>Example Text</em></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should make the selected text italic', () => {
|
it('the button should make the selected text italic', () => {
|
||||||
cy.get('.demo__preview button:first')
|
cy.get('.demo__preview button:first')
|
||||||
.click()
|
.click()
|
||||||
|
|||||||
@@ -10,6 +10,27 @@ context('/api/extensions/link', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse a tags correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><a href="#">Example Text</a></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><a href="#" target="_blank" rel="noopener noreferrer nofollow">Example Text</a></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse a tags with target attribute correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><a href="#" target="_self">Example Text</a></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><a href="#" target="_self" rel="noopener noreferrer nofollow">Example Text</a></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse a tags with rel attribute correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><a href="#" rel="follow">Example Text</a></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><a href="#" target="_blank" rel="noopener noreferrer nofollow">Example Text</a></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should add a link to the selected text', () => {
|
it('the button should add a link to the selected text', () => {
|
||||||
cy.window().then(win => {
|
cy.window().then(win => {
|
||||||
cy.stub(win, 'prompt').returns('https://tiptap.dev')
|
cy.stub(win, 'prompt').returns('https://tiptap.dev')
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ context('/api/extensions/ordered-list', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse ordered lists correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<ol><li><p>Example Text</p></li></ol>')
|
||||||
|
expect(editor.getHTML()).to.eq('<ol><li><p>Example Text</p></li></ol>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse ordered lists without paragraphs correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<ol><li>Example Text</li></ol>')
|
||||||
|
expect(editor.getHTML()).to.eq('<ol><li><p>Example Text</p></li></ol>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should make the selected line a ordered list item', () => {
|
it('the button should make the selected line a ordered list item', () => {
|
||||||
cy.get('.ProseMirror ol')
|
cy.get('.ProseMirror ol')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
|||||||
@@ -38,10 +38,15 @@ export default {
|
|||||||
ListItem(),
|
ListItem(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<ul>
|
<ol>
|
||||||
<li>A list item</li>
|
<li>A list item</li>
|
||||||
<li>And another one</li>
|
<li>And another one</li>
|
||||||
</ul>
|
</ol>
|
||||||
|
|
||||||
|
<ol start="5">
|
||||||
|
<li>This item starts at 5</li>
|
||||||
|
<li>And another one</li>
|
||||||
|
</ol>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,19 @@ context('/api/extensions/paragraph', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse paragraphs correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p>Example Text</p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
|
||||||
|
|
||||||
|
editor.setContent('<p><x-unknown>Example Text</x-unknown></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
|
||||||
|
|
||||||
|
editor.setContent('<p style="display: block;">Example Text</p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p>Example Text</p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('text should be wrapped in a paragraph by default', () => {
|
it('text should be wrapped in a paragraph by default', () => {
|
||||||
cy.get('.ProseMirror')
|
cy.get('.ProseMirror')
|
||||||
.type('Example Text')
|
.type('Example Text')
|
||||||
|
|||||||
@@ -10,6 +10,34 @@ context('/api/extensions/strike', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse s tags correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><s>Example Text</s></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><s>Example Text</s></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should transform del tags to s tags', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><del>Example Text</del></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><s>Example Text</s></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should transform strike tags to s tags', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><strike>Example Text</strike></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><s>Example Text</s></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should transform any tag with text decoration line through to s tags', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><span style="text-decoration: line-through">Example Text</span></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><s>Example Text</s></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should strike the selected text', () => {
|
it('the button should strike the selected text', () => {
|
||||||
cy.get('.demo__preview button:first')
|
cy.get('.demo__preview button:first')
|
||||||
.click()
|
.click()
|
||||||
@@ -51,7 +79,7 @@ context('/api/extensions/strike', () => {
|
|||||||
|
|
||||||
it('should make a striked text from the markdown shortcut', () => {
|
it('should make a striked text from the markdown shortcut', () => {
|
||||||
cy.get('.ProseMirror')
|
cy.get('.ProseMirror')
|
||||||
.type('~Strike~')
|
.type('~~Strike~~')
|
||||||
.find('s')
|
.find('s')
|
||||||
.should('contain', 'Strike')
|
.should('contain', 'Strike')
|
||||||
})
|
})
|
||||||
|
|||||||
18
docs/src/demos/Extensions/TextAlign/index.spec.js
Normal file
18
docs/src/demos/Extensions/TextAlign/index.spec.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
context('/api/extensions/text', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit('/api/extensions/text')
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.clearContent()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('text should be wrapped in a paragraph by default', () => {
|
||||||
|
cy.get('.ProseMirror')
|
||||||
|
.type('Example Text')
|
||||||
|
.find('p')
|
||||||
|
.should('contain', 'Example Text')
|
||||||
|
})
|
||||||
|
})
|
||||||
57
docs/src/demos/Extensions/TextAlign/index.vue
Normal file
57
docs/src/demos/Extensions/TextAlign/index.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="editor">
|
||||||
|
<button @click="editor.chain().focus().textAlign('left').run()">
|
||||||
|
left
|
||||||
|
</button>
|
||||||
|
<button @click="editor.chain().focus().textAlign('center').run()">
|
||||||
|
center
|
||||||
|
</button>
|
||||||
|
<button @click="editor.chain().focus().textAlign('right').run()">
|
||||||
|
right
|
||||||
|
</button>
|
||||||
|
<editor-content :editor="editor" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import { EditorContent } from '@tiptap/vue'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import Paragraph from '@tiptap/extension-paragraph'
|
||||||
|
import Heading from '@tiptap/extension-heading'
|
||||||
|
import Text from '@tiptap/extension-text'
|
||||||
|
import TextAlign from '@tiptap/extension-text-align'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
EditorContent,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editor: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.editor = new Editor({
|
||||||
|
extensions: [
|
||||||
|
Document(),
|
||||||
|
Paragraph(),
|
||||||
|
Text(),
|
||||||
|
Heading(),
|
||||||
|
TextAlign(),
|
||||||
|
],
|
||||||
|
content: `
|
||||||
|
<h2>Heading</h2>
|
||||||
|
<p style="text-align: center">first paragraph</p>
|
||||||
|
<p style="text-align: right">second paragraph</p>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.editor.destroy()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -10,6 +10,20 @@ context('/api/extensions/underline', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse u tags correctly', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><u>Example Text</u></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><u>Example Text</u></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should transform any tag with text decoration underline to u tags', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.setContent('<p><span style="text-decoration: underline">Example Text</span></p>')
|
||||||
|
expect(editor.getHTML()).to.eq('<p><u>Example Text</u></p>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('the button should underline the selected text', () => {
|
it('the button should underline the selected text', () => {
|
||||||
cy.get('.demo__preview button:first')
|
cy.get('.demo__preview button:first')
|
||||||
.click()
|
.click()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const Editor = ({
|
|||||||
content: value,
|
content: value,
|
||||||
...props,
|
...props,
|
||||||
}).on('transaction', () => {
|
}).on('transaction', () => {
|
||||||
onChange(e.json())
|
onChange(e.getJSON())
|
||||||
})
|
})
|
||||||
|
|
||||||
setEditor(e)
|
setEditor(e)
|
||||||
|
|||||||
@@ -3,20 +3,38 @@
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
The editor provides a ton of commands to programmtically add or change content or alter the selection. If you want to build your own editor you definitely want to learn more about them.
|
||||||
|
|
||||||
|
## Execute a command
|
||||||
|
All available commands are accessible through an editor instance. Let’s say you want to make text bold when a user clicks on a button. That’s how that would look like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
editor.bold()
|
||||||
|
```
|
||||||
|
|
||||||
|
While that’s perfectly fine and does make the selected bold, you’d likely want to change multiple commands in one run. Let’s have a look at how that works.
|
||||||
|
|
||||||
## Chain commands
|
## Chain commands
|
||||||
|
Most commands can be executed combined to one call. First of all, that’s shorter than separate function call in most cases. Here is an example to make the selected text bold:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
editor.chain().focus().bold().run()
|
editor.chain().focus().bold().run()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `.chain()` is required to start a new chain and the `.run()` is needed to actually execute all the commands in between. Between those two functions, this example combines to different commands.
|
||||||
|
|
||||||
|
When a user clicks on a button outside of the content, the editor isn’t in focus anymore. That’s why you probably want to add a `.focus()` call to most of your commands, that brings back the focus to the editor and the user can continue to type.
|
||||||
|
|
||||||
|
All chained commands are kind of queued up. They are combined to one single transaction. That means, the content is only updated once, also the `update` event is only triggered once.
|
||||||
|
|
||||||
## List of commands
|
## List of commands
|
||||||
|
Have a look at all of the core commands listed below. They should give you a good first impression of what’s possible.
|
||||||
|
|
||||||
### Content
|
### Content
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| --------------- | ----------------------------------------------------------- |
|
| --------------- | ----------------------------------------------------------- |
|
||||||
| .clearContent() | Clear the whole document. |
|
| .clearContent() | Clear the whole document. |
|
||||||
| .insertHTML() | Insert a string of HTML at the currently selected position. |
|
| .insertgetHTML() | Insert a string of HTML at the currently selected position. |
|
||||||
| .insertText() | Insert a string of text at the currently selected position. |
|
| .insertText() | Insert a string of text at the currently selected position. |
|
||||||
| .setContent() | Replace the whole document with new content. |
|
| .setContent() | Replace the whole document with new content. |
|
||||||
|
|
||||||
@@ -47,3 +65,6 @@ editor.chain().focus().bold().run()
|
|||||||
| .focus() | Focus the editor at the given position. |
|
| .focus() | Focus the editor at the given position. |
|
||||||
| .scrollIntoView() | Scroll the selection into view. |
|
| .scrollIntoView() | Scroll the selection into view. |
|
||||||
| .selectAll() | Select the whole document. |
|
| .selectAll() | Select the whole document. |
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
All extension can add additional commands (and most do), check out the specific [documentation for the provided extensions](/api/extensions) to learn more about that. Of course, you can [add your custom extensions](/guide/custom-extensions) with custom commands aswell.
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ This class is a central building block of tiptap. It does most of the heavy lift
|
|||||||
| `onBlur` | `Function` | `undefined` | Returns an object with the `event` and current `state` and `view` of Prosemirror on blur. |
|
| `onBlur` | `Function` | `undefined` | Returns an object with the `event` and current `state` and `view` of Prosemirror on blur. |
|
||||||
| `onFocus` | `Function` | `undefined` | Returns an object with the `event` and current `state` and `view` of Prosemirror on focus. |
|
| `onFocus` | `Function` | `undefined` | Returns an object with the `event` and current `state` and `view` of Prosemirror on focus. |
|
||||||
| `onInit` | `Function` | `undefined` | Returns an object with the current `state` and `view` of Prosemirror on init. |
|
| `onInit` | `Function` | `undefined` | Returns an object with the current `state` and `view` of Prosemirror on init. |
|
||||||
| `onUpdate` | `Function` | `undefined` | Returns an object with the current `state` of Prosemirror, a `json()` and `html()` function and the `transaction` on every change. |
|
| `onUpdate` | `Function` | `undefined` | Returns an object with the current `state` of Prosemirror, a `getJSON()` and `getHTML()` function and the `transaction` on every change. |
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
| Method | Parameters | Description |
|
| Method | Parameters | Description |
|
||||||
| -------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
| -------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||||
| `html()` | – | Returns the current content as HTML. |
|
| `getHTML()` | – | Returns the current content as HTML. |
|
||||||
| `json()` | – | Returns the current content as JSON. |
|
| `getJSON()` | – | Returns the current content as JSON. |
|
||||||
| `destroy()` | – | Stops the editor instance and unbinds all events. |
|
| `destroy()` | – | Stops the editor instance and unbinds all events. |
|
||||||
| `chain()` | - | Create a command chain to call multiple commands at once. |
|
| `chain()` | - | Create a command chain to call multiple commands at once. |
|
||||||
| `setOptions()` | `options` A list of options | Update editor options. |
|
| `setOptions()` | `options` A list of options | Update editor options. |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# CodeBlock
|
# CodeBlock
|
||||||
With the CodeBlock extension you can add fenced code blocks to your documents. It’ll wrap the code in `<pre>` and `<code>` HTML tags.
|
With the CodeBlock extension you can add fenced code blocks to your documents. It’ll wrap the code in `<pre>` and `<code>` HTML tags.
|
||||||
|
|
||||||
Type three backticks and a space <code>```</code> and a code block is instantly added for you.
|
Type <code>``` </code> (three backticks and a space) or <code>∼∼∼ </code> (three tildes and a space) and a code block is instantly added for you. You can even specify the language, try writing <code>```css </code>. That should add a `language-css` class to the `<code>`-tag.
|
||||||
|
|
||||||
::: warning Restrictions
|
::: warning Restrictions
|
||||||
The CodeBlock extension doesn’t come with styling and has no syntax highlighting built-in. It’s on our roadmap though.
|
The CodeBlock extension doesn’t come with styling and has no syntax highlighting built-in. It’s on our roadmap though.
|
||||||
@@ -28,7 +28,8 @@ yarn add @tiptap/extension-code-block
|
|||||||
| codeBlock | — | Wrap content in a code block. |
|
| codeBlock | — | Wrap content in a code block. |
|
||||||
|
|
||||||
## Keyboard shortcuts
|
## Keyboard shortcuts
|
||||||
* `Shift` `Control` `\`
|
* Windows/Linux: `Control` `Shift` `C`
|
||||||
|
* macOS: `Cmd` `Shift` `C`
|
||||||
|
|
||||||
## Source code
|
## Source code
|
||||||
[packages/extension-code-block/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-code-block/)
|
[packages/extension-code-block/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-code-block/)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ yarn add @tiptap/extension-code
|
|||||||
| code | — | Mark text as inline code. |
|
| code | — | Mark text as inline code. |
|
||||||
|
|
||||||
## Keyboard shortcuts
|
## Keyboard shortcuts
|
||||||
* `Alt ` <code>`</code>
|
* `Alt` <code>`</code>
|
||||||
|
|
||||||
## Source code
|
## Source code
|
||||||
[packages/extension-code/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-code/)
|
[packages/extension-code/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-code/)
|
||||||
|
|||||||
16
docs/src/docPages/api/extensions/image.md
Normal file
16
docs/src/docPages/api/extensions/image.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Image
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
# With npm
|
||||||
|
npm install @tiptap/extension-image
|
||||||
|
|
||||||
|
# Or: With Yarn
|
||||||
|
yarn add @tiptap/extension-image
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source code
|
||||||
|
[packages/extension-image/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-image/)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
<demo name="Extensions/Image" highlight="12,30" />
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Strike
|
# Strike
|
||||||
Use this extension to render ~~striked text~~. If you pass `<s>`, `<del>`, `<strike>` tags, or text with inline `style` attributes setting `text-decoration: line-through` in the editor’s initial content, they all will be rendered accordingly.
|
Use this extension to render ~~striked text~~. If you pass `<s>`, `<del>`, `<strike>` tags, or text with inline `style` attributes setting `text-decoration: line-through` in the editor’s initial content, they all will be rendered accordingly.
|
||||||
|
|
||||||
Type <code>~text between tildes~</code> and it will be magically ~~striked through~~ while you type.
|
Type <code>∼∼text between tildes∼∼</code> and it will be magically ~~striked through~~ while you type.
|
||||||
|
|
||||||
::: warning Restrictions
|
::: warning Restrictions
|
||||||
The extension will generate the corresponding `<s>` HTML tags when reading contents of the `Editor` instance. All text striked through, regardless of the method will be normalized to `<s>` HTML tags.
|
The extension will generate the corresponding `<s>` HTML tags when reading contents of the `Editor` instance. All text striked through, regardless of the method will be normalized to `<s>` HTML tags.
|
||||||
|
|||||||
16
docs/src/docPages/api/extensions/text-align.md
Normal file
16
docs/src/docPages/api/extensions/text-align.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Text Align
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
# With npm
|
||||||
|
npm install @tiptap/extension-text-align
|
||||||
|
|
||||||
|
# Or: With Yarn
|
||||||
|
yarn add @tiptap/extension-text-align
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source code
|
||||||
|
[packages/extension-text-align/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-text-align/)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
<demo name="Extensions/TextAlign" highlight="12,30" />
|
||||||
@@ -128,7 +128,7 @@ const schema = getSchema([
|
|||||||
|
|
||||||
If you need to render the content on the server side, e. g. for a blog post that was written with tiptap, you’ll probably need a way to do just that without an actual editor instance.
|
If you need to render the content on the server side, e. g. for a blog post that was written with tiptap, you’ll probably need a way to do just that without an actual editor instance.
|
||||||
|
|
||||||
That’s what `generateHtml()` is for. It’s a utility function that renders HTML without an actual editor instance.
|
That’s what `generategetHTML()` is for. It’s a utility function that renders HTML without an actual editor instance.
|
||||||
|
|
||||||
:::warning Work in progress
|
:::warning Work in progress
|
||||||
Currently, that works only in the browser (client side), but we plan to bring this to Node.js (to use it on the server side).
|
Currently, that works only in the browser (client side), but we plan to bring this to Node.js (to use it on the server side).
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Basic
|
# Basic
|
||||||
|
BUG: Headings can’t be transformed to a bullet or ordered list.
|
||||||
|
|
||||||
<live-demo name="Examples/Basic" />
|
|
||||||
<demo name="Examples/Basic" />
|
<demo name="Examples/Basic" />
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
# Collaborative editing
|
# Collaborative editing
|
||||||
:::warning Public
|
This example shows how you can use tiptap to let different users collaboratively work on the same text in real-time.
|
||||||
The content of this editor is shared with other users.
|
|
||||||
|
It connects client with WebRTC and merges changes to the document (no matter where they come from) with the awesome library [Y.js](https://github.com/yjs/yjs) by Kevin Jahns. Be aware that in a real-world scenario you would probably add a server, which is also able to merge changes with Y.js.
|
||||||
|
|
||||||
|
If you want to learn more about collaborative text editing, [check out our guide on that topic](/guide/collaborative-editing). Anyway, it’s showtime now:
|
||||||
|
|
||||||
|
:::warning The content of this editor is shared with other users from the Internet.
|
||||||
|
Don’t share your password, credit card numbers or other things you wouldn’t make public.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
<!-- <demo name="Examples/Collaboration" :show-source="false"/> -->
|
<!-- <demo name="Examples/Collaboration" :show-source="false"/> -->
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Focus
|
# Focus
|
||||||
|
|
||||||
<demo name="Examples/Focus" highlight="13,33-36,38" />
|
<demo name="Examples/Focus" highlight="15,37-40,42" />
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Minimalist
|
# Minimalist
|
||||||
|
|
||||||
<demo name="Examples/Minimalist" highlight="7-9,26-28" />
|
<demo name="Examples/Minimalist" highlight="7-9,25-27" />
|
||||||
|
|||||||
24
docs/src/docPages/guide/collaborative-editing.md
Normal file
24
docs/src/docPages/guide/collaborative-editing.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Collaborative editing
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Collaborative editing allows multiple users to work on the same text document in real-time. It’s a complex topic that you should be aware before adding it blindly to you app. No worries though, here is everything you need to know.
|
||||||
|
|
||||||
|
## Configure collaboration
|
||||||
|
|
||||||
|
### WebRTC provider
|
||||||
|
|
||||||
|
### Websocket provider
|
||||||
|
|
||||||
|
### Add cursors
|
||||||
|
|
||||||
|
### Offline support
|
||||||
|
|
||||||
|
## Store the content
|
||||||
|
|
||||||
|
### Client-only implementation
|
||||||
|
|
||||||
|
### Server implementation
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
Let’s extend tiptap with a custom extension!
|
One of the strength of tiptap is it’s extendability. You don’t depend on the provided extensions, it’s intended to extend the editor to your liking. With custom extensions you can add new content types and new functionalities, on top of what already exists or on top of that.
|
||||||
|
|
||||||
## Option 1: Change defaults
|
## Option 1: Change defaults
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ The whole editor is rendered inside of a container with the class `.ProseMirror`
|
|||||||
|
|
||||||
```css
|
```css
|
||||||
/* Scoped to the editor */
|
/* Scoped to the editor */
|
||||||
.ProseMirror p {
|
.ProseMirror p {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -19,7 +19,7 @@ If you’re rendering the stored content somewhere, there won’t be a `.ProseMi
|
|||||||
|
|
||||||
```css
|
```css
|
||||||
/* Global styling */
|
/* Global styling */
|
||||||
p {
|
p {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -29,7 +29,6 @@ p {
|
|||||||
Most extensions have a `class` option, which you can use to add a custom CSS class to the HTML tag.
|
Most extensions have a `class` option, which you can use to add a custom CSS class to the HTML tag.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
/* Add custom classes */
|
|
||||||
new Editor({
|
new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
Document(),
|
Document(),
|
||||||
@@ -55,7 +54,6 @@ The rendered HTML will look like that:
|
|||||||
You can even customize the markup for every extension. This will make a custom bold extension that doesn’t render a `<strong>` tag, but a `<b>` tag:
|
You can even customize the markup for every extension. This will make a custom bold extension that doesn’t render a `<strong>` tag, but a `<b>` tag:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
/* Customizing the markup */
|
|
||||||
import Bold from '@tiptap/extension-bold'
|
import Bold from '@tiptap/extension-bold'
|
||||||
|
|
||||||
const CustomBold = Bold
|
const CustomBold = Bold
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ You can store your content as JSON and restore the content from HTML, or the oth
|
|||||||
JSON is probably easier to loop through, for example to look for a mention and it’s more like what tiptap uses under the hood. Anyway, if you want to use JSON to store the content we provide a method to retrieve the content as JSON:
|
JSON is probably easier to loop through, for example to look for a mention and it’s more like what tiptap uses under the hood. Anyway, if you want to use JSON to store the content we provide a method to retrieve the content as JSON:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const json = editor.json()
|
const json = editor.getJSON()
|
||||||
```
|
```
|
||||||
|
|
||||||
You can store that in your database (or send it to an API) and restore the document initially like that:
|
You can store that in your database (or send it to an API) and restore the document initially like that:
|
||||||
@@ -59,7 +59,7 @@ editor.setContent({
|
|||||||
HTML can be easily rendered in other places, for example in emails and it’s wildly used, so it’s probably easier to switch the editor at some point. Anyway, every editor instance provides a method to get HTML from the current document:
|
HTML can be easily rendered in other places, for example in emails and it’s wildly used, so it’s probably easier to switch the editor at some point. Anyway, every editor instance provides a method to get HTML from the current document:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const html = editor.html()
|
const html = editor.getHTML()
|
||||||
```
|
```
|
||||||
|
|
||||||
This can then be used to restore the document initially:
|
This can then be used to restore the document initially:
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ Although tiptap tries to hide most of the complexity of ProseMirror, it’s buil
|
|||||||
|
|
||||||
**Renderless.** We don’t tell you what a menu should look like or where it should be rendered in the DOM. That’s why tiptap is renderless and comes without any CSS. You are in full control over markup and styling.
|
**Renderless.** We don’t tell you what a menu should look like or where it should be rendered in the DOM. That’s why tiptap is renderless and comes without any CSS. You are in full control over markup and styling.
|
||||||
|
|
||||||
**Framework-agnostic.** We don’t care what framework you use. Tiptap is ready to be used with plain JavaScript, Vue.js or React. That makes it even possible to write a renderer for Svelte and others.
|
**Framework-agnostic.** We don’t care what framework you use. Tiptap is ready to be used with plain JavaScript or Vue.js. That makes it even possible to write a renderer for React, Svelte and others.
|
||||||
|
|
||||||
**TypeScript.** Tiptap 2 is written in TypeScript. That gives you a nice autocomplete for the API (if your IDE for it), helps to find bugs early and makes it possible to generate [a complete API documentation](#) on top of the extensive human written documentation.
|
**TypeScript.** Tiptap 2 is written in TypeScript. That gives you a nice autocomplete for the API (if your IDE supports that), helps to find bugs early and makes it possible to generate [a complete API documentation](#) on top of the extensive human written documentation.
|
||||||
|
|
||||||
## Who uses tiptap?
|
## Who uses tiptap?
|
||||||
- [GitLab](https://gitlab.com)
|
- [GitLab](https://gitlab.com)
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
@@ -24,11 +28,11 @@
|
|||||||
border: 4px solid rgba(0, 0, 0, 0);
|
border: 4px solid rgba(0, 0, 0, 0);
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: rgba($colorBlack, 0);
|
background-color: rgba($colorWhite, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:hover::-webkit-scrollbar-thumb {
|
:hover::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba($colorBlack, 0.1);
|
background-color: rgba($colorWhite, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-button {
|
::-webkit-scrollbar-button {
|
||||||
@@ -43,7 +47,6 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -52,17 +55,12 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
font-feature-settings: 'cv05' 1;
|
font-feature-settings: 'cv05' 1;
|
||||||
background-color: $colorBackground;
|
background-color: $colorBlack;
|
||||||
height: 100%;
|
color: $colorText;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus {
|
*[id] {
|
||||||
outline: none;
|
scroll-margin-top: 6rem;
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
list-style: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -74,22 +72,19 @@ a {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
transition: color 0.2s $ease, background-color 0.2s $ease;
|
transition: color 0.2s $ease, background-color 0.2s $ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $colorWhite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: 'JetBrainsMono', monospace;
|
font-family: 'JetBrainsMono', monospace;
|
||||||
background-color: rgba($colorBlack, 0.05);
|
|
||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
color: rgba($colorBlack, 0.7);
|
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
|
||||||
padding-left: 1rem;
|
|
||||||
border-left: 3px solid rgba($colorBlack, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-active {
|
.is-active {
|
||||||
background: black;
|
background: black;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -103,21 +98,23 @@ blockquote {
|
|||||||
pre {
|
pre {
|
||||||
background: rgba($colorBlack, 0.9);
|
background: rgba($colorBlack, 0.9);
|
||||||
color: rgba($colorWhite, 0.9);
|
color: rgba($colorWhite, 0.9);
|
||||||
padding: 1rem;
|
padding: 0.75rem 1rem;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background: none;
|
background: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#table-of-contents {
|
blockquote {
|
||||||
display: none;
|
padding-left: 1rem;
|
||||||
|
border-left: 3px solid rgba($colorBlack, 0.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,24 +112,24 @@
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
// @font-face {
|
||||||
font-family: 'Inter';
|
// font-family: 'Inter';
|
||||||
font-style: normal;
|
// font-style: normal;
|
||||||
font-weight: 700;
|
// font-weight: 700;
|
||||||
src:
|
// src:
|
||||||
url("~@/assets/fonts/Inter-Bold.woff2") format("woff2"),
|
// url("~@/assets/fonts/Inter-Bold.woff2") format("woff2"),
|
||||||
url("~@/assets/fonts/Inter-Bold.woff") format("woff"),
|
// url("~@/assets/fonts/Inter-Bold.woff") format("woff"),
|
||||||
;
|
// ;
|
||||||
}
|
// }
|
||||||
@font-face {
|
// @font-face {
|
||||||
font-family: 'Inter';
|
// font-family: 'Inter';
|
||||||
font-style: italic;
|
// font-style: italic;
|
||||||
font-weight: 700;
|
// font-weight: 700;
|
||||||
src:
|
// src:
|
||||||
url("~@/assets/fonts/Inter-BoldItalic.woff2") format("woff2"),
|
// url("~@/assets/fonts/Inter-BoldItalic.woff2") format("woff2"),
|
||||||
url("~@/assets/fonts/Inter-BoldItalic.woff") format("woff"),
|
// url("~@/assets/fonts/Inter-BoldItalic.woff") format("woff"),
|
||||||
;
|
// ;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// @font-face {
|
// @font-face {
|
||||||
// font-family: 'Inter';
|
// font-family: 'Inter';
|
||||||
@@ -150,15 +150,15 @@
|
|||||||
// ;
|
// ;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@font-face {
|
// @font-face {
|
||||||
font-family: 'Inter';
|
// font-family: 'Inter';
|
||||||
font-style: normal;
|
// font-style: normal;
|
||||||
font-weight: 900;
|
// font-weight: 900;
|
||||||
|
|
||||||
src: url("~@/assets/fonts/Inter-Black.woff2") format("woff2"),
|
// src: url("~@/assets/fonts/Inter-Black.woff2") format("woff2"),
|
||||||
url("~@/assets/fonts/Inter-Black.woff") format("woff"),
|
// url("~@/assets/fonts/Inter-Black.woff") format("woff"),
|
||||||
;
|
// ;
|
||||||
}
|
// }
|
||||||
// @font-face {
|
// @font-face {
|
||||||
// font-family: 'Inter';
|
// font-family: 'Inter';
|
||||||
// font-style: italic;
|
// font-style: italic;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app">
|
<!-- <div class="app">
|
||||||
<header class="app__header">
|
<header class="app__header">
|
||||||
<div class="app__header-inner">
|
<div class="app__header-inner">
|
||||||
<g-link class="app__logo" to="/">
|
<g-link class="app__logo" to="/">
|
||||||
@@ -71,6 +71,102 @@
|
|||||||
<page-navigation />
|
<page-navigation />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="app">
|
||||||
|
<div class="app__sidebar">
|
||||||
|
<div class="app__title">
|
||||||
|
<g-link class="app__name" to="/">
|
||||||
|
{{ $static.metadata.siteName }}
|
||||||
|
</g-link>
|
||||||
|
<g-link to="https://github.com/ueberdosis/tiptap">
|
||||||
|
<svg
|
||||||
|
class="app__github"
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 15 15"
|
||||||
|
><path
|
||||||
|
d="M7.49936 0.849976C3.82767 0.849976 0.849976 3.82727 0.849976 7.5002C0.849976 10.4379 2.75523 12.9306 5.39775 13.8103C5.73047 13.8712 5.85171 13.6658 5.85171 13.4895C5.85171 13.3315 5.846 12.9134 5.84273 12.3586C3.99301 12.7603 3.60273 11.467 3.60273 11.467C3.30022 10.6987 2.86423 10.4942 2.86423 10.4942C2.26044 10.0819 2.90995 10.0901 2.90995 10.0901C3.57742 10.137 3.9285 10.7755 3.9285 10.7755C4.52167 11.7916 5.48512 11.4981 5.86396 11.3278C5.92438 10.8984 6.09625 10.6052 6.28608 10.4391C4.80948 10.2709 3.25695 9.7006 3.25695 7.15238C3.25695 6.42612 3.51618 5.83295 3.94157 5.36797C3.87299 5.19977 3.64478 4.52372 4.00689 3.60804C4.00689 3.60804 4.56494 3.42923 5.83538 4.28938C6.36568 4.14201 6.93477 4.06853 7.50018 4.06567C8.06518 4.06853 8.63386 4.14201 9.16498 4.28938C10.4346 3.42923 10.9918 3.60804 10.9918 3.60804C11.3548 4.52372 11.1266 5.19977 11.0584 5.36797C11.4846 5.83295 11.7418 6.42612 11.7418 7.15238C11.7418 9.70713 10.1868 10.2693 8.70571 10.4338C8.94412 10.6391 9.15681 11.0449 9.15681 11.6654C9.15681 12.5542 9.14865 13.2715 9.14865 13.4895C9.14865 13.6675 9.26867 13.8744 9.60588 13.8095C12.2464 12.9281 14.15 10.4375 14.15 7.5002C14.15 3.82727 11.1723 0.849976 7.49936 0.849976Z"
|
||||||
|
fill="currentColor"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/></svg>
|
||||||
|
</g-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<portal-target name="desktop-nav" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app__content">
|
||||||
|
<div class="app__top-bar">
|
||||||
|
<div class="app__inner app__top-bar-inner">
|
||||||
|
<input class="app__search" type="search" placeholder="Search">
|
||||||
|
<button
|
||||||
|
class="app__menu-icon"
|
||||||
|
@click="menuIsVisible = true"
|
||||||
|
v-if="!menuIsVisible"
|
||||||
|
>
|
||||||
|
<icon name="menu" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="app__close-icon"
|
||||||
|
@click="menuIsVisible = false"
|
||||||
|
v-if="menuIsVisible"
|
||||||
|
>
|
||||||
|
<icon name="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="app__mobile-nav" v-if="menuIsVisible">
|
||||||
|
<portal-target name="mobile-nav" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main class="app__main">
|
||||||
|
<div class="app__inner">
|
||||||
|
<slot />
|
||||||
|
<p>
|
||||||
|
<br>
|
||||||
|
<a :href="editLink" target="_blank">
|
||||||
|
<span>Edit this page on GitHub</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Made with 🖤 by <a href="https://twitter.com/_ueberdosis">überdosis</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<div class="app__page-navigation">
|
||||||
|
<div class="app__inner">
|
||||||
|
<page-navigation />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<portal :to="portal">
|
||||||
|
<nav class="app__navigation">
|
||||||
|
<div class="app__link-group" v-for="(linkGroup, i) in linkGroups" :key="i">
|
||||||
|
<div class="app__link-group-title">
|
||||||
|
{{ linkGroup.title }}
|
||||||
|
</div>
|
||||||
|
<ul class="app__link-list">
|
||||||
|
<li v-for="(item, j) in linkGroup.items" :key="j">
|
||||||
|
<g-link :class="{ 'app__link': true, 'app__link--draft': item.draft === true, 'app__link--with-children': item.items }" :to="item.link" :exact="item.link === '/'">
|
||||||
|
{{ item.title }}
|
||||||
|
</g-link>
|
||||||
|
|
||||||
|
<ul v-if="item.items" class="app__link-list">
|
||||||
|
<li v-for="(item, k) in item.items" :key="k">
|
||||||
|
<g-link :class="{ 'app__link': true, 'app__link--draft': item.draft === true }" :to="item.link" exact>
|
||||||
|
{{ item.title }}
|
||||||
|
</g-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</portal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -86,26 +182,36 @@ query {
|
|||||||
import linkGroups from '@/links.yaml'
|
import linkGroups from '@/links.yaml'
|
||||||
import Icon from '@/components/Icon'
|
import Icon from '@/components/Icon'
|
||||||
import PageNavigation from '@/components/PageNavigation'
|
import PageNavigation from '@/components/PageNavigation'
|
||||||
import GithubButton from 'vue-github-button'
|
// import GithubButton from 'vue-github-button'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
PageNavigation,
|
PageNavigation,
|
||||||
GithubButton,
|
// GithubButton,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
linkGroups,
|
linkGroups,
|
||||||
menuIsVisible: false,
|
menuIsVisible: false,
|
||||||
|
windowWidth: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
portal() {
|
||||||
|
if (this.windowWidth && this.windowWidth < 800) {
|
||||||
|
return 'mobile-nav'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'desktop-nav'
|
||||||
|
},
|
||||||
|
|
||||||
currentPath() {
|
currentPath() {
|
||||||
return this.$route.matched[0].path
|
return this.$route.matched[0].path
|
||||||
},
|
},
|
||||||
|
|
||||||
editLink() {
|
editLink() {
|
||||||
const { currentPath } = this
|
const { currentPath } = this
|
||||||
const filePath = currentPath === '' ? '/introduction' : currentPath
|
const filePath = currentPath === '' ? '/introduction' : currentPath
|
||||||
@@ -114,20 +220,37 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
$route() {
|
||||||
|
this.menuIsVisible = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
initSearch() {
|
initSearch() {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
docsearch({
|
docsearch({
|
||||||
apiKey: '1abe7fb0f0dac150d0e963d2eda930fe',
|
apiKey: '1abe7fb0f0dac150d0e963d2eda930fe',
|
||||||
indexName: 'ueberdosis_tiptap',
|
indexName: 'ueberdosis_tiptap',
|
||||||
inputSelector: '.search',
|
inputSelector: '.app__search',
|
||||||
debug: false,
|
debug: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleResize() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initSearch()
|
this.initSearch()
|
||||||
|
this.handleResize()
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
$codeBackground: rgba($colorBlack, 0.9);
|
$codeBackground: $colorBlack;
|
||||||
$codeText: #eee;
|
|
||||||
$codeGrey: #616161;
|
|
||||||
$codeRed: #ff6666;
|
|
||||||
$codeOrange: #fd9170;
|
|
||||||
$colorYellow: #ffcb6b;
|
|
||||||
$codeGreen: #b9ea80;
|
|
||||||
$codeBlue: #89ddff;
|
|
||||||
$codeTeal: #80cbc4;
|
|
||||||
$codePurple: #c792ea;
|
|
||||||
|
|
||||||
.prism-editor__textarea {
|
.prism-editor__textarea {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -36,14 +27,14 @@ pre[class*="language-"] {
|
|||||||
|
|
||||||
:not(pre) > code[class*="language-"] {
|
:not(pre) > code[class*="language-"] {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
color: $codeText;
|
color: $colorWhite;
|
||||||
background: $codeBackground;
|
background: $codeBackground;
|
||||||
border-radius: 0.2em;
|
border-radius: 0.2em;
|
||||||
padding: 0.1em;
|
padding: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre > code[class*="language-"] {
|
pre > code[class*="language-"] {
|
||||||
display: inline-block; // TODO: breaks on mobile safari
|
display: block;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
@@ -51,21 +42,17 @@ pre > code[class*="language-"] {
|
|||||||
pre[class*="language-"] {
|
pre[class*="language-"] {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
color: $codeText;
|
color: $colorWhite;
|
||||||
background: $codeBackground;
|
background: $codeBackground;
|
||||||
padding: 1.2rem 1.5rem !important;
|
padding: 1.25rem !important;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
max-height: unquote("max(300px, 60vh)");
|
max-height: unquote("max(300px, 60vh)");
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba($colorWhite, 0.25);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pre[class*="language-"].language-css > code,
|
pre[class*="language-"].language-css > code,
|
||||||
pre[class*="language-"].language-sass > code,
|
pre[class*="language-"].language-sass > code,
|
||||||
pre[class*="language-"].language-scss > code {
|
pre[class*="language-"].language-scss > code {
|
||||||
color: $codeOrange;
|
color: $colorOrange;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="language-"] .namespace {
|
[class*="language-"] .namespace {
|
||||||
@@ -73,7 +60,7 @@ pre[class*="language-"].language-scss > code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.token.atrule {
|
.token.atrule {
|
||||||
color: $codePurple;
|
color: $colorPurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.attr-name {
|
.token.attr-name {
|
||||||
@@ -81,15 +68,15 @@ pre[class*="language-"].language-scss > code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.token.attr-value {
|
.token.attr-value {
|
||||||
color: $codeGreen;
|
color: $colorGreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.attribute {
|
.token.attribute {
|
||||||
color: $codeGreen;
|
color: $colorGreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.boolean {
|
.token.boolean {
|
||||||
color: $codePurple;
|
color: $colorPurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.builtin {
|
.token.builtin {
|
||||||
@@ -97,11 +84,11 @@ pre[class*="language-"].language-scss > code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.token.cdata {
|
.token.cdata {
|
||||||
color: $codeTeal;
|
color: $colorTeal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.char {
|
.token.char {
|
||||||
color: $codeTeal;
|
color: $colorTeal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.class {
|
.token.class {
|
||||||
@@ -113,27 +100,27 @@ pre[class*="language-"].language-scss > code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.token.comment {
|
.token.comment {
|
||||||
color: $codeGrey;
|
color: $colorGrey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.constant {
|
.token.constant {
|
||||||
color: $codePurple;
|
color: $colorPurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.deleted {
|
.token.deleted {
|
||||||
color: $codeRed;
|
color: $colorRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.doctype {
|
.token.doctype {
|
||||||
color: $codeGrey;
|
color: $colorGrey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.entity {
|
.token.entity {
|
||||||
color: $codeRed;
|
color: $colorRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.function {
|
.token.function {
|
||||||
color: $codePurple;
|
color: $colorPurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.hexcode {
|
.token.hexcode {
|
||||||
@@ -141,49 +128,49 @@ pre[class*="language-"].language-scss > code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.token.id {
|
.token.id {
|
||||||
color: $codePurple;
|
color: $colorPurple;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.important {
|
.token.important {
|
||||||
color: $codePurple;
|
color: $colorPurple;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.inserted {
|
.token.inserted {
|
||||||
color: $codeTeal;
|
color: $colorTeal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.keyword {
|
.token.keyword {
|
||||||
color: $codePurple;
|
color: $colorPurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.number {
|
.token.number {
|
||||||
color: $codeOrange;
|
color: $colorOrange;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.operator {
|
.token.operator {
|
||||||
color: $codeBlue;
|
color: $colorBlue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.prolog {
|
.token.prolog {
|
||||||
color: $codeGrey;
|
color: $colorGrey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.property {
|
.token.property {
|
||||||
color: $codeTeal;
|
color: $colorTeal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.pseudo-class {
|
.token.pseudo-class {
|
||||||
color: $codeGreen;
|
color: $colorGreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.pseudo-element {
|
.token.pseudo-element {
|
||||||
color: $codeGreen;
|
color: $colorGreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.punctuation {
|
.token.punctuation {
|
||||||
color: $codeBlue;
|
color: $colorBlue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.regex {
|
.token.regex {
|
||||||
@@ -191,35 +178,39 @@ pre[class*="language-"].language-scss > code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.token.selector {
|
.token.selector {
|
||||||
color: $codeRed;
|
color: $colorRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.string {
|
.token.string {
|
||||||
color: $codeGreen;
|
color: $colorGreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.symbol {
|
.token.symbol {
|
||||||
color: $codePurple;
|
color: $colorPurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.tag {
|
.token.tag {
|
||||||
color: $codeRed;
|
color: $colorRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.unit {
|
.token.unit {
|
||||||
color: $codeOrange;
|
color: $colorOrange;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.url {
|
.token.url {
|
||||||
color: $codeRed;
|
color: $colorRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.variable {
|
.token.variable {
|
||||||
color: $codeRed;
|
color: $colorRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-highlight {
|
.line-highlight {
|
||||||
background: rgba($colorWhite, 0.1) !important;
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($colorWhite, 0.1),
|
||||||
|
rgba($colorWhite, 0),
|
||||||
|
) !important;
|
||||||
left: -1.5rem !important;
|
left: -1.5rem !important;
|
||||||
right: -1.5rem !important;
|
right: -1.5rem !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
|||||||
@@ -1,35 +1,160 @@
|
|||||||
$navHeight: 4.5rem;
|
$navHeight: 4.5rem;
|
||||||
$menuBreakPoint: 750px;
|
$mobileBreakPoint: 600px;
|
||||||
|
$menuBreakPoint: 800px;
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100%;
|
|
||||||
|
|
||||||
&__logo {
|
&__title {
|
||||||
font-weight: 700;
|
display: flex;
|
||||||
font-size: 1.4rem;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: $colorWhite;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__github {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .algolia-autocomplete {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid rgba($colorWhite, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font: inherit;
|
||||||
|
color: $colorWhite;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sidebar {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: $menuBreakPoint) {
|
||||||
|
display: block;
|
||||||
|
width: 20rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 2rem;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
border-right: 1px solid rgba($colorWhite, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__top-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem 0;
|
||||||
|
position: sticky;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
max-height: 100vh;
|
||||||
|
|
||||||
|
@media (min-width: $menuBreakPoint) {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: $colorBlack;
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translate3d(0,0,0);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__top-bar-inner {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-icon,
|
||||||
|
&__close-icon {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: $colorText;
|
||||||
|
margin-left: 1rem;
|
||||||
|
transition: color 0.2s $ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $colorWhite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $menuBreakPoint) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 50rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
@media (min-width: $mobileBreakPoint) {
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link-group {
|
&__link-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@media (min-width: $mobileBreakPoint) {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__link-group-title {
|
&__link-group-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.025rem;
|
letter-spacing: 0.025rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: rgba($colorBlack, 0.3);
|
color: rgba($colorWhite, 0.3);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__link-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
&__link-list &__link-list {
|
&__link-list &__link-list {
|
||||||
display: none;
|
display: none;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
border-left: 2px solid rgba($colorBlack, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active + &__link-list {
|
.active + &__link-list {
|
||||||
@@ -37,137 +162,55 @@ $menuBreakPoint: 750px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
display: block;
|
display: flex;
|
||||||
padding: 0.1rem 0.5rem;
|
justify-content: space-between;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-weight: 500;
|
font-size: 0.85rem;
|
||||||
color: rgba($colorBlack, 0.6);
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
margin-left: -0.5rem;
|
margin-left: -0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $colorBlack;
|
color: $colorWhite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active--exact {
|
||||||
color: $colorBlack;
|
color: $colorWhite;
|
||||||
background-color: rgba($colorBlack, 0.05);
|
background-color: rgba($colorWhite, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--draft {
|
&--draft {
|
||||||
color: rgba($colorBlack, 0.2);
|
color: rgba($colorWhite, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--with-children::after {
|
&--with-children::after {
|
||||||
content: '→';
|
content: '↓';
|
||||||
color: rgba($colorBlack, 0.2);
|
color: rgba($colorWhite, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
|
||||||
align-self: center;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: $navHeight;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
background-color: rgba($colorBackground, 0.8);
|
|
||||||
border-bottom: 1px solid rgba($colorBlack, 0.05);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header-inner {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
max-width: 62rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 1rem;
|
|
||||||
|
|
||||||
@media (min-width: $menuBreakPoint) {
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 62rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
|
|
||||||
@media (min-width: $menuBreakPoint) {
|
|
||||||
padding-left: 2rem;
|
|
||||||
padding-right: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__menu-icon,
|
|
||||||
&__close-icon {
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
margin-left: 1rem;
|
|
||||||
|
|
||||||
@media (min-width: $menuBreakPoint) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sidebar-wrapper {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 2;
|
|
||||||
top: $navHeight;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: $colorBackground;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
&.is-mobile-visible {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: $menuBreakPoint) {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
position: sticky;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
align-self: flex-start;
|
|
||||||
top: 0;
|
|
||||||
width: 18rem;
|
|
||||||
height: 100vh;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
padding-right: 3rem;
|
|
||||||
padding-top: $navHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sidebar {
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
padding-top: 2rem;
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
margin-left: -0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
&__main {
|
||||||
|
padding: 1rem 0;
|
||||||
|
|
||||||
|
@media (min-width: $mobileBreakPoint) {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__page-navigation {
|
||||||
|
border-top: 1px solid rgba($colorWhite, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__mobile-nav {
|
||||||
|
padding: 1rem;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
overflow-x: hidden;
|
||||||
padding-top: $navHeight + 2rem;
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
|
||||||
|
@media (min-width: $mobileBreakPoint) {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,8 @@
|
|||||||
# - title: Placeholder
|
# - title: Placeholder
|
||||||
# link: /examples/placeholder
|
# link: /examples/placeholder
|
||||||
# draft: true
|
# draft: true
|
||||||
# - title: Focus
|
- title: Focus
|
||||||
# link: /examples/focus
|
link: /examples/focus
|
||||||
# - title: Title
|
# - title: Title
|
||||||
# link: /examples/title
|
# link: /examples/title
|
||||||
# draft: true
|
# draft: true
|
||||||
@@ -87,12 +87,12 @@
|
|||||||
link: /guide/store-content
|
link: /guide/store-content
|
||||||
- title: Custom extensions
|
- title: Custom extensions
|
||||||
link: /guide/custom-extensions
|
link: /guide/custom-extensions
|
||||||
- title: Use Vue Components
|
# - title: Use Vue Components
|
||||||
link: /guide/use-vue-components
|
|
||||||
draft: true
|
|
||||||
# - title: Collaborative editing
|
|
||||||
# link: /guide/use-vue-components
|
# link: /guide/use-vue-components
|
||||||
# draft: true
|
# draft: true
|
||||||
|
- title: Collaborative editing
|
||||||
|
link: /guide/collaborative-editing
|
||||||
|
draft: true
|
||||||
|
|
||||||
- title: API
|
- title: API
|
||||||
items:
|
items:
|
||||||
@@ -134,6 +134,9 @@
|
|||||||
link: /api/extensions/history
|
link: /api/extensions/history
|
||||||
- title: HorizontalRule
|
- title: HorizontalRule
|
||||||
link: /api/extensions/horizontal-rule
|
link: /api/extensions/horizontal-rule
|
||||||
|
- title: Image
|
||||||
|
link: /api/extensions/image
|
||||||
|
draft: true
|
||||||
- title: Italic
|
- title: Italic
|
||||||
link: /api/extensions/italic
|
link: /api/extensions/italic
|
||||||
- title: Link
|
- title: Link
|
||||||
@@ -163,6 +166,9 @@
|
|||||||
# draft: true
|
# draft: true
|
||||||
- title: Text
|
- title: Text
|
||||||
link: /api/extensions/text
|
link: /api/extensions/text
|
||||||
|
- title: Text Align
|
||||||
|
link: /api/extensions/text-align
|
||||||
|
draft: true
|
||||||
# - title: TodoItem
|
# - title: TodoItem
|
||||||
# link: /api/extensions/todo-item
|
# link: /api/extensions/todo-item
|
||||||
# draft: true
|
# draft: true
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import Prism from 'prismjs'
|
import Prism from 'prismjs'
|
||||||
import 'prismjs/components/prism-jsx.js'
|
import 'prismjs/components/prism-jsx.js'
|
||||||
import 'prismjs/components/prism-scss.js'
|
import 'prismjs/components/prism-scss.js'
|
||||||
|
import PortalVue from 'portal-vue'
|
||||||
import App from '~/layouts/App'
|
import App from '~/layouts/App'
|
||||||
|
|
||||||
export default function (Vue) {
|
export default function (Vue) {
|
||||||
|
Vue.use(PortalVue)
|
||||||
Vue.component('Layout', App)
|
Vue.component('Layout', App)
|
||||||
Vue.component('Demo', () => import(/* webpackChunkName: "demo" */ '~/components/Demo'))
|
Vue.component('Demo', () => import(/* webpackChunkName: "demo" */ '~/components/Demo'))
|
||||||
Vue.component('LiveDemo', () => import(/* webpackChunkName: "live-demo" */ '~/components/LiveDemo'))
|
Vue.component('LiveDemo', () => import(/* webpackChunkName: "live-demo" */ '~/components/LiveDemo'))
|
||||||
|
|||||||
@@ -1,42 +1,60 @@
|
|||||||
.doc-page {
|
.doc-page {
|
||||||
&__markdown ::v-deep {
|
&__markdown ::v-deep {
|
||||||
|
$spacing: 0.75em;
|
||||||
|
|
||||||
a {
|
> div,
|
||||||
text-decoration: underline;
|
> p,
|
||||||
|
> ul,
|
||||||
|
> ol,
|
||||||
|
> blockquote {
|
||||||
|
margin-top: 2 * $spacing;
|
||||||
|
margin-bottom: 2 * $spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
> ul li,
|
||||||
h2,
|
> ol li,
|
||||||
h3,
|
> ul ul,
|
||||||
h4,
|
> ul ol,
|
||||||
h5,
|
> ol ol,
|
||||||
h6 {
|
> ol ul {
|
||||||
position: relative;
|
margin-top: 0.5 * $spacing;
|
||||||
|
margin-bottom: 0.5 * $spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h1,
|
||||||
|
> h2,
|
||||||
|
> h3,
|
||||||
|
> h4,
|
||||||
|
> h5,
|
||||||
|
> h6 {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: $colorWhite;
|
||||||
|
margin-top: 3 * $spacing;
|
||||||
|
margin-bottom: $spacing;
|
||||||
|
|
||||||
|
& + * {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
> h1,
|
||||||
font-weight: 400;
|
> h4,
|
||||||
}
|
> h5,
|
||||||
|
> h6 {
|
||||||
h1,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
a {
|
a {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2,
|
> h2,
|
||||||
h3 {
|
> h3 {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 100%;
|
right: 100%;
|
||||||
color: rgba($colorBlack, 0.4);
|
color: rgba($colorWhite, 0.4);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
@@ -49,16 +67,75 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> * + * {
|
:first-child {
|
||||||
margin-top: 1.5em;
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> pre {
|
||||||
|
border: 1px solid rgba($colorWhite, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
> p code,
|
||||||
|
> li code,
|
||||||
|
> table code,
|
||||||
|
> .remark-container code {
|
||||||
|
color: $colorPurple;
|
||||||
|
background-color: rgba($colorPurple, 0.1);
|
||||||
|
box-decoration-break: clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p a,
|
||||||
|
> li a,
|
||||||
|
> table a,
|
||||||
|
> .remark-container a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-of-contents {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
& + ul {
|
||||||
|
list-style: none;
|
||||||
|
border: 1px solid rgba($colorWhite, 0.1);
|
||||||
|
padding: 1.25rem !important;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: block;
|
||||||
|
content: 'On this page';
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.025rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba($colorWhite, 0.3);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style: none;
|
||||||
|
margin-left: 1rem;
|
||||||
> * + * {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p a img[src^="https://img.shields.io"] {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> ul {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -75,13 +152,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
> ol {
|
||||||
counter-reset: item;
|
counter-reset: item;
|
||||||
|
|
||||||
> * + * {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 2.5rem;
|
padding-left: 2.5rem;
|
||||||
@@ -96,9 +169,9 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
background-color: rgba($colorBlack, 0.1);
|
background-color: rgba($colorWhite, 0.1);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
color: rgba($colorBlack, 0.4);
|
color: rgba($colorWhite, 0.4);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
content: counter(item);
|
content: counter(item);
|
||||||
@@ -107,20 +180,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
> table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
text-align: left;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
font-weight: 600;
|
color: $colorWhite;
|
||||||
background-color: rgba($colorBlack, 0.05);
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid rgba($colorWhite, 0.2);
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
@@ -134,7 +216,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
border-bottom: 1px solid rgba($colorBlack, 0.05);
|
border-bottom: 1px solid rgba($colorWhite, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:last-child td {
|
tr:last-child td {
|
||||||
@@ -142,21 +224,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.remark-container {
|
> .remark-container {
|
||||||
padding: 1rem;
|
padding: 1.25rem;
|
||||||
border: 2px solid rgba($colorBlack, 0.1);
|
border: 1px solid rgba($colorWhite, 0.1);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
border-color:#ffd8a8;
|
border-color: rgba($colorYellow, 0.1);
|
||||||
background-color: #fff4e6;
|
background-color: rgba($colorYellow, 0.1);
|
||||||
color: #ca9c63;
|
color: $colorYellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.info {
|
&.info {
|
||||||
border-color:#a5d8ff;
|
border-color: rgba($colorBlue, 0.1);
|
||||||
background-color: #e7f5ff;
|
background-color: rgba($colorBlue, 0.1);
|
||||||
color: #228be6;
|
color: $colorBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: rgba($colorRed, 0.1);
|
||||||
|
background-color: rgba($colorRed, 0.1);
|
||||||
|
color: $colorRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remark-container-title {
|
.remark-container-title {
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
$colorWhite: #FFF;
|
$colorWhite: #FFF;
|
||||||
$colorBlack: #000;
|
$colorBlack: #0D0D0D;
|
||||||
$colorRed: #fa5252;
|
$colorText: rgba($colorWhite, 0.75);
|
||||||
$colorBackground: mix($colorBlack, $colorWhite, 2%);
|
$colorGrey: #616161;
|
||||||
|
$colorPurple: #A975FF;
|
||||||
|
$colorRed: #FB5151;
|
||||||
|
$colorOrange: #fd9170;
|
||||||
|
$colorYellow: #FFCB6B;
|
||||||
|
$colorBlue: #68CEF8;
|
||||||
|
$colorTeal: #80cbc4;
|
||||||
|
$colorGreen: #9DEF8F;
|
||||||
|
|
||||||
/* Default Equations */
|
/* Default Equations */
|
||||||
$linear: cubic-bezier(0.250, 0.250, 0.750, 0.750);
|
$linear: cubic-bezier(0.250, 0.250, 0.750, 0.750);
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -32,20 +32,20 @@
|
|||||||
"@types/prosemirror-model": "^1.7.4",
|
"@types/prosemirror-model": "^1.7.4",
|
||||||
"@types/prosemirror-state": "^1.2.5",
|
"@types/prosemirror-state": "^1.2.5",
|
||||||
"@types/prosemirror-transform": "^1.1.1",
|
"@types/prosemirror-transform": "^1.1.1",
|
||||||
"@types/prosemirror-view": "^1.15.0",
|
"@types/prosemirror-view": "^1.16.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.3.0",
|
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||||
"@typescript-eslint/parser": "^4.3.0",
|
"@typescript-eslint/parser": "^4.6.0",
|
||||||
"cypress": "^5.3.0",
|
"cypress": "^5.5.0",
|
||||||
"eslint": "^7.10.0",
|
"eslint": "^7.12.1",
|
||||||
"eslint-config-airbnb-base": "^14.2.0",
|
"eslint-config-airbnb-base": "^14.2.0",
|
||||||
"eslint-plugin-cypress": "^2.11.2",
|
"eslint-plugin-cypress": "^2.11.2",
|
||||||
"eslint-plugin-html": "^6.1.0",
|
"eslint-plugin-html": "^6.1.0",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^7.0.1",
|
||||||
"lerna": "^3.22.1",
|
"lerna": "^3.22.1",
|
||||||
"microbundle": "^0.12.4",
|
"microbundle": "^0.12.4",
|
||||||
"typedoc": "^0.19.2",
|
"typedoc": "^0.19.2",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.5",
|
||||||
"vue": "^2.6.12"
|
"vue": "^2.6.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { Editor, Command, CommandsSpec } from './src/Editor'
|
|||||||
export default Editor
|
export default Editor
|
||||||
export { Editor, Command, CommandsSpec }
|
export { Editor, Command, CommandsSpec }
|
||||||
export { default as ComponentRenderer } from './src/ComponentRenderer'
|
export { default as ComponentRenderer } from './src/ComponentRenderer'
|
||||||
export { default as Extension } from './src/Extension'
|
|
||||||
export { default as Node } from './src/Node'
|
export * from './src/Extension'
|
||||||
export { default as Mark } from './src/Mark'
|
export * from './src/NodeExtension'
|
||||||
export { Extensions } from './src/types'
|
export * from './src/MarkExtension'
|
||||||
|
export * from './src/types'
|
||||||
|
|
||||||
export { default as nodeInputRule } from './src/inputRules/nodeInputRule'
|
export { default as nodeInputRule } from './src/inputRules/nodeInputRule'
|
||||||
export { default as markInputRule } from './src/inputRules/markInputRule'
|
export { default as markInputRule } from './src/inputRules/markInputRule'
|
||||||
@@ -16,7 +17,5 @@ export { default as capitalize } from './src/utils/capitalize'
|
|||||||
export { default as getSchema } from './src/utils/getSchema'
|
export { default as getSchema } from './src/utils/getSchema'
|
||||||
export { default as generateHtml } from './src/utils/generateHtml'
|
export { default as generateHtml } from './src/utils/generateHtml'
|
||||||
export { default as getHtmlFromFragment } from './src/utils/getHtmlFromFragment'
|
export { default as getHtmlFromFragment } from './src/utils/getHtmlFromFragment'
|
||||||
export { default as getTopNodeFromExtensions } from './src/utils/getTopNodeFromExtensions'
|
|
||||||
export { default as getNodesFromExtensions } from './src/utils/getNodesFromExtensions'
|
|
||||||
export { default as getMarksFromExtensions } from './src/utils/getMarksFromExtensions'
|
|
||||||
export { default as getMarkAttrs } from './src/utils/getMarkAttrs'
|
export { default as getMarkAttrs } from './src/utils/getMarkAttrs'
|
||||||
|
export { default as mergeAttributes } from './src/utils/mergeAttributes'
|
||||||
|
|||||||
@@ -12,24 +12,20 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/clone-deep": "^4.0.1",
|
|
||||||
"@types/prosemirror-dropcursor": "^1.0.0",
|
"@types/prosemirror-dropcursor": "^1.0.0",
|
||||||
"@types/prosemirror-gapcursor": "^1.0.1",
|
"@types/prosemirror-gapcursor": "^1.0.1",
|
||||||
"@types/prosemirror-schema-list": "^1.0.1",
|
"@types/prosemirror-schema-list": "^1.0.1",
|
||||||
"clone-deep": "^4.0.1",
|
|
||||||
"collect.js": "^4.28.2",
|
|
||||||
"deepmerge": "^4.2.2",
|
|
||||||
"prosemirror-commands": "^1.1.3",
|
"prosemirror-commands": "^1.1.3",
|
||||||
"prosemirror-dropcursor": "^1.3.2",
|
"prosemirror-dropcursor": "^1.3.2",
|
||||||
"prosemirror-gapcursor": "^1.1.5",
|
"prosemirror-gapcursor": "^1.1.5",
|
||||||
"prosemirror-inputrules": "^1.1.3",
|
"prosemirror-inputrules": "^1.1.3",
|
||||||
"prosemirror-keymap": "^1.1.3",
|
"prosemirror-keymap": "^1.1.3",
|
||||||
"prosemirror-model": "^1.11.2",
|
"prosemirror-model": "^1.12.0",
|
||||||
"prosemirror-schema-list": "^1.1.4",
|
"prosemirror-schema-list": "^1.1.4",
|
||||||
"prosemirror-state": "^1.3.3",
|
"prosemirror-state": "^1.3.3",
|
||||||
"prosemirror-tables": "^1.1.1",
|
"prosemirror-tables": "^1.1.1",
|
||||||
"prosemirror-utils": "^0.9.6",
|
"prosemirror-utils": "^1.0.0-0",
|
||||||
"prosemirror-view": "^1.16.0"
|
"prosemirror-view": "^1.16.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "microbundle"
|
"build": "microbundle"
|
||||||
|
|||||||
@@ -8,17 +8,15 @@ import markIsActive from './utils/markIsActive'
|
|||||||
import getNodeAttrs from './utils/getNodeAttrs'
|
import getNodeAttrs from './utils/getNodeAttrs'
|
||||||
import getMarkAttrs from './utils/getMarkAttrs'
|
import getMarkAttrs from './utils/getMarkAttrs'
|
||||||
import removeElement from './utils/removeElement'
|
import removeElement from './utils/removeElement'
|
||||||
import getSchemaTypeByName from './utils/getSchemaTypeByName'
|
import getSchemaTypeNameByName from './utils/getSchemaTypeNameByName'
|
||||||
import getHtmlFromFragment from './utils/getHtmlFromFragment'
|
import getHtmlFromFragment from './utils/getHtmlFromFragment'
|
||||||
import createStyleTag from './utils/createStyleTag'
|
import createStyleTag from './utils/createStyleTag'
|
||||||
import CommandManager from './CommandManager'
|
import CommandManager from './CommandManager'
|
||||||
import ExtensionManager from './ExtensionManager'
|
import ExtensionManager from './ExtensionManager'
|
||||||
import EventEmitter from './EventEmitter'
|
import EventEmitter from './EventEmitter'
|
||||||
import Extension from './Extension'
|
import { Extensions, UnionToIntersection, PickValue } from './types'
|
||||||
import Node from './Node'
|
|
||||||
import Mark from './Mark'
|
|
||||||
import defaultPlugins from './plugins'
|
import defaultPlugins from './plugins'
|
||||||
import * as coreCommands from './commands'
|
import * as extensions from './extensions'
|
||||||
import style from './style'
|
import style from './style'
|
||||||
|
|
||||||
export type Command = (props: {
|
export type Command = (props: {
|
||||||
@@ -37,19 +35,19 @@ export interface CommandsSpec {
|
|||||||
[key: string]: CommandSpec
|
[key: string]: CommandSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Commands {}
|
export interface AllExtensions {}
|
||||||
|
|
||||||
export type CommandNames = Extract<keyof Commands, string>
|
export type AllCommands = UnionToIntersection<ReturnType<PickValue<ReturnType<AllExtensions[keyof AllExtensions]>, 'addCommands'>>>
|
||||||
|
|
||||||
export type SingleCommands = {
|
export type SingleCommands = {
|
||||||
[Item in keyof Commands]: Commands[Item] extends (...args: any[]) => any
|
[Item in keyof AllCommands]: AllCommands[Item] extends (...args: any[]) => any
|
||||||
? (...args: Parameters<Commands[Item]>) => boolean
|
? (...args: Parameters<AllCommands[Item]>) => boolean
|
||||||
: never
|
: never
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChainedCommands = {
|
export type ChainedCommands = {
|
||||||
[Item in keyof Commands]: Commands[Item] extends (...args: any[]) => any
|
[Item in keyof AllCommands]: AllCommands[Item] extends (...args: any[]) => any
|
||||||
? (...args: Parameters<Commands[Item]>) => ChainedCommands
|
? (...args: Parameters<AllCommands[Item]>) => ChainedCommands
|
||||||
: never
|
: never
|
||||||
} & {
|
} & {
|
||||||
run: () => boolean
|
run: () => boolean
|
||||||
@@ -64,7 +62,7 @@ interface HTMLElement {
|
|||||||
interface EditorOptions {
|
interface EditorOptions {
|
||||||
element: Element,
|
element: Element,
|
||||||
content: EditorContent,
|
content: EditorContent,
|
||||||
extensions: (Extension | Node | Mark)[],
|
extensions: Extensions,
|
||||||
injectCSS: boolean,
|
injectCSS: boolean,
|
||||||
autoFocus: 'start' | 'end' | number | boolean | null,
|
autoFocus: 'start' | 'end' | number | boolean | null,
|
||||||
editable: boolean,
|
editable: boolean,
|
||||||
@@ -117,12 +115,11 @@ export class Editor extends EventEmitter {
|
|||||||
this.createCommandManager()
|
this.createCommandManager()
|
||||||
this.createExtensionManager()
|
this.createExtensionManager()
|
||||||
this.createSchema()
|
this.createSchema()
|
||||||
this.extensionManager.resolveConfigs()
|
|
||||||
this.createView()
|
this.createView()
|
||||||
this.registerCommands(coreCommands)
|
// this.registerCommands(coreCommands)
|
||||||
this.injectCSS()
|
this.injectCSS()
|
||||||
|
|
||||||
this.proxy.focus(this.options.autoFocus)
|
window.setTimeout(() => this.proxy.focus(this.options.autoFocus), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,7 +232,10 @@ export class Editor extends EventEmitter {
|
|||||||
* Creates an extension manager.
|
* Creates an extension manager.
|
||||||
*/
|
*/
|
||||||
private createExtensionManager() {
|
private createExtensionManager() {
|
||||||
this.extensionManager = new ExtensionManager(this.options.extensions, this.proxy)
|
const coreExtensions = Object.entries(extensions).map(([, extension]) => extension())
|
||||||
|
const allExtensions = [...coreExtensions, ...this.options.extensions]
|
||||||
|
|
||||||
|
this.extensionManager = new ExtensionManager(allExtensions, this.proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -346,7 +346,7 @@ export class Editor extends EventEmitter {
|
|||||||
* @param attrs Attributes of the node or mark
|
* @param attrs Attributes of the node or mark
|
||||||
*/
|
*/
|
||||||
public isActive(name: string, attrs = {}) {
|
public isActive(name: string, attrs = {}) {
|
||||||
const schemaType = getSchemaTypeByName(name, this.schema)
|
const schemaType = getSchemaTypeNameByName(name, this.schema)
|
||||||
|
|
||||||
if (schemaType === 'node') {
|
if (schemaType === 'node') {
|
||||||
return nodeIsActive(this.state, this.schema.nodes[name], attrs)
|
return nodeIsActive(this.state, this.schema.nodes[name], attrs)
|
||||||
@@ -376,17 +376,24 @@ export class Editor extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Get the document as JSON.
|
* Get the document as JSON.
|
||||||
*/
|
*/
|
||||||
public json() {
|
public getJSON() {
|
||||||
return this.state.doc.toJSON()
|
return this.state.doc.toJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the document as HTML.
|
* Get the document as HTML.
|
||||||
*/
|
*/
|
||||||
public html() {
|
public getHTML() {
|
||||||
return getHtmlFromFragment(this.state.doc, this.schema)
|
return getHtmlFromFragment(this.state.doc, this.schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is no content.
|
||||||
|
*/
|
||||||
|
public isEmpty() {
|
||||||
|
return !this.state.doc.textContent.length
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the editor.
|
* Destroy the editor.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,114 +1,113 @@
|
|||||||
import cloneDeep from 'clone-deep'
|
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import { Editor, CommandsSpec } from './Editor'
|
import { Editor } from './Editor'
|
||||||
|
import { GlobalAttributes } from './types'
|
||||||
|
|
||||||
type AnyObject = {
|
export interface ExtensionSpec<Options = {}, Commands = {}> {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default options
|
||||||
|
*/
|
||||||
|
defaultOptions?: Options,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global attributes
|
||||||
|
*/
|
||||||
|
addGlobalAttributes?: (this: {
|
||||||
|
options: Options,
|
||||||
|
}) => GlobalAttributes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands
|
||||||
|
*/
|
||||||
|
addCommands?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
}) => Commands,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyboard shortcuts
|
||||||
|
*/
|
||||||
|
addKeyboardShortcuts?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
}) => {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input rules
|
||||||
|
*/
|
||||||
|
addInputRules?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
}) => any[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paste rules
|
||||||
|
*/
|
||||||
|
addPasteRules?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
}) => any[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProseMirror plugins
|
||||||
|
*/
|
||||||
|
addProseMirrorPlugins?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
}) => Plugin[],
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoInfer<T> = [T][T extends any ? 0 : never]
|
/**
|
||||||
|
* Extension interface for internal usage
|
||||||
|
*/
|
||||||
|
export type Extension = Required<Omit<ExtensionSpec, 'defaultOptions'> & {
|
||||||
|
type: string,
|
||||||
|
options: {
|
||||||
|
[key: string]: any
|
||||||
|
},
|
||||||
|
}>
|
||||||
|
|
||||||
type MergeStrategy = 'extend' | 'overwrite'
|
/**
|
||||||
|
* Default extension
|
||||||
type Configs = {
|
*/
|
||||||
[key: string]: {
|
export const defaultExtension: Extension = {
|
||||||
stategy: MergeStrategy
|
name: 'extension',
|
||||||
value: any
|
type: 'extension',
|
||||||
}[]
|
options: {},
|
||||||
|
addGlobalAttributes: () => [],
|
||||||
|
addCommands: () => ({}),
|
||||||
|
addKeyboardShortcuts: () => ({}),
|
||||||
|
addInputRules: () => [],
|
||||||
|
addPasteRules: () => [],
|
||||||
|
addProseMirrorPlugins: () => [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionProps<Options> {
|
export function createExtension<Options extends {}, Commands extends {}>(config: ExtensionSpec<Options, Commands>) {
|
||||||
name: string
|
const extend = <ExtendedOptions = Options, ExtendedCommands = Commands>(extendedConfig: Partial<ExtensionSpec<ExtendedOptions, ExtendedCommands>>) => {
|
||||||
editor: Editor
|
return createExtension({
|
||||||
options: Options
|
...config,
|
||||||
|
...extendedConfig,
|
||||||
|
} as ExtensionSpec<ExtendedOptions, ExtendedCommands>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionMethods<Props, Options> {
|
const setOptions = (options?: Partial<Options>) => {
|
||||||
name: string
|
const { defaultOptions, ...rest } = config
|
||||||
options: Options
|
|
||||||
commands: (params: Props) => CommandsSpec
|
|
||||||
inputRules: (params: Props) => any[]
|
|
||||||
pasteRules: (params: Props) => any[]
|
|
||||||
keys: (params: Props) => {
|
|
||||||
[key: string]: Function
|
|
||||||
}
|
|
||||||
plugins: (params: Props) => Plugin[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Extension<
|
return {
|
||||||
Options = {},
|
...defaultExtension,
|
||||||
Props = ExtensionProps<Options>,
|
...rest,
|
||||||
Methods extends ExtensionMethods<Props, Options> = ExtensionMethods<Props, Options>,
|
options: {
|
||||||
> {
|
...defaultOptions,
|
||||||
type = 'extension'
|
...options,
|
||||||
|
} as Options,
|
||||||
config: AnyObject = {}
|
|
||||||
|
|
||||||
configs: Configs = {}
|
|
||||||
|
|
||||||
options: Partial<Options> = {}
|
|
||||||
|
|
||||||
protected storeConfig(key: string, value: any, stategy: MergeStrategy) {
|
|
||||||
const item = {
|
|
||||||
stategy,
|
|
||||||
value,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.configs[key]) {
|
|
||||||
this.configs[key].push(item)
|
|
||||||
} else {
|
|
||||||
this.configs[key] = [item]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public configure(options: Partial<Options>) {
|
return Object.assign(setOptions, { config, extend })
|
||||||
this.options = { ...this.options, ...options }
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public name(value: Methods['name']) {
|
|
||||||
this.storeConfig('name', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public defaults(value: Options) {
|
|
||||||
this.storeConfig('defaults', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public commands(value: Methods['commands']) {
|
|
||||||
this.storeConfig('commands', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public keys(value: Methods['keys']) {
|
|
||||||
this.storeConfig('keys', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public inputRules(value: Methods['inputRules']) {
|
|
||||||
this.storeConfig('inputRules', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public pasteRules(value: Methods['pasteRules']) {
|
|
||||||
this.storeConfig('pasteRules', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public plugins(value: Methods['plugins']) {
|
|
||||||
this.storeConfig('plugins', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public extend<T extends Extract<keyof Methods, string>>(key: T, value: Methods[T]) {
|
|
||||||
this.storeConfig(key, value, 'extend')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public create() {
|
|
||||||
return <NewOptions = Options>(options?: Partial<NoInfer<NewOptions>>) => {
|
|
||||||
return cloneDeep(this, true).configure(options as NewOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,53 @@
|
|||||||
import collect from 'collect.js'
|
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import { Schema } from 'prosemirror-model'
|
|
||||||
// import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
|
// import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
|
||||||
import { inputRules } from 'prosemirror-inputrules'
|
import { inputRules } from 'prosemirror-inputrules'
|
||||||
// import { EditorView, Decoration } from 'prosemirror-view'
|
// import { EditorView, Decoration } from 'prosemirror-view'
|
||||||
|
import { Schema } from 'prosemirror-model'
|
||||||
import { Editor } from './Editor'
|
import { Editor } from './Editor'
|
||||||
// import capitalize from './utils/capitalize'
|
// import capitalize from './utils/capitalize'
|
||||||
import { Extensions } from './types'
|
import { Extensions } from './types'
|
||||||
import getTopNodeFromExtensions from './utils/getTopNodeFromExtensions'
|
|
||||||
import getNodesFromExtensions from './utils/getNodesFromExtensions'
|
|
||||||
import getMarksFromExtensions from './utils/getMarksFromExtensions'
|
|
||||||
import resolveExtensionConfig from './utils/resolveExtensionConfig'
|
|
||||||
import getSchema from './utils/getSchema'
|
import getSchema from './utils/getSchema'
|
||||||
|
import getSchemaTypeByName from './utils/getSchemaTypeByName'
|
||||||
|
|
||||||
export default class ExtensionManager {
|
export default class ExtensionManager {
|
||||||
|
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
|
||||||
|
schema: Schema
|
||||||
|
|
||||||
extensions: Extensions
|
extensions: Extensions
|
||||||
|
|
||||||
constructor(extensions: Extensions, editor: Editor) {
|
constructor(extensions: Extensions, editor: Editor) {
|
||||||
this.editor = editor
|
this.editor = editor
|
||||||
this.extensions = extensions
|
this.extensions = extensions
|
||||||
}
|
this.schema = getSchema(this.extensions)
|
||||||
|
|
||||||
resolveConfigs() {
|
|
||||||
this.extensions.forEach(extension => {
|
this.extensions.forEach(extension => {
|
||||||
const { editor } = this
|
const context = {
|
||||||
const { name } = extension.config
|
options: extension.options,
|
||||||
const options = {
|
editor: this.editor,
|
||||||
...extension.config.defaults,
|
type: getSchemaTypeByName(extension.name, this.schema),
|
||||||
...extension.options,
|
|
||||||
}
|
|
||||||
const type = extension.type === 'node'
|
|
||||||
? editor.schema.nodes[name]
|
|
||||||
: editor.schema.marks[name]
|
|
||||||
|
|
||||||
resolveExtensionConfig(extension, 'commands', {
|
|
||||||
name, options, editor, type,
|
|
||||||
})
|
|
||||||
resolveExtensionConfig(extension, 'inputRules', {
|
|
||||||
name, options, editor, type,
|
|
||||||
})
|
|
||||||
resolveExtensionConfig(extension, 'pasteRules', {
|
|
||||||
name, options, editor, type,
|
|
||||||
})
|
|
||||||
resolveExtensionConfig(extension, 'keys', {
|
|
||||||
name, options, editor, type,
|
|
||||||
})
|
|
||||||
resolveExtensionConfig(extension, 'plugins', {
|
|
||||||
name, options, editor, type,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (extension.config.commands) {
|
|
||||||
editor.registerCommands(extension.config.commands)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema(): Schema {
|
const commands = extension.addCommands.bind(context)()
|
||||||
return getSchema(this.extensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
get topNode(): any {
|
editor.registerCommands(commands)
|
||||||
return getTopNodeFromExtensions(this.extensions)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
get nodes(): any {
|
|
||||||
return getNodesFromExtensions(this.extensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
get marks(): any {
|
|
||||||
return getMarksFromExtensions(this.extensions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get plugins(): Plugin[] {
|
get plugins(): Plugin[] {
|
||||||
const plugins = collect(this.extensions)
|
const plugins = this.extensions
|
||||||
.flatMap(extension => extension.config.plugins)
|
.map(extension => {
|
||||||
.filter(plugin => plugin)
|
const context = {
|
||||||
.toArray()
|
options: extension.options,
|
||||||
|
editor: this.editor,
|
||||||
|
type: getSchemaTypeByName(extension.name, this.schema),
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension.addProseMirrorPlugins.bind(context)()
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...plugins,
|
...plugins,
|
||||||
@@ -91,25 +58,43 @@ export default class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get inputRules(): any {
|
get inputRules(): any {
|
||||||
return collect(this.extensions)
|
return this.extensions
|
||||||
.flatMap(extension => extension.config.inputRules)
|
.map(extension => {
|
||||||
.filter(plugin => plugin)
|
const context = {
|
||||||
.toArray()
|
options: extension.options,
|
||||||
|
editor: this.editor,
|
||||||
|
type: getSchemaTypeByName(extension.name, this.schema),
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension.addInputRules.bind(context)()
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
get pasteRules(): any {
|
get pasteRules(): any {
|
||||||
return collect(this.extensions)
|
return this.extensions
|
||||||
.flatMap(extension => extension.config.pasteRules)
|
.map(extension => {
|
||||||
.filter(plugin => plugin)
|
const context = {
|
||||||
.toArray()
|
options: extension.options,
|
||||||
|
editor: this.editor,
|
||||||
|
type: getSchemaTypeByName(extension.name, this.schema),
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension.addPasteRules.bind(context)()
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
get keymaps() {
|
get keymaps() {
|
||||||
return collect(this.extensions)
|
return this.extensions.map(extension => {
|
||||||
.map(extension => extension.config.keys)
|
const context = {
|
||||||
.filter(keys => keys)
|
options: extension.options,
|
||||||
.map(keys => keymap(keys))
|
editor: this.editor,
|
||||||
.toArray()
|
type: getSchemaTypeByName(extension.name, this.schema),
|
||||||
|
}
|
||||||
|
|
||||||
|
return keymap(extension.addKeyboardShortcuts.bind(context)())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get nodeViews() {
|
get nodeViews() {
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { MarkSpec, MarkType } from 'prosemirror-model'
|
|
||||||
import Extension, { ExtensionMethods } from './Extension'
|
|
||||||
import { Editor } from './Editor'
|
|
||||||
|
|
||||||
export interface MarkProps<Options> {
|
|
||||||
name: string
|
|
||||||
editor: Editor
|
|
||||||
options: Options
|
|
||||||
type: MarkType
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarkMethods<Props, Options> extends ExtensionMethods<Props, Options> {
|
|
||||||
topMark: boolean
|
|
||||||
schema: (params: Omit<Props, 'type' | 'editor'>) => MarkSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Mark<
|
|
||||||
Options = {},
|
|
||||||
Props = MarkProps<Options>,
|
|
||||||
Methods extends MarkMethods<Props, Options> = MarkMethods<Props, Options>,
|
|
||||||
> extends Extension<Options, Props, Methods> {
|
|
||||||
type = 'mark'
|
|
||||||
|
|
||||||
public schema(value: Methods['schema']) {
|
|
||||||
this.storeConfig('schema', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
151
packages/core/src/MarkExtension.ts
Normal file
151
packages/core/src/MarkExtension.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
DOMOutputSpec, MarkSpec, Mark, MarkType,
|
||||||
|
} from 'prosemirror-model'
|
||||||
|
import { Plugin } from 'prosemirror-state'
|
||||||
|
import { ExtensionSpec, defaultExtension } from './Extension'
|
||||||
|
import { Attributes, Overwrite } from './types'
|
||||||
|
import { Editor } from './Editor'
|
||||||
|
|
||||||
|
export interface MarkExtensionSpec<Options = {}, Commands = {}> extends Overwrite<ExtensionSpec<Options, Commands>, {
|
||||||
|
/**
|
||||||
|
* Inclusive
|
||||||
|
*/
|
||||||
|
inclusive?: MarkSpec['inclusive'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excludes
|
||||||
|
*/
|
||||||
|
excludes?: MarkSpec['excludes'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group
|
||||||
|
*/
|
||||||
|
group?: MarkSpec['group'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spanning
|
||||||
|
*/
|
||||||
|
spanning?: MarkSpec['spanning'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse HTML
|
||||||
|
*/
|
||||||
|
parseHTML?: (
|
||||||
|
this: {
|
||||||
|
options: Options,
|
||||||
|
},
|
||||||
|
) => MarkSpec['parseDOM'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render HTML
|
||||||
|
*/
|
||||||
|
renderHTML?: ((
|
||||||
|
this: {
|
||||||
|
options: Options,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
mark: Mark,
|
||||||
|
attributes: { [key: string]: any },
|
||||||
|
}
|
||||||
|
) => DOMOutputSpec) | null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attributes
|
||||||
|
*/
|
||||||
|
addAttributes?: (
|
||||||
|
this: {
|
||||||
|
options: Options,
|
||||||
|
},
|
||||||
|
) => Attributes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands
|
||||||
|
*/
|
||||||
|
addCommands?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: MarkType,
|
||||||
|
}) => Commands,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyboard shortcuts
|
||||||
|
*/
|
||||||
|
addKeyboardShortcuts?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: MarkType,
|
||||||
|
}) => {
|
||||||
|
[key: string]: any
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input rules
|
||||||
|
*/
|
||||||
|
addInputRules?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: MarkType,
|
||||||
|
}) => any[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paste rules
|
||||||
|
*/
|
||||||
|
addPasteRules?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: MarkType,
|
||||||
|
}) => any[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProseMirror plugins
|
||||||
|
*/
|
||||||
|
addProseMirrorPlugins?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: MarkType,
|
||||||
|
}) => Plugin[],
|
||||||
|
}> {}
|
||||||
|
|
||||||
|
export type MarkExtension = Required<Omit<MarkExtensionSpec, 'defaultOptions'> & {
|
||||||
|
type: string,
|
||||||
|
options: {
|
||||||
|
[key: string]: any
|
||||||
|
},
|
||||||
|
}>
|
||||||
|
|
||||||
|
const defaultMark: MarkExtension = {
|
||||||
|
...defaultExtension,
|
||||||
|
type: 'mark',
|
||||||
|
name: 'mark',
|
||||||
|
inclusive: null,
|
||||||
|
excludes: null,
|
||||||
|
group: null,
|
||||||
|
spanning: null,
|
||||||
|
parseHTML: () => null,
|
||||||
|
renderHTML: null,
|
||||||
|
addAttributes: () => ({}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMark<Options extends {}, Commands extends {}>(config: MarkExtensionSpec<Options, Commands>) {
|
||||||
|
const extend = <ExtendedOptions = Options, ExtendedCommands = Commands>(extendedConfig: Partial<MarkExtensionSpec<ExtendedOptions, ExtendedCommands>>) => {
|
||||||
|
return createMark({
|
||||||
|
...config,
|
||||||
|
...extendedConfig,
|
||||||
|
} as MarkExtensionSpec<ExtendedOptions, ExtendedCommands>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setOptions = (options?: Partial<Options>) => {
|
||||||
|
const { defaultOptions, ...rest } = config
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultMark,
|
||||||
|
...rest,
|
||||||
|
options: {
|
||||||
|
...defaultOptions,
|
||||||
|
...options,
|
||||||
|
} as Options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(setOptions, { config, extend })
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { NodeSpec, NodeType } from 'prosemirror-model'
|
|
||||||
import Extension, { ExtensionMethods } from './Extension'
|
|
||||||
import { Editor } from './Editor'
|
|
||||||
|
|
||||||
export interface NodeProps<Options> {
|
|
||||||
name: string
|
|
||||||
editor: Editor
|
|
||||||
options: Options
|
|
||||||
type: NodeType
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeMethods<Props, Options> extends ExtensionMethods<Props, Options> {
|
|
||||||
topNode: boolean
|
|
||||||
schema: (params: Omit<Props, 'type' | 'editor'>) => NodeSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Node<
|
|
||||||
Options = {},
|
|
||||||
Props = NodeProps<Options>,
|
|
||||||
Methods extends NodeMethods<Props, Options> = NodeMethods<Props, Options>,
|
|
||||||
> extends Extension<Options, Props, Methods> {
|
|
||||||
type = 'node'
|
|
||||||
|
|
||||||
public topNode(value: Methods['topNode'] = true) {
|
|
||||||
this.storeConfig('topNode', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public schema(value: Methods['schema']) {
|
|
||||||
this.storeConfig('schema', value, 'overwrite')
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
193
packages/core/src/NodeExtension.ts
Normal file
193
packages/core/src/NodeExtension.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import {
|
||||||
|
DOMOutputSpec, NodeSpec, Node, NodeType,
|
||||||
|
} from 'prosemirror-model'
|
||||||
|
import { Plugin } from 'prosemirror-state'
|
||||||
|
import { ExtensionSpec, defaultExtension } from './Extension'
|
||||||
|
import { Attributes, Overwrite } from './types'
|
||||||
|
import { Editor } from './Editor'
|
||||||
|
|
||||||
|
export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrite<ExtensionSpec<Options, Commands>, {
|
||||||
|
/**
|
||||||
|
* TopNode
|
||||||
|
*/
|
||||||
|
topNode?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content
|
||||||
|
*/
|
||||||
|
content?: NodeSpec['content'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks
|
||||||
|
*/
|
||||||
|
marks?: NodeSpec['marks'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group
|
||||||
|
*/
|
||||||
|
group?: NodeSpec['group'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline
|
||||||
|
*/
|
||||||
|
inline?: NodeSpec['inline'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atom
|
||||||
|
*/
|
||||||
|
atom?: NodeSpec['atom'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selectable
|
||||||
|
*/
|
||||||
|
selectable?: NodeSpec['selectable'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draggable
|
||||||
|
*/
|
||||||
|
draggable?: NodeSpec['draggable'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code
|
||||||
|
*/
|
||||||
|
code?: NodeSpec['code'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining
|
||||||
|
*/
|
||||||
|
defining?: NodeSpec['defining'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Isolating
|
||||||
|
*/
|
||||||
|
isolating?: NodeSpec['isolating'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse HTML
|
||||||
|
*/
|
||||||
|
parseHTML?: (
|
||||||
|
this: {
|
||||||
|
options: Options,
|
||||||
|
},
|
||||||
|
) => NodeSpec['parseDOM'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render HTML
|
||||||
|
*/
|
||||||
|
renderHTML?: ((
|
||||||
|
this: {
|
||||||
|
options: Options,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
node: Node,
|
||||||
|
attributes: { [key: string]: any },
|
||||||
|
}
|
||||||
|
) => DOMOutputSpec) | null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Attributes
|
||||||
|
*/
|
||||||
|
addAttributes?: (
|
||||||
|
this: {
|
||||||
|
options: Options,
|
||||||
|
},
|
||||||
|
) => Attributes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands
|
||||||
|
*/
|
||||||
|
addCommands?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: NodeType,
|
||||||
|
}) => Commands,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyboard shortcuts
|
||||||
|
*/
|
||||||
|
addKeyboardShortcuts?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: NodeType,
|
||||||
|
}) => {
|
||||||
|
[key: string]: any
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input rules
|
||||||
|
*/
|
||||||
|
addInputRules?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: NodeType,
|
||||||
|
}) => any[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paste rules
|
||||||
|
*/
|
||||||
|
addPasteRules?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: NodeType,
|
||||||
|
}) => any[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProseMirror plugins
|
||||||
|
*/
|
||||||
|
addProseMirrorPlugins?: (this: {
|
||||||
|
options: Options,
|
||||||
|
editor: Editor,
|
||||||
|
type: NodeType,
|
||||||
|
}) => Plugin[],
|
||||||
|
}> {}
|
||||||
|
|
||||||
|
export type NodeExtension = Required<Omit<NodeExtensionSpec, 'defaultOptions'> & {
|
||||||
|
type: string,
|
||||||
|
options: {
|
||||||
|
[key: string]: any
|
||||||
|
},
|
||||||
|
}>
|
||||||
|
|
||||||
|
const defaultNode: NodeExtension = {
|
||||||
|
...defaultExtension,
|
||||||
|
type: 'node',
|
||||||
|
name: 'node',
|
||||||
|
topNode: false,
|
||||||
|
content: null,
|
||||||
|
marks: null,
|
||||||
|
group: null,
|
||||||
|
inline: null,
|
||||||
|
atom: null,
|
||||||
|
selectable: null,
|
||||||
|
draggable: null,
|
||||||
|
code: null,
|
||||||
|
defining: null,
|
||||||
|
isolating: null,
|
||||||
|
parseHTML: () => null,
|
||||||
|
renderHTML: null,
|
||||||
|
addAttributes: () => ({}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNode<Options extends {}, Commands extends {}>(config: NodeExtensionSpec<Options, Commands>) {
|
||||||
|
const extend = <ExtendedOptions = Options, ExtendedCommands = Commands>(extendedConfig: Partial<NodeExtensionSpec<ExtendedOptions, ExtendedCommands>>) => {
|
||||||
|
return createNode({
|
||||||
|
...config,
|
||||||
|
...extendedConfig,
|
||||||
|
} as NodeExtensionSpec<ExtendedOptions, ExtendedCommands>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setOptions = (options?: Partial<Options>) => {
|
||||||
|
const { defaultOptions, ...rest } = config
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultNode,
|
||||||
|
...rest,
|
||||||
|
options: {
|
||||||
|
...defaultOptions,
|
||||||
|
...options,
|
||||||
|
} as Options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(setOptions, { config, extend })
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type BlurCommand = () => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
blur: BlurCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const blur: BlurCommand = () => ({ view }) => {
|
|
||||||
const element = view.dom as HTMLElement
|
|
||||||
|
|
||||||
element.blur()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type ClearContentCommand = (emitUpdate?: Boolean) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
clearContent: ClearContentCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clearContent: ClearContentCommand = (emitUpdate = false) => ({ commands }) => {
|
|
||||||
return commands.setContent('', emitUpdate)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { deleteSelection as originalDeleteSelection } from 'prosemirror-commands'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type DeleteSelectionCommand = () => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
deleteSelection: DeleteSelectionCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteSelection: DeleteSelectionCommand = () => ({ state, dispatch }) => {
|
|
||||||
return originalDeleteSelection(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
export { blur } from './blur'
|
|
||||||
export { clearContent } from './clearContent'
|
|
||||||
export { deleteSelection } from './deleteSelection'
|
|
||||||
export { focus } from './focus'
|
|
||||||
export { insertHTML } from './insertHTML'
|
|
||||||
export { insertText } from './insertText'
|
|
||||||
export { liftListItem } from './liftListItem'
|
|
||||||
export { removeMark } from './removeMark'
|
|
||||||
export { removeMarks } from './removeMarks'
|
|
||||||
export { scrollIntoView } from './scrollIntoView'
|
|
||||||
export { selectAll } from './selectAll'
|
|
||||||
export { selectParentNode } from './selectParentNode'
|
|
||||||
export { setBlockType } from './setBlockType'
|
|
||||||
export { setContent } from './setContent'
|
|
||||||
export { sinkListItem } from './sinkListItem'
|
|
||||||
export { splitListItem } from './splitListItem'
|
|
||||||
export { toggleBlockType } from './toggleBlockType'
|
|
||||||
export { toggleList } from './toggleList'
|
|
||||||
export { toggleMark } from './toggleMark'
|
|
||||||
export { updateMark } from './updateMark'
|
|
||||||
export { toggleWrap } from './toggleWrap'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type InsertTextCommand = (value: string) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
insertText: InsertTextCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const insertText: InsertTextCommand = value => ({ tr }) => {
|
|
||||||
tr.insertText(value)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { liftListItem as originalLiftListItem } from 'prosemirror-schema-list'
|
|
||||||
import { NodeType } from 'prosemirror-model'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import getNodeType from '../utils/getNodeType'
|
|
||||||
|
|
||||||
type LiftListItem = (typeOrName: string | NodeType) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
liftListItem: LiftListItem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const liftListItem: LiftListItem = typeOrName => ({ state, dispatch }) => {
|
|
||||||
const type = getNodeType(typeOrName, state.schema)
|
|
||||||
|
|
||||||
return originalLiftListItem(type)(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { MarkType } from 'prosemirror-model'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import getMarkType from '../utils/getMarkType'
|
|
||||||
import getMarkRange from '../utils/getMarkRange'
|
|
||||||
|
|
||||||
type RemoveMarkCommand = (typeOrName: string | MarkType) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
removeMark: RemoveMarkCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeMark: RemoveMarkCommand = typeOrName => ({ tr, state }) => {
|
|
||||||
const { selection } = tr
|
|
||||||
const type = getMarkType(typeOrName, state.schema)
|
|
||||||
let { from, to } = selection
|
|
||||||
const { $from, empty } = selection
|
|
||||||
|
|
||||||
if (empty) {
|
|
||||||
const range = getMarkRange($from, type)
|
|
||||||
|
|
||||||
if (range) {
|
|
||||||
from = range.from
|
|
||||||
to = range.to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.removeMark(from, to, type)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type RemoveMarksCommand = () => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
removeMarks: RemoveMarksCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeMarks: RemoveMarksCommand = () => ({ tr, state }) => {
|
|
||||||
const { selection } = tr
|
|
||||||
const { from, to, empty } = selection
|
|
||||||
|
|
||||||
if (empty) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
Object
|
|
||||||
.entries(state.schema.marks)
|
|
||||||
.forEach(([, mark]) => {
|
|
||||||
tr.removeMark(from, to, mark as any)
|
|
||||||
})
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type ScrollIntoViewCommand = () => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
scrollIntoView: ScrollIntoViewCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const scrollIntoView: ScrollIntoViewCommand = () => ({ tr }) => {
|
|
||||||
tr.scrollIntoView()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { selectAll as originalSelectAll } from 'prosemirror-commands'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type SelectAllCommand = () => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
selectAll: SelectAllCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const selectAll: SelectAllCommand = () => ({ state, dispatch }) => {
|
|
||||||
return originalSelectAll(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { selectParentNode as originalSelectParentNode } from 'prosemirror-commands'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type SelectParentNodeCommand = () => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
selectParentNode: SelectParentNodeCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const selectParentNode: SelectParentNodeCommand = () => ({ state, dispatch }) => {
|
|
||||||
return originalSelectParentNode(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { NodeType } from 'prosemirror-model'
|
|
||||||
import { setBlockType as originalSetBlockType } from 'prosemirror-commands'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import getNodeType from '../utils/getNodeType'
|
|
||||||
|
|
||||||
type SetBlockTypeCommand = (
|
|
||||||
typeOrName: string | NodeType,
|
|
||||||
attrs?: {},
|
|
||||||
) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
setBlockType: SetBlockTypeCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setBlockType: SetBlockTypeCommand = (typeOrName, attrs = {}) => ({ state, dispatch }) => {
|
|
||||||
const type = getNodeType(typeOrName, state.schema)
|
|
||||||
|
|
||||||
return originalSetBlockType(type, attrs)(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { TextSelection } from 'prosemirror-state'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
|
|
||||||
type SetContentCommand = (
|
|
||||||
content: string,
|
|
||||||
emitUpdate?: Boolean,
|
|
||||||
parseOptions?: any,
|
|
||||||
) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
setContent: SetContentCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setContent: SetContentCommand = (content = '', emitUpdate = false, parseOptions = {}) => ({ tr, editor }) => {
|
|
||||||
const { createDocument } = editor
|
|
||||||
const { doc } = tr
|
|
||||||
const document = createDocument(content, parseOptions)
|
|
||||||
const selection = TextSelection.create(doc, 0, doc.content.size)
|
|
||||||
|
|
||||||
tr.setSelection(selection)
|
|
||||||
.replaceSelectionWith(document, false)
|
|
||||||
.setMeta('preventUpdate', !emitUpdate)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { sinkListItem as originalSinkListItem } from 'prosemirror-schema-list'
|
|
||||||
import { NodeType } from 'prosemirror-model'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import getNodeType from '../utils/getNodeType'
|
|
||||||
|
|
||||||
type SinkListItem = (typeOrName: string | NodeType) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
sinkListItem: SinkListItem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sinkListItem: SinkListItem = typeOrName => ({ state, dispatch }) => {
|
|
||||||
const type = getNodeType(typeOrName, state.schema)
|
|
||||||
|
|
||||||
return originalSinkListItem(type)(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { splitListItem as originalSplitListItem } from 'prosemirror-schema-list'
|
|
||||||
import { NodeType } from 'prosemirror-model'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import getNodeType from '../utils/getNodeType'
|
|
||||||
|
|
||||||
type SplitListItem = (typeOrName: string | NodeType) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
splitListItem: SplitListItem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const splitListItem: SplitListItem = typeOrName => ({ state, dispatch }) => {
|
|
||||||
const type = getNodeType(typeOrName, state.schema)
|
|
||||||
|
|
||||||
return originalSplitListItem(type)(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { NodeType } from 'prosemirror-model'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import nodeIsActive from '../utils/nodeIsActive'
|
|
||||||
import getNodeType from '../utils/getNodeType'
|
|
||||||
|
|
||||||
type ToggleBlockTypeCommand = (
|
|
||||||
typeOrName: string | NodeType,
|
|
||||||
toggleType: string | NodeType,
|
|
||||||
attrs?: {}
|
|
||||||
) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
toggleBlockType: ToggleBlockTypeCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const toggleBlockType: ToggleBlockTypeCommand = (typeOrName, toggleTypeOrName, attrs = {}) => ({ state, commands }) => {
|
|
||||||
const type = getNodeType(typeOrName, state.schema)
|
|
||||||
const toggleType = getNodeType(toggleTypeOrName, state.schema)
|
|
||||||
const isActive = nodeIsActive(state, type, attrs)
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
return commands.setBlockType(toggleType)
|
|
||||||
}
|
|
||||||
|
|
||||||
return commands.setBlockType(type, attrs)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { wrapInList, liftListItem } from 'prosemirror-schema-list'
|
|
||||||
import { findParentNode } from 'prosemirror-utils'
|
|
||||||
import { Node, NodeType, Schema } from 'prosemirror-model'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import getNodeType from '../utils/getNodeType'
|
|
||||||
|
|
||||||
type ToggleListCommand = (
|
|
||||||
listType: string | NodeType,
|
|
||||||
itemType: string | NodeType,
|
|
||||||
) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
toggleList: ToggleListCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isList(node: Node, schema: Schema) {
|
|
||||||
return (node.type === schema.nodes.bullet_list
|
|
||||||
|| node.type === schema.nodes.ordered_list
|
|
||||||
|| node.type === schema.nodes.todo_list)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const toggleList: ToggleListCommand = (listTypeOrName, itemTypeOrName) => ({ tr, state, dispatch }) => {
|
|
||||||
const listType = getNodeType(listTypeOrName, state.schema)
|
|
||||||
const itemType = getNodeType(itemTypeOrName, state.schema)
|
|
||||||
const { schema, selection } = state
|
|
||||||
const { $from, $to } = selection
|
|
||||||
const range = $from.blockRange($to)
|
|
||||||
|
|
||||||
if (!range) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentList = findParentNode(node => isList(node, schema))(selection)
|
|
||||||
|
|
||||||
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
|
|
||||||
if (parentList.node.type === listType) {
|
|
||||||
return liftListItem(itemType)(state, dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isList(parentList.node, schema) && listType.validContent(parentList.node.content)) {
|
|
||||||
tr.setNodeMarkup(parentList.pos, listType)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapInList(listType)(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { wrapIn, lift } from 'prosemirror-commands'
|
|
||||||
import { NodeType } from 'prosemirror-model'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import nodeIsActive from '../utils/nodeIsActive'
|
|
||||||
import getNodeType from '../utils/getNodeType'
|
|
||||||
|
|
||||||
type ToggleWrapCommand = (typeOrName: string | NodeType, attrs?: {}) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
toggleWrap: ToggleWrapCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const toggleWrap: ToggleWrapCommand = (typeOrName, attrs) => ({
|
|
||||||
state, dispatch,
|
|
||||||
}) => {
|
|
||||||
const type = getNodeType(typeOrName, state.schema)
|
|
||||||
const isActive = nodeIsActive(state, type, attrs)
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
return lift(state, dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapIn(type, attrs)(state, dispatch)
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { MarkType } from 'prosemirror-model'
|
|
||||||
import { Command } from '../Editor'
|
|
||||||
import getMarkType from '../utils/getMarkType'
|
|
||||||
import getMarkRange from '../utils/getMarkRange'
|
|
||||||
|
|
||||||
type UpdateMarkCommand = (
|
|
||||||
typeOrName: string | MarkType,
|
|
||||||
attrs: {},
|
|
||||||
) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
updateMark: UpdateMarkCommand,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateMark: UpdateMarkCommand = (typeOrName, attrs = {}) => ({ tr, state }) => {
|
|
||||||
const { selection, doc } = tr
|
|
||||||
let { from, to } = selection
|
|
||||||
const { $from, empty } = selection
|
|
||||||
const type = getMarkType(typeOrName, state.schema)
|
|
||||||
|
|
||||||
if (empty) {
|
|
||||||
const range = getMarkRange($from, type)
|
|
||||||
|
|
||||||
if (range) {
|
|
||||||
from = range.from
|
|
||||||
to = range.to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasMark = doc.rangeHasMark(from, to, type)
|
|
||||||
|
|
||||||
if (hasMark) {
|
|
||||||
tr.removeMark(from, to, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.addMark(from, to, type.create(attrs))
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
22
packages/core/src/extensions/blur.ts
Normal file
22
packages/core/src/extensions/blur.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Command } from '../Editor'
|
||||||
|
import { createExtension } from '../Extension'
|
||||||
|
|
||||||
|
export const Blur = createExtension({
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
blur: (): Command => ({ view }) => {
|
||||||
|
const element = view.dom as HTMLElement
|
||||||
|
|
||||||
|
element.blur()
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
declare module '../Editor' {
|
||||||
|
interface AllExtensions {
|
||||||
|
Blur: typeof Blur,
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/core/src/extensions/clearContent.ts
Normal file
18
packages/core/src/extensions/clearContent.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Command } from '../Editor'
|
||||||
|
import { createExtension } from '../Extension'
|
||||||
|
|
||||||
|
export const ClearContent = createExtension({
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
clearContent: (emitUpdate: Boolean = false): Command => ({ commands }) => {
|
||||||
|
return commands.setContent('', emitUpdate)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
declare module '../Editor' {
|
||||||
|
interface AllExtensions {
|
||||||
|
ClearContent: typeof ClearContent,
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/core/src/extensions/deleteSelection.ts
Normal file
19
packages/core/src/extensions/deleteSelection.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { deleteSelection } from 'prosemirror-commands'
|
||||||
|
import { Command } from '../Editor'
|
||||||
|
import { createExtension } from '../Extension'
|
||||||
|
|
||||||
|
export const DeleteSelection = createExtension({
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
deleteSelection: (): Command => ({ state, dispatch }) => {
|
||||||
|
return deleteSelection(state, dispatch)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
declare module '../Editor' {
|
||||||
|
interface AllExtensions {
|
||||||
|
DeleteSelection: typeof DeleteSelection,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
import { TextSelection } from 'prosemirror-state'
|
import { TextSelection } from 'prosemirror-state'
|
||||||
import { Editor, Command } from '../Editor'
|
import { Editor, Command } from '../Editor'
|
||||||
|
import { createExtension } from '../Extension'
|
||||||
import minMax from '../utils/minMax'
|
import minMax from '../utils/minMax'
|
||||||
|
|
||||||
type Position = 'start' | 'end' | number | boolean | null
|
type Position = 'start' | 'end' | number | boolean | null
|
||||||
type FocusCommand = (position?: Position) => Command
|
|
||||||
|
|
||||||
declare module '../Editor' {
|
|
||||||
interface Commands {
|
|
||||||
focus: FocusCommand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResolvedSelection {
|
interface ResolvedSelection {
|
||||||
from: number,
|
from: number,
|
||||||
@@ -43,7 +37,10 @@ function resolveSelection(editor: Editor, position: Position = null): ResolvedSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const focus: FocusCommand = (position = null) => ({ editor, view, tr }) => {
|
export const Focus = createExtension({
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
focus: (position: Position = null): Command => ({ editor, view, tr }) => {
|
||||||
if ((view.hasFocus() && position === null) || position === false) {
|
if ((view.hasFocus() && position === null) || position === false) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -58,4 +55,13 @@ export const focus: FocusCommand = (position = null) => ({ editor, view, tr }) =
|
|||||||
view.focus()
|
view.focus()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
declare module '../Editor' {
|
||||||
|
interface AllExtensions {
|
||||||
|
Focus: typeof Focus,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
23
packages/core/src/extensions/index.ts
Normal file
23
packages/core/src/extensions/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export { Blur } from './blur'
|
||||||
|
export { ClearContent } from './clearContent'
|
||||||
|
export { DeleteSelection } from './deleteSelection'
|
||||||
|
export { Focus } from './focus'
|
||||||
|
export { InsertHTML } from './insertHTML'
|
||||||
|
export { InsertText } from './insertText'
|
||||||
|
export { LiftListItem } from './liftListItem'
|
||||||
|
export { RemoveMark } from './removeMark'
|
||||||
|
export { RemoveMarks } from './removeMarks'
|
||||||
|
export { ScrollIntoView } from './scrollIntoView'
|
||||||
|
export { SelectAll } from './selectAll'
|
||||||
|
export { SelectParentNode } from './selectParentNode'
|
||||||
|
export { SetNodeAttributes } from './setNodeAttributes'
|
||||||
|
export { SetBlockType } from './setBlockType'
|
||||||
|
export { SetContent } from './setContent'
|
||||||
|
export { SinkListItem } from './sinkListItem'
|
||||||
|
export { SplitBlock } from './splitBlock'
|
||||||
|
export { SplitListItem } from './splitListItem'
|
||||||
|
export { ToggleBlockType } from './toggleBlockType'
|
||||||
|
export { ToggleList } from './toggleList'
|
||||||
|
export { ToggleMark } from './toggleMark'
|
||||||
|
export { UpdateMark } from './updateMark'
|
||||||
|
export { ToggleWrap } from './toggleWrap'
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user