Skip to content

Commit

Permalink
Merge pull request #820 from streamich/inline-floating-menu
Browse files Browse the repository at this point in the history
Peritext inline floating menu
  • Loading branch information
streamich authored Feb 14, 2025
2 parents 3474c38 + 3a0e5b6 commit 79477ce
Show file tree
Hide file tree
Showing 18 changed files with 1,356 additions and 434 deletions.
90 changes: 45 additions & 45 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,6 @@
"engines": {
"node": ">=10.0"
},
"keywords": [
"collaborative",
"multiplayer",
"local-first",
"localfirst",
"crdt",
"rdt",
"ot",
"operational-transformation",
"replicated",
"sync",
"synchronization",
"distributed-state",
"marshaling",
"serializations",
"json-patch",
"json-binary",
"json-brand",
"json-cli",
"json-clone",
"json-crdt-patch",
"json-crdt-extensions",
"json-crdt-peritext-ui",
"json-crdt",
"json-equal",
"json-expression",
"json-hash",
"json-ot",
"json-pack",
"json-patch-multicore",
"json-patch-ot",
"json-patch",
"json-pointer",
"json-random",
"json-schema",
"json-size",
"json-stable",
"json-text",
"json-type",
"json-type-value",
"json-walk"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"typings": "lib/index.d.ts",
Expand All @@ -81,7 +39,7 @@
"format": "biome format ./src",
"format:fix": "biome format --write ./src",
"lint": "biome lint ./src",
"lint:fix": "biome lint --apply ./src",
"lint:fix": "biome lint --write ./src",
"clean": "npx [email protected] lib es6 es2019 es2020 esm typedocs coverage gh-pages yarn-error.log src/**/__bench__/node_modules src/**/__bench__/yarn-error.log",
"build:es2020": "tsc --project tsconfig.build.json --module commonjs --target es2020 --outDir lib",
"build:esm": "tsc --project tsconfig.build.json --module ESNext --target ESNEXT --outDir esm",
Expand Down Expand Up @@ -156,7 +114,7 @@
"json-crdt-traces": "https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d",
"json-logic-js": "^2.0.2",
"nano-theme": "^1.4.3",
"nice-ui": "^1.25.0",
"nice-ui": "^1.28.0",
"quill-delta": "^5.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down Expand Up @@ -228,5 +186,47 @@
"@semantic-release/npm",
"@semantic-release/git"
]
}
},
"keywords": [
"collaborative",
"multiplayer",
"local-first",
"localfirst",
"crdt",
"rdt",
"ot",
"operational-transformation",
"replicated",
"sync",
"synchronization",
"distributed-state",
"marshaling",
"serializations",
"json-patch",
"json-binary",
"json-brand",
"json-cli",
"json-clone",
"json-crdt-patch",
"json-crdt-extensions",
"json-crdt-peritext-ui",
"json-crdt",
"json-equal",
"json-expression",
"json-hash",
"json-ot",
"json-pack",
"json-patch-multicore",
"json-patch-ot",
"json-patch",
"json-pointer",
"json-random",
"json-schema",
"json-size",
"json-stable",
"json-text",
"json-type",
"json-type-value",
"json-walk"
]
}
2 changes: 2 additions & 0 deletions src/json-crdt-extensions/peritext/slice/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const enum SliceTypeCon {
iaside = -25, // Inline <aside>
iembed = -26, // inline embed (any media, dropdown, Google Docs-like chips: date, person, file, etc.)
bookmark = -27, // UI for creating a link to this slice
overline = -28, // <span style="text-decoration: overline">
}

/**
Expand Down Expand Up @@ -131,6 +132,7 @@ export enum SliceTypeName {
iaside = SliceTypeCon.iaside,
iembed = SliceTypeCon.iembed,
bookmark = SliceTypeCon.bookmark,
overline = SliceTypeCon.overline,
}

export enum SliceHeaderMask {
Expand Down
21 changes: 11 additions & 10 deletions src/json-crdt-peritext-ui/dom/CursorController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class CursorController implements UiLifeCycles, Printable {

public readonly focus = new ValueSyncStore<boolean>(false);

private readonly onFocus = (): void => {
private readonly onFocus = (event: Event): void => {
this.focus.next(true);
};

Expand All @@ -129,15 +129,16 @@ export class CursorController implements UiLifeCycles, Printable {

private x = 0;
private y = 0;
private mouseDown: boolean = false;
public readonly mouseDown = new ValueSyncStore<boolean>(false);

private readonly onMouseDown = (ev: MouseEvent): void => {
if (!this.focus.value && this.opts.txt.editor.hasCursor()) return;
const {clientX, clientY} = ev;
this.x = clientX;
this.y = clientY;
switch (ev.detail) {
case 1: {
this.mouseDown = false;
this.mouseDown.next(false);
const at = this.posAtPoint(clientX, clientY);
if (at === -1) return;
this.selAnchor = at;
Expand All @@ -150,32 +151,32 @@ export class CursorController implements UiLifeCycles, Printable {
ev.preventDefault();
et.cursor({at, edge: 'new'});
} else {
this.mouseDown = true;
this.mouseDown.next(true);
ev.preventDefault();
et.cursor({at});
}
break;
}
case 2:
this.mouseDown = false;
this.mouseDown.next(false);
ev.preventDefault();
this.opts.et.cursor({unit: 'word'});
break;
case 3:
this.mouseDown = false;
this.mouseDown.next(false);
ev.preventDefault();
this.opts.et.cursor({unit: 'block'});
break;
case 4:
this.mouseDown = false;
this.mouseDown.next(false);
ev.preventDefault();
this.opts.et.cursor({unit: 'all'});
break;
}
};

private readonly onMouseMove = (ev: MouseEvent): void => {
if (!this.mouseDown) return;
if (!this.mouseDown.value) return;
const at = this.selAnchor;
if (at < 0) return;
const {clientX, clientY} = ev;
Expand All @@ -190,7 +191,7 @@ export class CursorController implements UiLifeCycles, Printable {
};

private readonly onMouseUp = (ev: MouseEvent): void => {
this.mouseDown = false;
this.mouseDown.next(false);
};

private onKeyDown = (event: KeyboardEvent): void => {
Expand Down Expand Up @@ -242,6 +243,6 @@ export class CursorController implements UiLifeCycles, Printable {
/** ----------------------------------------------------- {@link Printable} */

