initial commit

This commit is contained in:
Philipp Kühn
2018-08-20 23:02:21 +02:00
parent b37be519d8
commit d111afe7ac
64 changed files with 11545 additions and 0 deletions

441
examples/App.vue Normal file
View 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>

View 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>

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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;
}
}

View File

@@ -0,0 +1,2 @@
$color-black: #222222;
$color-white: #ffffff;

View 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
View 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
View 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')

View 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}`,
],
}
}
}