Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: different level format #40

Merged
merged 1 commit into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"printWidth": 80,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": false,
Expand Down
16 changes: 14 additions & 2 deletions src/BlockHub/Block/Block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MapStore } from '@Kernel/Store/MapStore'
import { blockHub } from '../BlockHub'
import { RichTextController } from '@RichText/RichText'
import { AttributeValue } from '@Kernel/Store/TextStore'
import { intersectAttributes } from '@Utils/intersectAttributes'

export class Block {
static id = 0
Expand Down Expand Up @@ -77,13 +78,24 @@ export class Block {
this.props.set('rotate', rotate)
}

getBlockFormat() {
const controllers = Object.values(this.controllerMap)
const attributesList = controllers.map((controller) => controller.getCommonAttributes())
return intersectAttributes(attributesList)
}

formatBlock(name: string, value: AttributeValue) {
for (const controller of Object.values(this.controllerMap)) {
controller.formatAll(name, value)
controller.format(name, value)
}
}

getController(...params: any) {
getController(...params: any): RichTextController {
// should be overridden by subclass
return {
isFocus: () => false,
getCommonAttributes: () => ({}),
format: (name: string, value: AttributeValue) => {},
}
}
}
23 changes: 3 additions & 20 deletions src/BlockHub/TableBlock/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { type TableBlock } from './TableBlock'
import RichText from '@RichText/RichText.vue'
import { type ArrayStore } from '@Kernel/Store/ArrayStore'
import type { AttributeValue, TextStore } from '@Kernel/Store/TextStore'
import type { TextStore } from '@Kernel/Store/TextStore'
import { shallowRef } from 'vue'

const { block } = defineProps<{
Expand All @@ -17,34 +17,17 @@ for (const row of block.data) {
}
tableData.value.push(rowData as Array<TextStore>)
}

function formatBlock(name: string, value: AttributeValue) {
block.formatBlock(name, value)
}

function format(name: string, value: AttributeValue) {
block.getController().format(name, value)
}
</script>

<template>
<div
class="table absolute"
class="table absolute border border-dashed border-secondary-border"
:style="{
left: `${block.x}px`,
top: `${block.y}px`,
padding: '30px',
}"
>
<button
class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2"
@click="formatBlock('bold', true)"
>
bold block
</button>
<button class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2" @click="format('bold', true)">
bold
</button>
<input type="color" @input="(event) => format('color', (event.target as HTMLInputElement).value)" />
<div class="row" v-for="(row, rowIndex) of tableData">
<div class="cell inline-block border w-[100px]" v-for="(cell, columnIndex) of row">
<RichText
Expand Down
64 changes: 1 addition & 63 deletions src/BlockHub/TextBoxBlock/TextBox.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import { type AttributeValue } from '@Kernel/Store/TextStore'
import { type TextBoxBlock } from './TextBoxBlock'
import RichText from '@RichText/RichText.vue'
import { ref } from 'vue'
Expand All @@ -10,14 +9,6 @@ const { block } = defineProps<{

const { x, y, textStore, bindController } = block

function formatBlock(name: string, value: AttributeValue) {
block.formatBlock(name, value)
}

function format(name: string, value: AttributeValue) {
block.getController().format(name, value)
}

const width = ref(block.width)
const height = ref(block.height)
block.props.events.update.on(({ key, to }) => {
Expand All @@ -37,62 +28,9 @@ block.props.events.update.on(({ key, to }) => {
top: `${y}px`,
width: `${width}px`,
height: `${height}px`,
padding: '30px',
}"
>
<button
class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2"
@click="formatBlock('bold', true)"
>
bold block
</button>
<button
class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2"
@click="formatBlock('italic', true)"
>
italic block
</button>
<button
class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2"
@click="formatBlock('underline', true)"
>
underline block
</button>
<button
class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2"
@click="formatBlock('strike', true)"
>
strike block
</button>
<br />
<button class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2" @click="format('bold', true)">
bold
</button>
<button class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2" @click="format('italic', true)">
italic
</button>
<button
class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2"
@click="format('underline', true)"
>
underline
</button>
<button class="border border-primary rounded-sm text-primary bg-white hover:bg-primary hover:text-white m-2 px-2" @click="format('strike', true)">
strike
</button>
<select @change="(event) => format('fontSize', (event.target as HTMLSelectElement).value)">
<option v-for="option in [14, 20, 30, 50, 100]" :value="option">
{{ option }}
</option>
</select>
<input type="color" @input="(event) => format('color', (event.target as HTMLInputElement).value)" />
<input type="color" @input="(event) => format('background', (event.target as HTMLInputElement).value)" />

<br />
width
<input type="range" value="500" min="0" max="1000" @input="(event) => (block.width = Number((event.target as HTMLInputElement).value))" /><br />
height
<input type="range" value="200" min="0" max="400" @input="(event) => (block.height = Number((event.target as HTMLInputElement).value))" /><br />

<RichText :textStore="textStore" :bindController="bindController" />
</div>
</template>
18 changes: 18 additions & 0 deletions src/Kernel/Store/TextStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EventManager } from '@Kernel/EventManager'
import { Command } from '@Kernel/HistoryManager'
import { history } from '@Kernel/index'
import { intersectAttributes } from '@Utils/intersectAttributes'

export type AttributeValue = string | number | boolean
interface Attributes {
Expand All @@ -26,6 +27,18 @@ export class TextStore {
if (atom.text.length === 0) {
continue
}

if (atom.text === '\n') {
result.push(currentAtom)
currentAtom = atom
continue
}
if (currentAtom.text === '\n') {
result.push(currentAtom)
currentAtom = this._store[i]
continue
}

if (JSON.stringify(currentAtom.attributes) === JSON.stringify(atom.attributes)) {
currentAtom.text += atom.text
} else {
Expand Down Expand Up @@ -153,6 +166,11 @@ export class TextStore {
return structuredClone(this._store)
}

get commonAttributes() {
const attributesList = this.atoms.map((atom) => atom.attributes)
return intersectAttributes(attributesList)
}

insert(index: number, atom: TextAtom) {
const command = new Command(
() => this._insertAtom(index, atom),
Expand Down
37 changes: 27 additions & 10 deletions src/RichText/RichText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { EventManager } from '@Kernel/EventManager'
import { kernel } from '@Kernel/index'

export interface RichTextController {
isFocus(): boolean
getCommonAttributes(): { [key: string]: AttributeValue }
format(name: string, value: AttributeValue): void
formatAll(name: string, value: AttributeValue): void
}

export class RichText {
element?: HTMLElement
focus: boolean = false
textStore: TextStore
eventHandler = new EventHandler(this)
selectionHandler = new SelectionHandler(this)
Expand All @@ -21,6 +23,7 @@ export class RichText {

events = {
selectChange: new EventManager<Selection>(),
formatChange: new EventManager<Selection>(),
}

constructor(textStore: TextStore) {
Expand All @@ -32,6 +35,13 @@ export class RichText {
atoms: atoms,
})
})
this.events.formatChange.on((selection) => {
const atoms = textStore.getAtoms(selection.index, selection.length)
kernel.richTextObserver.emit({
selection: selection,
atoms: atoms,
})
})
}

mount(element: HTMLElement) {
Expand All @@ -41,19 +51,26 @@ export class RichText {

get controller(): RichTextController {
return {
format: (name: string, value: AttributeValue) => {
const { index, length } = this.getSelection()
this.textStore.format(index, length, {
[name]: value,
})
this.setSelectionByInput({ index, length })
isFocus: () => {
return this.focus
},
getCommonAttributes: () => {
return this.textStore.commonAttributes
},
formatAll: (name: string, value: AttributeValue) => {
const index = 0
const length = this.textStore.length
format: (name: string, value: AttributeValue) => {
const selectedLength = this.getSelection().length
let { index, length } = this.getSelection()
if (selectedLength === 0) {
index = 0
length = this.textStore.length
}
this.textStore.format(index, length, {
[name]: value,
})
if (selectedLength > 0) {
this.setSelectionByInput({ index, length })
}
this.events.formatChange.emit({ index, length })
},
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/RichText/RichText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ history.events.update.on((eventType) => {
</script>

<template>
<div ref="richTextRef" contenteditable="true" class="rich-text h-full focus:outline-none whitespace-break-spaces" spellcheck="false">
<div
ref="richTextRef"
contenteditable="true"
class="rich-text h-full focus:outline-none whitespace-break-spaces"
spellcheck="false"
>
<Row v-for="row of rows" :atoms="row" />
</div>
</template>
3 changes: 2 additions & 1 deletion src/RichText/components/Atom.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const { atom } = defineProps<{
:style="{
color: atom.attributes.color as string,
background: atom.attributes.background as string,
fontSize: `${atom.attributes.fontSize}px`
fontFamily: `${atom.attributes.fontFamily ?? 'sans-serif'}`,
fontSize: `${atom.attributes.fontSize ?? 16}px`
}"
>{{ atom.text }}</span
>
Expand Down
6 changes: 6 additions & 0 deletions src/RichText/handler/EventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type RichText } from '@RichText/RichText'
import { handleInput } from './utils/handleInput'
import { sleep } from '@Utils/sleep'

export class EventHandler {
richText: RichText
Expand Down Expand Up @@ -64,6 +65,11 @@ export class EventHandler {

mount() {
const element = this.richText.element as HTMLElement
element.addEventListener('focus', () => (this.richText.focus = true))
element.addEventListener('blur', async () => {
await sleep(100)
this.richText.focus = false
})
element.addEventListener('beforeinput', this.onBeforeInput.bind(this))
element.addEventListener('compositionstart', this.onCompositionStart.bind(this))
element.addEventListener('compositionend', this.onCompositionEnd.bind(this))
Expand Down
Loading