From 6fd29e9cf6965dc7f5d19025b077b3ebd50b7297 Mon Sep 17 00:00:00 2001 From: Zhilin Liu Date: Thu, 4 Jan 2024 15:16:55 +0800 Subject: [PATCH] feat: different level format --- .prettierrc | 2 +- src/BlockHub/Block/Block.ts | 16 ++- src/BlockHub/TableBlock/Table.vue | 23 +---- src/BlockHub/TextBoxBlock/TextBox.vue | 64 +----------- src/Kernel/Store/TextStore.ts | 18 ++++ src/RichText/RichText.ts | 37 +++++-- src/RichText/RichText.vue | 7 +- src/RichText/components/Atom.vue | 3 +- src/RichText/handler/EventHandler.ts | 6 ++ .../ToolBar/components/Home/FontStyle.vue | 98 +++++++++++++------ src/Utils/intersectAttributes.ts | 18 ++++ src/Utils/sleep.ts | 3 + 12 files changed, 168 insertions(+), 127 deletions(-) create mode 100644 src/Utils/intersectAttributes.ts create mode 100644 src/Utils/sleep.ts diff --git a/.prettierrc b/.prettierrc index 02af236..83f04b6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { - "printWidth": 80, + "printWidth": 120, "tabWidth": 2, "useTabs": false, "semi": false, diff --git a/src/BlockHub/Block/Block.ts b/src/BlockHub/Block/Block.ts index d62d46c..e4b35d1 100644 --- a/src/BlockHub/Block/Block.ts +++ b/src/BlockHub/Block/Block.ts @@ -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 @@ -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) => {}, + } } } diff --git a/src/BlockHub/TableBlock/Table.vue b/src/BlockHub/TableBlock/Table.vue index f5845f6..05fa25f 100644 --- a/src/BlockHub/TableBlock/Table.vue +++ b/src/BlockHub/TableBlock/Table.vue @@ -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<{ @@ -17,34 +17,17 @@ for (const row of block.data) { } tableData.value.push(rowData as Array) } - -function formatBlock(name: string, value: AttributeValue) { - block.formatBlock(name, value) -} - -function format(name: string, value: AttributeValue) { - block.getController().format(name, value) -} diff --git a/src/Kernel/Store/TextStore.ts b/src/Kernel/Store/TextStore.ts index 12c8a02..715b532 100644 --- a/src/Kernel/Store/TextStore.ts +++ b/src/Kernel/Store/TextStore.ts @@ -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 { @@ -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 { @@ -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), diff --git a/src/RichText/RichText.ts b/src/RichText/RichText.ts index 409e989..a66cfe5 100644 --- a/src/RichText/RichText.ts +++ b/src/RichText/RichText.ts @@ -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) @@ -21,6 +23,7 @@ export class RichText { events = { selectChange: new EventManager(), + formatChange: new EventManager(), } constructor(textStore: TextStore) { @@ -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) { @@ -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 }) }, } } diff --git a/src/RichText/RichText.vue b/src/RichText/RichText.vue index a0a720d..88ced79 100644 --- a/src/RichText/RichText.vue +++ b/src/RichText/RichText.vue @@ -43,7 +43,12 @@ history.events.update.on((eventType) => { diff --git a/src/RichText/components/Atom.vue b/src/RichText/components/Atom.vue index c5c0fb4..0fc92a8 100644 --- a/src/RichText/components/Atom.vue +++ b/src/RichText/components/Atom.vue @@ -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 }} diff --git a/src/RichText/handler/EventHandler.ts b/src/RichText/handler/EventHandler.ts index 31054fc..81048d0 100644 --- a/src/RichText/handler/EventHandler.ts +++ b/src/RichText/handler/EventHandler.ts @@ -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 @@ -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)) diff --git a/src/UserInterface/ToolBar/components/Home/FontStyle.vue b/src/UserInterface/ToolBar/components/Home/FontStyle.vue index fae0ba4..362dac4 100644 --- a/src/UserInterface/ToolBar/components/Home/FontStyle.vue +++ b/src/UserInterface/ToolBar/components/Home/FontStyle.vue @@ -7,38 +7,56 @@ import { TextItalic, TextUnderline, Strikethrough, - BackgroundColor, } from '@icon-park/vue-next' import MenuWrapper from '../MenuWrapper.vue' import { kernel } from '@Kernel/index' import { selectionBlk } from '@Kernel/index' +import { intersectAttributes } from '@Utils/intersectAttributes' +import { ref } from 'vue' +import { type AttributeValue } from '@Kernel/Store/TextStore' -kernel.richTextObserver.on((newState) => { - console.log('newState', newState) -}) - -const handleBoldClick = () => { - selectionBlk.blocks.forEach((b) => { - b.formatBlock('bold', true) +const format = (name: string, value: AttributeValue) => { + selectionBlk.blocks.forEach((block) => { + const controller = block.getController() + if (controller.isFocus()) { + controller.format(name, value) + } else { + block.formatBlock(name, value) + } }) } + +const fontStyle = ref() +selectionBlk.events.update.on(() => { + const selectedBlocksFormat = selectionBlk.blocks.map((block) => block.getBlockFormat()) + fontStyle.value = intersectAttributes(selectedBlocksFormat) +}) +kernel.richTextObserver.on(async (newState) => { + const richTextAttributes = newState.atoms.map((atom) => atom.attributes) + fontStyle.value = intersectAttributes(richTextAttributes) +}) diff --git a/src/Utils/intersectAttributes.ts b/src/Utils/intersectAttributes.ts new file mode 100644 index 0000000..dd23003 --- /dev/null +++ b/src/Utils/intersectAttributes.ts @@ -0,0 +1,18 @@ +import { AttributeValue } from '@Kernel/Store/TextStore' + +export function intersectAttributes(attributesList: Array<{ [key: string]: AttributeValue }>) { + const result = attributesList[0] + for (let i = 1; i < attributesList.length; i++) { + const attributes = attributesList[i] + if (Object.keys(attributes).length === 0) { + return Object.create(null) + } + for (const [key, value] of Object.entries(attributes)) { + if (key in result && result[key] === value) { + continue + } + delete result[key] + } + } + return result ?? Object.create(null) +} diff --git a/src/Utils/sleep.ts b/src/Utils/sleep.ts new file mode 100644 index 0000000..463939f --- /dev/null +++ b/src/Utils/sleep.ts @@ -0,0 +1,3 @@ +export async function sleep(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)) +}