Skip to content

Commit

Permalink
Feat/dnd (#85)
Browse files Browse the repository at this point in the history
* feat: listen dnd events

* fix: data type attribute

* feat: introduce ContentBlock

* feat: handle states of editor

* fix: lints error
  • Loading branch information
vincentdchan authored Nov 24, 2023
1 parent 0cad93c commit f7fbce2
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 27 deletions.
9 changes: 9 additions & 0 deletions packages/blocky-core/css/blocky-core.css
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
.blocky-editor-block {
margin-top: 2px;
display: flex;
position: relative;
}

.blocky-content {
Expand Down Expand Up @@ -202,3 +203,11 @@
background-color: var(--color-neutral-muted);
border-radius: 6px;
}

.blocky-drag-over-bar {
position: absolute;
width: 100%;
background-color: rgb(171, 211, 222);
left: 0px;
height: 2px;
}
99 changes: 88 additions & 11 deletions packages/blocky-core/src/block/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Changeset,
} from "@pkg/data";
import Delta from "quill-delta-es";
import { Subject } from "rxjs";
import { Observable, Subject, fromEvent, takeUntil } from "rxjs";
import { type Editor } from "@pkg/view/editor";
import { type EditorController } from "@pkg/view/controller";
import { RenderOption } from "@pkg/view/renderer";
Expand Down Expand Up @@ -136,6 +136,12 @@ export interface IBlockDefinition {
new (e: BlockCreatedEvent): Block;
}

export enum BlockDragOverState {
None = 0,
Top = 1,
Bottom = 2,
}

