initial commit
56
.eslintrc.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ['html'],
|
||||||
|
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
globals: {
|
||||||
|
document: false,
|
||||||
|
navigator: false,
|
||||||
|
window: false,
|
||||||
|
Echo: false,
|
||||||
|
collect: false,
|
||||||
|
cy: false,
|
||||||
|
it: false,
|
||||||
|
describe: false,
|
||||||
|
FileReader: false,
|
||||||
|
atob: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: [
|
||||||
|
'airbnb-base',
|
||||||
|
],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
// required semicolons
|
||||||
|
'semi': 'off',
|
||||||
|
|
||||||
|
// indent
|
||||||
|
'no-tabs': 'off',
|
||||||
|
'indent': 'off',
|
||||||
|
|
||||||
|
// disable some import stuff
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'import/no-dynamic-require': 'off',
|
||||||
|
|
||||||
|
// disable for '__svg__'
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
|
||||||
|
'arrow-parens': ['error', 'as-needed'],
|
||||||
|
|
||||||
|
'padded-blocks': 'off',
|
||||||
|
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
|
||||||
|
'global-require': 'off',
|
||||||
|
}
|
||||||
|
}
|
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw*
|
441
examples/App.vue
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app" spellcheck="false">
|
||||||
|
|
||||||
|
<editor :editable="true" class="editor" :doc="data" :extensions="plugins" @update="onUpdate">
|
||||||
|
|
||||||
|
<div class="menububble" slot="menububble" slot-scope="{ marks, focus }">
|
||||||
|
<template v-if="marks">
|
||||||
|
<form class="menububble__form" v-if="linkMenuIsActive" @submit.prevent="setLinkUrl(linkUrl, marks.link, focus)">
|
||||||
|
<input class="menububble__input" type="text" v-model="linkUrl" placeholder="https://" ref="linkInput" @keydown.esc="hideLinkMenu"/>
|
||||||
|
<button class="menububble__button" @click="setLinkUrl(null, marks.link, focus)" type="button">
|
||||||
|
<icon name="remove" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<template v-else>
|
||||||
|
<button class="menububble__button" @click="marks.bold.command" :class="{ 'is-active': marks.bold.active() }">
|
||||||
|
<icon name="bold" />
|
||||||
|
</button>
|
||||||
|
<button class="menububble__button" @click="marks.italic.command" :class="{ 'is-active': marks.italic.active() }">
|
||||||
|
<icon name="italic" />
|
||||||
|
</button>
|
||||||
|
<button class="menububble__button" @click="marks.code.command" :class="{ 'is-active': marks.code.active() }">
|
||||||
|
<icon name="code" />
|
||||||
|
</button>
|
||||||
|
<button class="menububble__button" @click="showLinkMenu(marks.link)" :class="{ 'is-active': marks.link.active() }">
|
||||||
|
<icon name="link" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="menubar" :class="{ 'is-focused': focused }" slot="menubar" slot-scope="{ nodes, focused }">
|
||||||
|
<div v-if="nodes">
|
||||||
|
<button class="menubar__button" @click="nodes.paragraph.command" :class="{ 'is-active': nodes.paragraph.active() }">
|
||||||
|
<icon name="paragraph" />
|
||||||
|
</button>
|
||||||
|
<button class="menubar__button" @click="nodes.heading.command({ level: 1 })" :class="{ 'is-active': nodes.heading.active({ level: 1 }) }">
|
||||||
|
H1
|
||||||
|
</button>
|
||||||
|
<button class="menubar__button" @click="nodes.heading.command({ level: 2 })" :class="{ 'is-active': nodes.heading.active({ level: 2 }) }">
|
||||||
|
H2
|
||||||
|
</button>
|
||||||
|
<button class="menubar__button" @click="nodes.heading.command({ level: 3 })" :class="{ 'is-active': nodes.heading.active({ level: 3 }) }">
|
||||||
|
H3
|
||||||
|
</button>
|
||||||
|
<button class="menubar__button" @click="nodes.bullet_list.command" :class="{ 'is-active': nodes.bullet_list.active() }">
|
||||||
|
<icon name="ul" />
|
||||||
|
</button>
|
||||||
|
<button class="menubar__button" @click="nodes.ordered_list.command" :class="{ 'is-active': nodes.ordered_list.active() }">
|
||||||
|
<icon name="ol" />
|
||||||
|
</button>
|
||||||
|
<button class="menubar__button" @click="nodes.code_block.command" :class="{ 'is-active': nodes.code_block.active() }">
|
||||||
|
<icon name="code" />
|
||||||
|
</button>
|
||||||
|
<button class="menubar__button" @click="nodes.todo_list.command" :class="{ 'is-active': nodes.todo_list.active() }">
|
||||||
|
<icon name="checklist" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor__content" slot="content" slot-scope="props"></div>
|
||||||
|
</editor>
|
||||||
|
|
||||||
|
<pre>{{ data }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Icon from 'Components/Icon'
|
||||||
|
import { Editor } from 'vue-mirror'
|
||||||
|
import MentionPlugin from './plugins/Mention.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Editor,
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
linkUrl: null,
|
||||||
|
linkMenuIsActive: false,
|
||||||
|
plugins: [
|
||||||
|
new MentionPlugin(),
|
||||||
|
],
|
||||||
|
data: {
|
||||||
|
"type": "doc",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": {
|
||||||
|
"level": 1,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "A renderless rich-text editor for Vue.js "
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "This editor is based on "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "link",
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://prosemirror.net"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "Prosemirror"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": ", "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "italic"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "fully extendable "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "and renderless. There is a plugin system that lets you render each node as "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "bold"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "a vue component. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "Things like mentions "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "mention",
|
||||||
|
"attrs": {
|
||||||
|
"id": "Philipp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": " are also supported."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "code_block",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "body {\n display: none;\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "todo_list",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "todo_item",
|
||||||
|
"attrs": {
|
||||||
|
"done": true
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "There is always something to do"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "todo_item",
|
||||||
|
"attrs": {
|
||||||
|
"done": false
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "This list will never end"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bullet_list",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "list_item",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "A regular list"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "list_item",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "With regular items"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "It's amazing 👏"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showLinkMenu(type) {
|
||||||
|
this.linkUrl = type.attrs.href
|
||||||
|
this.linkMenuIsActive = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.linkInput.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
hideLinkMenu() {
|
||||||
|
this.linkUrl = null
|
||||||
|
this.linkMenuIsActive = false
|
||||||
|
},
|
||||||
|
setLinkUrl(url, type, focus) {
|
||||||
|
type.command({ href: url })
|
||||||
|
this.hideLinkMenu()
|
||||||
|
focus()
|
||||||
|
},
|
||||||
|
onUpdate(state) {
|
||||||
|
this.data = state.doc.toJSON()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "~variables";
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
position: relative;
|
||||||
|
max-width: 30rem;
|
||||||
|
margin: 0 auto 5rem auto;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
pre {
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: $color-black;
|
||||||
|
color: $color-white;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
|
||||||
|
code {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menububble {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
z-index: 20;
|
||||||
|
background: $color-black;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s, visibility 0.2s;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
display: inline-flex;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: $color-white;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba($color-white, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: rgba($color-white, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
font: inherit;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: $color-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menubar {
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
|
||||||
|
|
||||||
|
&.is-focused {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: visibility 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-flex;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: $color-black;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba($color-black, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: rgba($color-black, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention {
|
||||||
|
background: rgba($color-black, 0.1);
|
||||||
|
color: rgba($color-black, 0.6);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-type="todo_list"] {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li[data-type="todo_item"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-checkbox {
|
||||||
|
border: 2px solid $color-black;
|
||||||
|
height: 0.9em;
|
||||||
|
width: 0.9em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: 0.4s background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
li[data-done="true"] {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
li[data-done="true"] .todo-checkbox {
|
||||||
|
background-color: $color-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
li[data-done="false"] {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
74
examples/Components/Icon/index.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="icon" :class="[`icon--${name}`, `icon--${size}`, modifierClasses('icon'), { 'has-align-fix': fixAlign }]">
|
||||||
|
<svg class="icon__svg">
|
||||||
|
<use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="'#icon--' + name"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: {},
|
||||||
|
size: {
|
||||||
|
default: 'normal',
|
||||||
|
},
|
||||||
|
modifier: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
fixAlign: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.icon {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 0.8rem;
|
||||||
|
height: 0.8rem;
|
||||||
|
margin: 0 .3rem;
|
||||||
|
top: -.05rem;
|
||||||
|
fill: currentColor;
|
||||||
|
|
||||||
|
// &.has-align-fix {
|
||||||
|
// top: -.1rem;
|
||||||
|
// }
|
||||||
|
|
||||||
|
&__svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// svg sprite
|
||||||
|
body > svg,
|
||||||
|
.icon use > svg,
|
||||||
|
symbol {
|
||||||
|
path,
|
||||||
|
rect,
|
||||||
|
circle,
|
||||||
|
g {
|
||||||
|
fill: currentColor;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
*[d="M0 0h24v24H0z"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
1
examples/assets/images/icons/bold.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>text-bold</title><path d="M17.194,10.962A6.271,6.271,0,0,0,12.844.248H4.3a1.25,1.25,0,0,0,0,2.5H5.313a.25.25,0,0,1,.25.25V21a.25.25,0,0,1-.25.25H4.3a1.25,1.25,0,1,0,0,2.5h9.963a6.742,6.742,0,0,0,2.93-12.786Zm-4.35-8.214a3.762,3.762,0,0,1,0,7.523H8.313a.25.25,0,0,1-.25-.25V3a.25.25,0,0,1,.25-.25Zm1.42,18.5H8.313a.25.25,0,0,1-.25-.25V13.021a.25.25,0,0,1,.25-.25h4.531c.017,0,.033,0,.049,0l.013,0h1.358a4.239,4.239,0,0,1,0,8.477Z"/></svg>
|
After Width: | Height: | Size: 504 B |
1
examples/assets/images/icons/checklist.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>checklist-alternate</title><path d="M21,0H3A3,3,0,0,0,0,3V21a3,3,0,0,0,3,3H21a3,3,0,0,0,3-3V3A3,3,0,0,0,21,0Zm1,21a1,1,0,0,1-1,1H3a1,1,0,0,1-1-1V3A1,1,0,0,1,3,2H21a1,1,0,0,1,1,1Z"/><path d="M11.249,4.5a1.251,1.251,0,0,0-1.75.25L7.365,7.6l-.482-.481A1.25,1.25,0,0,0,5.116,8.883l1.5,1.5A1.262,1.262,0,0,0,8.5,10.249l3-4A1.25,1.25,0,0,0,11.249,4.5Z"/><path d="M11.249,13.5a1.251,1.251,0,0,0-1.75.25L7.365,16.6l-.482-.481a1.25,1.25,0,1,0-1.767,1.768l1.5,1.5A1.265,1.265,0,0,0,8.5,19.249l3-4A1.25,1.25,0,0,0,11.249,13.5Z"/><path d="M18.5,7.749H14a1.25,1.25,0,0,0,0,2.5h4.5a1.25,1.25,0,0,0,0-2.5Z"/><path d="M18.5,15.749H14a1.25,1.25,0,0,0,0,2.5h4.5a1.25,1.25,0,1,0,0-2.5Z"/></svg>
|
After Width: | Height: | Size: 742 B |
1
examples/assets/images/icons/code.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>angle-brackets</title><path d="M9.147,21.552a1.244,1.244,0,0,1-.895-.378L.84,13.561a2.257,2.257,0,0,1,0-3.125L8.252,2.823a1.25,1.25,0,0,1,1.791,1.744l-6.9,7.083a.5.5,0,0,0,0,.7l6.9,7.082a1.25,1.25,0,0,1-.9,2.122Z"/><path d="M14.854,21.552a1.25,1.25,0,0,1-.9-2.122l6.9-7.083a.5.5,0,0,0,0-.7l-6.9-7.082a1.25,1.25,0,0,1,1.791-1.744l7.411,7.612a2.257,2.257,0,0,1,0,3.125l-7.412,7.614A1.244,1.244,0,0,1,14.854,21.552Zm6.514-9.373h0Z"/></svg>
|
After Width: | Height: | Size: 503 B |
1
examples/assets/images/icons/italic.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>text-italic</title><path d="M22.5.248H14.863a1.25,1.25,0,0,0,0,2.5h1.086a.25.25,0,0,1,.211.384L4.78,21.017a.5.5,0,0,1-.422.231H1.5a1.25,1.25,0,0,0,0,2.5H9.137a1.25,1.25,0,0,0,0-2.5H8.051a.25.25,0,0,1-.211-.384L19.22,2.98a.5.5,0,0,1,.422-.232H22.5a1.25,1.25,0,0,0,0-2.5Z"/></svg>
|
After Width: | Height: | Size: 345 B |
1
examples/assets/images/icons/link.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>hyperlink-2</title><path d="M12.406,14.905a1,1,0,0,0-.543,1.307,1,1,0,0,1-.217,1.09L8.818,20.131a2,2,0,0,1-2.828,0L3.868,18.01a2,2,0,0,1,0-2.829L6.7,12.353a1.013,1.013,0,0,1,1.091-.217,1,1,0,0,0,.763-1.849,3.034,3.034,0,0,0-3.268.652L2.454,13.767a4.006,4.006,0,0,0,0,5.657l2.122,2.121a4,4,0,0,0,5.656,0l2.829-2.828a3.008,3.008,0,0,0,.651-3.27A1,1,0,0,0,12.406,14.905Z"/><path d="M7.757,16.241a1.011,1.011,0,0,0,1.414,0L16.95,8.463a1,1,0,0,0-1.414-1.414L7.757,14.827A1,1,0,0,0,7.757,16.241Z"/><path d="M21.546,4.574,19.425,2.453a4.006,4.006,0,0,0-5.657,0L10.939,5.281a3.006,3.006,0,0,0-.651,3.269,1,1,0,1,0,1.849-.764A1,1,0,0,1,12.354,6.7l2.828-2.828a2,2,0,0,1,2.829,0l2.121,2.121a2,2,0,0,1,0,2.829L17.3,11.645a1.015,1.015,0,0,1-1.091.217,1,1,0,0,0-.765,1.849,3.026,3.026,0,0,0,3.27-.651l2.828-2.828A4.007,4.007,0,0,0,21.546,4.574Z"/></svg>
|
After Width: | Height: | Size: 906 B |
1
examples/assets/images/icons/ol.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>list-numbers</title><path d="M7.75,4.5h15a1,1,0,0,0,0-2h-15a1,1,0,0,0,0,2Z"/><path d="M22.75,11h-15a1,1,0,1,0,0,2h15a1,1,0,0,0,0-2Z"/><path d="M22.75,19.5h-15a1,1,0,0,0,0,2h15a1,1,0,0,0,0-2Z"/><path d="M2.212,17.248A2,2,0,0,0,.279,18.732a.75.75,0,1,0,1.45.386.5.5,0,1,1,.483.63.75.75,0,1,0,0,1.5.5.5,0,1,1-.482.635.75.75,0,1,0-1.445.4,2,2,0,1,0,3.589-1.648.251.251,0,0,1,0-.278,2,2,0,0,0-1.662-3.111Z"/><path d="M4.25,10.748a2,2,0,0,0-4,0,.75.75,0,0,0,1.5,0,.5.5,0,0,1,1,0,1.031,1.031,0,0,1-.227.645L.414,14.029A.75.75,0,0,0,1,15.248H3.5a.75.75,0,0,0,0-1.5H3.081a.249.249,0,0,1-.195-.406L3.7,12.33A2.544,2.544,0,0,0,4.25,10.748Z"/><path d="M4,5.248H3.75A.25.25,0,0,1,3.5,5V1.623A1.377,1.377,0,0,0,2.125.248H1.5a.75.75,0,0,0,0,1.5h.25A.25.25,0,0,1,2,2V5a.25.25,0,0,1-.25.25H1.5a.75.75,0,0,0,0,1.5H4a.75.75,0,0,0,0-1.5Z"/></svg>
|
After Width: | Height: | Size: 893 B |
1
examples/assets/images/icons/paragraph.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>paragraph</title><path d="M22.5.248H7.228a6.977,6.977,0,1,0,0,13.954H9.546a.25.25,0,0,1,.25.25V22.5a1.25,1.25,0,0,0,2.5,0V3a.25.25,0,0,1,.25-.25h3.682a.25.25,0,0,1,.25.25V22.5a1.25,1.25,0,0,0,2.5,0V3a.249.249,0,0,1,.25-.25H22.5a1.25,1.25,0,0,0,0-2.5ZM9.8,11.452a.25.25,0,0,1-.25.25H7.228a4.477,4.477,0,1,1,0-8.954H9.546A.25.25,0,0,1,9.8,3Z"/></svg>
|
After Width: | Height: | Size: 415 B |
1
examples/assets/images/icons/remove.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>delete-2-alternate</title><path d="M20.485,3.511A12.01,12.01,0,1,0,24,12,12.009,12.009,0,0,0,20.485,3.511Zm-1.767,15.21A9.51,9.51,0,1,1,21.5,12,9.508,9.508,0,0,1,18.718,18.721Z"/><path d="M16.987,7.01a1.275,1.275,0,0,0-1.8,0l-3.177,3.177L8.829,7.01A1.277,1.277,0,0,0,7.024,8.816L10.2,11.993,7.024,15.171a1.277,1.277,0,0,0,1.805,1.806L12.005,13.8l3.177,3.178a1.277,1.277,0,0,0,1.8-1.806l-3.176-3.178,3.176-3.177A1.278,1.278,0,0,0,16.987,7.01Z"/></svg>
|
After Width: | Height: | Size: 517 B |
1
examples/assets/images/icons/ul.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>list-bullets</title><circle cx="2.5" cy="3.998" r="2.5"/><path d="M8.5,5H23a1,1,0,0,0,0-2H8.5a1,1,0,0,0,0,2Z"/><circle cx="2.5" cy="11.998" r="2.5"/><path d="M23,11H8.5a1,1,0,0,0,0,2H23a1,1,0,0,0,0-2Z"/><circle cx="2.5" cy="19.998" r="2.5"/><path d="M23,19H8.5a1,1,0,0,0,0,2H23a1,1,0,0,0,0-2Z"/></svg>
|
After Width: | Height: | Size: 368 B |
52
examples/assets/sass/main.scss
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
@import "~variables";
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, San Francisco, Roboto, Segoe UI, Helvetica Neue, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
color: $color-black;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10% 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
pre {
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
2
examples/assets/sass/variables.scss
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
$color-black: #222222;
|
||||||
|
$color-white: #ffffff;
|
80
examples/helpers/svg-sprite-loader.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
;(function(window, document) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var isSvg = document.createElementNS && document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ).createSVGRect;
|
||||||
|
var localStorage = 'localStorage' in window && window['localStorage'] !== null ? window.localStorage : false;
|
||||||
|
|
||||||
|
function svgSpriteInjector(source, opts) {
|
||||||
|
var file;
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
if (source instanceof Node) {
|
||||||
|
file = source.getAttribute('data-svg-sprite');
|
||||||
|
opts.revision = source.getAttribute('data-svg-sprite-revision') || opts.revision;
|
||||||
|
} else if (typeof source === 'string') {
|
||||||
|
file = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSvg) {
|
||||||
|
if (file) {
|
||||||
|
injector(file, opts);
|
||||||
|
} else {
|
||||||
|
console.error('svg-sprite-injector: undefined sprite filename!');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('svg-sprite-injector require ie9 or greater!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function injector(filepath, opts) {
|
||||||
|
var name = 'injectedSVGSprite' + filepath,
|
||||||
|
revision = opts.revision,
|
||||||
|
request;
|
||||||
|
|
||||||
|
// localStorage cache
|
||||||
|
if (revision !== undefined && localStorage && localStorage[name + 'Rev'] == revision) {
|
||||||
|
return injectOnLoad(localStorage[name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async load
|
||||||
|
request = new XMLHttpRequest();
|
||||||
|
request.open('GET', filepath, true);
|
||||||
|
request.onreadystatechange = function (e) {
|
||||||
|
var data;
|
||||||
|
|
||||||
|
if (request.readyState === 4 && request.status >= 200 && request.status < 400) {
|
||||||
|
injectOnLoad(data = request.responseText);
|
||||||
|
if (revision !== undefined && localStorage) {
|
||||||
|
localStorage[name] = data;
|
||||||
|
localStorage[name + 'Rev'] = revision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectOnLoad(data) {
|
||||||
|
if (data) {
|
||||||
|
if (document.body) {
|
||||||
|
injectData(data);
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', injectData.bind(null, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectData(data) {
|
||||||
|
var body = document.body;
|
||||||
|
body.insertAdjacentHTML('afterbegin', data);
|
||||||
|
if (body.firstChild.tagName === 'svg') {
|
||||||
|
body.firstChild.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof exports === 'object') {
|
||||||
|
module.exports = svgSpriteInjector;
|
||||||
|
} else {
|
||||||
|
window.svgSpriteInjector = svgSpriteInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
} (window, document));
|
13
examples/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Editor</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
examples/main.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'babel-polyfill'
|
||||||
|
import Vue from 'vue'
|
||||||
|
import svgSpriteLoader from 'helpers/svg-sprite-loader'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const __svg__ = { path: './assets/images/icons/*.svg', name: 'assets/images/[hash].sprite.svg' }
|
||||||
|
svgSpriteLoader(__svg__.filename)
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
Vue.mixin({
|
||||||
|
methods: {
|
||||||
|
modifierClasses(base, modifier = this.modifier) {
|
||||||
|
const classList = [modifier].flatten()
|
||||||
|
|
||||||
|
if (classList.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${base}--${classList.join(` ${base}--`)}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app')
|
30
examples/plugins/Mention.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
|
||||||
|
export default class MentionNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'mention'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
attrs: {
|
||||||
|
id: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group: 'inline',
|
||||||
|
inline: true,
|
||||||
|
draggable: true,
|
||||||
|
toDOM: node => [
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
dataId: node.attrs.id,
|
||||||
|
class: 'mention',
|
||||||
|
},
|
||||||
|
`@${node.attrs.id}`,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
90
package.json
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"name": "tiptap",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "babel-node webpack/server.js --env=development",
|
||||||
|
"build": ".babel-node webpack/build.js --env=production"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"stage-2",
|
||||||
|
"env"
|
||||||
|
],
|
||||||
|
"comments": false,
|
||||||
|
"compact": true
|
||||||
|
},
|
||||||
|
"postcss": {
|
||||||
|
"plugins": {
|
||||||
|
"autoprefixer": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"ie >= 9"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^9.1.1",
|
||||||
|
"babel-cli": "^6.24.1",
|
||||||
|
"babel-core": "^6.26.3",
|
||||||
|
"babel-eslint": "^8.2.5",
|
||||||
|
"babel-loader": "^7.1.5",
|
||||||
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
|
"babel-polyfill": "^6.26.0",
|
||||||
|
"babel-preset-env": "^1.7.0",
|
||||||
|
"babel-preset-stage-2": "^6.24.1",
|
||||||
|
"babel-runtime": "^6.25.0",
|
||||||
|
"browser-sync": "^2.24.5",
|
||||||
|
"copy-webpack-plugin": "^4.5.2",
|
||||||
|
"css-loader": "^1.0.0",
|
||||||
|
"eslint": "^5.0.1",
|
||||||
|
"eslint-config-airbnb-base": "^13.0.0",
|
||||||
|
"eslint-plugin-html": "^4.0.5",
|
||||||
|
"eslint-plugin-import": "^2.13.0",
|
||||||
|
"eslint-plugin-vue": "4.7.1",
|
||||||
|
"file-loader": "^1.1.11",
|
||||||
|
"glob": "^7.1.2",
|
||||||
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"http-proxy-middleware": "^0.18.0",
|
||||||
|
"http-server": "^0.11.1",
|
||||||
|
"imagemin-webpack-plugin": "^2.1.5",
|
||||||
|
"mini-css-extract-plugin": "^0.4.1",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"modernizr-loader": "1.0.1",
|
||||||
|
"node-sass": "^4.9.1",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^5.0.0",
|
||||||
|
"ora": "^3.0.0",
|
||||||
|
"postcss": "^7.0.2",
|
||||||
|
"postcss-loader": "^3.0.0",
|
||||||
|
"postcss-scss": "^2.0.0",
|
||||||
|
"sass-loader": "^7.0.3",
|
||||||
|
"style-loader": "^0.22.1",
|
||||||
|
"vue-loader": "^15.2.4",
|
||||||
|
"vue-style-loader": "^4.1.0",
|
||||||
|
"vue-template-compiler": "^2.5.13",
|
||||||
|
"webpack": "^4.15.1",
|
||||||
|
"webpack-dev-middleware": "^3.1.3",
|
||||||
|
"webpack-hot-middleware": "^2.22.2",
|
||||||
|
"webpack-manifest-plugin": "^2.0.3",
|
||||||
|
"webpack-svgstore-plugin": "^4.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-commands": "^1.0.7",
|
||||||
|
"prosemirror-dropcursor": "^1.0.1",
|
||||||
|
"prosemirror-gapcursor": "^1.0.2",
|
||||||
|
"prosemirror-history": "^1.0.2",
|
||||||
|
"prosemirror-inputrules": "^1.0.1",
|
||||||
|
"prosemirror-keymap": "^1.0.1",
|
||||||
|
"prosemirror-markdown": "^1.1.1",
|
||||||
|
"prosemirror-model": "^1.5.0",
|
||||||
|
"prosemirror-schema-basic": "^1.0.0",
|
||||||
|
"prosemirror-schema-list": "^1.0.1",
|
||||||
|
"prosemirror-state": "^1.2.1",
|
||||||
|
"prosemirror-tables": "^0.7.6",
|
||||||
|
"prosemirror-utils": "^0.6.5",
|
||||||
|
"prosemirror-view": "^1.4.3",
|
||||||
|
"vue": "^2.5.17"
|
||||||
|
}
|
||||||
|
}
|
215
src/components/editor.vue
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
<script>
|
||||||
|
import { EditorState, Plugin } from 'prosemirror-state'
|
||||||
|
import { EditorView } from 'prosemirror-view'
|
||||||
|
import { Schema, DOMParser } from 'prosemirror-model'
|
||||||
|
import { dropCursor } from 'prosemirror-dropcursor'
|
||||||
|
import { gapCursor } from 'prosemirror-gapcursor'
|
||||||
|
import { history } from 'prosemirror-history'
|
||||||
|
import { keymap } from 'prosemirror-keymap'
|
||||||
|
import { baseKeymap } from 'prosemirror-commands'
|
||||||
|
import { inputRules } from 'prosemirror-inputrules'
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildMenuActions,
|
||||||
|
PluginManager,
|
||||||
|
initNodeViews,
|
||||||
|
menuBubble,
|
||||||
|
builtInKeymap,
|
||||||
|
} from '../utils'
|
||||||
|
import builtInNodes from '../nodes'
|
||||||
|
import builtInMarks from '../marks'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
props: {
|
||||||
|
doc: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const plugins = new PluginManager([
|
||||||
|
...builtInNodes,
|
||||||
|
...builtInMarks,
|
||||||
|
...this.extensions,
|
||||||
|
])
|
||||||
|
const { nodes, marks, views } = plugins
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: null,
|
||||||
|
view: null,
|
||||||
|
pluginplugins: [],
|
||||||
|
plugins,
|
||||||
|
schema: null,
|
||||||
|
nodes,
|
||||||
|
marks,
|
||||||
|
views,
|
||||||
|
keymaps: [],
|
||||||
|
commands: {},
|
||||||
|
menuActions: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render(createElement) {
|
||||||
|
const slots = []
|
||||||
|
|
||||||
|
Object
|
||||||
|
.entries(this.$scopedSlots)
|
||||||
|
.forEach(([name, slot]) => {
|
||||||
|
if (name === 'content') {
|
||||||
|
this.contentNode = slot({})
|
||||||
|
slots.push(this.contentNode)
|
||||||
|
} else if (name === 'menubar') {
|
||||||
|
this.menubarNode = slot({
|
||||||
|
nodes: this.menuActions ? this.menuActions.nodes : null,
|
||||||
|
marks: this.menuActions ? this.menuActions.marks : null,
|
||||||
|
focused: this.view ? this.view.focused : false,
|
||||||
|
focus: () => this.view.focus(),
|
||||||
|
})
|
||||||
|
slots.push(this.menubarNode)
|
||||||
|
} else if (name === 'menububble') {
|
||||||
|
this.menububbleNode = slot({
|
||||||
|
nodes: this.menuActions ? this.menuActions.nodes : null,
|
||||||
|
marks: this.menuActions ? this.menuActions.marks : null,
|
||||||
|
focused: this.view ? this.view.focused : false,
|
||||||
|
focus: () => this.view.focus(),
|
||||||
|
})
|
||||||
|
slots.push(this.menububbleNode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return createElement('div', {
|
||||||
|
class: 'vue-editor',
|
||||||
|
}, slots)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
initEditor() {
|
||||||
|
this.schema = this.createSchema()
|
||||||
|
this.pluginplugins = this.createPlugins()
|
||||||
|
this.keymaps = this.createKeymaps()
|
||||||
|
this.inputRules = this.createInputRules()
|
||||||
|
this.state = this.createState()
|
||||||
|
this.clearSlot()
|
||||||
|
this.view = this.createView()
|
||||||
|
this.commands = this.createCommands()
|
||||||
|
this.updateMenuActions()
|
||||||
|
},
|
||||||
|
|
||||||
|
createSchema() {
|
||||||
|
return new Schema({
|
||||||
|
nodes: this.nodes,
|
||||||
|
marks: this.marks,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
createPlugins() {
|
||||||
|
return this.plugins.pluginplugins
|
||||||
|
},
|
||||||
|
|
||||||
|
createKeymaps() {
|
||||||
|
return this.plugins.keymaps({
|
||||||
|
schema: this.schema,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
createInputRules() {
|
||||||
|
return this.plugins.inputRules({
|
||||||
|
schema: this.schema,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
createCommands() {
|
||||||
|
return this.plugins.commands({
|
||||||
|
schema: this.schema,
|
||||||
|
view: this.view,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
createState() {
|
||||||
|
return EditorState.create({
|
||||||
|
schema: this.schema,
|
||||||
|
doc: this.getDocument(),
|
||||||
|
plugins: [
|
||||||
|
...this.pluginplugins,
|
||||||
|
...this.getPlugins(),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getDocument() {
|
||||||
|
if (this.doc) {
|
||||||
|
return this.schema.nodeFromJSON(this.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DOMParser.fromSchema(this.schema).parse(this.contentNode.elm)
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSlot() {
|
||||||
|
this.contentNode.elm.innerHTML = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlugins() {
|
||||||
|
return [
|
||||||
|
menuBubble(this.menububbleNode),
|
||||||
|
inputRules({
|
||||||
|
rules: this.inputRules,
|
||||||
|
}),
|
||||||
|
...this.keymaps,
|
||||||
|
keymap(builtInKeymap),
|
||||||
|
keymap(baseKeymap),
|
||||||
|
dropCursor(),
|
||||||
|
gapCursor(),
|
||||||
|
history(),
|
||||||
|
new Plugin({
|
||||||
|
props: {
|
||||||
|
editable: () => this.editable,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
createView() {
|
||||||
|
return new EditorView(this.contentNode.elm, {
|
||||||
|
state: this.state,
|
||||||
|
dispatchTransaction: this.dispatchTransaction,
|
||||||
|
nodeViews: initNodeViews({
|
||||||
|
nodes: this.views,
|
||||||
|
editable: this.editable,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMenuActions() {
|
||||||
|
this.menuActions = buildMenuActions({
|
||||||
|
schema: this.schema,
|
||||||
|
state: this.view.state,
|
||||||
|
commands: this.commands,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchTransaction(transaction) {
|
||||||
|
this.state = this.state.apply(transaction)
|
||||||
|
this.view.updateState(this.state)
|
||||||
|
this.$emit('update', this.state)
|
||||||
|
this.updateMenuActions()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.initEditor()
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
89
src/helpers/index.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
chainCommands,
|
||||||
|
deleteSelection,
|
||||||
|
joinBackward,
|
||||||
|
selectNodeBackward,
|
||||||
|
joinForward,
|
||||||
|
selectNodeForward,
|
||||||
|
joinUp,
|
||||||
|
joinDown,
|
||||||
|
lift,
|
||||||
|
newlineInCode,
|
||||||
|
exitCode,
|
||||||
|
createParagraphNear,
|
||||||
|
liftEmptyBlock,
|
||||||
|
splitBlock,
|
||||||
|
splitBlockKeepMarks,
|
||||||
|
selectParentNode,
|
||||||
|
selectAll,
|
||||||
|
wrapIn,
|
||||||
|
setBlockType,
|
||||||
|
toggleMark,
|
||||||
|
autoJoin,
|
||||||
|
baseKeymap,
|
||||||
|
pcBaseKeymap,
|
||||||
|
macBaseKeymap,
|
||||||
|
} from 'prosemirror-commands'
|
||||||
|
|
||||||
|
import {
|
||||||
|
addListNodes,
|
||||||
|
wrapInList,
|
||||||
|
splitListItem,
|
||||||
|
liftListItem,
|
||||||
|
sinkListItem,
|
||||||
|
} from 'prosemirror-schema-list'
|
||||||
|
|
||||||
|
import {
|
||||||
|
wrappingInputRule,
|
||||||
|
textblockTypeInputRule,
|
||||||
|
} from 'prosemirror-inputrules'
|
||||||
|
|
||||||
|
import removeMark from './removeMark'
|
||||||
|
import toggleBlockType from './toggleBlockType'
|
||||||
|
import toggleList from './toggleList'
|
||||||
|
import updateMark from './updateMark'
|
||||||
|
|
||||||
|
export {
|
||||||
|
// prosemirror-commands
|
||||||
|
chainCommands,
|
||||||
|
deleteSelection,
|
||||||
|
joinBackward,
|
||||||
|
selectNodeBackward,
|
||||||
|
joinForward,
|
||||||
|
selectNodeForward,
|
||||||
|
joinUp,
|
||||||
|
joinDown,
|
||||||
|
lift,
|
||||||
|
newlineInCode,
|
||||||
|
exitCode,
|
||||||
|
createParagraphNear,
|
||||||
|
liftEmptyBlock,
|
||||||
|
splitBlock,
|
||||||
|
splitBlockKeepMarks,
|
||||||
|
selectParentNode,
|
||||||
|
selectAll,
|
||||||
|
wrapIn,
|
||||||
|
setBlockType,
|
||||||
|
toggleMark,
|
||||||
|
autoJoin,
|
||||||
|
baseKeymap,
|
||||||
|
pcBaseKeymap,
|
||||||
|
macBaseKeymap,
|
||||||
|
|
||||||
|
// prosemirror-schema-list
|
||||||
|
addListNodes,
|
||||||
|
wrapInList,
|
||||||
|
splitListItem,
|
||||||
|
liftListItem,
|
||||||
|
sinkListItem,
|
||||||
|
|
||||||
|
// prosemirror-inputrules
|
||||||
|
wrappingInputRule,
|
||||||
|
textblockTypeInputRule,
|
||||||
|
|
||||||
|
// custom
|
||||||
|
removeMark,
|
||||||
|
toggleBlockType,
|
||||||
|
toggleList,
|
||||||
|
updateMark,
|
||||||
|
}
|
6
src/helpers/removeMark.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default function (type) {
|
||||||
|
return (state, dispatch) => {
|
||||||
|
const { from, to } = state.selection
|
||||||
|
return dispatch(state.tr.removeMark(from, to, type))
|
||||||
|
}
|
||||||
|
}
|
14
src/helpers/toggleBlockType.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { setBlockType } from 'prosemirror-commands'
|
||||||
|
import { nodeIsActive } from 'vue-mirror/utils'
|
||||||
|
|
||||||
|
export default function (type, toggletype, attrs = {}) {
|
||||||
|
return (state, dispatch, view) => {
|
||||||
|
const isActive = nodeIsActive(state, type, attrs)
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
return setBlockType(toggletype)(state, dispatch, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
return setBlockType(type, attrs)(state, dispatch, view)
|
||||||
|
}
|
||||||
|
}
|
49
src/helpers/toggleList.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { nodeIsActive } from 'vue-mirror/utils'
|
||||||
|
import { wrapInList, liftListItem } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default function toggleList(type, itemType) {
|
||||||
|
return (state, dispatch, view) => {
|
||||||
|
const isActive = nodeIsActive(state, type)
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
return liftListItem(itemType)(state, dispatch, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapInList(type)(state, dispatch, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://discuss.prosemirror.net/t/list-type-toggle/948
|
||||||
|
|
||||||
|
// import { wrapInList, liftListItem } from 'prosemirror-schema-list'
|
||||||
|
|
||||||
|
// function isList(node, schema) {
|
||||||
|
// return (node.type === schema.nodes.bullet_list || node.type === schema.nodes.ordered_list)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default function toggleList(listType, schema) {
|
||||||
|
// const lift = liftListItem(schema.nodes.list_item)
|
||||||
|
// const wrap = wrapInList(listType)
|
||||||
|
|
||||||
|
// return (state, dispatch) => {
|
||||||
|
// const { $from, $to } = state.selection
|
||||||
|
// const range = $from.blockRange($to)
|
||||||
|
// if (!range) {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (range.depth >= 2 && $from.node(range.depth - 1).type === listType) {
|
||||||
|
// return lift(state, dispatch)
|
||||||
|
// } else if (range.depth >= 2 && isList($from.node(range.depth - 1), schema)) {
|
||||||
|
// const tr = state.tr
|
||||||
|
// const node = $from.before(range.depth - 1)
|
||||||
|
// console.log({node})
|
||||||
|
// // TODO: how do I pass the node above to `setNodeType`?
|
||||||
|
// // tr.setNodeType(range.start, listType);
|
||||||
|
// if (dispatch) dispatch(tr)
|
||||||
|
// return false
|
||||||
|
// } else {
|
||||||
|
// return wrap(state, dispatch)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
6
src/helpers/updateMark.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default function (type, attrs) {
|
||||||
|
return (state, dispatch) => {
|
||||||
|
const { from, to } = state.selection
|
||||||
|
return dispatch(state.tr.addMark(from, to, type.create(attrs)))
|
||||||
|
}
|
||||||
|
}
|
3
src/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Editor from './components/editor.vue'
|
||||||
|
|
||||||
|
export { Editor }
|
39
src/marks/Bold.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Mark } from 'vue-mirror/utils'
|
||||||
|
import { toggleMark } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class BoldMark extends Mark {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'bold'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
parseDOM: [
|
||||||
|
{
|
||||||
|
tag: 'strong',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'b',
|
||||||
|
getAttrs: node => node.style.fontWeight != 'normal' && null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
style: 'font-weight',
|
||||||
|
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
toDOM: () => ['strong', 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
'Mod-b': toggleMark(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type }) {
|
||||||
|
return toggleMark(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
29
src/marks/Code.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Mark } from 'vue-mirror/utils'
|
||||||
|
import { toggleMark } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class CodeMark extends Mark {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'code'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'code' },
|
||||||
|
],
|
||||||
|
toDOM: () => ['code', 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
'Mod-`': toggleMark(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type }) {
|
||||||
|
return toggleMark(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
31
src/marks/Italic.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Mark } from 'vue-mirror/utils'
|
||||||
|
import { toggleMark } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class ItalicMark extends Mark {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'italic'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'i' },
|
||||||
|
{ tag: 'em' },
|
||||||
|
{ style: 'font-style=italic' },
|
||||||
|
],
|
||||||
|
toDOM: () => ['em', 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
'Mod-i': toggleMark(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type }) {
|
||||||
|
return toggleMark(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
55
src/marks/Link.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Mark } from 'vue-mirror/utils'
|
||||||
|
import { updateMark, removeMark } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class LinkMark extends Mark {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'link'
|
||||||
|
}
|
||||||
|
|
||||||
|
get view() {
|
||||||
|
return {
|
||||||
|
props: ['node'],
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
console.log('click on link')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<a :href="node.attrs.href" rel="noopener noreferrer nofollow" ref="content" @click="onClick"></a>
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
attrs: {
|
||||||
|
href: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inclusive: false,
|
||||||
|
parseDOM: [
|
||||||
|
{
|
||||||
|
tag: 'a[href]',
|
||||||
|
getAttrs: dom => ({
|
||||||
|
href: dom.getAttribute('href'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
toDOM: node => ['a', {
|
||||||
|
...node.attrs,
|
||||||
|
rel: 'noopener noreferrer nofollow',
|
||||||
|
}, 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type, attrs }) {
|
||||||
|
if (attrs.href) {
|
||||||
|
return updateMark(type, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return removeMark(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
11
src/marks/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Code from './Code'
|
||||||
|
import Italic from './Italic'
|
||||||
|
import Link from './Link'
|
||||||
|
import Bold from './Bold'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
new Code(),
|
||||||
|
new Italic(),
|
||||||
|
new Link(),
|
||||||
|
new Bold(),
|
||||||
|
]
|
39
src/nodes/Blockquote.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { wrappingInputRule, setBlockType, wrapIn } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class BlockquoteNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'blockquote'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
content: 'block+',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
draggable: false,
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'blockquote' },
|
||||||
|
],
|
||||||
|
toDOM: () => ['blockquote', 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type }) {
|
||||||
|
return setBlockType(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
'Ctrl->': wrapIn(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules({ type }) {
|
||||||
|
return [
|
||||||
|
wrappingInputRule(/^\s*>\s$/, type),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
src/nodes/BulletList.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { wrappingInputRule, wrapInList, toggleList } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class BulletNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'bullet_list'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
content: 'list_item+',
|
||||||
|
group: 'block',
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'ul' },
|
||||||
|
],
|
||||||
|
toDOM: () => ['ul', 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type, schema }) {
|
||||||
|
return toggleList(type, schema.nodes.list_item)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
'Shift-Ctrl-8': wrapInList(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules({ type }) {
|
||||||
|
return [
|
||||||
|
wrappingInputRule(/^\s*([-+*])\s$/, type),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
41
src/nodes/CodeBlock.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class CodeBlockNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'code_block'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
content: 'text*',
|
||||||
|
marks: '',
|
||||||
|
group: 'block',
|
||||||
|
code: true,
|
||||||
|
defining: true,
|
||||||
|
draggable: false,
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'pre', preserveWhitespace: 'full' },
|
||||||
|
],
|
||||||
|
toDOM: () => ['pre', ['code', 0]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type, schema }) {
|
||||||
|
return toggleBlockType(type, schema.nodes.paragraph)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
'Shift-Ctrl-\\': setBlockType(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules({ type }) {
|
||||||
|
return [
|
||||||
|
textblockTypeInputRule(/^```$/, type),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
src/nodes/Doc.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
|
||||||
|
export default class DocNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'doc'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
content: 'block+',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
33
src/nodes/HardBreak.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { chainCommands, exitCode } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class HardBreakNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'hard_break'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
selectable: false,
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'br' },
|
||||||
|
],
|
||||||
|
toDOM: () => ['br'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
const command = chainCommands(exitCode, (state, dispatch) => {
|
||||||
|
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'Mod-Enter': command,
|
||||||
|
'Shift-Enter': command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
59
src/nodes/Heading.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { setBlockType, textblockTypeInputRule, toggleBlockType } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class HeadingNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'heading'
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultOptions() {
|
||||||
|
return {
|
||||||
|
maxLevel: 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get levels() {
|
||||||
|
return Array.from(new Array(this.options.maxLevel), (value, index) => index + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
attrs: {
|
||||||
|
level: {
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: 'inline*',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
draggable: false,
|
||||||
|
parseDOM: this.levels.map(level => ({ tag: `h${level}`, attrs: { level } })),
|
||||||
|
toDOM: node => [`h${node.attrs.level}`, 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type, schema, attrs }) {
|
||||||
|
return toggleBlockType(type, schema.nodes.paragraph, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return this.levels.reduce((items, level) => ({
|
||||||
|
...items,
|
||||||
|
...{
|
||||||
|
[`Shift-Ctrl-${level}`]: setBlockType(type, { level }),
|
||||||
|
},
|
||||||
|
}), {})
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules({ type }) {
|
||||||
|
return [
|
||||||
|
textblockTypeInputRule(
|
||||||
|
new RegExp(`^(#{1,${this.options.maxLevel}})\\s$`),
|
||||||
|
type,
|
||||||
|
match => ({ level: match[1].length }),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
31
src/nodes/ListItem.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { splitListItem, liftListItem, sinkListItem } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class OrderedListNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'list_item'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
content: 'paragraph block*',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
draggable: false,
|
||||||
|
parseDOM: [
|
||||||
|
{ tag: 'li' },
|
||||||
|
],
|
||||||
|
toDOM: () => ['li', 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
Enter: splitListItem(type),
|
||||||
|
Tab: sinkListItem(type),
|
||||||
|
'Shift-Tab': liftListItem(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
52
src/nodes/OrderedList.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { wrappingInputRule, wrapInList, toggleList } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class OrderedListNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'ordered_list'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
attrs: {
|
||||||
|
order: {
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: 'list_item+',
|
||||||
|
group: 'block',
|
||||||
|
parseDOM: [
|
||||||
|
{
|
||||||
|
tag: 'ol',
|
||||||
|
getAttrs: dom => ({
|
||||||
|
order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
toDOM: node => (node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type, schema }) {
|
||||||
|
return toggleList(type, schema.nodes.list_item)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
'Shift-Ctrl-9': wrapInList(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules({ type }) {
|
||||||
|
return [
|
||||||
|
wrappingInputRule(
|
||||||
|
/^(\d+)\.\s$/,
|
||||||
|
type,
|
||||||
|
match => ({ order: +match[1] }),
|
||||||
|
(match, node) => node.childCount + node.attrs.order === +match[1],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
src/nodes/Paragraph.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { setBlockType } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class ParagraphNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'paragraph'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
content: 'inline*',
|
||||||
|
group: 'block',
|
||||||
|
draggable: false,
|
||||||
|
parseDOM: [{
|
||||||
|
tag: 'p',
|
||||||
|
}],
|
||||||
|
toDOM: () => ['p', 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type }) {
|
||||||
|
return setBlockType(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
'Shift-Ctrl-0': setBlockType(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
src/nodes/Text.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
|
||||||
|
export default class TextNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
group: 'inline',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
69
src/nodes/TodoItem.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { splitListItem, liftListItem } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class TodoItemNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'todo_item'
|
||||||
|
}
|
||||||
|
|
||||||
|
get view() {
|
||||||
|
return {
|
||||||
|
props: ['node', 'updateAttrs', 'editable'],
|
||||||
|
methods: {
|
||||||
|
onChange() {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.updateAttrs({
|
||||||
|
done: !this.node.attrs.done,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<li data-type="todo_item" :data-done="node.attrs.done.toString()">
|
||||||
|
<span class="todo-checkbox" contenteditable="false" @click="onChange"></span>
|
||||||
|
<div class="todo-content" ref="content" :contenteditable="editable.toString()"></div>
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
attrs: {
|
||||||
|
done: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
draggable: false,
|
||||||
|
content: 'paragraph',
|
||||||
|
toDOM(node) {
|
||||||
|
const { done } = node.attrs
|
||||||
|
|
||||||
|
return ['li', {
|
||||||
|
'data-type': 'todo_item',
|
||||||
|
'data-done': done.toString(),
|
||||||
|
},
|
||||||
|
['span', { class: 'todo-checkbox', contenteditable: 'false' }],
|
||||||
|
['div', { class: 'todo-content' }, 0],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
parseDOM: [{
|
||||||
|
priority: 51,
|
||||||
|
tag: '[data-type="todo_item"]',
|
||||||
|
getAttrs: dom => ({
|
||||||
|
done: dom.getAttribute('data-done') === 'true',
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys({ type }) {
|
||||||
|
return {
|
||||||
|
Enter: splitListItem(type),
|
||||||
|
'Shift-Tab': liftListItem(type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
src/nodes/TodoList.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Node } from 'vue-mirror/utils'
|
||||||
|
import { wrapInList, wrappingInputRule } from 'vue-mirror/helpers'
|
||||||
|
|
||||||
|
export default class BulletNode extends Node {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'todo_list'
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return {
|
||||||
|
group: 'block',
|
||||||
|
content: 'todo_item+',
|
||||||
|
toDOM: () => ['ul', { 'data-type': 'todo_list' }, 0],
|
||||||
|
parseDOM: [{
|
||||||
|
priority: 51,
|
||||||
|
tag: '[data-type="todo_list"]',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command({ type }) {
|
||||||
|
return wrapInList(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules({ type }) {
|
||||||
|
return [
|
||||||
|
wrappingInputRule(/^\s*(\[ \])\s$/, type),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
29
src/nodes/index.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import Blockquote from './Blockquote'
|
||||||
|
import BulletList from './BulletList'
|
||||||
|
import CodeBlock from './CodeBlock'
|
||||||
|
import Doc from './Doc'
|
||||||
|
import HardBreak from './HardBreak'
|
||||||
|
import Heading from './Heading'
|
||||||
|
import ListItem from './ListItem'
|
||||||
|
import OrderedList from './OrderedList'
|
||||||
|
import Paragraph from './Paragraph'
|
||||||
|
import Text from './Text'
|
||||||
|
import TodoList from './TodoList'
|
||||||
|
import TodoItem from './TodoItem'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// essentials
|
||||||
|
new Doc(),
|
||||||
|
new Paragraph(),
|
||||||
|
new Text(),
|
||||||
|
|
||||||
|
new Blockquote(),
|
||||||
|
new CodeBlock(),
|
||||||
|
new Heading({ maxLevel: 3 }),
|
||||||
|
new HardBreak(),
|
||||||
|
new OrderedList(),
|
||||||
|
new BulletList(),
|
||||||
|
new ListItem(),
|
||||||
|
new TodoList(),
|
||||||
|
new TodoItem(),
|
||||||
|
]
|
74
src/utils/ComponentView.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default class ComponentView {
|
||||||
|
constructor(component, {
|
||||||
|
node,
|
||||||
|
view,
|
||||||
|
getPos,
|
||||||
|
decorations,
|
||||||
|
editable,
|
||||||
|
}) {
|
||||||
|
this.component = component
|
||||||
|
this.node = node
|
||||||
|
this.view = view
|
||||||
|
this.getPos = getPos
|
||||||
|
this.decorations = decorations
|
||||||
|
this.editable = editable
|
||||||
|
|
||||||
|
this.dom = this.createDOM()
|
||||||
|
this.contentDOM = this._vm.$refs.content
|
||||||
|
}
|
||||||
|
|
||||||
|
createDOM() {
|
||||||
|
const Component = Vue.extend(this.component)
|
||||||
|
this._vm = new Component({
|
||||||
|
propsData: {
|
||||||
|
node: this.node,
|
||||||
|
view: this.view,
|
||||||
|
getPos: this.getPos,
|
||||||
|
decorations: this.decorations,
|
||||||
|
editable: this.editable,
|
||||||
|
updateAttrs: attrs => this.updateAttrs(attrs),
|
||||||
|
updateContent: content => this.updateContent(content),
|
||||||
|
},
|
||||||
|
}).$mount()
|
||||||
|
return this._vm.$el
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAttrs(attrs) {
|
||||||
|
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), null, {
|
||||||
|
...this.node.attrs,
|
||||||
|
...attrs,
|
||||||
|
})
|
||||||
|
this.view.dispatch(transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContent(content) {
|
||||||
|
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), this.node.type, { content })
|
||||||
|
this.view.dispatch(transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreMutation() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
update(node, decorations) {
|
||||||
|
if (node.type !== this.node.type) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === this.node && this.decorations === decorations) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.node = node
|
||||||
|
this.decorations = decorations
|
||||||
|
this._vm._props.node = node
|
||||||
|
this._vm._props.decorations = decorations
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this._vm.$destroy()
|
||||||
|
}
|
||||||
|
}
|
84
src/utils/PluginManager.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { keymap } from 'prosemirror-keymap'
|
||||||
|
|
||||||
|
export default class PluginManager {
|
||||||
|
|
||||||
|
constructor(plugins = []) {
|
||||||
|
this.plugins = plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodes() {
|
||||||
|
return this.plugins
|
||||||
|
.filter(plugin => plugin.type === 'node')
|
||||||
|
.reduce((nodes, { name, schema }) => ({
|
||||||
|
...nodes,
|
||||||
|
[name]: schema,
|
||||||
|
}), {})
|
||||||
|
}
|
||||||
|
|
||||||
|
get marks() {
|
||||||
|
return this.plugins
|
||||||
|
.filter(plugin => plugin.type === 'mark')
|
||||||
|
.reduce((marks, { name, schema }) => ({
|
||||||
|
...marks,
|
||||||
|
[name]: schema,
|
||||||
|
}), {})
|
||||||
|
}
|
||||||
|
|
||||||
|
get pluginplugins() {
|
||||||
|
return this.plugins
|
||||||
|
.filter(plugin => plugin.plugins)
|
||||||
|
.reduce((allPlugins, { plugins }) => ([
|
||||||
|
...allPlugins,
|
||||||
|
...plugins,
|
||||||
|
]), [])
|
||||||
|
}
|
||||||
|
|
||||||
|
get views() {
|
||||||
|
return this.plugins
|
||||||
|
.filter(plugin => plugin.view)
|
||||||
|
.reduce((views, { name, view }) => ({
|
||||||
|
...views,
|
||||||
|
[name]: view,
|
||||||
|
}), {})
|
||||||
|
}
|
||||||
|
|
||||||
|
keymaps({ schema }) {
|
||||||
|
return this.plugins
|
||||||
|
.filter(plugin => plugin.keys)
|
||||||
|
.map(plugin => plugin.keys({
|
||||||
|
type: schema[`${plugin.type}s`][plugin.name],
|
||||||
|
schema,
|
||||||
|
}))
|
||||||
|
.map(keys => keymap(keys))
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules({ schema }) {
|
||||||
|
return this.plugins
|
||||||
|
.filter(plugin => plugin.inputRules)
|
||||||
|
.map(plugin => plugin.inputRules({
|
||||||
|
type: schema[`${plugin.type}s`][plugin.name],
|
||||||
|
schema,
|
||||||
|
}))
|
||||||
|
.reduce((allInputRules, inputRules) => ([
|
||||||
|
...allInputRules,
|
||||||
|
...inputRules,
|
||||||
|
]), [])
|
||||||
|
}
|
||||||
|
|
||||||
|
commands({ schema, view }) {
|
||||||
|
return this.plugins
|
||||||
|
.filter(plugin => plugin.command)
|
||||||
|
.reduce((commands, { name, type, command }) => ({
|
||||||
|
...commands,
|
||||||
|
[name]: attrs => {
|
||||||
|
view.focus()
|
||||||
|
command({
|
||||||
|
type: schema[`${type}s`][name],
|
||||||
|
attrs,
|
||||||
|
schema,
|
||||||
|
})(view.state, view.dispatch, view)
|
||||||
|
},
|
||||||
|
}), {})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
45
src/utils/buildMenuActions.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { markIsActive, nodeIsActive, getMarkAttrs } from '.'
|
||||||
|
|
||||||
|
export default function ({ schema, state, commands }) {
|
||||||
|
|
||||||
|
const nodes = Object.entries(schema.nodes)
|
||||||
|
.map(([name]) => {
|
||||||
|
const active = (attrs = {}) => nodeIsActive(state, schema.nodes[name], attrs)
|
||||||
|
const command = commands[name] ? commands[name] : () => {}
|
||||||
|
return { name, active, command }
|
||||||
|
})
|
||||||
|
.reduce((actions, { name, active, command }) => ({
|
||||||
|
...actions,
|
||||||
|
[name]: {
|
||||||
|
active,
|
||||||
|
command,
|
||||||
|
},
|
||||||
|
}), {})
|
||||||
|
|
||||||
|
const marks = Object.entries(schema.marks)
|
||||||
|
.map(([name]) => {
|
||||||
|
const active = () => markIsActive(state, schema.marks[name])
|
||||||
|
const attrs = getMarkAttrs(state, schema.marks[name])
|
||||||
|
const command = commands[name] ? commands[name] : () => {}
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
attrs,
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reduce((actions, { name, active, attrs, command }) => ({
|
||||||
|
...actions,
|
||||||
|
[name]: {
|
||||||
|
active,
|
||||||
|
attrs,
|
||||||
|
command,
|
||||||
|
},
|
||||||
|
}), {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes,
|
||||||
|
marks,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
src/utils/builtInKeymap.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { lift, selectParentNode } from 'prosemirror-commands'
|
||||||
|
import { undo, redo } from 'prosemirror-history'
|
||||||
|
import { undoInputRule } from 'prosemirror-inputrules'
|
||||||
|
import { isMac } from 'vue-mirror/utils'
|
||||||
|
|
||||||
|
const keymap = {
|
||||||
|
'Mod-z': undo,
|
||||||
|
'Shift-Mod-z': undo,
|
||||||
|
'Mod-BracketLeft': lift,
|
||||||
|
Backspace: undoInputRule,
|
||||||
|
Escape: selectParentNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMac) {
|
||||||
|
keymap['Mod-y'] = redo
|
||||||
|
}
|
||||||
|
|
||||||
|
export default keymap
|
16
src/utils/getMarkAttrs.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export default function (state, type) {
|
||||||
|
const { from, to } = state.selection
|
||||||
|
let marks = []
|
||||||
|
|
||||||
|
state.doc.nodesBetween(from, to, node => {
|
||||||
|
marks = [...marks, ...node.marks]
|
||||||
|
})
|
||||||
|
|
||||||
|
const mark = marks.find(mark => mark.type.name === type.name)
|
||||||
|
|
||||||
|
if (mark) {
|
||||||
|
return mark.attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
12
src/utils/index.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export { default as buildMenuActions } from './buildMenuActions'
|
||||||
|
export { default as builtInKeymap } from './builtInKeymap'
|
||||||
|
export { default as ComponentView } from './ComponentView'
|
||||||
|
export { default as initNodeViews } from './initNodeViews'
|
||||||
|
export { default as isMac } from './isMac'
|
||||||
|
export { default as getMarkAttrs } from './getMarkAttrs'
|
||||||
|
export { default as markIsActive } from './markIsActive'
|
||||||
|
export { default as nodeIsActive } from './nodeIsActive'
|
||||||
|
export { default as menuBubble } from './menuBubble'
|
||||||
|
export { default as Node } from './node'
|
||||||
|
export { default as Mark } from './mark'
|
||||||
|
export { default as PluginManager } from './PluginManager'
|
18
src/utils/initNodeViews.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ComponentView } from '.'
|
||||||
|
|
||||||
|
export default function initNodeViews({ nodes, editable }) {
|
||||||
|
const nodeViews = {}
|
||||||
|
Object.keys(nodes).forEach(nodeName => {
|
||||||
|
nodeViews[nodeName] = (node, view, getPos, decorations) => {
|
||||||
|
const component = nodes[nodeName]
|
||||||
|
return new ComponentView(component, {
|
||||||
|
node,
|
||||||
|
view,
|
||||||
|
getPos,
|
||||||
|
decorations,
|
||||||
|
editable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nodeViews
|
||||||
|
}
|
1
src/utils/isMac.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false
|
46
src/utils/mark.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
export default class Mark {
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = {
|
||||||
|
...this.defaultOptions,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultOptions() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return 'mark'
|
||||||
|
}
|
||||||
|
|
||||||
|
get view() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
get plugins() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
command() {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
src/utils/markIsActive.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export default function (state, type) {
|
||||||
|
const {
|
||||||
|
from,
|
||||||
|
$from,
|
||||||
|
to,
|
||||||
|
empty,
|
||||||
|
} = state.selection
|
||||||
|
|
||||||
|
if (empty) {
|
||||||
|
return !!type.isInSet(state.storedMarks || $from.marks())
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!state.doc.rangeHasMark(from, to, type)
|
||||||
|
}
|
73
src/utils/menuBubble.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Plugin } from 'prosemirror-state'
|
||||||
|
|
||||||
|
class Toolbar {
|
||||||
|
|
||||||
|
constructor({ node, editorView }) {
|
||||||
|
this.editorView = editorView
|
||||||
|
this.node = node
|
||||||
|
this.element = this.node.elm
|
||||||
|
this.element.style.visibility = 'hidden'
|
||||||
|
this.element.style.opacity = 0
|
||||||
|
|
||||||
|
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
update(view, lastState) {
|
||||||
|
const { state } = view
|
||||||
|
|
||||||
|
// Don't do anything if the document/selection didn't change
|
||||||
|
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the tooltip if the selection is empty
|
||||||
|
if (state.selection.empty) {
|
||||||
|
this.hide()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Otherwise, reposition it and update its content
|
||||||
|
this.show()
|
||||||
|
const { from, to } = state.selection
|
||||||
|
|
||||||
|
// These are in screen coordinates
|
||||||
|
const start = view.coordsAtPos(from)
|
||||||
|
const end = view.coordsAtPos(to)
|
||||||
|
|
||||||
|
// The box in which the tooltip is positioned, to use as base
|
||||||
|
const box = this.element.offsetParent.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Find a center-ish x position from the selection endpoints (when
|
||||||
|
// crossing lines, end may be more to the left)
|
||||||
|
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
||||||
|
this.element.style.left = `${left - box.left}px`
|
||||||
|
this.element.style.bottom = `${box.bottom - start.top}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.element.style.visibility = 'visible'
|
||||||
|
this.element.style.opacity = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(event) {
|
||||||
|
if (event && event.relatedTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.element.style.visibility = 'hidden'
|
||||||
|
this.element.style.opacity = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.editorView.dom.removeEventListener('blur', this.hide)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (node) {
|
||||||
|
return new Plugin({
|
||||||
|
view(editorView) {
|
||||||
|
return new Toolbar({ editorView, node })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
46
src/utils/node.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
export default class Node {
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = {
|
||||||
|
...this.defaultOptions,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultOptions() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return 'node'
|
||||||
|
}
|
||||||
|
|
||||||
|
get view() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
get plugins() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
command() {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRules() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
src/utils/nodeIsActive.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { findParentNode } from 'prosemirror-utils'
|
||||||
|
|
||||||
|
export default function (state, type, attrs) {
|
||||||
|
const predicate = node => node.type === type
|
||||||
|
const parent = findParentNode(predicate)(state.selection)
|
||||||
|
|
||||||
|
if (attrs === {} || !parent) {
|
||||||
|
return !!parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent.node.hasMarkup(type, attrs)
|
||||||
|
}
|
24
webpack/build.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import ora from 'ora'
|
||||||
|
import webpack from 'webpack'
|
||||||
|
import config from './webpack.config'
|
||||||
|
|
||||||
|
const spinner = ora('Building …')
|
||||||
|
|
||||||
|
export default new Promise((resolve, reject) => {
|
||||||
|
spinner.start()
|
||||||
|
|
||||||
|
webpack(config, (error, stats) => {
|
||||||
|
if (error) {
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
process.stdout.write(stats.toString() + "\n");
|
||||||
|
return reject(new Error('Build failed with errors.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve('Build complete.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(success => spinner.succeed(success))
|
||||||
|
.catch(error => spinner.fail(error))
|
6
webpack/paths.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export const rootPath = __dirname
|
||||||
|
export const srcPath = path.resolve(rootPath, '../examples')
|
||||||
|
export const buildPath = path.resolve(rootPath, '../dist')
|
||||||
|
export const sassImportPath = srcPath
|
59
webpack/server.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import browserSync from 'browser-sync'
|
||||||
|
import webpack from 'webpack'
|
||||||
|
import httpProxyMiddleware from 'http-proxy-middleware'
|
||||||
|
import webpackDevMiddleware from 'webpack-dev-middleware'
|
||||||
|
import webpackHotMiddleware from 'webpack-hot-middleware'
|
||||||
|
import config from './webpack.config'
|
||||||
|
import { sassImport } from './utilities'
|
||||||
|
import { srcPath, sassImportPath } from './paths'
|
||||||
|
|
||||||
|
const bundler = webpack(config)
|
||||||
|
const middlewares = []
|
||||||
|
|
||||||
|
middlewares.push(httpProxyMiddleware('/api', {
|
||||||
|
target: 'http://local.app.scrumpy.io/api',
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: {
|
||||||
|
'^/api': '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add webpack stuff
|
||||||
|
middlewares.push(webpackDevMiddleware(bundler, {
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
chunks: false,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add hot reloading
|
||||||
|
middlewares.push(webpackHotMiddleware(bundler))
|
||||||
|
|
||||||
|
// start browsersync
|
||||||
|
const url = 'http://local.app.scrumpy.io'
|
||||||
|
const bs = browserSync.create()
|
||||||
|
const server = bs.init({
|
||||||
|
server: {
|
||||||
|
baseDir: `${srcPath}/`,
|
||||||
|
middleware: middlewares,
|
||||||
|
},
|
||||||
|
files: [],
|
||||||
|
logLevel: 'silent',
|
||||||
|
open: false,
|
||||||
|
notify: false,
|
||||||
|
injectChanges: false,
|
||||||
|
ghostMode: {
|
||||||
|
clicks: false,
|
||||||
|
forms: false,
|
||||||
|
scroll: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`${url}:${server.options.get('port')}`)
|
||||||
|
|
||||||
|
// sass import
|
||||||
|
bs.watch(path.join(sassImportPath, '**/!(index|index_sub).scss'), { ignoreInitial: true }, () => {
|
||||||
|
sassImport(sassImportPath)
|
||||||
|
})
|
47
webpack/utilities.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import glob from 'glob'
|
||||||
|
import minimist from 'minimist'
|
||||||
|
|
||||||
|
let argv = minimist(process.argv.slice(2))
|
||||||
|
|
||||||
|
export function removeEmpty(array) {
|
||||||
|
return array.filter(entry => !!entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ifElse(condition) {
|
||||||
|
return (then, otherwise) => (condition ? then : otherwise)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const env = argv.env || 'development'
|
||||||
|
export const use = argv.use || null
|
||||||
|
export const isDev = use ? use === 'development' : env === 'development'
|
||||||
|
export const isProd = use ? use === 'production' : env === 'production'
|
||||||
|
export const isTest = env === 'testing'
|
||||||
|
export const ifDev = ifElse(isDev)
|
||||||
|
export const ifProd = ifElse(isProd)
|
||||||
|
export const ifTest = ifElse(isTest)
|
||||||
|
|
||||||
|
export function sassImport(basePath) {
|
||||||
|
const indexFileName = 'index.scss'
|
||||||
|
glob.sync(`${basePath}/**/${indexFileName}`).forEach(sourceFile => {
|
||||||
|
fs.writeFileSync(sourceFile, '// This is a dynamically generated file \n\n')
|
||||||
|
glob.sync(`${path.dirname(sourceFile)}/*.scss`).forEach(file => {
|
||||||
|
if (path.basename(file) !== indexFileName) {
|
||||||
|
fs.appendFileSync(sourceFile, `@import "${path.basename(file)}";\n`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const indexSubFileName = 'index_sub.scss'
|
||||||
|
glob.sync(`${basePath}/**/${indexSubFileName}`).forEach(sourceFile => {
|
||||||
|
fs.writeFileSync(sourceFile, '// This is a dynamically generated file \n\n')
|
||||||
|
glob.sync(`${path.dirname(sourceFile)}/**/*.scss`).forEach(file => {
|
||||||
|
if (path.basename(file) !== indexSubFileName) {
|
||||||
|
let importPath = (path.dirname(sourceFile) === path.dirname(file)) ? path.basename(file) : file
|
||||||
|
importPath = importPath.replace(`${path.dirname(sourceFile)}/`, '')
|
||||||
|
fs.appendFileSync(sourceFile, `@import "${importPath}";\n`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
257
webpack/webpack.config.js
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import webpack from 'webpack'
|
||||||
|
import { VueLoaderPlugin } from 'vue-loader'
|
||||||
|
import SvgStore from 'webpack-svgstore-plugin'
|
||||||
|
import CopyWebpackPlugin from 'copy-webpack-plugin'
|
||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin'
|
||||||
|
import ManifestPlugin from 'webpack-manifest-plugin'
|
||||||
|
import ImageminWebpackPlugin from 'imagemin-webpack-plugin'
|
||||||
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
||||||
|
import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin'
|
||||||
|
import { ifDev, ifProd, removeEmpty } from './utilities'
|
||||||
|
import { rootPath, srcPath, buildPath } from './paths'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
mode: ifDev('development', 'production'),
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
app: removeEmpty([
|
||||||
|
ifDev('webpack-hot-middleware/client?reload=true'),
|
||||||
|
`${srcPath}/assets/sass/main.scss`,
|
||||||
|
`${srcPath}/main.js`,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: `${buildPath}/`,
|
||||||
|
filename: `assets/js/[name]${ifProd('.[hash]', '')}.js`,
|
||||||
|
chunkFilename: `assets/js/[name]${ifProd('.[chunkhash]', '')}.js`,
|
||||||
|
publicPath: '/',
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.scss', '.vue'],
|
||||||
|
alias: {
|
||||||
|
vue$: 'vue/dist/vue.esm.js',
|
||||||
|
modernizr: path.resolve(rootPath, '../.modernizr'),
|
||||||
|
modules: path.resolve(rootPath, '../node_modules'),
|
||||||
|
images: `${srcPath}/assets/images`,
|
||||||
|
fonts: `${srcPath}/assets/fonts`,
|
||||||
|
variables: `${srcPath}/assets/sass/variables`,
|
||||||
|
settings: `${srcPath}/assets/sass/1-settings/index`,
|
||||||
|
utilityFunctions: `${srcPath}/assets/sass/2-utility-functions/index`,
|
||||||
|
'vue-mirror': path.resolve(rootPath, '../src'),
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
srcPath,
|
||||||
|
path.resolve(rootPath, '../node_modules'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
devtool: ifDev('eval-source-map', 'source-map'),
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.modernizr$/,
|
||||||
|
loader: 'modernizr-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: ifDev('babel-loader?cacheDirectory=true', 'babel-loader'),
|
||||||
|
exclude: /node_modules(?!\/quill)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(graphql|gql)$/,
|
||||||
|
loader: 'graphql-tag/loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: removeEmpty([
|
||||||
|
ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
|
||||||
|
'css-loader',
|
||||||
|
'postcss-loader',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: removeEmpty([
|
||||||
|
ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
|
||||||
|
'css-loader',
|
||||||
|
'postcss-loader',
|
||||||
|
'sass-loader',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpe?g|gif|svg|ico)(\?.*)?$/,
|
||||||
|
use: {
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: `assets/images/[name]${ifProd('.[hash]', '')}.[ext]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||||
|
use: {
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: `assets/fonts/[name]${ifProd('.[hash]', '')}.[ext]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// splitting out the vendor
|
||||||
|
optimization: {
|
||||||
|
namedModules: true,
|
||||||
|
splitChunks: {
|
||||||
|
name: 'vendor',
|
||||||
|
minChunks: 2,
|
||||||
|
},
|
||||||
|
noEmitOnErrors: true,
|
||||||
|
// concatenateModules: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: removeEmpty([
|
||||||
|
|
||||||
|
// create manifest file for server-side asset manipulation
|
||||||
|
new ManifestPlugin({
|
||||||
|
fileName: 'assets/manifest.json',
|
||||||
|
writeToFileEmit: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// define env
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': {
|
||||||
|
API_BASEURL: ifDev(
|
||||||
|
JSON.stringify('http://local.app.scrumpy.io/api/'),
|
||||||
|
JSON.stringify('https://app.scrumpy.io/api/'),
|
||||||
|
),
|
||||||
|
SOCKET_KEY: ifDev(
|
||||||
|
JSON.stringify('981d87f7695904cec025e4039dd4048b'),
|
||||||
|
JSON.stringify('981d87f7695904cec025e4039dd4048b'),
|
||||||
|
),
|
||||||
|
SOCKET_HOST: ifDev(
|
||||||
|
JSON.stringify('http://local.socket.scrumpy.io/'),
|
||||||
|
JSON.stringify('https://socket.scrumpy.io/'),
|
||||||
|
),
|
||||||
|
SUBSCRIPTIONS_HOST: ifDev(
|
||||||
|
JSON.stringify('ws://local.subscriptions.scrumpy.io/'),
|
||||||
|
JSON.stringify('wss://subscriptions.scrumpy.io/'),
|
||||||
|
),
|
||||||
|
STRIPE_KEY: ifDev(
|
||||||
|
JSON.stringify('pk_test_yU17swZxi2a289XgEI9F20qS'),
|
||||||
|
JSON.stringify('pk_live_XWgEzw9TgxpY8Tsf7PKXzk1k'),
|
||||||
|
),
|
||||||
|
CRISP_WEBSITE_ID: ifDev(
|
||||||
|
null,
|
||||||
|
JSON.stringify('463813ad-c274-4da1-8045-f5ceac88832b'),
|
||||||
|
),
|
||||||
|
ANALYTICS_ID: ifDev(
|
||||||
|
null,
|
||||||
|
JSON.stringify('UA-93829826-2'),
|
||||||
|
),
|
||||||
|
BUILD_VERSION: JSON.stringify(new Date().valueOf()),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// copy static files
|
||||||
|
new CopyWebpackPlugin([
|
||||||
|
{
|
||||||
|
context: `${srcPath}/assets/static`,
|
||||||
|
from: { glob: '**/*', dot: false },
|
||||||
|
to: `${buildPath}/assets`,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
// enable hot reloading
|
||||||
|
ifDev(new webpack.HotModuleReplacementPlugin()),
|
||||||
|
|
||||||
|
// make some packages available everywhere
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
// $: 'jquery',
|
||||||
|
// jQuery: 'jquery',
|
||||||
|
// 'window.jQuery': 'jquery',
|
||||||
|
collect: 'collect.js',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// html
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: 'index.html',
|
||||||
|
template: `${srcPath}/index.html`,
|
||||||
|
inject: true,
|
||||||
|
minify: ifProd({
|
||||||
|
removeComments: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeAttributeQuotes: true,
|
||||||
|
}),
|
||||||
|
buildVersion: new Date().valueOf(),
|
||||||
|
chunksSortMode: 'none',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new VueLoaderPlugin(),
|
||||||
|
|
||||||
|
// create css files
|
||||||
|
ifProd(new MiniCssExtractPlugin({
|
||||||
|
filename: `assets/css/[name]${ifProd('.[hash]', '')}.css`,
|
||||||
|
chunkFilename: `assets/css/[name]${ifProd('.[hash]', '')}.css`,
|
||||||
|
})),
|
||||||
|
|
||||||
|
// minify css files
|
||||||
|
ifProd(new OptimizeCssAssetsPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
reduceIdents: false,
|
||||||
|
autoprefixer: false,
|
||||||
|
zindex: false,
|
||||||
|
discardComments: {
|
||||||
|
removeAll: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
// svg icons
|
||||||
|
new SvgStore({
|
||||||
|
prefix: 'icon--',
|
||||||
|
svgoOptions: {
|
||||||
|
plugins: [
|
||||||
|
{ cleanupIDs: false },
|
||||||
|
{ collapseGroups: false },
|
||||||
|
{ removeTitle: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// image optimization
|
||||||
|
new ImageminWebpackPlugin({
|
||||||
|
optipng: ifDev(null, {
|
||||||
|
optimizationLevel: 3,
|
||||||
|
}),
|
||||||
|
jpegtran: ifDev(null, {
|
||||||
|
progressive: true,
|
||||||
|
quality: 80,
|
||||||
|
}),
|
||||||
|
svgo: ifDev(null, {
|
||||||
|
plugins: [
|
||||||
|
{ cleanupIDs: false },
|
||||||
|
{ removeViewBox: false },
|
||||||
|
{ removeUselessStrokeAndFill: false },
|
||||||
|
{ removeEmptyAttrs: false },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
]),
|
||||||
|
|
||||||
|
node: {
|
||||||
|
fs: 'empty',
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|