adds editorjs, fixes cards array reference bug
parent
35743b54e7
commit
bf8c166a71
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<section name="card-front" class="card card-front" :style="cssVars">
|
||||||
|
<header>
|
||||||
|
<h1>{{ card.name }}</h1>
|
||||||
|
<img :src="iconPath" />
|
||||||
|
</header>
|
||||||
|
<main ref="cardEl" class="card-content" />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
|
import { KV } from '@/types'
|
||||||
|
import { cardCSSVars } from '@/lib/card'
|
||||||
|
import iconPath from '@/lib/iconPath'
|
||||||
|
|
||||||
|
import Editor from '@editorjs/editorjs'
|
||||||
|
import List from '@editorjs/list'
|
||||||
|
import { Heading, Delimiter, Charges, DnDStats } from '@/editor'
|
||||||
|
|
||||||
|
const editorjsConfig = {
|
||||||
|
autofocus: false,
|
||||||
|
hideToolbar: true,
|
||||||
|
tools: {
|
||||||
|
list: { class: List, inlineToolbar: false },
|
||||||
|
heading: { class: Heading, inlineToolbar: false },
|
||||||
|
delimiter: { class: Delimiter, inlineToolbar: false },
|
||||||
|
charges: { class: Charges, inlineToolbar: false },
|
||||||
|
dndstats: { class: DnDStats, inlineToolbar: false }
|
||||||
|
},
|
||||||
|
onReady: () => {
|
||||||
|
console.log('editor is ready, what to do?')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CardFront',
|
||||||
|
props: {
|
||||||
|
card: Object,
|
||||||
|
icon: String,
|
||||||
|
color: String,
|
||||||
|
size: String
|
||||||
|
},
|
||||||
|
setup (props) {
|
||||||
|
const cardEl = ref(document.createElement('main'))
|
||||||
|
const editor = new Editor({
|
||||||
|
holder: cardEl.value,
|
||||||
|
data: props.card?.content || {},
|
||||||
|
...editorjsConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('card content', props.card?.content)
|
||||||
|
|
||||||
|
return { cardEl }
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
iconPath (): string {
|
||||||
|
const icon = this.card?.icon || this.icon || 'plus'
|
||||||
|
return iconPath(icon)
|
||||||
|
},
|
||||||
|
cssVars (): KV<string> {
|
||||||
|
return cardCSSVars(this.size, this.color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped src="@/assets/card.css" />
|
@ -1,13 +1,110 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="card && card.id" class="flip-card card" :style="cssVars">
|
<div class="flip-card card">
|
||||||
<section name="card-front" class="card-front" v-if="showFrontSide">
|
<CardFront :card="card" :icon="icon" :color="color" :size="size" />
|
||||||
<span>Front Side</span>
|
<CardBack :icon="card.icon || icon" :color="card.color || color" :size="size">
|
||||||
</section>
|
<button @click="$emit('click')">edit card</button>
|
||||||
<section name="card-back" class="card-back" v-if="showBackSide">
|
<button @click="$emit('delete')">delete card</button>
|
||||||
<div class="icon-wrapper">
|
</CardBack>
|
||||||
<img :src="iconPath" alt="card icon" />
|
|
||||||
</div>
|
|
||||||
<footer><slot name="back"></slot></footer>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import CardFront from '@/components/CardFront.vue'
|
||||||
|
import CardBack from '@/components/CardBack.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'FlipCard',
|
||||||
|
components: { CardFront, CardBack },
|
||||||
|
props: {
|
||||||
|
card: Object,
|
||||||
|
icon: String,
|
||||||
|
color: String,
|
||||||
|
size: String
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flip-card {
|
||||||
|
position: relative;
|
||||||
|
perspective: 600px;
|
||||||
|
transition: transform .2s ease-out .4s;
|
||||||
|
}
|
||||||
|
.flip-card.card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.flip-card.card > .card {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card > .active-background {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: -100vh;
|
||||||
|
left: -100vw;
|
||||||
|
width: 200vw;
|
||||||
|
height: 200vh;
|
||||||
|
background-color: #0008;
|
||||||
|
}
|
||||||
|
.flip-card.active {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.flip-card.active > .active-background {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-front, .card-back {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--highlight-color);
|
||||||
|
transform: rotateX(0) rotateY(0);
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transition: transform .4s ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.flip-card:not(.active):hover > .card-front {
|
||||||
|
transform: rotateX(0) rotateY(179deg);
|
||||||
|
}
|
||||||
|
.flip-card:not(.active):hover > .card-back {
|
||||||
|
z-index: 2;
|
||||||
|
transform: rotateX(0) rotateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-front {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.card-front h1[contenteditable="true"] { text-decoration: underline dotted; }
|
||||||
|
.card-front h1[contenteditable="true"]:focus { text-decoration: none; }
|
||||||
|
|
||||||
|
.card-back {
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2;
|
||||||
|
transform: rotateX(0) rotateY(-179deg);
|
||||||
|
}
|
||||||
|
.card-back button {
|
||||||
|
width: 80%;
|
||||||
|
margin: .1rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
margin-top: -3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (orientation:landscape) {
|
||||||
|
.action-close {
|
||||||
|
top: 3rem;
|
||||||
|
right: -3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
|
||||||
|
import icon from '../assets/editor/charges.svg.txt'
|
||||||
|
import iconCircle from '../assets/editor/charges-circle.svg.txt'
|
||||||
|
|
||||||
|
const title = 'Charges'
|
||||||
|
|
||||||
|
interface ChargesData {
|
||||||
|
variant: string;
|
||||||
|
amount: number;
|
||||||
|
size: number;
|
||||||
|
stretch: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Charges extends ContentlessBlock {
|
||||||
|
static MIN_SIZE = 1
|
||||||
|
static MAX_SIZE = 5
|
||||||
|
private _variant: string
|
||||||
|
private _amount: number
|
||||||
|
private _size: number
|
||||||
|
private _stretch: boolean
|
||||||
|
|
||||||
|
constructor (args: BlockToolArgs) {
|
||||||
|
super(args)
|
||||||
|
this._settingButtons = [
|
||||||
|
{ name: 'box', icon, action: (name: string) => this.setVariant(name) },
|
||||||
|
{ name: 'more', icon: icon, action: () => this.increaseAmount() },
|
||||||
|
{ name: 'bigger', icon: icon, action: () => this.increaseSize() },
|
||||||
|
{ name: 'circle', icon: iconCircle, action: (name: string) => this.setVariant(name) },
|
||||||
|
{ name: 'less', icon: icon, action: () => this.decreaseAmount() },
|
||||||
|
{ name: 'smaller', icon: icon, action: () => this.decreaseSize() },
|
||||||
|
{ name: 'toggle-stretch', icon: icon, action: () => this.toggleStretch() }
|
||||||
|
]
|
||||||
|
const { variant, amount, size, stretch } = (args.data || {}) as ChargesData
|
||||||
|
|
||||||
|
this._variant = variant || 'box'
|
||||||
|
this._amount = amount || 5
|
||||||
|
this._size = size || 1
|
||||||
|
this._stretch = !(stretch === false)
|
||||||
|
|
||||||
|
this._element = this._render()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setVariant (variant: string) {
|
||||||
|
if (this._variant === variant) return
|
||||||
|
|
||||||
|
const charges = Array.from(this._element.children)
|
||||||
|
|
||||||
|
charges.forEach(charge => {
|
||||||
|
charge.classList.remove(`card-charge-${this._variant}`)
|
||||||
|
charge.classList.add(`card-charge-${variant}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._variant = variant
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleStretch () {
|
||||||
|
if (this._stretch) this._element.classList.remove('card-charges-stretch')
|
||||||
|
else this._element.classList.add('card-charges-stretch')
|
||||||
|
this._stretch = !this._stretch
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCharge (): HTMLElement {
|
||||||
|
const charge = document.createElement('DIV')
|
||||||
|
charge.classList.add('card-charge', `card-charge-${this._variant}`, `card-charge-size-${this._size}`)
|
||||||
|
return charge
|
||||||
|
}
|
||||||
|
|
||||||
|
private increaseAmount () {
|
||||||
|
this._element.appendChild(this.createCharge())
|
||||||
|
this._amount++
|
||||||
|
}
|
||||||
|
|
||||||
|
private decreaseAmount () {
|
||||||
|
const child = this._element.lastElementChild
|
||||||
|
if (child) {
|
||||||
|
this._element.removeChild(child)
|
||||||
|
this._amount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private increaseSize () {
|
||||||
|
if (this._size >= Charges.MAX_SIZE) return
|
||||||
|
|
||||||
|
const charges = Array.from(this._element.children)
|
||||||
|
|
||||||
|
charges.forEach(charge => {
|
||||||
|
charge.classList.remove(`card-charge-size-${this._size}`)
|
||||||
|
charge.classList.add(`card-charge-size-${this._size + 1}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._size++
|
||||||
|
}
|
||||||
|
|
||||||
|
private decreaseSize () {
|
||||||
|
if (this._size <= Charges.MIN_SIZE) return
|
||||||
|
|
||||||
|
const charges = Array.from(this._element.children)
|
||||||
|
|
||||||
|
charges.forEach(charge => {
|
||||||
|
charge.classList.remove(`card-charge-size-${this._size}`)
|
||||||
|
charge.classList.add(`card-charge-size-${this._size - 1}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._size--
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _render (): HTMLElement {
|
||||||
|
const el = document.createElement('DIV')
|
||||||
|
el.classList.add('card-charges-wrapper', this._CSS.block)
|
||||||
|
|
||||||
|
if (this._stretch) el.classList.add('card-charges-stretch')
|
||||||
|
|
||||||
|
for (let i = 0; i < this._amount; i++) {
|
||||||
|
el.appendChild(this.createCharge())
|
||||||
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
public save (): ChargesData {
|
||||||
|
return {
|
||||||
|
variant: this._variant,
|
||||||
|
amount: this._amount,
|
||||||
|
size: this._size,
|
||||||
|
stretch: this._stretch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get toolbox () {
|
||||||
|
return { icon, title }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Charges
|
@ -0,0 +1,222 @@
|
|||||||
|
import {
|
||||||
|
BlockTool,
|
||||||
|
BlockToolData,
|
||||||
|
ToolboxConfig,
|
||||||
|
API,
|
||||||
|
HTMLPasteEvent,
|
||||||
|
ToolSettings,
|
||||||
|
SanitizerConfig
|
||||||
|
} from '@editorjs/editorjs'
|
||||||
|
|
||||||
|
export { HTMLPasteEvent } from '@editorjs/editorjs'
|
||||||
|
|
||||||
|
interface PasteConfig {
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentBlockConfig extends ToolSettings {
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentBlockSettingButton {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
action: (name: string, event?: MouseEvent) => void; // action triggered by button
|
||||||
|
isActive?: (name: string) => boolean; // determine if current button is active
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContentBlockSettings = ContentBlockSettingButton[]
|
||||||
|
|
||||||
|
export interface ContentBlockArgs {
|
||||||
|
api: API;
|
||||||
|
config?: ContentBlockConfig;
|
||||||
|
data?: BlockToolData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CSSClasses {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentBlockData extends BlockToolData {
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type importFunction = (str: string) => ContentBlockData
|
||||||
|
type exportFunction = (data: ContentBlockData) => string
|
||||||
|
|
||||||
|
export interface ConversionConfig {
|
||||||
|
import: string | importFunction;
|
||||||
|
export: string | exportFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContentBlock implements BlockTool {
|
||||||
|
// Default placeholder for Paragraph Tool
|
||||||
|
static get DEFAULT_PLACEHOLDER (): string {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
static _supportedTags: string[] = []
|
||||||
|
|
||||||
|
static _toolboxConfig: ToolboxConfig = {
|
||||||
|
icon: '<svg></svg>',
|
||||||
|
title: 'UnnamedContentPlugin'
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _defaultPlaceholder (): string {
|
||||||
|
return ContentBlock.DEFAULT_PLACEHOLDER
|
||||||
|
}
|
||||||
|
|
||||||
|
protected api: API
|
||||||
|
protected _element: HTMLElement
|
||||||
|
protected _data: ContentBlockData
|
||||||
|
protected _config: ContentBlockConfig
|
||||||
|
protected _placeholder: string
|
||||||
|
protected _CSS: CSSClasses = {}
|
||||||
|
protected onKeyUp: (event: KeyboardEvent) => void
|
||||||
|
protected _settingButtons: ContentBlockSettings = []
|
||||||
|
|
||||||
|
constructor ({ data, config, api }: ContentBlockArgs) {
|
||||||
|
this.api = api
|
||||||
|
this._config = config as ContentBlockConfig
|
||||||
|
this._CSS.block = this.api.styles.block
|
||||||
|
|
||||||
|
this.onKeyUp = (event: KeyboardEvent) => this._onKeyUp(event)
|
||||||
|
|
||||||
|
// Placeholder it is first Block
|
||||||
|
this._placeholder = config?.placeholder ? config.placeholder : this._defaultPlaceholder()
|
||||||
|
this._data = data as ContentBlockData
|
||||||
|
this._element = this._render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if text content is empty and set empty string to inner html.
|
||||||
|
// We need this because some browsers (e.g. Safari) insert <br> into empty contenteditanle elements
|
||||||
|
_onKeyUp (event: KeyboardEvent) {
|
||||||
|
if (event.code !== 'Backspace' && event.code !== 'Delete') return
|
||||||
|
|
||||||
|
if (this._element.textContent === '') {
|
||||||
|
this._element.innerHTML = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// render tool view
|
||||||
|
// whenever a redraw is needed the result is saved in this._element
|
||||||
|
protected _render (): HTMLElement {
|
||||||
|
const el = document.createElement('DIV')
|
||||||
|
el.classList.add(this._CSS.block)
|
||||||
|
el.dataset.placeholder = this._placeholder
|
||||||
|
el.addEventListener('keyup', this.onKeyUp)
|
||||||
|
el.innerHTML = this.data.text || ''
|
||||||
|
el.contentEditable = 'true'
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return Tool's view
|
||||||
|
public render (): HTMLElement {
|
||||||
|
return this._element
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method that specified how to merge two Text blocks.
|
||||||
|
// Called by Editor.js by backspace at the beginning of the Block
|
||||||
|
public merge (data: ContentBlockData) {
|
||||||
|
this.data = {
|
||||||
|
text: (this.data.text || '') + data.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Paragraph block data (by default checks for emptiness)
|
||||||
|
public validate (savedData: ContentBlockData): boolean {
|
||||||
|
if (!savedData.text) return false
|
||||||
|
return savedData.text.trim() !== ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract Tool's data from the view
|
||||||
|
public save (toolsContent: HTMLElement): ContentBlockData {
|
||||||
|
return {
|
||||||
|
text: toolsContent.innerHTML
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get CSS (): CSSClasses {
|
||||||
|
return this._CSS
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable Conversion Toolbar. Paragraph can be converted to/from other tools
|
||||||
|
*/
|
||||||
|
static get conversionConfig (): ConversionConfig {
|
||||||
|
return {
|
||||||
|
export: 'text', // to convert Paragraph to other block, use 'text' property of saved data
|
||||||
|
import: 'text' // to covert other block's exported string to Paragraph, fill 'text' property of tool data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitizer rules
|
||||||
|
static get sanitize (): SanitizerConfig {
|
||||||
|
return {
|
||||||
|
text: { br: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get data (): ContentBlockData {
|
||||||
|
const text = this._element?.innerHTML
|
||||||
|
if (text !== undefined) this._data.text = text
|
||||||
|
if (this._data.text === undefined) this._data.text = ''
|
||||||
|
return this._data
|
||||||
|
}
|
||||||
|
|
||||||
|
set data (data: ContentBlockData) {
|
||||||
|
this._data = data || {}
|
||||||
|
this._element.innerHTML = this._data.text || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderSettings (): HTMLElement {
|
||||||
|
const wrapper = document.createElement('DIV')
|
||||||
|
|
||||||
|
this._settingButtons.forEach(tune => {
|
||||||
|
// make sure the settings button does something
|
||||||
|
if (!tune.icon || typeof tune.action !== 'function') return
|
||||||
|
|
||||||
|
const { name, icon, action, isActive } = tune
|
||||||
|
|
||||||
|
const btn = document.createElement('SPAN')
|
||||||
|
btn.classList.add(this.api.styles.settingsButton)
|
||||||
|
|
||||||
|
if (typeof isActive === 'function' && isActive(name)) {
|
||||||
|
btn.classList.add(this.api.styles.settingsButtonActive)
|
||||||
|
}
|
||||||
|
btn.innerHTML = icon
|
||||||
|
btn.addEventListener('click', event => action(name, event))
|
||||||
|
|
||||||
|
wrapper.appendChild(btn)
|
||||||
|
})
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by Editor.js paste handling API.
|
||||||
|
// Provides configuration to handle the tools tags.
|
||||||
|
static get pasteConfig (): PasteConfig {
|
||||||
|
return {
|
||||||
|
tags: this._supportedTags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overwrite this if you need special handling of paste data
|
||||||
|
protected pasteHandler (element: HTMLElement): ContentBlockData {
|
||||||
|
return { text: element.innerText }
|
||||||
|
}
|
||||||
|
|
||||||
|
// On paste callback fired from Editor.
|
||||||
|
public onPaste (event: HTMLPasteEvent) {
|
||||||
|
const element = event.detail.data
|
||||||
|
this.data = this.pasteHandler(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon and title for displaying at the Toolbox
|
||||||
|
static get toolbox (): ToolboxConfig {
|
||||||
|
return this._toolboxConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentBlock
|
@ -0,0 +1,73 @@
|
|||||||
|
import { BlockTool, BlockToolData, ToolSettings, ToolboxConfig, API } from '@editorjs/editorjs'
|
||||||
|
import { ContentBlockSettings, CSSClasses } from './content-block'
|
||||||
|
|
||||||
|
export interface BlockToolArgs {
|
||||||
|
api: API;
|
||||||
|
config?: ToolSettings;
|
||||||
|
data?: BlockToolData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContentlessBlock implements BlockTool {
|
||||||
|
static get contentless () {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
protected api: API
|
||||||
|
protected _element: HTMLElement
|
||||||
|
protected _data: object
|
||||||
|
protected _config: ToolSettings
|
||||||
|
protected _CSS: CSSClasses = {}
|
||||||
|
protected _settingButtons: ContentBlockSettings = []
|
||||||
|
|
||||||
|
constructor ({ data, config, api }: BlockToolArgs) {
|
||||||
|
this.api = api
|
||||||
|
this._config = config as ToolSettings
|
||||||
|
this._data = data || {}
|
||||||
|
this._CSS.block = this.api.styles.block
|
||||||
|
this._element = this._render()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _render (): HTMLElement {
|
||||||
|
const el = document.createElement('DIV')
|
||||||
|
el.classList.add(this._CSS.block)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
public render (): HTMLElement {
|
||||||
|
return this._element
|
||||||
|
}
|
||||||
|
|
||||||
|
public save (_toolsContent: HTMLElement): object {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderSettings (): HTMLElement {
|
||||||
|
const wrapper = document.createElement('DIV')
|
||||||
|
|
||||||
|
this._settingButtons.forEach(tune => {
|
||||||
|
// make sure the settings button does something
|
||||||
|
if (!tune.icon || typeof tune.action !== 'function') return
|
||||||
|
|
||||||
|
const { name, icon, action, isActive } = tune
|
||||||
|
|
||||||
|
const btn = document.createElement('SPAN')
|
||||||
|
btn.classList.add(this.api.styles.settingsButton)
|
||||||
|
|
||||||
|
if (typeof isActive === 'function' && isActive(name)) {
|
||||||
|
btn.classList.add(this.api.styles.settingsButtonActive)
|
||||||
|
}
|
||||||
|
btn.innerHTML = icon
|
||||||
|
btn.addEventListener('click', event => action(name, event))
|
||||||
|
|
||||||
|
wrapper.appendChild(btn)
|
||||||
|
})
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
static get toolbox (): ToolboxConfig {
|
||||||
|
return { icon: '<svg></svg>', title: 'UnnamedPlugin' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentlessBlock
|
@ -0,0 +1,53 @@
|
|||||||
|
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
|
||||||
|
import icon from '../assets/editor/delimiter.svg.txt'
|
||||||
|
import iconR from '../assets/editor/delimiter_r.svg.txt'
|
||||||
|
import iconL from '../assets/editor/delimiter_l.svg.txt'
|
||||||
|
const title = 'Delimiter'
|
||||||
|
|
||||||
|
interface DelimiterData {
|
||||||
|
variant: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Delimiter extends ContentlessBlock {
|
||||||
|
private _variant = 'none'
|
||||||
|
|
||||||
|
constructor (args: BlockToolArgs) {
|
||||||
|
super(args)
|
||||||
|
this._settingButtons = [
|
||||||
|
{ name: 'straight', icon, action: (name: string) => this.setDelimiterType(name) },
|
||||||
|
{ name: 'pointing-left', icon: iconL, action: (name: string) => this.setDelimiterType(name) },
|
||||||
|
{ name: 'pointing-right', icon: iconR, action: (name: string) => this.setDelimiterType(name) }
|
||||||
|
]
|
||||||
|
const { variant } = (args.data || {}) as DelimiterData
|
||||||
|
if (variant) this.setDelimiterType(variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
private setDelimiterType (name: string) {
|
||||||
|
this._element.classList.remove('pointing-left')
|
||||||
|
this._element.classList.remove('pointing-right')
|
||||||
|
this._variant = 'none'
|
||||||
|
|
||||||
|
if (name === 'pointing-left' || name === 'pointing-right') {
|
||||||
|
this._variant = name
|
||||||
|
this._element.classList.add(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _render (): HTMLElement {
|
||||||
|
const el = document.createElement('HR')
|
||||||
|
el.classList.add('card-delimiter', this._CSS.block)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
public save (): DelimiterData {
|
||||||
|
return {
|
||||||
|
variant: this._variant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get toolbox () {
|
||||||
|
return { icon, title }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Delimiter
|
@ -0,0 +1,106 @@
|
|||||||
|
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
|
||||||
|
import icon from '../assets/editor/charges-circle.svg.txt'
|
||||||
|
|
||||||
|
const title = 'DnDStats'
|
||||||
|
|
||||||
|
interface DnDStatsData {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DnDStats extends ContentlessBlock {
|
||||||
|
static _toolboxConfig = { icon, title }
|
||||||
|
private _stats = [10, 10, 10, 10, 10, 10]
|
||||||
|
|
||||||
|
constructor (args: BlockToolArgs) {
|
||||||
|
super(args)
|
||||||
|
this.data = args.data as DnDStatsData
|
||||||
|
this._element = this._render()
|
||||||
|
}
|
||||||
|
|
||||||
|
public get data () {
|
||||||
|
return {
|
||||||
|
text: this._stats.join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public set data (data: DnDStatsData) {
|
||||||
|
if (data.text === undefined) data.text = ''
|
||||||
|
|
||||||
|
const newStats = data.text.split(',')
|
||||||
|
.map(x => parseInt(x, 10))
|
||||||
|
.filter(x => !Number.isNaN(x))
|
||||||
|
|
||||||
|
while (newStats.length < 6) newStats.push(10) // fill missing stats
|
||||||
|
|
||||||
|
this._stats = newStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a random four character long id
|
||||||
|
private randomId (): string {
|
||||||
|
const min = 46656 // '1000'
|
||||||
|
const max = 1679615 /* 'zzzz' */ - 46656 /* '1000' */
|
||||||
|
return (min + Math.floor(max * Math.random())).toString(36)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStatMod (value: number): string {
|
||||||
|
const mod = Math.floor((value - 10) / 2.0)
|
||||||
|
const sign = mod < 0 ? '' : '+'
|
||||||
|
return ` (${sign}${mod})`
|
||||||
|
}
|
||||||
|
|
||||||
|
private createStatBlock (title: string, value: number, changeHandler: (newValue: number) => void): HTMLElement {
|
||||||
|
const id = `dnd-stat-${title}-${this.randomId()}`
|
||||||
|
|
||||||
|
const labelWrapper = document.createElement('label')
|
||||||
|
const titleEl = document.createElement('span')
|
||||||
|
const statInputEl = document.createElement('input')
|
||||||
|
const statModEl = document.createElement('span')
|
||||||
|
|
||||||
|
// should allow focussing block with tab
|
||||||
|
labelWrapper.setAttribute('z-index', '1')
|
||||||
|
labelWrapper.classList.add('dnd-stat-block')
|
||||||
|
labelWrapper.setAttribute('for', id)
|
||||||
|
|
||||||
|
titleEl.classList.add('dnd-stat-title')
|
||||||
|
titleEl.innerText = title
|
||||||
|
|
||||||
|
statInputEl.id = id
|
||||||
|
statInputEl.value = `${value}`
|
||||||
|
statInputEl.addEventListener('input', () => {
|
||||||
|
const value = parseInt(statInputEl.value, 10)
|
||||||
|
statModEl.innerText = this.renderStatMod(value)
|
||||||
|
changeHandler(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
statModEl.innerText = this.renderStatMod(value)
|
||||||
|
|
||||||
|
labelWrapper.appendChild(titleEl)
|
||||||
|
labelWrapper.appendChild(statInputEl)
|
||||||
|
labelWrapper.appendChild(statModEl)
|
||||||
|
|
||||||
|
return labelWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _render (): HTMLElement {
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.classList.add('card-dnd-stats')
|
||||||
|
const stats = this._stats || [10, 10, 10, 10, 10, 10]
|
||||||
|
const titles = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
|
||||||
|
|
||||||
|
stats.forEach((stat, i) => {
|
||||||
|
const title = titles[i]
|
||||||
|
const block = this.createStatBlock(title, stat, newValue => {
|
||||||
|
this._stats[i] = newValue
|
||||||
|
})
|
||||||
|
el.appendChild(block)
|
||||||
|
})
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
public save (): DnDStatsData {
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DnDStats
|
@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
ContentBlock,
|
||||||
|
ContentBlockArgs,
|
||||||
|
ContentBlockConfig,
|
||||||
|
ContentBlockData
|
||||||
|
} from './content-block'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons = [null, icon1, icon2, icon3, icon4, icon5, icon6]
|
||||||
|
|
||||||
|
interface HeadingConfig extends ContentBlockConfig {
|
||||||
|
placeholder?: string;
|
||||||
|
levels?: HeadingLevel[];
|
||||||
|
defaultLevel?: HeadingLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeadingData extends ContentBlockData {
|
||||||
|
text: string;
|
||||||
|
level?: HeadingLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Heading extends ContentBlock {
|
||||||
|
static _supportedTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
|
||||||
|
static _toolboxConfig = { icon, title }
|
||||||
|
|
||||||
|
protected _config: HeadingConfig
|
||||||
|
private defaultLevel: HeadingLevel
|
||||||
|
private currentLevel: HeadingLevel
|
||||||
|
|
||||||
|
constructor (args: ContentBlockArgs) {
|
||||||
|
super(args)
|
||||||
|
this._config = args.config as HeadingConfig
|
||||||
|
|
||||||
|
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.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 HeadingData).text || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
this._settingButtons = this._config.levels.map(level => {
|
||||||
|
return {
|
||||||
|
name: `H${level}`,
|
||||||
|
icon: icons[level] || icon,
|
||||||
|
action: (name: string) => this.setLevel(name),
|
||||||
|
isActive: (name: string): boolean => this.isCurrentLevel(name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public get data (): HeadingData {
|
||||||
|
return this._data as HeadingData
|
||||||
|
}
|
||||||
|
|
||||||
|
public set data (data: HeadingData) {
|
||||||
|
const currentData = this._data as HeadingData
|
||||||
|
|
||||||
|
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 isCurrentLevel (name: string): boolean {
|
||||||
|
const currentLevel = `H${this.currentLevel}`
|
||||||
|
return name === currentLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
private setLevel (name: string) {
|
||||||
|
const level = parseInt(name[1], 10)
|
||||||
|
this.data = { level, text: this._element.innerHTML }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _render (): HTMLElement {
|
||||||
|
const el = document.createElement(`H${this.currentLevel}`)
|
||||||
|
el.innerHTML = this.data.text || ''
|
||||||
|
el.classList.add(this._CSS.block)
|
||||||
|
el.contentEditable = 'true'
|
||||||
|
el.dataset.placeholder = this._config.placeholder || ''
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pasted H1-H6 tags to substitute with header tool
|
||||||
|
protected pasteHandler (element: HTMLHeadingElement): HeadingData {
|
||||||
|
const text = element.innerHTML
|
||||||
|
let level = this.defaultLevel
|
||||||
|
|
||||||
|
const tagMatch = element.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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { level, text }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method that specified how to merge two Text blocks.
|
||||||
|
// Called by Editor.js by backspace at the beginning of the Block
|
||||||
|
public merge (data: HeadingData) {
|
||||||
|
this.data = {
|
||||||
|
text: this.data.text + (data.text || ''),
|
||||||
|
level: this.data.level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract tools data from view
|
||||||
|
public save (toolsContent: HTMLElement): HeadingData {
|
||||||
|
return {
|
||||||
|
text: toolsContent.innerHTML,
|
||||||
|
level: this.currentLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get sanitize () {
|
||||||
|
return { level: {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Heading
|
@ -0,0 +1,4 @@
|
|||||||
|
export { default as Delimiter } from './delimiter'
|
||||||
|
export { default as Heading } from './heading'
|
||||||
|
export { default as Charges } from './charges'
|
||||||
|
export { default as DnDStats } from './dnd-stats'
|
Loading…
Reference in New Issue