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>
|
||||
<div :id="card && card.id" class="flip-card card" :style="cssVars">
|
||||
<section name="card-front" class="card-front" v-if="showFrontSide">
|
||||
<span>Front Side</span>
|
||||
</section>
|
||||
<section name="card-back" class="card-back" v-if="showBackSide">
|
||||
<div class="icon-wrapper">
|
||||
<img :src="iconPath" alt="card icon" />
|
||||
</div>
|
||||
<footer><slot name="back"></slot></footer>
|
||||
</section>
|
||||
<div class="flip-card card">
|
||||
<CardFront :card="card" :icon="icon" :color="color" :size="size" />
|
||||
<CardBack :icon="card.icon || icon" :color="card.color || color" :size="size">
|
||||
<button @click="$emit('click')">edit card</button>
|
||||
<button @click="$emit('delete')">delete card</button>
|
||||
</CardBack>
|
||||
</div>
|
||||
</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