From bf8c166a710c0e71b9b08663a16432de153fa4a9 Mon Sep 17 00:00:00 2001 From: koehr Date: Mon, 6 Jul 2020 20:56:17 +0200 Subject: [PATCH] adds editorjs, fixes cards array reference bug --- package.json | 2 + src/assets/card.css | 11 +- src/components/CardBack.vue | 24 +--- src/components/CardFront.vue | 68 ++++++++++ src/components/FlipCard.vue | 117 +++++++++++++++-- src/editor/charges.ts | 134 +++++++++++++++++++ src/editor/content-block.ts | 222 ++++++++++++++++++++++++++++++++ src/editor/contentless-block.ts | 73 +++++++++++ src/editor/delimiter.ts | 53 ++++++++ src/editor/dnd-stats.ts | 106 +++++++++++++++ src/editor/heading.ts | 159 +++++++++++++++++++++++ src/editor/index.ts | 4 + src/lib/card.ts | 35 +++-- src/lib/deck.ts | 1 + src/shims.d.ts | 8 ++ src/state/index.ts | 2 +- src/views/Deck.vue | 5 +- yarn.lock | 23 ++++ 18 files changed, 998 insertions(+), 49 deletions(-) create mode 100644 src/components/CardFront.vue create mode 100644 src/editor/charges.ts create mode 100644 src/editor/content-block.ts create mode 100644 src/editor/contentless-block.ts create mode 100644 src/editor/delimiter.ts create mode 100644 src/editor/dnd-stats.ts create mode 100644 src/editor/heading.ts create mode 100644 src/editor/index.ts diff --git a/package.json b/package.json index e62d8e1..ec8e5f8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/assets/card.css b/src/assets/card.css index a7f403b..acf9493 100644 --- a/src/assets/card.css +++ b/src/assets/card.css @@ -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; } diff --git a/src/components/CardBack.vue b/src/components/CardBack.vue index c81e766..ace96c3 100644 --- a/src/components/CardBack.vue +++ b/src/components/CardBack.vue @@ -9,13 +9,12 @@ + + diff --git a/src/editor/charges.ts b/src/editor/charges.ts new file mode 100644 index 0000000..c171faf --- /dev/null +++ b/src/editor/charges.ts @@ -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 diff --git a/src/editor/content-block.ts b/src/editor/content-block.ts new file mode 100644 index 0000000..b6e0a48 --- /dev/null +++ b/src/editor/content-block.ts @@ -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: '', + 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
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 diff --git a/src/editor/contentless-block.ts b/src/editor/contentless-block.ts new file mode 100644 index 0000000..7e88964 --- /dev/null +++ b/src/editor/contentless-block.ts @@ -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: '', title: 'UnnamedPlugin' } + } +} + +export default ContentlessBlock diff --git a/src/editor/delimiter.ts b/src/editor/delimiter.ts new file mode 100644 index 0000000..f378ac9 --- /dev/null +++ b/src/editor/delimiter.ts @@ -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 diff --git a/src/editor/dnd-stats.ts b/src/editor/dnd-stats.ts new file mode 100644 index 0000000..485b44d --- /dev/null +++ b/src/editor/dnd-stats.ts @@ -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 diff --git a/src/editor/heading.ts b/src/editor/heading.ts new file mode 100644 index 0000000..06d8a86 --- /dev/null +++ b/src/editor/heading.ts @@ -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 diff --git a/src/editor/index.ts b/src/editor/index.ts new file mode 100644 index 0000000..354f460 --- /dev/null +++ b/src/editor/index.ts @@ -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' diff --git a/src/lib/card.ts b/src/lib/card.ts index 7cb421f..a9fbddc 100644 --- a/src/lib/card.ts +++ b/src/lib/card.ts @@ -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 { + const [w, h] = cardWHFromSize(size as CardSize || defaultCardSize) const ratio = w / h return { + backgroundColor: color || 'transparent', width: `calc(var(--card-height) * ${ratio})` } } diff --git a/src/lib/deck.ts b/src/lib/deck.ts index fce5fd5..04588f9 100644 --- a/src/lib/deck.ts +++ b/src/lib/deck.ts @@ -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 } diff --git a/src/shims.d.ts b/src/shims.d.ts index 1b483ad..b554df7 100644 --- a/src/shims.d.ts +++ b/src/shims.d.ts @@ -3,3 +3,11 @@ declare module "*.vue" { const Component: ReturnType export default Component } + +declare module '*.txt' { + const content: string + export default content +} + +declare module '@editorjs/paragraph' +declare module '@editorjs/list' diff --git a/src/state/index.ts b/src/state/index.ts index ffac4be..bdf5299 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -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' diff --git a/src/views/Deck.vue b/src/views/Deck.vue index 8cb60f4..152131f 100644 --- a/src/views/Deck.vue +++ b/src/views/Deck.vue @@ -18,7 +18,7 @@
-