public toString(tab?: string): string {
return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown} }`;
return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown.value} }`;
}
}
10 changes: 8 additions & 2 deletions src/json-crdt-peritext-ui/dom/DomController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import {KeyController} from '../dom/KeyController';
import {CompositionController} from '../dom/CompositionController';
import type {PeritextEventDefaults} from '../events/PeritextEventDefaults';
import type {PeritextEventTarget} from '../events/PeritextEventTarget';
import type {UiLifeCycles} from '../dom/types';
import type {PeritextRenderingSurfaceApi, UiLifeCycles} from '../dom/types';

export interface DomControllerOpts {
source: HTMLElement;
events: PeritextEventDefaults;
}

export class DomController implements UiLifeCycles, Printable {
export class DomController implements UiLifeCycles, Printable, PeritextRenderingSurfaceApi {
public readonly et: PeritextEventTarget;
public readonly keys: KeyController;
public readonly comp: CompositionController;
Expand Down Expand Up @@ -50,6 +50,12 @@ export class DomController implements UiLifeCycles, Printable {
this.richText.stop();
}

/** ----------------------------------- {@link PeritextRenderingSurfaceApi} */

public focus(): void {
this.opts.source.focus();
}

/** ----------------------------------------------------- {@link Printable} */

public toString(tab?: string): string {
Expand Down
7 changes: 7 additions & 0 deletions src/json-crdt-peritext-ui/dom/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ export interface UiLifeCyclesRender {
}

export type Rect = Pick<DOMRect, 'x' | 'y' | 'width' | 'height'>;

export interface PeritextRenderingSurfaceApi {
/**
* Focuses the rendering surface, so that it can receive keyboard input.
*/
focus(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export interface TopToolbarProps {
}

export const TopToolbar: React.FC<TopToolbarProps> = ({ctx}) => {
const pending = useSyncStore(ctx.peritext.editor.pending);
const peritext = ctx.peritext;
const editor = peritext.editor;
const pending = useSyncStore(editor.pending);

if (!ctx.dom) return null;

const [complete] = ctx.peritext.overlay.stat(ctx.peritext.editor.cursor);
const [complete] = editor.hasCursor() ? peritext.overlay.stat(editor.cursor) : [new Set()];

const inlineGroupButton = (type: string | number, name: React.ReactNode) => (
<Button
Expand Down
45 changes: 30 additions & 15 deletions src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {rule} from 'nano-theme';
import {CaretToolbar} from './CaretToolbar';
import type {CaretViewProps} from '../../react/cursor/CaretView';
import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar';
import {useToolbarPlugin} from './context';
import type {PeritextEventDetailMap} from '../../events/types';
import {useSyncStore, useSyncStoreOpt, useTimeout} from '../../react/hooks';
import {AfterTimeout} from '../../react/util/AfterTimeout';
import type {CaretViewProps} from '../../react/cursor/CaretView';

const height = 1.9;
const height = 1.8;

const blockClass = rule({
pos: 'relative',
w: '0px',
h: '100%',
bg: 'black',
va: 'bottom',
});

Expand All @@ -23,9 +22,6 @@ const overClass = rule({
isolation: 'isolate',
us: 'none',
transform: 'translateX(calc(-50% + 0px))',
// w: '1px',
// h: '1px',
// bd: '1px solid red',
});

export interface RenderCaretProps extends CaretViewProps {
Expand All @@ -34,17 +30,36 @@ export interface RenderCaretProps extends CaretViewProps {

export const RenderCaret: React.FC<RenderCaretProps> = ({children}) => {
const {toolbar} = useToolbarPlugin()!;
const showInlineToolbar = toolbar.showInlineToolbar;
const [showCaretToolbarValue, toolbarVisibilityChangeTime] = useSyncStore(showInlineToolbar);
const focus = useSyncStoreOpt(toolbar.surface.dom?.cursor.focus) || false;
const doHideForCoolDown = toolbarVisibilityChangeTime + 500 > Date.now();
const enableAfterCoolDown = useTimeout(500, [doHideForCoolDown]);

// biome-ignore lint/correctness/useExhaustiveDependencies: showInlineToolbar.next do not need to memoize
const handleClose = React.useCallback(() => {
setTimeout(() => {
if (showInlineToolbar.value) showInlineToolbar.next([false, Date.now()]);
}, 5);
}, []);

let toolbarElement = (
<CaretToolbar disabled={!enableAfterCoolDown} menu={toolbar.getCaretMenu()} onPopupClose={handleClose} />
);

const lastEventIsCaretPositionChange =
toolbar.lastEvent?.type === 'cursor' &&
typeof (toolbar.lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number';
if (doHideForCoolDown) {
toolbarElement = <AfterTimeout ms={500}>{toolbarElement}</AfterTimeout>;
}

return (
<span className={blockClass}>
{children}
<span className={overClass} contentEditable={false}>
{lastEventIsCaretPositionChange && <CaretToolbar />}
</span>
{/* <span
className={overClass}
contentEditable={false}
>
{(showCaretToolbarValue && focus) && (toolbarElement)}
</span> */}
</span>
);
};
Loading

0 comments on commit 79477ce

Please sign in to comment.