save card content, add charges component

master
koehr 4 years ago committed by Norman
parent fb5b5add6e
commit 600af1679a

@ -0,0 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><circle r="7" cx="7" cy="7" stroke="black" stroke-width="2" fill="none" /></svg>

After

Width:  |  Height:  |  Size: 164 B

@ -0,0 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><rect width="14" height="14" stroke="black" stroke-width="2" fill="none" /></svg>

After

Width:  |  Height:  |  Size: 165 B

@ -0,0 +1 @@
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"> <path d="M5.625 4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0-4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0 9.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm-4.5-5a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0-4.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0 9.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25z"/></svg>

After

Width:  |  Height:  |  Size: 488 B

@ -0,0 +1 @@
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"><path d="M5.819 4.607h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 1 1 0-2.138zm0-4.607h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 1 1 0-2.138zm0 9.357h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 0 1 0-2.137zM1.468 4.155V1.33c-.554.404-.926.606-1.118.606a.338.338 0 0 1-.244-.104A.327.327 0 0 1 0 1.59c0-.107.035-.184.105-.234.07-.05.192-.114.369-.192.264-.118.475-.243.633-.373.158-.13.298-.276.42-.438a3.94 3.94 0 0 1 .238-.298C1.802.019 1.872 0 1.975 0c.115 0 .208.042.277.127.07.085.105.202.105.351v3.556c0 .416-.15.624-.448.624a.421.421 0 0 1-.32-.127c-.08-.085-.121-.21-.121-.376zm-.283 6.664h1.572c.156 0 .275.03.358.091a.294.294 0 0 1 .123.25.323.323 0 0 1-.098.238c-.065.065-.164.097-.296.097H.629a.494.494 0 0 1-.353-.119.372.372 0 0 1-.126-.28c0-.068.027-.16.081-.273a.977.977 0 0 1 .178-.268c.267-.264.507-.49.722-.678.215-.188.368-.312.46-.371.165-.11.302-.222.412-.334.109-.112.192-.226.25-.344a.786.786 0 0 0 .085-.345.6.6 0 0 0-.341-.553.75.75 0 0 0-.345-.08c-.263 0-.47.11-.62.329-.02.029-.054.107-.101.235a.966.966 0 0 1-.16.295c-.059.069-.145.103-.26.103a.348.348 0 0 1-.25-.094.34.34 0 0 1-.099-.258c0-.132.031-.27.093-.413.063-.143.155-.273.279-.39.123-.116.28-.21.47-.282.189-.072.411-.107.666-.107.307 0 .569.045.786.137a1.182 1.182 0 0 1 .618.623 1.18 1.18 0 0 1-.096 1.083 2.03 2.03 0 0 1-.378.457c-.128.11-.344.282-.646.517-.302.235-.509.417-.621.547a1.637 1.637 0 0 0-.148.187z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1 @@
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"> <path d="M5.625 4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0-4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0 9.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm-4.5-5a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0-4.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0 9.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25z"/></svg>

After

Width:  |  Height:  |  Size: 488 B