/**
* Base class for all the blocks in the editor.
*
Expand Down Expand Up @@ -221,6 +227,82 @@ export class Block implements IDisposable {
}
}

/**
* Blocks except "title"
*
* Supports:
* - copy & paste selection.
* - drag & drop
*/
export class ContentBlock extends Block {
static DragOverClassName = "blocky-drag-over";

container: HTMLElement | null = null;
contentContainer: HTMLElement | null = null;

readonly #dragOver = new Subject<DragEvent>();
readonly #drop = new Subject<DragEvent>();

#dragOverBar: HTMLElement | undefined;

blockDidMount(e: BlockDidMountEvent): void {
this.container = e.element;
}

get dragOver$(): Observable<DragEvent> {
return this.#dragOver.pipe(takeUntil(this.dispose$));
}

get drop$(): Observable<DragEvent> {
return this.#drop.pipe(takeUntil(this.dispose$));
}

setDragOverState(state: BlockDragOverState): void {
const container = this.container;
if (!container) {
return;
}
switch (state) {
case BlockDragOverState.None:
container.classList.remove(ContentBlock.DragOverClassName);
if (this.#dragOverBar) {
this.#dragOverBar.remove();
this.#dragOverBar = undefined;
}
break;
case BlockDragOverState.Top:
case BlockDragOverState.Bottom: {
container.classList.add(ContentBlock.DragOverClassName);
const bar = this.createDragOverBar(state === BlockDragOverState.Top);
this.#dragOverBar = bar;
if (state === BlockDragOverState.Top) {
container.prepend(bar);
} else {
container.append(bar);
}
break;
}
}
}

protected initBlockDnd(contentContainer: HTMLElement) {
fromEvent<DragEvent>(contentContainer, "dragover").subscribe(
this.#dragOver
);
fromEvent<DragEvent>(contentContainer, "drop").subscribe(this.#drop);
}

protected createDragOverBar(isTop: boolean): HTMLElement {
const result = elem("div", "blocky-drag-over-bar");
if (isTop) {
result.style.top = "0px";
} else {
result.style.bottom = "0px";
}
return result;
}
}

export const zeroWidthChar = "\u200b";

/**
Expand All @@ -229,21 +311,14 @@ export const zeroWidthChar = "\u200b";
*
* If you want to write a custom block, extend this class
*/
export class ContentBlock extends Block {
#contentContainer: HTMLElement | undefined;
export class CustomBlock extends ContentBlock {
#selectSpan: HTMLSpanElement | undefined;

/**
* You elements should be rendered under the contentContainer
*/
get contentContainer() {
return this.#contentContainer!;
}

override blockDidMount(e: BlockDidMountEvent): void {
super.blockDidMount(e);
const { element, blockDef } = e;
const contentContainer = elem("div", "blocky-content");
this.#contentContainer = contentContainer;
this.contentContainer = contentContainer;

if (!blockDef.Editable) {
contentContainer.contentEditable = "false";
Expand All @@ -255,6 +330,8 @@ export class ContentBlock extends Block {
this.#selectSpan.setAttribute("data-id", e.blockElement.id);
this.#selectSpan.appendChild(nonWidthChar);
element.append(this.#selectSpan);

this.initBlockDnd(contentContainer);
}

/**
Expand Down
25 changes: 14 additions & 11 deletions packages/blocky-core/src/block/textBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
type BlockContentChangedEvent,
type BlockPasteEvent,
type CursorDomResult,
Block,
ContentBlock,
} from "./basic";
import { EditorState } from "@pkg/model";
import {
Expand Down Expand Up @@ -108,7 +108,7 @@ class CheckboxRenderer extends LeftPadRenderer {
* TextBlock is a very special block in the editor.
* It's handling all the editable element.
*/
export class TextBlock extends Block {
export class TextBlock extends ContentBlock {
static Name = "Text";
static Editable = true;

Expand Down Expand Up @@ -203,7 +203,6 @@ export class TextBlock extends Block {

#container: HTMLElement | undefined;
#bodyContainer: HTMLElement | null = null;
#contentContainer: HTMLElement | null = null;
#leftPadRenderer: LeftPadRenderer | null = null;
#embeds: Set<Embed> = new Set();

Expand Down Expand Up @@ -288,22 +287,26 @@ export class TextBlock extends Block {
}

protected findContentContainer(): HTMLElement {
const e = this.#contentContainer;
const e = this.contentContainer;
if (!e) {
throw new Error("content not found");
}
return e;
}

override blockDidMount({ element }: BlockDidMountEvent): void {
override blockDidMount(e: BlockDidMountEvent): void {
super.blockDidMount(e);
const { element } = e;
element.classList.add("blocky-flex");

this.#bodyContainer = this.#createTextBodyContainer();

this.#contentContainer = this.#createContentContainer();
this.#bodyContainer.append(this.#contentContainer);
this.contentContainer = this.#createContentContainer();
this.#bodyContainer.append(this.contentContainer);

element.appendChild(this.#bodyContainer);

this.initBlockDnd(this.contentContainer);
}

override blockFocused({ selection, cursor }: BlockFocusedEvent): void {
Expand Down Expand Up @@ -639,11 +642,11 @@ export class TextBlock extends Block {
}

if (renderedType !== textType) {
this.#bodyContainer?.removeChild(this.#contentContainer!);
this.#bodyContainer?.removeChild(this.contentContainer!);

const newContainer = this.#createContentContainer();
this.#bodyContainer?.insertBefore(newContainer, null);
this.#contentContainer = newContainer;
this.contentContainer = newContainer;

this.#forceRenderContentStyle(blockContainer, newContainer, textType);

Expand All @@ -658,7 +661,7 @@ export class TextBlock extends Block {
}

override get childrenBeginDOM(): HTMLElement | null {
return this.#contentContainer;
return this.contentContainer;
}

#isSpanNodeMatch(op: Op, dom: Node): boolean {
Expand Down Expand Up @@ -702,7 +705,7 @@ export class TextBlock extends Block {
) {
const contentContainer = this.#ensureContentContainerStyle(
blockContainer,
this.#contentContainer!
this.contentContainer!
);
this.#leftPadRenderer?.render();

Expand Down
22 changes: 21 additions & 1 deletion packages/blocky-core/src/view/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { ToolbarDelegate, type ToolbarFactory } from "./toolbarDelegate";
import { TextBlock } from "@pkg/block/textBlock";
import { EditorController } from "./controller";
import { type FollowerWidget } from "./followerWidget";
import { Block } from "@pkg/block/basic";
import { Block, ContentBlock } from "@pkg/block/basic";
import { getTextTypeForTextBlock } from "@pkg/block/textBlock";
import {
CollaborativeCursorManager,
Expand Down Expand Up @@ -136,6 +136,9 @@ export class Editor {
#themeData?: ThemeData;
#searchContext: SearchContext | undefined;

darggingNode: BlockDataElement | undefined;
prevDragOverBlock: ContentBlock | null = null;

readonly dispose$ = new Subject<void>();

readonly onEveryBlock: Subject<Block> = new Subject();
Expand Down Expand Up @@ -1593,6 +1596,23 @@ export class Editor {
}
}

handleHandleBlockDrop(block: ContentBlock) {
if (!this.darggingNode) {
return;
}
const dropBlockElement = block.elementData as BlockDataElement;
if (this.darggingNode.id === dropBlockElement.id) {
// it's the same
return;
}

const cloned = this.darggingNode.clone();
new Changeset(this.state)
.removeChild(this.darggingNode.parent!, this.darggingNode)
.insertChildrenAfter(dropBlockElement, [cloned])
.apply();
}

insertFollowerWidget(widget: FollowerWidget) {
this.#followerWidget?.dispose();
this.#followerWidget = widget;
Expand Down
35 changes: 33 additions & 2 deletions packages/blocky-core/src/view/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { elem, removeNode } from "blocky-common/es/dom";
import { isUndefined } from "lodash-es";
import { type IBlockDefinition } from "@pkg/block/basic";
import {
BlockDragOverState,
ContentBlock,
type IBlockDefinition,
} from "@pkg/block/basic";
import {
type BlockyDocument,
type DataBaseNode,
Expand Down Expand Up @@ -339,6 +343,15 @@ export class DocRenderer {
}
}

#resetPrevDragOverBlock() {
const editor = this.editor;
if (!editor.prevDragOverBlock) {
return;
}
editor.prevDragOverBlock.setDragOverState(BlockDragOverState.None);
editor.prevDragOverBlock = null;
}

#initBlockContainer(
blockContainer: HTMLElement,
blockNode: BlockDataElement,
Expand All @@ -348,7 +361,7 @@ export class DocRenderer {

blockContainer._mgNode = blockNode;
editor.state.setDom(blockNode.id, blockContainer);
blockContainer.setAttribute("data-type", blockDef.name);
blockContainer.setAttribute("data-type", blockDef.Name);
blockContainer.addEventListener("mouseenter", () => {
editor.placeSpannerAt(blockContainer, blockNode);
});
Expand All @@ -363,6 +376,24 @@ export class DocRenderer {
blockElement: blockNode,
clsPrefix,
});

if (block instanceof ContentBlock) {
block.dragOver$.subscribe((e) => {
e.preventDefault();
if (editor.prevDragOverBlock === block) {
return;
}
this.#resetPrevDragOverBlock();
editor.prevDragOverBlock = block;
block.setDragOverState(BlockDragOverState.Bottom);
});
block.drop$.subscribe((e) => {
e.preventDefault();
this.#resetPrevDragOverBlock();

editor.handleHandleBlockDrop(block);
});
}
}

protected typeOfDomNode(node: Node): number | undefined {
Expand Down
16 changes: 16 additions & 0 deletions packages/blocky-core/src/view/spannerDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type IDisposable } from "blocky-common/es";
import type { EditorController } from "@pkg/view/controller";
import type { BlockDataElement } from "@pkg/data";
import { UIDelegate } from "./uiDelegate";
import { fromEvent, takeUntil } from "rxjs";

export interface SpannerInstance extends IDisposable {
onFocusedNodeChanged?(focusedNode: BlockDataElement | undefined): void;
Expand Down Expand Up @@ -34,6 +35,21 @@ export class SpannerDelegate extends UIDelegate {
private factory: SpannerFactory
) {
super("blocky-editor-spanner-delegate blocky-cm-noselect");
// draggable
this.container.setAttribute("draggable", "true");

const dragStart$ = fromEvent<DragEvent>(this.container, "dragstart");
dragStart$
.pipe(takeUntil(this.dispose$))
.subscribe(this.#handleDragStart.bind(this));
}

#handleDragStart() {
const editor = this.editorController.editor;
if (!editor) {
return;
}
editor.darggingNode = this.focusedNode;
}

override mount(parent: HTMLElement): void {
Expand Down
Loading

1 comment on commit f7fbce2

@vercel
Copy link

@vercel vercel bot commented on f7fbce2 Nov 24, 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.