adds editorjs and some simple extensions
|
@ -14,6 +14,8 @@
|
|||
"vue-router": "^3.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@editorjs/editorjs": "^2.17.0",
|
||||
"@editorjs/list": "^1.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.18.0",
|
||||
"@typescript-eslint/parser": "^2.18.0",
|
||||
"@vue/cli-plugin-babel": "^4.2.0",
|
||||
|
@ -30,6 +32,7 @@
|
|||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^6.1.2",
|
||||
"lint-staged": "^9.5.0",
|
||||
"raw-loader": "^4.0.0",
|
||||
"typescript": "~3.7.5",
|
||||
"vue-property-decorator": "^8.4.0",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
|
|
|
@ -132,3 +132,7 @@ button.action-close {
|
|||
border-radius: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.codex-editor--narrow .codex-editor__redactor {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
1
src/assets/editor/delimiter.svg.txt
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="19" height="4" viewBox="0 0 19 4" xmlns="http://www.w3.org/2000/svg"><path d="M1.25 0H7a1.25 1.25 0 1 1 0 2.5H1.25a1.25 1.25 0 1 1 0-2.5zM11 0h5.75a1.25 1.25 0 0 1 0 2.5H11A1.25 1.25 0 0 1 11 0z"/></svg>
|
After Width: | Height: | Size: 216 B |
1
src/assets/editor/header.svg.txt
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="10" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 14"><path d="M7.6 8.15H2.25v4.525a1.125 1.125 0 0 1-2.25 0V1.125a1.125 1.125 0 1 1 2.25 0V5.9H7.6V1.125a1.125 1.125 0 0 1 2.25 0v11.55a1.125 1.125 0 0 1-2.25 0V8.15z"/></svg>
|
After Width: | Height: | Size: 254 B |
1
src/assets/editor/header1.svg.txt
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="16" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.14 1.494V4.98h4.62V1.494c0-.498.098-.871.293-1.12A.927.927 0 0 1 7.82 0c.322 0 .583.123.782.37.2.246.3.62.3 1.124v9.588c0 .503-.101.88-.303 1.128a.957.957 0 0 1-.779.374.921.921 0 0 1-.77-.378c-.193-.251-.29-.626-.29-1.124V6.989H2.14v4.093c0 .503-.1.88-.302 1.128a.957.957 0 0 1-.778.374.921.921 0 0 1-.772-.378C.096 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.285.374A.922.922 0 0 1 1.06 0c.321 0 .582.123.782.37.199.246.299.62.299 1.124zm11.653 9.985V5.27c-1.279.887-2.14 1.33-2.583 1.33a.802.802 0 0 1-.563-.228.703.703 0 0 1-.245-.529c0-.232.08-.402.241-.511.161-.11.446-.25.854-.424.61-.259 1.096-.532 1.462-.818a5.84 5.84 0 0 0 .97-.962c.282-.355.466-.573.552-.655.085-.082.246-.123.483-.123.267 0 .481.093.642.28.161.186.242.443.242.77v7.813c0 .914-.345 1.371-1.035 1.371-.307 0-.554-.093-.74-.28-.187-.186-.28-.461-.28-.825z"/></svg>
|
After Width: | Height: | Size: 918 B |
1
src/assets/editor/header2.svg.txt
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm10.99 9.288h3.527c.351 0 .62.072.804.216.185.144.277.34.277.588 0 .22-.073.408-.22.56-.146.154-.368.23-.665.23h-4.972c-.338 0-.601-.093-.79-.28a.896.896 0 0 1-.284-.659c0-.162.06-.377.182-.645s.255-.478.399-.631a38.617 38.617 0 0 1 1.621-1.598c.482-.444.827-.735 1.034-.875.369-.261.676-.523.922-.787.245-.263.432-.534.56-.81.129-.278.193-.549.193-.815 0-.288-.069-.546-.206-.773a1.428 1.428 0 0 0-.56-.53 1.618 1.618 0 0 0-.774-.19c-.59 0-1.054.26-1.392.777-.045.068-.12.252-.226.554-.106.302-.225.534-.358.696-.133.162-.328.243-.585.243a.76.76 0 0 1-.56-.223c-.149-.148-.223-.351-.223-.608 0-.31.07-.635.21-.972.139-.338.347-.645.624-.92a3.093 3.093 0 0 1 1.054-.665c.426-.169.924-.253 1.496-.253.69 0 1.277.108 1.764.324.315.144.592.343.83.595.24.252.425.544.558.875.133.33.2.674.2 1.03 0 .558-.14 1.066-.416 1.523-.277.457-.56.815-.848 1.074-.288.26-.771.666-1.45 1.22-.677.554-1.142.984-1.394 1.29a3.836 3.836 0 0 0-.331.44z"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/editor/header3.svg.txt
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm11.61 4.919c.418 0 .778-.123 1.08-.368.301-.245.452-.597.452-1.055 0-.35-.12-.65-.36-.902-.241-.252-.566-.378-.974-.378-.277 0-.505.038-.684.116a1.1 1.1 0 0 0-.426.306 2.31 2.31 0 0 0-.296.49c-.093.2-.178.388-.255.565a.479.479 0 0 1-.245.225.965.965 0 0 1-.409.081.706.706 0 0 1-.5-.22c-.152-.148-.228-.345-.228-.59 0-.236.071-.484.214-.745a2.72 2.72 0 0 1 .627-.746 3.149 3.149 0 0 1 1.024-.568 4.122 4.122 0 0 1 1.368-.214c.44 0 .842.06 1.205.18.364.12.679.294.947.52.267.228.47.49.606.79.136.3.204.622.204.967 0 .454-.099.843-.296 1.168-.198.324-.48.64-.848.95.354.19.653.408.895.653.243.245.426.516.548.813.123.298.184.619.184.964 0 .413-.083.812-.248 1.198-.166.386-.41.73-.732 1.031a3.49 3.49 0 0 1-1.147.708c-.443.17-.932.256-1.467.256a3.512 3.512 0 0 1-1.464-.293 3.332 3.332 0 0 1-1.699-1.64c-.142-.314-.214-.573-.214-.777 0-.263.085-.475.255-.636a.89.89 0 0 1 .637-.242c.127 0 .25.037.367.112a.53.53 0 0 1 .232.27c.236.63.489 1.099.759 1.405.27.306.65.46 1.14.46a1.714 1.714 0 0 0 1.46-.824c.17-.273.256-.588.256-.947 0-.53-.145-.947-.436-1.249-.29-.302-.694-.453-1.212-.453-.09 0-.231.01-.422.028-.19.018-.313.027-.367.027-.25 0-.443-.062-.579-.187-.136-.125-.204-.299-.204-.521 0-.218.081-.394.245-.528.163-.134.406-.2.728-.2h.28z"/></svg>
|
After Width: | Height: | Size: 1.7 KiB |
1
src/assets/editor/header4.svg.txt
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="20" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm13.003 10.09v-1.252h-3.38c-.427 0-.746-.097-.96-.29-.213-.193-.32-.456-.32-.788 0-.085.016-.171.048-.259.031-.088.078-.18.141-.276.063-.097.128-.19.195-.28.068-.09.15-.2.25-.33l3.568-4.774a5.44 5.44 0 0 1 .576-.683.763.763 0 0 1 .542-.212c.682 0 1.023.39 1.023 1.171v5.212h.29c.346 0 .623.047.832.142.208.094.313.3.313.62 0 .26-.086.45-.256.568-.17.12-.427.179-.768.179h-.41v1.252c0 .346-.077.603-.23.771-.152.168-.356.253-.612.253a.78.78 0 0 1-.61-.26c-.154-.173-.232-.427-.232-.764zm-2.895-2.76h2.895V4.91L12.26 8.823z"/></svg>
|
After Width: | Height: | Size: 1 KiB |
1
src/assets/editor/header5.svg.txt
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm14.16 2.645h-3.234l-.388 2.205c.644-.344 1.239-.517 1.783-.517.436 0 .843.082 1.222.245.38.164.712.39.998.677.286.289.51.63.674 1.025.163.395.245.82.245 1.273 0 .658-.148 1.257-.443 1.797-.295.54-.72.97-1.276 1.287-.556.318-1.197.477-1.923.477-.813 0-1.472-.15-1.978-.45-.506-.3-.865-.643-1.076-1.031-.21-.388-.316-.727-.316-1.018 0-.177.073-.345.22-.504a.725.725 0 0 1 .556-.238c.381 0 .665.22.85.66.182.404.427.719.736.943.309.225.654.337 1.035.337.35 0 .656-.09.919-.272.263-.182.466-.431.61-.749.142-.318.214-.678.214-1.082 0-.436-.078-.808-.232-1.117a1.607 1.607 0 0 0-.62-.69 1.674 1.674 0 0 0-.864-.229c-.39 0-.67.048-.837.143-.168.095-.41.262-.725.5-.316.239-.576.358-.78.358a.843.843 0 0 1-.592-.242c-.173-.16-.259-.344-.259-.548 0-.022.025-.177.075-.463l.572-3.26c.063-.39.181-.675.354-.852.172-.177.454-.265.844-.265h3.595c.708 0 1.062.27 1.062.81a.711.711 0 0 1-.26.572c-.172.145-.426.218-.762.218z"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/editor/header6.svg.txt
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zM12.53 7.058a3.093 3.093 0 0 1 1.004-.814 2.734 2.734 0 0 1 1.214-.264c.43 0 .827.08 1.19.24.365.161.684.39.957.686.274.296.485.645.635 1.048a3.6 3.6 0 0 1 .223 1.262c0 .637-.145 1.216-.437 1.736-.292.52-.699.926-1.221 1.218-.522.292-1.114.438-1.774.438-.76 0-1.416-.186-1.967-.557-.552-.37-.974-.919-1.265-1.645-.292-.726-.438-1.613-.438-2.662 0-.855.088-1.62.265-2.293.176-.674.43-1.233.76-1.676.33-.443.73-.778 1.2-1.004.47-.226 1.006-.339 1.608-.339.579 0 1.089.113 1.53.34.44.225.773.506.997.84.224.335.335.656.335.964 0 .185-.07.354-.21.505a.698.698 0 0 1-.536.227.874.874 0 0 1-.529-.18 1.039 1.039 0 0 1-.36-.498 1.42 1.42 0 0 0-.495-.655 1.3 1.3 0 0 0-.786-.247c-.24 0-.479.069-.716.207a1.863 1.863 0 0 0-.6.56c-.33.479-.525 1.333-.584 2.563zm1.832 4.213c.456 0 .834-.186 1.133-.56.298-.373.447-.862.447-1.468 0-.412-.07-.766-.21-1.062a1.584 1.584 0 0 0-.577-.678 1.47 1.47 0 0 0-.807-.234c-.28 0-.548.074-.804.224-.255.149-.461.365-.617.647a2.024 2.024 0 0 0-.234.994c0 .61.158 1.12.475 1.527.316.407.714.61 1.194.61z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -1,45 +0,0 @@
|
|||
<template>
|
||||
<ol>
|
||||
<li :key="n" v-for="n in amount" :style="style"> </li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardBoxes extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
|
||||
private get amount () {
|
||||
return parseInt(this.params[0], 10)
|
||||
}
|
||||
|
||||
private get style () {
|
||||
const size = parseFloat(this.params[1])
|
||||
return {
|
||||
width: `calc(${size}em - 0.25em)`,
|
||||
height: `calc(${size}em - 0.25em)`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
ol {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
list-style: none;
|
||||
margin: 0 0 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
ol > li {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
font-size: 1rem;
|
||||
margin: 0 .3em .3em 0;
|
||||
border: 0.25em solid var(--highlight-color);
|
||||
}
|
||||
</style>
|
|
@ -1,51 +0,0 @@
|
|||
<template>
|
||||
<ul>
|
||||
<li v-for="(param, i) in params"
|
||||
:key="`param${i}`"
|
||||
v-editable:[i]="editable"
|
||||
@keydown="handleKey(i, $event)">
|
||||
{{ param }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardBulletList extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
@Prop() public readonly editable!: boolean
|
||||
|
||||
private addEntry (index: number) {
|
||||
const newParams = [...this.params]
|
||||
newParams.splice(index + 1, 0, '')
|
||||
this.$emit('replace', newParams)
|
||||
}
|
||||
|
||||
private removeEntry (index: number) {
|
||||
const newParams = [...this.params]
|
||||
newParams.splice(index, 1)
|
||||
this.$emit('replace', newParams)
|
||||
}
|
||||
|
||||
private handleKey (index: number, event: KeyboardEvent) {
|
||||
const { key, shiftKey } = event
|
||||
if (key === 'Enter' && shiftKey) {
|
||||
event.preventDefault()
|
||||
this.addEntry(index)
|
||||
} else if (key === 'Backspace') {
|
||||
const text = (event.target as HTMLElement).innerText
|
||||
if (text.trim() === '') this.removeEntry(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
padding-left: .5em;
|
||||
}
|
||||
</style>
|
|
@ -1,22 +0,0 @@
|
|||
<template>
|
||||
<p>
|
||||
<span v-editable:0="editable" class="title">{{ params[0] }}</span>
|
||||
<span v-editable:1="editable">{{ params[1] }}</span>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardDescription extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
@Prop() public readonly editable!: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
p { margin: 0; line-height: 1.2; }
|
||||
p > .title { font-weight: bold; font-style: italic; }
|
||||
p > .title::after { content: ' '; }
|
||||
</style>
|
|
@ -1,73 +0,0 @@
|
|||
<template>
|
||||
<ol>
|
||||
<li :key="titles[i]" v-for="(v, i) in params">
|
||||
<span class="title">{{ titles[i] }}</span>
|
||||
<span class="description">{{ v }} ({{ mod(v) }})</span>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardDndstats extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
|
||||
private titles = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
|
||||
|
||||
private mod (v: number): string {
|
||||
switch (v) {
|
||||
case 1: return '-5'
|
||||
case 2:
|
||||
case 3: return '-4'
|
||||
case 4:
|
||||
case 5: return '-3'
|
||||
case 6:
|
||||
case 7: return '-2'
|
||||
case 8:
|
||||
case 9: return '-1'
|
||||
case 10:
|
||||
case 11: return '0'
|
||||
case 12:
|
||||
case 13: return '+1'
|
||||
case 14:
|
||||
case 15: return '+2'
|
||||
case 16:
|
||||
case 17: return '+3'
|
||||
case 18:
|
||||
case 19: return '+4'
|
||||
case 20:
|
||||
case 21: return '+5'
|
||||
case 22:
|
||||
case 23: return '+6'
|
||||
case 24:
|
||||
case 25: return '+7'
|
||||
case 26:
|
||||
case 27: return '+8'
|
||||
case 28:
|
||||
case 29: return '+9'
|
||||
default: return '+10'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
ol {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ol > li {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.title { font-weight: bold; }
|
||||
</style>
|
|
@ -1,136 +0,0 @@
|
|||
<template>
|
||||
<menu class="menu-bar" :class="{ active }">
|
||||
<button class="editor-button-bold" :class="{ active: value.bold }" @click="menuAction('bold')" />
|
||||
<button class="editor-button-italic" :class="{ active: value.italic }" @click="menuAction('italic')" />
|
||||
|
||||
<button class="editor-button-paragraph" :class="{ active: value.paragraph }" @click="menuAction('paragraph')" />
|
||||
<button class="editor-button-heading2" :class="{ active: value.heading2 }" @click="menuAction('heading2')" />
|
||||
<button class="editor-button-heading3" :class="{ active: value.heading3 }" @click="menuAction('heading3')" />
|
||||
|
||||
<button class="editor-button-bullet-list" :class="{ active: value.bulletList }" @click="menuAction('bulletList')" />
|
||||
<button class="editor-button-horizontal-rule" :class="{ active: value.separator}" @click="menuAction('separator')" />
|
||||
|
||||
<button class="editor-button-dropdown" :class="{ active: dropdownOpen }" @click="toggleDropdown" />
|
||||
|
||||
<div class="extended-menu" v-show="dropdownOpen">
|
||||
<button class="extended-menu-button" @click="extMenuAction('statBlock')">Stat Block (DnD5e)</button>
|
||||
<button class="extended-menu-button" @click="extMenuAction('boxes')">Empty Boxes</button>
|
||||
</div>
|
||||
</menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import { blocks, marks, State } from '@/editor'
|
||||
|
||||
@Component
|
||||
export default class DeckCardEditorMenu extends Vue {
|
||||
@Prop() public readonly active!: boolean
|
||||
@Prop() public readonly value!: State
|
||||
|
||||
private dropdownOpen = false
|
||||
|
||||
private menuAction (name: string) {
|
||||
const newState = { ...this.value }
|
||||
|
||||
if (blocks.indexOf(name) >= 0) { // blocks behave like radio buttons
|
||||
blocks.forEach(block => {
|
||||
newState[block] = false
|
||||
})
|
||||
newState[name] = true
|
||||
} else if (marks.indexOf(name)) { // marks behave like checkboxes
|
||||
newState[name] = !newState[name]
|
||||
}
|
||||
|
||||
this.$emit('input', newState)
|
||||
this.$emit('action', name)
|
||||
}
|
||||
|
||||
private toggleDropdown () {
|
||||
this.dropdownOpen = !this.dropdownOpen
|
||||
this.$emit('action', 'refocus')
|
||||
}
|
||||
|
||||
private extMenuAction (name: string) {
|
||||
this.menuAction(name)
|
||||
this.dropdownOpen = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-front > main > .menu-bar {
|
||||
position: absolute;
|
||||
width: 70%;
|
||||
margin: -3rem 0 0 -1rem;
|
||||
padding: .2rem 1rem;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity .3s .2s, visibility .3s .2s;
|
||||
background-color: var(--highlight-color);
|
||||
z-index: 2;
|
||||
}
|
||||
.card-front > main > .menu-bar.active {
|
||||
opacity: 1.0;
|
||||
visibility: visible;
|
||||
}
|
||||
.menu-bar > button {
|
||||
position: relative;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
margin: 0 .1rem;
|
||||
background-color: #EEE;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 75%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.menu-bar > button:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1.6rem;
|
||||
width: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
color: black;
|
||||
}
|
||||
.menu-bar > button.active {
|
||||
background-color: #FF0;
|
||||
}
|
||||
.editor-button-bold { background-image: url(../assets/zondicons/format-bold.svg); }
|
||||
.editor-button-italic { background-image: url(../assets/zondicons/format-italic.svg); }
|
||||
.editor-button-bullet-list { background-image: url(../assets/zondicons/list-bullet.svg); }
|
||||
|
||||
.editor-button-heading2:after { content: 'H2'; }
|
||||
.editor-button-heading3:after { content: 'H3'; }
|
||||
.editor-button-paragraph:after { content: 'P'; }
|
||||
.editor-button-horizontal-rule:after { content: '—'; }
|
||||
|
||||
.editor-button-stat-block:after { content: 'ST'; }
|
||||
|
||||
.menu-bar > button.editor-button-dropdown {
|
||||
width: 3.6rem;
|
||||
}
|
||||
.menu-bar > button.editor-button-dropdown:after {
|
||||
content: ' more ';
|
||||
width: 90%;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.extended-menu {
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
padding-top: .5rem;
|
||||
background: var(--highlight-color);
|
||||
}
|
||||
.extended-menu-button {
|
||||
width: 97%;
|
||||
height: 1.6rem;
|
||||
margin: 0 .1rem;
|
||||
background-color: #EEE;
|
||||
color: black;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
|
@ -1,149 +1,48 @@
|
|||
<template>
|
||||
<main>
|
||||
<deck-card-editor-menu
|
||||
:active="contentInFocus"
|
||||
@action="editorAction"
|
||||
v-model="menuState"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref="content"
|
||||
class="card-content"
|
||||
:contenteditable="active"
|
||||
@focus="start"
|
||||
@click="syncMenuStateIfFocussed"
|
||||
@keyup="syncMenuStateOnKeyPress"
|
||||
@blur="stop"
|
||||
>
|
||||
<h2>card content</h2>
|
||||
<hr />
|
||||
<p><b>foo:</b> boom</p>
|
||||
<p><b>bar:</b> blam</p>
|
||||
<hr />
|
||||
<p>Some description maybe?</p>
|
||||
</div>
|
||||
</main>
|
||||
<main :id="id" class="card-content"></main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import DeckCardEditorMenu from '@/components/deck-card-editor-menu.vue'
|
||||
import {
|
||||
menuActionToCommand,
|
||||
getActiveMarksAndBlocks,
|
||||
State,
|
||||
movementKeys,
|
||||
controlSequenceKeys,
|
||||
isRootNode,
|
||||
isTextNode,
|
||||
moveCaretToEOL
|
||||
} from '@/editor'
|
||||
|
||||
@Component({
|
||||
components: { DeckCardEditorMenu }
|
||||
})
|
||||
import Editor from '@editorjs/editorjs'
|
||||
import List from '@editorjs/list'
|
||||
import { Heading, Delimiter } from '@/editor'
|
||||
|
||||
@Component
|
||||
export default class DeckCardEditor extends Vue {
|
||||
@Prop() public readonly cardId!: string
|
||||
@Prop() public readonly active!: boolean
|
||||
@Prop() public readonly content!: Card['content']
|
||||
|
||||
private contentInFocus = false
|
||||
private editor!: Editor
|
||||
|
||||
private defaultMenuState (): State {
|
||||
return {
|
||||
bold: false,
|
||||
italic: false,
|
||||
paragraph: true,
|
||||
heading1: false,
|
||||
heading2: false,
|
||||
heading3: false,
|
||||
bulletList: false,
|
||||
spacer: false,
|
||||
separator: false,
|
||||
statBlock: false
|
||||
}
|
||||
private get id () {
|
||||
return `${this.cardId}-editor`
|
||||
}
|
||||
|
||||
private menuState = this.defaultMenuState()
|
||||
|
||||
private resetMenuState () {
|
||||
this.menuState = this.defaultMenuState()
|
||||
}
|
||||
|
||||
private setMenuState (marks: string[], block: string) {
|
||||
this.resetMenuState()
|
||||
marks.forEach(mark => { this.menuState[mark] = true })
|
||||
if (block !== 'paragraph') {
|
||||
this.menuState.paragraph = false
|
||||
this.menuState[block] = true
|
||||
}
|
||||
}
|
||||
|
||||
private editorAction (action: string) {
|
||||
const content = this.$refs.content as HTMLElement
|
||||
content.focus()
|
||||
|
||||
const cmd = menuActionToCommand[action]
|
||||
if (cmd) cmd()
|
||||
|
||||
this.$nextTick(() => this.syncMenuState())
|
||||
}
|
||||
|
||||
private syncMenuState () {
|
||||
const sel = window.getSelection()?.focusNode
|
||||
if (!sel) return
|
||||
|
||||
const { marks, block } = getActiveMarksAndBlocks(sel as HTMLElement)
|
||||
this.setMenuState(marks, block)
|
||||
}
|
||||
|
||||
private syncMenuStateIfFocussed () {
|
||||
if (this.contentInFocus) this.syncMenuState()
|
||||
}
|
||||
|
||||
private syncMenuStateOnKeyPress (event: KeyboardEvent) {
|
||||
// undo/redo/cut/paste
|
||||
const isCtrlSq = event.ctrlKey && controlSequenceKeys.indexOf(event.key) >= 0
|
||||
// arrow keys, enter, delete, etc
|
||||
const isMove = movementKeys.indexOf(event.key) >= 0
|
||||
|
||||
if (isCtrlSq || isMove) {
|
||||
return this.syncMenuState()
|
||||
} else if (!event.ctrlKey && event.key.length === 1) {
|
||||
// this should capture all normal typable letters and numbers
|
||||
// TODO: this needs to be done on text pasting as well
|
||||
|
||||
// some browsers create bogus root level text nodes, so whenever
|
||||
// something is typed in such a root level node, we simply wrap it with
|
||||
// a paragraph
|
||||
const sel = window.getSelection()?.focusNode
|
||||
if (sel && isTextNode(sel) && isRootNode(sel.parentElement as HTMLElement)) {
|
||||
console.debug(`Typed letter "${event.key} into root node, throwing a <p> at it!"`)
|
||||
document.execCommand('formatblock', false, 'P')
|
||||
|
||||
// Firefox behaves nicely and leaves the caret alone after surrounding
|
||||
// the text node with a <p>. Unlike Chromium that moves the caret to
|
||||
// the beginning of the new paragraph. To mitigate that, we set the
|
||||
// caret to end-of-line manually.
|
||||
moveCaretToEOL()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private start () {
|
||||
this.contentInFocus = true
|
||||
this.syncMenuState()
|
||||
// insert paragraphs instead of DIVs on enter
|
||||
document.execCommand('defaultParagraphSeparator', false, 'p')
|
||||
}
|
||||
|
||||
private stop () {
|
||||
this.contentInFocus = false
|
||||
private mounted () {
|
||||
this.editor = new Editor({
|
||||
holderId: this.id,
|
||||
autofocus: false,
|
||||
tools: {
|
||||
header: Heading,
|
||||
list: List,
|
||||
delimiter: Delimiter
|
||||
},
|
||||
// data: {},
|
||||
placeholder: 'Click here to write your card.'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card-content p {
|
||||
.card-content .cdx-block {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-content .ce-paragraph, .card-content p {
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
@ -174,19 +73,20 @@ export default class DeckCardEditor extends Vue {
|
|||
border-bottom: 1px solid var(--highlight-color);
|
||||
}
|
||||
|
||||
.card-content hr {
|
||||
.card-content .card-delimiter {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
padding: 0;
|
||||
border: 2px solid var(--highlight-color);
|
||||
}
|
||||
.card-content hr.pointing-right {
|
||||
.card-content .card-delimiter.pointing-right {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
border-style: solid;
|
||||
border-width: 2px 0 2px 220px;
|
||||
border-color: transparent transparent transparent var(--highlight-color);
|
||||
}
|
||||
.card-content hr.pointing-left {
|
||||
.card-content .card-delimiter.pointing-left {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
border-style: solid;
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<template>
|
||||
<div :style="{flex: params[0]}"> </div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardFill extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
|
@ -1,17 +0,0 @@
|
|||
<template>
|
||||
<p v-editable:0="editable">{{ params[0] }}</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardNote extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
@Prop() public readonly editable!: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
p { margin: 0 0 .5em 0; line-height: 1.2; font-style: italic; }
|
||||
</style>
|
|
@ -1,22 +0,0 @@
|
|||
<template>
|
||||
<p>
|
||||
<span class="title" v-editable:0="editable">{{ params[0] }}</span>
|
||||
<span class="description" v-editable:1="editable">{{ params[1] }}</span>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardProperty extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
@Prop() public readonly editable!: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
p { margin: 0 0 0 1em; line-height: 1.2; text-indent: -1em; }
|
||||
p > .title { font-weight: bold; }
|
||||
p > .title::after { content: ' '; }
|
||||
</style>
|
|
@ -1,34 +0,0 @@
|
|||
<template>
|
||||
<hr :class="params[0]" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardRule extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
hr {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
border: 2px solid var(--highlight-color);
|
||||
}
|
||||
hr.pointing-right {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
border-style: solid;
|
||||
border-width: 2px 0 2px 220px;
|
||||
border-color: transparent transparent transparent var(--highlight-color);
|
||||
}
|
||||
hr.pointing-left {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
border-style: solid;
|
||||
border-width: 2px 220px 2px 0;
|
||||
border-color: transparent var(--highlight-color) transparent transparent;
|
||||
}
|
||||
</style>
|
|
@ -1,25 +0,0 @@
|
|||
<template>
|
||||
<h4 v-editable:0="editable">{{ params[0] }}</h4>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardSection extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
@Prop() public readonly editable!: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h4 {
|
||||
font-size: 1.4rem;
|
||||
color: var(--highlight-color);
|
||||
margin: 0 0 .2em 0;
|
||||
font-weight: normal;
|
||||
font-variant: small-caps;
|
||||
line-height: .9em;
|
||||
border-bottom: 1px solid var(--highlight-color);
|
||||
}
|
||||
</style>
|
|
@ -1,22 +0,0 @@
|
|||
<template>
|
||||
<h3 v-editable:0="editable">{{ params[0] }}</h3>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardSubtitle extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
@Prop() public readonly editable!: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
color: var(--highlight-color);
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
|
@ -1,17 +0,0 @@
|
|||
<template>
|
||||
<p v-editable:0="editable">{{ params[0] }}</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class DeckCardText extends Vue {
|
||||
@Prop() public readonly params!: string[]
|
||||
@Prop() public readonly editable!: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
p { margin: 0 0 .5em 0; line-height: 1.2; }
|
||||
</style>
|
|
@ -10,14 +10,18 @@
|
|||
<section name="card-front" class="card-front">
|
||||
<header>
|
||||
<h1 :contenteditable="isSelection"
|
||||
@focus="selectLine"
|
||||
@blur="editField('name', $event)"
|
||||
@keypress.enter.prevent="editField('name', $event)">
|
||||
{{ card.name }}
|
||||
</h1>
|
||||
<img :src="icon" />
|
||||
</header>
|
||||
<deck-card-editor :active="isSelection" :content="card.content" @input="$emit('edit', $event)" />
|
||||
<deck-card-editor
|
||||
:card-id="card.id"
|
||||
:active="isSelection"
|
||||
:content="card.content"
|
||||
@input="$emit('edit', $event)"
|
||||
/>
|
||||
</section>
|
||||
<section name="card-back" class="card-back">
|
||||
<div class="icon-wrapper">
|
||||
|
@ -33,7 +37,6 @@
|
|||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import { cardWHtoStyle, iconPath } from '@/lib'
|
||||
import DeckCardEditor from '@/components/deck-card-editor.vue'
|
||||
import { selectLine } from '@/editor'
|
||||
|
||||
@Component({
|
||||
components: { DeckCardEditor }
|
||||
|
@ -104,10 +107,6 @@ export default class DeckCard extends Vue {
|
|||
|
||||
return style
|
||||
}
|
||||
|
||||
private selectLine () {
|
||||
selectLine()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -201,6 +200,7 @@ export default class DeckCard extends Vue {
|
|||
border-radius: 1rem;
|
||||
font-size: 1.2rem;
|
||||
color: black;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-back {
|
||||
|
|
45
src/editor/block-tool.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { BlockTool, BlockToolData, ToolConfig, ToolboxConfig, API } from '@editorjs/editorjs'
|
||||
|
||||
export interface BlockToolArgs {
|
||||
api: API;
|
||||
config: ToolConfig;
|
||||
data?: BlockToolData;
|
||||
}
|
||||
|
||||
export class BlockToolExt implements BlockTool {
|
||||
protected api: API
|
||||
protected _element: HTMLElement
|
||||
protected _data: object
|
||||
protected _config: ToolConfig
|
||||
|
||||
constructor ({ data, config, api }: BlockToolArgs) {
|
||||
this.api = api
|
||||
this._config = config
|
||||
this._data = data || {}
|
||||
this._element = this._render()
|
||||
}
|
||||
|
||||
protected get _CSS (): { [key: string]: string } {
|
||||
return { block: this.api.styles.block }
|
||||
}
|
||||
|
||||
protected _render (): HTMLElement {
|
||||
const el = document.createElement('DIV')
|
||||
el.classList.add(this._CSS.block)
|
||||
return el
|
||||
}
|
||||
|
||||
render (): HTMLElement {
|
||||
return this._element
|
||||
}
|
||||
|
||||
save (_toolsContent: HTMLElement): object {
|
||||
return {}
|
||||
}
|
||||
|
||||
static get toolbox (): ToolboxConfig {
|
||||
return { icon: '<svg></svg>', title: 'UnnamedPlugin' }
|
||||
}
|
||||
}
|
||||
|
||||
export default BlockToolExt
|
|
@ -1,33 +0,0 @@
|
|||
import { getFocussedNode } from './node'
|
||||
|
||||
function applyRange (callback: (range: Range) => void) {
|
||||
const range = document.createRange()
|
||||
callback(range)
|
||||
|
||||
const sel = window.getSelection()
|
||||
if (sel) {
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
}
|
||||
}
|
||||
function collapseRange (node: Node, toStart = false) {
|
||||
applyRange(range => {
|
||||
range.selectNode(node)
|
||||
range.collapse(toStart)
|
||||
})
|
||||
}
|
||||
|
||||
export function moveCaretToBOL () {
|
||||
const node = getFocussedNode()
|
||||
if (node) collapseRange(node, true)
|
||||
}
|
||||
export function moveCaretToEOL () {
|
||||
const node = getFocussedNode()
|
||||
if (node) collapseRange(node, false)
|
||||
}
|
||||
export function selectLine () {
|
||||
const node = getFocussedNode()
|
||||
if (node) {
|
||||
applyRange(range => range.selectNodeContents(node))
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
export const movementKeys = [
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'Delete',
|
||||
'Backspace',
|
||||
'Enter',
|
||||
'Home',
|
||||
'End',
|
||||
'PageUp',
|
||||
'PageDown'
|
||||
]
|
||||
|
||||
export const controlSequenceKeys = ['p', 'x', 'y', 'z', 'Z']
|
||||
|
||||
export const elementNameToMenuState: KV<string> = {
|
||||
B: 'bold',
|
||||
STRONG: 'bold',
|
||||
I: 'italic',
|
||||
EM: 'italic',
|
||||
P: 'paragraph',
|
||||
H1: 'heading1',
|
||||
H2: 'heading2',
|
||||
H3: 'heading3',
|
||||
UL: 'bulletList',
|
||||
OL: 'numberedList',
|
||||
HR: 'separator'
|
||||
}
|
||||
|
||||
export const marks = ['bold', 'italic']
|
||||
export const blocks = [
|
||||
'paragraph',
|
||||
'heading1',
|
||||
'heading2',
|
||||
'heading3',
|
||||
'bulletList',
|
||||
'spacer',
|
||||
'separator',
|
||||
'statBlock'
|
||||
]
|
29
src/editor/delimiter.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { ToolConstructable } from '@editorjs/editorjs'
|
||||
import BlockTool from './block-tool'
|
||||
import icon from '../assets/editor/delimiter.svg.txt'
|
||||
const title = 'Delimiter'
|
||||
|
||||
export class Delimiter extends BlockTool {
|
||||
static get contentless () {
|
||||
return true
|
||||
}
|
||||
|
||||
protected get _CSS () {
|
||||
return {
|
||||
block: this.api.styles.block,
|
||||
wrapper: 'card-delimiter'
|
||||
}
|
||||
}
|
||||
|
||||
protected _render (): HTMLElement {
|
||||
const el = document.createElement('HR')
|
||||
el.classList.add(this._CSS.wrapper, this._CSS.block)
|
||||
return el
|
||||
}
|
||||
|
||||
static get toolbox () {
|
||||
return { icon, title }
|
||||
}
|
||||
}
|
||||
|
||||
export default Delimiter as ToolConstructable
|
202
src/editor/heading.ts
Normal file
|
@ -0,0 +1,202 @@
|
|||
import { ToolConstructable, ToolConfig, HTMLPasteEvent } from '@editorjs/editorjs'
|
||||
import { BlockToolExt, BlockToolArgs } from './block-tool'
|
||||
import icon from '../assets/editor/header.svg.txt'
|
||||
import icon1 from '../assets/editor/header1.svg.txt'
|
||||
import icon2 from '../assets/editor/header2.svg.txt'
|
||||
import icon3 from '../assets/editor/header3.svg.txt'
|
||||
import icon4 from '../assets/editor/header4.svg.txt'
|
||||
import icon5 from '../assets/editor/header5.svg.txt'
|
||||
import icon6 from '../assets/editor/header6.svg.txt'
|
||||
const title = 'Heading'
|
||||
|
||||
enum HeadingLevel {
|
||||
One = 1,
|
||||
Two = 2,
|
||||
Three = 3,
|
||||
Four = 4,
|
||||
Five = 5,
|
||||
Six = 6
|
||||
}
|
||||
|
||||
interface HeaderConfig extends ToolConfig {
|
||||
placeholder?: string;
|
||||
levels?: HeadingLevel[];
|
||||
defaultLevel?: HeadingLevel;
|
||||
}
|
||||
|
||||
interface HeaderData {
|
||||
text?: string;
|
||||
level?: HeadingLevel;
|
||||
}
|
||||
|
||||
class Heading extends BlockToolExt {
|
||||
protected _config: HeaderConfig
|
||||
protected settingsButtons: HTMLElement[] = []
|
||||
private levels: HeadingLevel[]
|
||||
private defaultLevel: HeadingLevel
|
||||
private currentLevel: HeadingLevel
|
||||
private icons: Map<HeadingLevel, string> = new Map([
|
||||
[HeadingLevel.One, icon1],
|
||||
[HeadingLevel.Two, icon2],
|
||||
[HeadingLevel.Three, icon3],
|
||||
[HeadingLevel.Four, icon4],
|
||||
[HeadingLevel.Five, icon5],
|
||||
[HeadingLevel.Six, icon6]
|
||||
])
|
||||
|
||||
constructor (args: BlockToolArgs) {
|
||||
super(args)
|
||||
this._config = args.config as HeaderConfig
|
||||
|
||||
if (this._config.levels === undefined) {
|
||||
this._config.levels = [HeadingLevel.Two, HeadingLevel.Three]
|
||||
}
|
||||
if (this._config.defaultLevel === undefined) {
|
||||
this._config.defaultLevel = HeadingLevel.Two
|
||||
}
|
||||
if (this._config.levels.indexOf(this._config.defaultLevel) === -1) {
|
||||
console.warn('(ง\'̀-\'́)ง Heading Tool: the default level specified was not found in available levels')
|
||||
}
|
||||
this.levels = this._config.levels
|
||||
this.defaultLevel = this._config.defaultLevel
|
||||
this.currentLevel = this.defaultLevel
|
||||
|
||||
// setting data will rerender the element with the right settings
|
||||
this.data = {
|
||||
level: this.currentLevel,
|
||||
text: (args.data as HeaderData).text || ''
|
||||
}
|
||||
}
|
||||
|
||||
public renderSettings (): HTMLElement {
|
||||
const wrapper = document.createElement('DIV')
|
||||
|
||||
this.levels.forEach(level => {
|
||||
const { settingsButton, settingsButtonActive } = this._CSS
|
||||
const btn = document.createElement('SPAN')
|
||||
btn.classList.add(settingsButton)
|
||||
btn.dataset.level = `${level}`
|
||||
btn.innerHTML = this.icons.get(level) || icon
|
||||
|
||||
if (this.currentLevel === level) btn.classList.add(settingsButtonActive)
|
||||
|
||||
btn.addEventListener('click', () => this.setLevel(level))
|
||||
wrapper.appendChild(btn)
|
||||
this.settingsButtons.push(btn)
|
||||
})
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
public get data (): HeaderData {
|
||||
return this._data as HeaderData
|
||||
}
|
||||
|
||||
public set data (data: HeaderData) {
|
||||
const currentData = this._data as HeaderData
|
||||
|
||||
if (data.level === undefined) data.level = currentData.level || this.defaultLevel
|
||||
if (data.text === undefined) data.text = currentData.text || ''
|
||||
|
||||
this._data = data
|
||||
this.currentLevel = data.level
|
||||
|
||||
const newHeader = this._render()
|
||||
if (this._element.parentNode) {
|
||||
this._element.parentNode.replaceChild(newHeader, this._element)
|
||||
}
|
||||
this._element = newHeader
|
||||
}
|
||||
|
||||
private setLevel (level: HeadingLevel) {
|
||||
this.data = { level, text: this._element.innerHTML }
|
||||
}
|
||||
|
||||
protected get _CSS () {
|
||||
return {
|
||||
block: this.api.styles.block,
|
||||
settingsButton: this.api.styles.settingsButton,
|
||||
settingsButtonActive: this.api.styles.settingsButtonActive,
|
||||
wrapper: 'card-heading'
|
||||
}
|
||||
}
|
||||
|
||||
protected _render (): HTMLElement {
|
||||
console.log('render', `H${this.currentLevel}`, this.data)
|
||||
|
||||
const el = document.createElement(`H${this.currentLevel}`)
|
||||
el.innerHTML = this.data.text || ''
|
||||
el.classList.add(this._CSS.wrapper, this._CSS.block)
|
||||
el.contentEditable = 'true'
|
||||
el.dataset.placeholder = this._config.placeholder || ''
|
||||
return el
|
||||
}
|
||||
|
||||
// Handle pasted H1-H6 tags to substitute with header tool
|
||||
onPaste (event: HTMLPasteEvent) {
|
||||
const content = event.detail.data
|
||||
const text = content.innerHTML
|
||||
let level = this.defaultLevel
|
||||
|
||||
const tagMatch = content.tagName.match(/H(\d)/)
|
||||
if (tagMatch) level = parseInt(tagMatch[1], 10)
|
||||
|
||||
// Fallback to nearest level when specified not available
|
||||
if (this._config.levels) {
|
||||
level = this._config.levels.reduce((prevLevel, currLevel) => {
|
||||
return Math.abs(currLevel - level) < Math.abs(prevLevel - level) ? currLevel : prevLevel
|
||||
})
|
||||
}
|
||||
|
||||
this.data = { level, text }
|
||||
}
|
||||
|
||||
// Used by Editor.js paste handling API.
|
||||
// Provides configuration to handle H1-H6 tags.
|
||||
static get pasteConfig () {
|
||||
return {
|
||||
tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
|
||||
}
|
||||
}
|
||||
|
||||
// Method that specified how to merge two Text blocks.
|
||||
// Called by Editor.js by backspace at the beginning of the Block
|
||||
public merge (data: HeaderData) {
|
||||
this.data = {
|
||||
text: this.data.text + (data.text || ''),
|
||||
level: this.data.level
|
||||
}
|
||||
}
|
||||
|
||||
// validate text block data
|
||||
validate (blockData: HeaderData): boolean {
|
||||
if (!blockData.text) return false
|
||||
return blockData.text.trim() !== ''
|
||||
}
|
||||
|
||||
// extract tools data from view
|
||||
save (toolsContent: HTMLElement): HeaderData {
|
||||
return {
|
||||
text: toolsContent.innerHTML,
|
||||
level: this.currentLevel
|
||||
}
|
||||
}
|
||||
|
||||
// Allow Heading to be converted to/from other blocks
|
||||
static get conversionConfig () {
|
||||
return {
|
||||
export: 'text', // use 'text' property for other blocks
|
||||
import: 'text' // fill 'text' property from other block's export string
|
||||
}
|
||||
}
|
||||
|
||||
static get sanitize (): object {
|
||||
return { level: {} }
|
||||
}
|
||||
|
||||
static get toolbox () {
|
||||
return { icon, title }
|
||||
}
|
||||
}
|
||||
|
||||
export default Heading as ToolConstructable
|
|
@ -1,81 +1,2 @@
|
|||
import { elementNameToMenuState, marks, blocks } from './constants'
|
||||
|
||||
export {
|
||||
isRootNode,
|
||||
isRootChild,
|
||||
isElementNode,
|
||||
isTextNode,
|
||||
isEmptyTextNode,
|
||||
getFocussedNode
|
||||
} from './node'
|
||||
|
||||
export {
|
||||
moveCaretToBOL,
|
||||
moveCaretToEOL,
|
||||
selectLine
|
||||
} from './caret'
|
||||
|
||||
export type State = KV<boolean>
|
||||
export {
|
||||
movementKeys,
|
||||
controlSequenceKeys,
|
||||
marks,
|
||||
blocks
|
||||
} from './constants'
|
||||
|
||||
function simpleAction (cmd: string, arg?: string): () => boolean {
|
||||
return () => {
|
||||
return document.execCommand(cmd, false, arg)
|
||||
}
|
||||
}
|
||||
|
||||
export const menuActionToCommand: KV<() => boolean> = {
|
||||
paragraph: simpleAction('formatblock', 'P'),
|
||||
heading1: simpleAction('formatblock', 'H1'),
|
||||
heading2: simpleAction('formatblock', 'H2'),
|
||||
heading3: simpleAction('formatblock', 'H3'),
|
||||
bulletList: simpleAction('insertUnorderedList'),
|
||||
numberedList: simpleAction('insertOrderedList'),
|
||||
separator: simpleAction('insertHorizontalRule'),
|
||||
bold: simpleAction('bold'),
|
||||
italic: simpleAction('italic')
|
||||
}
|
||||
|
||||
export function getActiveMarksAndBlocks (el: HTMLElement): {
|
||||
marks: string[];
|
||||
block: string;
|
||||
} {
|
||||
let activeBlock = 'paragraph'
|
||||
const activeMarks: string[] = []
|
||||
|
||||
const focussedEl = el.nodeName === '#text' ? el.parentElement : el
|
||||
if (!focussedEl) return { marks: activeMarks, block: activeBlock }
|
||||
|
||||
const focussedState = elementNameToMenuState[focussedEl.nodeName]
|
||||
if (!focussedState) return { marks: activeMarks, block: activeBlock }
|
||||
|
||||
if (blocks.indexOf(focussedState) >= 0) {
|
||||
activeBlock = focussedState
|
||||
return { marks: activeMarks, block: activeBlock }
|
||||
}
|
||||
|
||||
let wrappingEl = focussedEl.parentElement
|
||||
let wrappingState: string
|
||||
|
||||
if (marks.indexOf(focussedState) >= 0) {
|
||||
activeMarks.push(focussedState)
|
||||
|
||||
while (wrappingEl) {
|
||||
wrappingState = elementNameToMenuState[wrappingEl.nodeName]
|
||||
if (marks.indexOf(wrappingState) < 0) {
|
||||
if (blocks.indexOf(wrappingState) >= 0) activeBlock = wrappingState
|
||||
break
|
||||
}
|
||||
|
||||
activeMarks.push(wrappingState)
|
||||
wrappingEl = wrappingEl.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
return { marks: activeMarks, block: activeBlock }
|
||||
}
|
||||
export { default as Delimiter } from './delimiter'
|
||||
export { default as Heading } from './heading'
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
const { TEXT_NODE, ELEMENT_NODE } = Node
|
||||
export function getFocussedNode (): Node | null {
|
||||
return window.getSelection()?.focusNode || null
|
||||
}
|
||||
export function isTextNode ({ nodeType }: Node): boolean {
|
||||
return nodeType === TEXT_NODE
|
||||
}
|
||||
export function isElementNode ({ nodeType }: Node): boolean {
|
||||
return nodeType === ELEMENT_NODE
|
||||
}
|
||||
export function isEmptyTextNode (node: Node): boolean {
|
||||
return isTextNode(node) && (node as CharacterData).data === ''
|
||||
}
|
||||
export function isRootNode (node: Node): boolean {
|
||||
return (node as HTMLElement).contentEditable === 'true'
|
||||
}
|
||||
export function isRootChild (node: Node): boolean {
|
||||
// TODO: maybe use a data attribute or something for saver identification
|
||||
return node.parentElement?.contentEditable === 'true'
|
||||
}
|
12
src/modules.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
||||
|
||||
declare module '*.txt' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '@editorjs/paragraph'
|
||||
declare module '@editorjs/list'
|
4
src/shims-vue.d.ts
vendored
|
@ -1,4 +0,0 @@
|
|||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
0
src/shims.d.ts → src/types.d.ts
vendored
10
vue.config.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
chainWebpack: config => {
|
||||
config.module
|
||||
.rule('raw')
|
||||
.test(/\.txt$/)
|
||||
.use('raw-loader')
|
||||
.loader('raw-loader')
|
||||
.end()
|
||||
}
|
||||
}
|
43
yarn.lock
|
@ -770,6 +770,19 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@editorjs/editorjs@^2.17.0":
|
||||
version "2.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.17.0.tgz#38f20d7f99bc21868904b6b937905b6daad5a2a2"
|
||||
integrity sha512-5rMjZLdiFOiUGESe5MZagvuVaLggORXBEolbbDLLVWHslR+r4+TACOXBcN8A6m9hMmnpHIJsC3442MZEWdNfQA==
|
||||
dependencies:
|
||||
codex-notifier "^1.1.2"
|
||||
codex-tooltip "^1.0.0"
|
||||
|
||||
"@editorjs/list@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/list/-/list-1.4.0.tgz#e92459a8ac2305bc4385245e329c8b5c8437456a"
|
||||
integrity sha512-iYDXGbVXvsAJbSxbjFMP4p7kS1zhQyNDqVNzkfMRhItulzKYlOMlFjTIGHqu5SxPy6NrcckhVFaWdfGDn5/gEA==
|
||||
|
||||
"@hapi/address@2.x.x":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||
|
@ -2359,6 +2372,16 @@ code-point-at@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||
|
||||
codex-notifier@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895"
|
||||
integrity sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==
|
||||
|
||||
codex-tooltip@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.0.tgz#720353b27fadc40f2d054d171479b016ffcb63ea"
|
||||
integrity sha512-Wa/p/om166GVjg+q436BERBZZz3yvTnCDDzMV2kjKIzsUkj6vCWphTSTo+M0QJRfwODKzhXYaw8+S4EXPW6r0g==
|
||||
|
||||
collection-visit@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
|
||||
|
@ -6234,11 +6257,6 @@ ora@^3.4.0:
|
|||
strip-ansi "^5.2.0"
|
||||
wcwidth "^1.0.1"
|
||||
|
||||
orderedmap@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.1.1.tgz#c618e77611b3b21d0fe3edc92586265e0059c789"
|
||||
integrity sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ==
|
||||
|
||||
original@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
|
||||
|
@ -7004,13 +7022,6 @@ promise-inflight@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
||||
|
||||
prosemirror-model@1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.8.2.tgz#c74eaacb0bbfea49b59a6d89fef5516181666a56"
|
||||
integrity sha512-piffokzW7opZVCjf/9YaoXvTC0g7zMRWKJib1hpphPfC+4x6ZXe5CiExgycoWZJe59VxxP7uHX8aFiwg2i9mUQ==
|
||||
dependencies:
|
||||
orderedmap "^1.1.0"
|
||||
|
||||
proxy-addr@~2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||
|
@ -7154,6 +7165,14 @@ raw-body@2.4.0:
|
|||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
raw-loader@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.0.tgz#d639c40fb9d72b5c7f8abc1fb2ddb25b29d3d540"
|
||||
integrity sha512-iINUOYvl1cGEmfoaLjnZXt4bKfT2LJnZZib5N/LLyAphC+Dd11vNP9CNVb38j+SAJpFI1uo8j9frmih53ASy7Q==
|
||||
dependencies:
|
||||
loader-utils "^1.2.3"
|
||||
schema-utils "^2.5.0"
|
||||
|
||||
read-pkg-up@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
|
||||
|
|