@ -1,5 +1,5 @@
<template>
<main :id="id" class="card-content"></main>
<main ref="cardEl" class="card-content"></main>
</template>
<script lang="ts">
@ -7,7 +7,7 @@ import { Component, Prop, Vue } from 'vue-property-decorator'
import Editor from '@editorjs/editorjs'
import List from '@editorjs/list'
import { Heading, Delimiter } from '@/editor'
import { Heading, Delimiter, Charges } from '@/editor'
@Component
export default class DeckCardEditor extends Vue {
@ -23,16 +23,25 @@ export default class DeckCardEditor extends Vue {
private mounted () {
this.editor = new Editor({
holderId: this.id,
holder: this.$refs.cardEl as HTMLElement,
autofocus: false,
tools: {
// header: Heading,
list: { class: List, inlineToolbar: true },
heading: { class: Heading, inlineToolbar: true },
delimiter: { class: Delimiter, inlineToolbar: false },
heading: { class: Heading, inlineToolbar: true }
charges: { class: Charges, inlineToolbar: false }
},
// data: {},
placeholder: 'Click here to write your card.'
data: this.content,
placeholder: 'Click here to write your card.',
onChange: () => {
console.log('editor change, saving')
this.editor.save().then(value => {
this.$emit('change', { field: 'content', value })
}).catch(error => {
console.error('error saving data', error)
})
}
})
}
}
@ -45,7 +54,7 @@ export default class DeckCardEditor extends Vue {
.card-content .ce-paragraph, .card-content p {
margin: 0;
line-height: 1.2;
line-height: 1.3;
}
.card-content ul {
@ -94,5 +103,28 @@ export default class DeckCardEditor extends Vue {
border-width: 2px 220px 2px 0;
border-color: transparent var(--highlight-color) transparent transparent;
}
.card-content .cdx-list__item {
padding: 0;
line-height: 1.3;
}
.card-content .card-charges-wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
min-height: 1em;
}
.card-content .card-charges-wrapper.card-charges-stretch { justify-content: space-around; }
.card-content .card-charges-wrapper > .card-charge {
width: 1.0em;
height: 1.0em;
border: 2px solid var(--highlight-color);
margin: .5em .2em;
}
.card-content .card-charges-wrapper > .card-charge-circle { border-radius: 100%; }
.card-content .card-charges-wrapper > .card-charge-size-1 { width: 1.0em; height: 1.0em; }
.card-content .card-charges-wrapper > .card-charge-size-2 { width: 1.2em; height: 1.2em; }
.card-content .card-charges-wrapper > .card-charge-size-3 { width: 1.4em; height: 1.4em; }
.card-content .card-charges-wrapper > .card-charge-size-4 { width: 1.6em; height: 1.6em; }
.card-content .card-charges-wrapper > .card-charge-size-5 { width: 1.8em; height: 1.8em; }
[contenteditable="true"] { outline: none; }
</style>

@ -20,7 +20,7 @@
:card-id="card.id"
:active="isSelection"
:content="card.content"
@input="$emit('edit', $event)"
@change="$emit('edit', $event)"
/>
</section>
<section name="card-back" class="card-back">

@ -0,0 +1,137 @@
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)
console.log('new charges', 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())
}
console.log('rendered', this._amount, 'charges', el)
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

