adds editorjs, fixes cards array reference bug

vue3
koehr 4 years ago
parent 35743b54e7
commit bf8c166a71

@ -15,6 +15,8 @@
"vue-router": "4.0.0-alpha.12"
},
"devDependencies": {
"@editorjs/editorjs": "^2.18.0",
"@editorjs/list": "^1.5.0",
"@vue/compiler-sfc": "3.0.0-beta.15",
"copy-webpack-plugin": "^6.0.2",
"css-loader": "^3.6.0",

@ -1,8 +1,6 @@
.card-front, .card-back {
.card-front {
display: flex;
flex-flow: column nowrap;
}
.card-front {
justify-content: flex-start;
}
.card-front > header {
@ -41,13 +39,6 @@
overflow: hidden;
}
.card-back {
justify-content: center;
}
.card-back > .icon-wrapper {
margin: 3em;
}
.card-content .cdx-block {
padding: 0;
}

@ -9,13 +9,12 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { KV, ICard } from '@/types'
import { CardSize, defaultCardSize } from '@/consts'
import { cardSizeToStyle } from '@/lib/card'
import { KV } from '@/types'
import { cardCSSVars } from '@/lib/card'
import iconPath from '@/lib/iconPath'
export default defineComponent({
name: 'Card',
name: 'CardBack',
props: {
icon: String,
color: String,
@ -26,19 +25,8 @@ export default defineComponent({
const icon = this.icon || 'plus'
return iconPath(icon)
},
showBackSide (): boolean {
return true
},
showFrontSide (): boolean {
return false
},
cssVars (): KV<string> {
const backgroundColor = this.color || 'transparent'
const size = this.size as CardSize || defaultCardSize
return {
backgroundColor,
...cardSizeToStyle(size)
}
return cardCSSVars(this.size, this.color)
}
}
})
@ -48,7 +36,7 @@ export default defineComponent({
.card-back {
display: flex;
flex-flow: column nowrap;
justify-content: space-evenly;
justify-content: center;
text-align: center;
line-height: 4rem;
font-size: 2rem;
@ -67,7 +55,7 @@ export default defineComponent({
display: none;
}
.card-back > .icon-wrapper {
width: 90%;
width: 70%;
margin: auto;
}
</style>

@ -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'

@ -1,16 +1,34 @@
import { CardSize } from '../consts'
import { ICard } from '../types'
import { CardSize, defaultCardSize } from '../consts'
import { KV, ICard } from '../types'
import randomId from './randomId'
export function defaultCard (): ICard {
export function defaultCard (icon = 'robe', name = 'no title yet'): ICard {
return {
id: randomId(),
name: 'no title yet',
name,
icon,
tags: [],
icon: 'robe',
content: {
time: Date.now(),
blocks: [],
blocks: [{
type: 'heading',
data: {
text: 'Next Level RPG Card',
level: 2
}
}, {
type: 'delimiter',
data: { variant: 'pointing-left' }
}, {
type: 'paragraph',
data: { text: 'This card is a rich text editor so you can basically do whatever you want.' }
}, {
type: 'paragraph',
data: { text: ' ' }
}, {
type: 'paragraph',
data: { text: 'You see that delimiter over there? It seems to be wrong, or maybe you like it that way. In any way you can change it by clicking on it and then on the little tool button on the right.' }
}],
version: '2.17.0'
}
}
@ -20,11 +38,12 @@ export function cardWHFromSize (size: CardSize): number[] {
return size.split('x').map(v => parseFloat(v))
}
export function cardSizeToStyle (size: CardSize): { width: string } {
const [w, h] = cardWHFromSize(size)
export function cardCSSVars (size?: CardSize | string, color?: string): KV<string> {
const [w, h] = cardWHFromSize(size as CardSize || defaultCardSize)
const ratio = w / h
return {
backgroundColor: color || 'transparent',
width: `calc(var(--card-height) * ${ratio})`
}
}

@ -17,6 +17,7 @@ export const defaultDeckValues: IDeck = {
export function defaultDeck (): IDeck {
const newDeck = { ...defaultDeckValues }
newDeck.cards = [] // make sure not to copy a reference
newDeck.id = randomId()
return newDeck
}

8
src/shims.d.ts vendored

@ -3,3 +3,11 @@ declare module "*.vue" {
const Component: ReturnType<typeof defineComponent>
export default Component
}
declare module '*.txt' {
const content: string
export default content
}
declare module '@editorjs/paragraph'
declare module '@editorjs/list'

@ -1,4 +1,4 @@
import { reactive, ref } from 'vue'
import { ref } from 'vue'
import { State, KV } from '../types'
import { DeckDB } from '../storage'
import { defaultDeck } from '../lib/deck'

@ -18,7 +18,7 @@
</header>
<section name="deck-cards" class="cards" :class="{ centered: deck.cards.length === 0 }">
<CardBack v-for="card in deck.cards"
<FlipCard v-for="card in deck.cards"
:key="card.id"
:id="card.id"
:card="card"
@ -37,12 +37,13 @@ import { useRoute } from 'vue-router'
import { IDeck } from '@/types'
import state from '@/state'
import iconPath from '@/lib/iconPath'
import FlipCard from '@/components/FlipCard.vue'
import CardBack from '@/components/CardBack.vue'
const name = 'Deck'
export default defineComponent({
components: { CardBack },
components: { FlipCard, CardBack },
setup () {
const route = useRoute()

@ -28,6 +28,19 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@editorjs/editorjs@^2.18.0":
version "2.18.0"
resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.18.0.tgz#bde42183013f5ca98693e77986cc9d8b2c9a1244"
integrity sha512-9OKs580JFKoXCAw7llb19E+qxY6QuzgDBq50cKbyOS1Lt+BglTq/zBdXxmRWNRTlCMxjTB1vgnq70+OjEyDSlw==
dependencies:
codex-notifier "^1.1.2"
codex-tooltip "^1.0.1"
"@editorjs/list@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@editorjs/list/-/list-1.5.0.tgz#8675c82caa25f50744a4b072a7911163ce26bd74"
integrity sha512-LzZuJwJ2HxCkuaPrp3zYdQGvMC8dzXjewqWEBZ9mpq0fVwBAse4o9QB2mWvJxZ93UtLqQE7f9vrbHotG2uW9Qg==
"@jimp/bmp@^0.9.8":
version "0.9.8"
resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.9.8.tgz#5933ab8fb359889bec380b0f7802163374933624"
@ -1484,6 +1497,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.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.1.tgz#f6e4f39d81507f9c455b667f1287746d14ee8056"
integrity sha512-1xLb1NZbxguNtf02xBRhDphq/EXvMMeEbY0ievjQTHqf8UjXsD41evGk9rqcbjpl+JOjNgtwnp1OaU/X/h6fhQ==
collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"

Loading…
Cancel
Save