Skip to content

Commit

Permalink
Feat/quote (#96)
Browse files Browse the repository at this point in the history
* feat: add quote support

* feat: quote style
  • Loading branch information
vincentdchan authored Dec 3, 2023
1 parent 26e21b6 commit ec86a36
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 66 deletions.
11 changes: 11 additions & 0 deletions packages/blocky-core/css/blocky-core.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@
min-width: 26px;
}

.blocky-left-pad.quote {
justify-content: start;
padding-bottom: 4px;
width: 14px;
min-width: 14px;
}

.blocky-quote-pad {
border-left: 3px solid black;
}

.blocky-bullet-content:before {
content: "•";
color: var(--blocky-primary-color);
Expand Down
63 changes: 63 additions & 0 deletions packages/blocky-core/src/block/leftPadRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { elem, removeNode } from "blocky-common/es/dom";
import { EditorState } from "@pkg/model";
import { BlockDataElement, Changeset } from "@pkg/data";
import { Subject, fromEvent, takeUntil } from "rxjs";

export class LeftPadRenderer {
readonly dispose$ = new Subject<void>();
constructor(readonly container: HTMLDivElement) {}
render() {}
dispose(): void {
this.dispose$.next();
removeNode(this.container);
}
}
const checkedColor = "rgb(240, 153, 56)";
export class CheckboxRenderer extends LeftPadRenderer {
#checkboxContainer: HTMLDivElement;
#centerElement: HTMLDivElement;
#checked = false;
constructor(
container: HTMLDivElement,
private state: EditorState,
private blockElement: BlockDataElement
) {
super(container);
this.#checkboxContainer = elem("div", "blocky-checkbox");
container.append(this.#checkboxContainer);

this.#centerElement = elem("div", "blocky-checkbox-center");
this.#centerElement.style.backgroundColor = checkedColor;
this.#checkboxContainer.appendChild(this.#centerElement);
this.#centerElement.style.visibility = "hidden";
this.#checkboxContainer.style.boxShadow = `0px 0px 0px 1px gray`;

fromEvent(this.#checkboxContainer, "click")
.pipe(takeUntil(this.dispose$))
.subscribe(this.#handleClick);
}

#handleClick = () => {
const checked = !!this.blockElement.getAttribute("checked");
new Changeset(this.state)
.updateAttributes(this.blockElement, { checked: !checked })
.apply({
refreshCursor: true,
});
};

override render(): void {
const checked = !!this.blockElement.getAttribute("checked");
if (checked == this.#checked) {
return;
}
if (checked) {
this.#centerElement.style.visibility = "";
this.#checkboxContainer.style.boxShadow = `0px 0px 0px 1px ${checkedColor}`;
} else {
this.#centerElement.style.visibility = "hidden";
this.#checkboxContainer.style.boxShadow = `0px 0px 0px 1px gray`;
}
this.#checked = checked;
}
}
92 changes: 26 additions & 66 deletions packages/blocky-core/src/block/textBlock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isNumber, isObject, isString } from "lodash-es";
import { elem, removeNode } from "blocky-common/es/dom";
import { elem } from "blocky-common/es/dom";
import { removeLineBreaks, type Position } from "blocky-common/es";
import {
type BlockDidMountEvent,
Expand All @@ -9,7 +9,6 @@ import {
type CursorDomResult,
} from "./basic";
import { ContentBlock } from "./contentBlock";
import { EditorState } from "@pkg/model";
import {
type AttributesObject,
BlockyTextModel,
Expand All @@ -26,7 +25,8 @@ import { HTMLConverter } from "@pkg/helper/htmlConverter";
import { EditorController } from "@pkg/view/controller";
import type { SpanStyle } from "@pkg/registry/spanRegistry";
import type { Embed } from "@pkg/registry/embedRegistry";
import { Subject, fromEvent, takeUntil } from "rxjs";
import { fromEvent, takeUntil } from "rxjs";
import { LeftPadRenderer, CheckboxRenderer } from "./leftPadRenderer";

const TextContentClass = "blocky-block-text-content";

Expand All @@ -43,67 +43,6 @@ function textTypeCanIndent(textType: TextType): boolean {
return textType === TextType.Normal || textType === TextType.Bulleted;
}

class LeftPadRenderer {
readonly dispose$ = new Subject<void>();
constructor(readonly container: HTMLDivElement) {}
render() {}
dispose(): void {
this.dispose$.next();
removeNode(this.container);
}
}

const checkedColor = "rgb(240, 153, 56)";

class CheckboxRenderer extends LeftPadRenderer {
#checkboxContainer: HTMLDivElement;
#centerElement: HTMLDivElement;
#checked = false;
constructor(
container: HTMLDivElement,
private state: EditorState,
private blockElement: BlockDataElement
) {
super(container);
this.#checkboxContainer = elem("div", "blocky-checkbox");
container.append(this.#checkboxContainer);

this.#centerElement = elem("div", "blocky-checkbox-center");
this.#centerElement.style.backgroundColor = checkedColor;
this.#checkboxContainer.appendChild(this.#centerElement);
this.#centerElement.style.visibility = "hidden";
this.#checkboxContainer.style.boxShadow = `0px 0px 0px 1px gray`;

fromEvent(this.#checkboxContainer, "click")
.pipe(takeUntil(this.dispose$))
.subscribe(this.#handleClick);
}

#handleClick = () => {
const checked = !!this.blockElement.getAttribute("checked");
new Changeset(this.state)
.updateAttributes(this.blockElement, { checked: !checked })
.apply({
refreshCursor: true,
});
};

override render(): void {
const checked = !!this.blockElement.getAttribute("checked");
if (checked == this.#checked) {
return;
}
if (checked) {
this.#centerElement.style.visibility = "";
this.#checkboxContainer.style.boxShadow = `0px 0px 0px 1px ${checkedColor}`;
} else {
this.#centerElement.style.visibility = "hidden";
this.#checkboxContainer.style.boxShadow = `0px 0px 0px 1px gray`;
}
this.#checked = checked;
}
}

/**
* TextBlock is a very special block in the editor.
* It's handling all the editable element.
Expand Down Expand Up @@ -553,8 +492,11 @@ export class TextBlock extends ContentBlock {
spanStyle.onSpanCreated?.(element);
}

#createLeftPadContainer(): HTMLDivElement {
const container = elem("div", "blocky-left-pad");
#createLeftPadContainer(className?: string): HTMLDivElement {
const container = elem(
"div",
"blocky-left-pad" + (isString(className) ? " " + className : "")
);
container.contentEditable = "false";
return container;
}
Expand Down Expand Up @@ -590,6 +532,15 @@ export class TextBlock extends ContentBlock {
return new CheckboxRenderer(container, this.editor.state, this.props);
}

#createQuoteRenderer(): LeftPadRenderer {
const container = this.#createLeftPadContainer("quote");

const quoteContent = elem("div", "blocky-quote-pad");
container.appendChild(quoteContent);

return new LeftPadRenderer(container);
}

// TODO: dispatch through plugin
#forceRenderContentStyle(
blockContainer: HTMLElement,
Expand Down Expand Up @@ -625,6 +576,15 @@ export class TextBlock extends ContentBlock {
return;
}

case TextType.Quote: {
this.#leftPadRenderer = this.#createQuoteRenderer();
blockContainer.insertBefore(
this.#leftPadRenderer.container,
blockContainer.firstChild
);
return;
}

case TextType.Heading1:
case TextType.Heading2:
case TextType.Heading3: {
Expand Down
4 changes: 4 additions & 0 deletions packages/blocky-core/src/data/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum TextType {
Quote = "quote",
Checkbox = "checkbox",
Bulleted = "bulleted",
Numbered = "numbered",
Expand All @@ -10,6 +11,9 @@ export enum TextType {

export function textTypePrecedence(textType: TextType): number {
switch (textType) {
case TextType.Quote:
return -4;

case TextType.Numbered:
return -3;

Expand Down
50 changes: 50 additions & 0 deletions packages/blocky-core/src/plugins/quotePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { isWhiteSpace } from "blocky-common/es";
import { type IPlugin, TextBlock, type PluginContext } from "@pkg/index";
import { Changeset, TextType } from "@pkg/data";
import Delta from "quill-delta-es";
import { isNumber, isString } from "lodash-es";
import { filter, takeUntil } from "rxjs";

function makeQuotePlugin(): IPlugin {
return {
name: "quote",
onInitialized(context: PluginContext) {
const { editor, dispose$ } = context;
editor.textInput$
.pipe(
takeUntil(dispose$),
filter((evt) => evt.blockElement.t === TextBlock.Name) // don't apply on Title block
)
.subscribe((evt) => {
const { beforeString, blockElement } = evt;
const { state } = editor;
const changeset = new Changeset(state);
const delta = new Delta();

let index = 0;
for (const op of evt.applyDelta.ops) {
if (isString(op.insert)) {
const before = beforeString.slice(0, index);
if (isWhiteSpace(op.insert)) {
if (before === "|") {
delta.delete(2);
changeset.updateAttributes(blockElement, {
textType: TextType.Quote,
});
}
break;
}
index += op.insert.length;
} else if (isNumber(op.retain)) {
index += op.retain;
}
}

changeset.textEdit(blockElement, "textContent", () => delta);
changeset.apply();
});
},
};
}

export default makeQuotePlugin;
2 changes: 2 additions & 0 deletions packages/blocky-core/src/view/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import makeCodeTextPlugin from "@pkg/plugins/codeTextPlugin";
import makeBulletListPlugin from "@pkg/plugins/bulletListPlugin";
import makeHeadingsPlugin from "@pkg/plugins/headingsPlugin";
import makeNumberListPlugin from "@pkg/plugins/numberListPlugin";
import makeQuotePlugin from "@pkg/plugins/quotePlugin";
import makeUndoPlugin from "@pkg/plugins/undoPlugin";
import { isUndefined } from "lodash-es";

Expand All @@ -46,6 +47,7 @@ export function makeDefaultEditorPlugins(): IPlugin[] {
makeBulletListPlugin(),
makeNumberListPlugin(),
makeHeadingsPlugin(),
makeQuotePlugin(),
];
}

Expand Down

1 comment on commit ec86a36

@vercel
Copy link

@vercel vercel bot commented on ec86a36 Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.