@ -38,7 +38,15 @@ export interface CSSClasses {
}
export interface ContentBlockData extends BlockToolData {
text: string;
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 {
@ -97,7 +105,7 @@ export class ContentBlock implements BlockTool {
el.classList.add(this._CSS.block)
el.dataset.placeholder = this._placeholder
el.addEventListener('keyup', this.onKeyUp)
el.innerHTML = this.data.text
el.innerHTML = this.data.text || ''
el.contentEditable = 'true'
return el
@ -112,7 +120,7 @@ export class ContentBlock implements BlockTool {
// Called by Editor.js by backspace at the beginning of the Block
public merge (data: ContentBlockData) {
this.data = {
text: this.data.text + data.text
text: (this.data.text || '') + data.text
}
}
@ -129,17 +137,14 @@ export class ContentBlock implements BlockTool {
}
}
// On paste callback fired from Editor.
public onPaste (event: HTMLPasteEvent) {
this.data = {
text: event.detail.data.innerHTML
}
public get CSS (): CSSClasses {
return this._CSS
}
/**
* Enable Conversion Toolbar. Paragraph can be converted to/from other tools
*/
static get conversionConfig () {
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
@ -197,6 +202,17 @@ export class ContentBlock implements BlockTool {
}
}
// 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

@ -4,7 +4,13 @@ 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 = [
@ -12,13 +18,17 @@ class Delimiter extends ContentlessBlock {
{ 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)
}
}
@ -29,6 +39,12 @@ class Delimiter extends ContentlessBlock {
return el
}
public save (): DelimiterData {
return {
variant: this._variant
}
}
static get toolbox () {
return { icon, title }
}

@ -2,8 +2,7 @@ import {
ContentBlock,
ContentBlockArgs,
ContentBlockConfig,
ContentBlockData,
HTMLPasteEvent
ContentBlockData
} from './content-block'
import icon from '../assets/editor/header.svg.txt'
@ -33,7 +32,7 @@ interface HeadingConfig extends ContentBlockConfig {
defaultLevel?: HeadingLevel;
}
interface HeaderData extends ContentBlockData {
interface HeadingData extends ContentBlockData {
text: string;
level?: HeadingLevel;
}
@ -65,7 +64,7 @@ class Heading extends ContentBlock {
// setting data will rerender the element with the right settings
this.data = {
level: this.currentLevel,
text: (args.data as HeaderData).text || ''
text: (args.data as HeadingData).text || ''
}
this._settingButtons = this._config.levels.map(level => {
@ -78,12 +77,12 @@ class Heading extends ContentBlock {
})
}
public get data () {
return this._data
public get data (): HeadingData {
return this._data as HeadingData
}
public set data (data: HeaderData) {
const currentData = this._data as HeaderData
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 || ''
@ -118,12 +117,11 @@ class Heading extends ContentBlock {
}
// Handle pasted H1-H6 tags to substitute with header tool
public onPaste (event: HTMLPasteEvent) {
const content = event.detail.data
const text = content.innerHTML
protected pasteHandler (element: HTMLHeadingElement): HeadingData {
const text = element.innerHTML
let level = this.defaultLevel
const tagMatch = content.tagName.match(/H(\d)/)
const tagMatch = element.tagName.match(/H(\d)/)
if (tagMatch) level = parseInt(tagMatch[1], 10)
// Fallback to nearest level when specified not available
@ -133,12 +131,12 @@ class Heading extends ContentBlock {
})
}
this.data = { level, text }
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: HeaderData) {
public merge (data: HeadingData) {
this.data = {
text: this.data.text + (data.text || ''),
level: this.data.level
@ -146,7 +144,7 @@ class Heading extends ContentBlock {
}
// extract tools data from view
public save (toolsContent: HTMLElement): HeaderData {
public save (toolsContent: HTMLElement): HeadingData {
return {
text: toolsContent.innerHTML,
level: this.currentLevel

@ -1,207 +0,0 @@
import { ToolConstructable, ToolConfig, HTMLPasteEvent } from '@editorjs/editorjs'
import { BlockToolExt, BlockToolArgs } from './block-tool'
import icon from '../assets/editor/header.svg.txt'
import icon1 from '../assets/editor/header1.svg.txt'
import icon2 from '../assets/editor/header2.svg.txt'
import icon3 from '../assets/editor/header3.svg.txt'
import icon4 from '../assets/editor/header4.svg.txt'
import icon5 from '../assets/editor/header5.svg.txt'
import icon6 from '../assets/editor/header6.svg.txt'
const title = 'Heading'
interface PasteConfig {
tags: string[];
}
enum HeadingLevel {
One = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6
}
interface HeaderConfig extends ToolConfig {
placeholder?: string;
levels?: HeadingLevel[];
defaultLevel?: HeadingLevel;
}
interface HeaderData {
text?: string;
level?: HeadingLevel;
}
class Heading extends BlockToolExt {
protected _config: HeaderConfig
protected settingsButtons: HTMLElement[] = []
private levels: HeadingLevel[]
private defaultLevel: HeadingLevel
private currentLevel: HeadingLevel
private icons: Map<HeadingLevel, string> = new Map([
[HeadingLevel.One, icon1],
[HeadingLevel.Two, icon2],
[HeadingLevel.Three, icon3],
[HeadingLevel.Four, icon4],
[HeadingLevel.Five, icon5],
[HeadingLevel.Six, icon6]
])
constructor (args: BlockToolArgs) {
super(args)
this._config = args.config as HeaderConfig
if (this._config.levels === undefined) {
this._config.levels = [HeadingLevel.Two, HeadingLevel.Three]
}
if (this._config.defaultLevel === undefined) {
this._config.defaultLevel = HeadingLevel.Two
}
if (this._config.levels.indexOf(this._config.defaultLevel) === -1) {
console.warn('(ง\'̀-\'́)ง Heading Tool: the default level specified was not found in available levels')
}
this.levels = this._config.levels
this.defaultLevel = this._config.defaultLevel
this.currentLevel = this.defaultLevel
// setting data will rerender the element with the right settings
this.data = {
level: this.currentLevel,
text: (args.data as HeaderData).text || ''
}
}
public renderSettings (): HTMLElement {
const wrapper = document.createElement('DIV')
this.levels.forEach(level => {
const { settingsButton, settingsButtonActive } = this._CSS
const btn = document.createElement('SPAN')
btn.classList.add(settingsButton)
btn.dataset.level = `${level}`
btn.innerHTML = this.icons.get(level) || icon
if (this.currentLevel === level) btn.classList.add(settingsButtonActive)
btn.addEventListener('click', () => this.setLevel(level))
wrapper.appendChild(btn)
this.settingsButtons.push(btn)
})
return wrapper
}
public get data (): HeaderData {
return this._data as HeaderData
}
public set data (data: HeaderData) {
const currentData = this._data as HeaderData
if (data.level === undefined) data.level = currentData.level || this.defaultLevel
if (data.text === undefined) data.text = currentData.text || ''
this._data = data
this.currentLevel = data.level
const newHeader = this._render()
if (this._element.parentNode) {
this._element.parentNode.replaceChild(newHeader, this._element)
}
this._element = newHeader
}
private setLevel (level: HeadingLevel) {
this.data = { level, text: this._element.innerHTML }
}
protected get _CSS () {
return {
block: this.api.styles.block,
settingsButton: this.api.styles.settingsButton,
settingsButtonActive: this.api.styles.settingsButtonActive,
wrapper: 'card-heading'
}
}
protected _render (): HTMLElement {
console.log('render', `H${this.currentLevel}`, this.data)
const el = document.createElement(`H${this.currentLevel}`)
el.innerHTML = this.data.text || ''
el.classList.add(this._CSS.wrapper, this._CSS.block)
el.contentEditable = 'true'
el.dataset.placeholder = this._config.placeholder || ''
return el
}
// Handle pasted H1-H6 tags to substitute with header tool
onPaste (event: HTMLPasteEvent) {
const content = event.detail.data
const text = content.innerHTML
let level = this.defaultLevel
const tagMatch = content.tagName.match(/H(\d)/)
if (tagMatch) level = parseInt(tagMatch[1], 10)
// Fallback to nearest level when specified not available
if (this._config.levels) {
level = this._config.levels.reduce((prevLevel, currLevel) => {
return Math.abs(currLevel - level) < Math.abs(prevLevel - level) ? currLevel : prevLevel
})
}
this.data = { level, text }
}
// Used by Editor.js paste handling API.
// Provides configuration to handle H1-H6 tags.
static get pasteConfig (): PasteConfig {
return {
tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
}
}
// Method that specified how to merge two Text blocks.
// Called by Editor.js by backspace at the beginning of the Block
public merge (data: HeaderData) {
this.data = {
text: this.data.text + (data.text || ''),
level: this.data.level
}
}
// validate text block data
validate (blockData: HeaderData): boolean {
if (!blockData.text) return false
return blockData.text.trim() !== ''
}
// extract tools data from view
save (toolsContent: HTMLElement): HeaderData {
return {
text: toolsContent.innerHTML,
level: this.currentLevel
}
}
// Allow Heading to be converted to/from other blocks
static get conversionConfig () {
return {
export: 'text', // use 'text' property for other blocks
import: 'text' // fill 'text' property from other block's export string
}
}
static get sanitize (): object {
return { level: {} }
}
static get toolbox () {
return { icon, title }
}
}
export default Heading as ToolConstructable

@ -1,2 +1,4 @@
export { default as Delimiter } from './delimiter'
export { default as Heading } from './heading'
export { default as List } from './list'
export { default as Charges } from './charges'

@ -0,0 +1,254 @@
import {
ContentBlock,
ContentBlockArgs,
ContentBlockConfig,
ContentBlockData,
ConversionConfig
} from './content-block'
import icon from '../assets/editor/list.svg.txt'
import iconUL from '../assets/editor/list_unordered.svg.txt'
import iconOL from '../assets/editor/list_ordered.svg.txt'
const title = 'List'
enum ListStyle {
Ordered = 'OL',
Unordered = 'UL'
}
const icons = {
[ListStyle.Ordered]: iconOL,
[ListStyle.Unordered]: iconUL
}
interface ListConfig extends ContentBlockConfig {
placeholder?: string;
styles: ListStyle[];
defaultStyle: ListStyle;
}
interface ListData extends ContentBlockData {
style: ListStyle;
items: string[];
}
interface ListConversionConfig extends ConversionConfig {
import: (str: string) => ListData;
export: (data: ListData) => string;
}
class List extends ContentBlock {
static _supportedTags = ['UL', 'OL', 'LI']
static _toolboxConfig = { icon, title }
protected _config: ListConfig
private defaultStyle: ListStyle
private currentStyle: ListStyle
constructor (args: ContentBlockArgs) {
super(args)
this._config = args.config as ListConfig
if (this._config.styles === undefined) {
this._config.styles = [ListStyle.Unordered, ListStyle.Ordered]
}
if (this._config.defaultStyle === undefined) {
this._config.defaultStyle = ListStyle.Unordered
}
if (this._config.styles.indexOf(this._config.defaultStyle) === -1) {
console.warn('(ง\'̀-\'́)ง List Tool: the default style specified was not found in available styles')
}
this.defaultStyle = this._config.defaultStyle
this.currentStyle = this.defaultStyle
this.data = {
style: this.currentStyle,
items: (args.data as ListData).items
}
this._settingButtons = this._config.styles.map(listStyle => {
return {
name: listStyle,
icon: icons[listStyle] || icon,
action: (name: string) => this.setStyle(name as ListStyle),
isActive: (name: string): boolean => name === this.currentStyle
}
})
}
public get data (): ListData {
// first render
if (!this._element) return this._data as ListData
const items = this.queryItems()
const data = this._data as ListData
data.items = Array.from(items).map(item => item.innerText)
return data
}
public set data (data: ListData) {
const currentData = this._data as ListData
if (data.style === undefined) data.style = currentData.style || this.defaultStyle
if (data.items === undefined) data.items = currentData.items || []
this._data = data
this.currentStyle = data.style
const newList = this._render()
if (this._element.parentNode) {
this._element.parentNode.replaceChild(newList, this._element)
}
this._element = newList
}
private setStyle (style: ListStyle) {
this.data = { style, items: this.data.items || [] }
}
protected _render (): HTMLElement {
const el = document.createElement(this.currentStyle)
el.classList.add('cdx-list', this._CSS.block)
el.contentEditable = 'true'
el.dataset.placeholder = this._config.placeholder || ''
const items = this.data.items
if (!items || !items.length) {
const li = document.createElement('LI')
li.innerText = ''
li.classList.add('cdx-list__item')
el.appendChild(li)
} else {
items.forEach(item => {
const li = document.createElement('LI')
li.innerText = item
li.classList.add('cdx-list__item')
el.appendChild(li)
})
}
el.addEventListener('keydown', event => {
// on pressing Enter
if (event.keyCode === 13) return this.getOutOfList(event)
// on pressing Backspace
if (event.keyCode === 8) return this.backspace(event)
}, false)
return el
}
private queryItems (): NodeListOf<HTMLLIElement> {
return this._element.querySelectorAll('.cdx-list__item')
}
private get currentItem (): Element | undefined {
const selection = window.getSelection()
if (selection === null) return
let currentNode = selection.anchorNode
if (currentNode === null) return
if (currentNode.nodeType !== Node.ELEMENT_NODE) {
currentNode = currentNode.parentNode
}
return (currentNode as HTMLElement).closest('.cdx-list__item') || undefined
}
// leave list by pressing enter on an empty list item
private getOutOfList (event: KeyboardEvent) {
const items = this.queryItems()
if (items.length < 2) return
const lastItem = items[items.length - 1]
const currentItem = this.currentItem
// prevent generation of new li if last li is empty
if (currentItem === lastItem && !lastItem.innerText.trim().length) {
if (!currentItem.parentElement) return // somethings really wrong
currentItem.parentElement.removeChild(currentItem)
this.api.blocks.insertNewBlock()
event.preventDefault()
event.stopPropagation()
}
}
public backspace (event: KeyboardEvent) {
const items = this.queryItems()
const firstItem = items[0]
if (firstItem === undefined) return
if (items.length < 2 && !firstItem.innerText.trim()) event.preventDefault()
}
public selectItem (event: KeyboardEvent) {
event.preventDefault()
const selection = window.getSelection()
if (selection === null) return // no selection, no problem
const currentNode = selection.anchorNode?.parentNode
if (currentNode === null) return
const currentItem = (currentNode as Element).closest('.cdx-list__item')
if (currentItem === null) return
const range = new Range()
range.selectNodeContents(currentItem)
selection.removeAllRanges()
selection.addRange(range)
}
protected pasteHandler (element: HTMLUListElement | HTMLOListElement | HTMLLIElement): ListData {
const tag = element.tagName
const style = tag === ListStyle.Ordered ? ListStyle.Ordered : ListStyle.Unordered
const data: ListData = { style, items: [] }
if (tag === 'LI') { // does this really happen?
data.items.push(element.innerText)
} else {
const items = Array.from(element.querySelectorAll('LI'))
data.items = items.map(item => (item as HTMLElement).innerText).filter(item => !!item.trim())
}
return data
}
// allow to use native enter behavior
static get enableLineBreaks (): boolean {
return true
}
static get sanitize () {
return {
style: {},
items: {
br: true
}
}
}
static get conversionConfig (): ListConversionConfig {
return {
export: data => {
return data.items.join(' * ')
},
import: str => {
return {
items: [str],
style: ListStyle.Unordered
}
}
}
}
public save (): ListData {
console.log('saving list', this.data)
return this.data
}
}
export default List

@ -45,6 +45,10 @@ export function defaultCard (): Card {
count: 1,
tags: [],
icon: 'robe',
content: []
content: {
time: Date.now(),
blocks: [],
version: '2.17.0'
}
}
}

18
src/types.d.ts vendored

@ -2,19 +2,15 @@ interface KV<V> {
[key: string]: V;
}
interface TextNode {
type: 'text';
text: string;
}
interface SimpleNode {
interface ContentBlock {
type: string;
data: object;
}
interface ContentNode {
type: string;
content: (ContentNode | TextNode | SimpleNode)[];
attrs?: object;
interface CardContent {
time: number;
blocks: ContentBlock[];
version: string;
}
interface Card {
@ -23,7 +19,7 @@ interface Card {
count: number;
tags: string[];
icon: string;
content: (ContentNode | SimpleNode)[];
content: CardContent;
backIcon?: string;
color?: string;
}

@ -86,22 +86,29 @@ export default class DeckView extends Vue {
if (this.deck === null) return
const newCard = defaultCard()
newCard.content = [{
newCard.content = {
time: Date.now(),
blocks: [{
type: 'heading',
attrs: { level: 2 },
content: [{
type: 'text',
text: 'feel free to edit this card'
}]
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: 'horizontal_rule'
type: 'paragraph',
data: { text: ' ' }
}, {
type: 'paragraph',
content: [{
type: 'text',
text: 'This is a rich-text editor, so you can basically do whatever you want.'
}]
}]
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'
}
this.deck.cards.push(newCard)
this.$storage.persist()

Loading…
Cancel
Save