Skip to content

Commit 79477ce

Browse files
authored
Merge pull request #820 from streamich/inline-floating-menu
Peritext inline floating menu
2 parents 3474c38 + 3a0e5b6 commit 79477ce

File tree

18 files changed

+1356
-434
lines changed

18 files changed

+1356
-434
lines changed

package.json

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,6 @@
1616
"engines": {
1717
"node": ">=10.0"
1818
},
19-
"keywords": [
20-
"collaborative",
21-
"multiplayer",
22-
"local-first",
23-
"localfirst",
24-
"crdt",
25-
"rdt",
26-
"ot",
27-
"operational-transformation",
28-
"replicated",
29-
"sync",
30-
"synchronization",
31-
"distributed-state",
32-
"marshaling",
33-
"serializations",
34-
"json-patch",
35-
"json-binary",
36-
"json-brand",
37-
"json-cli",
38-
"json-clone",
39-
"json-crdt-patch",
40-
"json-crdt-extensions",
41-
"json-crdt-peritext-ui",
42-
"json-crdt",
43-
"json-equal",
44-
"json-expression",
45-
"json-hash",
46-
"json-ot",
47-
"json-pack",
48-
"json-patch-multicore",
49-
"json-patch-ot",
50-
"json-patch",
51-
"json-pointer",
52-
"json-random",
53-
"json-schema",
54-
"json-size",
55-
"json-stable",
56-
"json-text",
57-
"json-type",
58-
"json-type-value",
59-
"json-walk"
60-
],
6119
"main": "lib/index.js",
6220
"types": "lib/index.d.ts",
6321
"typings": "lib/index.d.ts",
@@ -81,7 +39,7 @@
8139
"format": "biome format ./src",
8240
"format:fix": "biome format --write ./src",
8341
"lint": "biome lint ./src",
84-
"lint:fix": "biome lint --apply ./src",
42+
"lint:fix": "biome lint --write ./src",
8543
"clean": "npx rimraf@5.0.5 lib es6 es2019 es2020 esm typedocs coverage gh-pages yarn-error.log src/**/__bench__/node_modules src/**/__bench__/yarn-error.log",
8644
"build:es2020": "tsc --project tsconfig.build.json --module commonjs --target es2020 --outDir lib",
8745
"build:esm": "tsc --project tsconfig.build.json --module ESNext --target ESNEXT --outDir esm",
@@ -156,7 +114,7 @@
156114
"json-crdt-traces": "https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d",
157115
"json-logic-js": "^2.0.2",
158116
"nano-theme": "^1.4.3",
159-
"nice-ui": "^1.25.0",
117+
"nice-ui": "^1.28.0",
160118
"quill-delta": "^5.1.0",
161119
"react": "^18.3.1",
162120
"react-dom": "^18.3.1",
@@ -228,5 +186,47 @@
228186
"@semantic-release/npm",
229187
"@semantic-release/git"
230188
]
231-
}
189+
},
190+
"keywords": [
191+
"collaborative",
192+
"multiplayer",
193+
"local-first",
194+
"localfirst",
195+
"crdt",
196+
"rdt",
197+
"ot",
198+
"operational-transformation",
199+
"replicated",
200+
"sync",
201+
"synchronization",
202+
"distributed-state",
203+
"marshaling",
204+
"serializations",
205+
"json-patch",
206+
"json-binary",
207+
"json-brand",
208+
"json-cli",
209+
"json-clone",
210+
"json-crdt-patch",
211+
"json-crdt-extensions",
212+
"json-crdt-peritext-ui",
213+
"json-crdt",
214+
"json-equal",
215+
"json-expression",
216+
"json-hash",
217+
"json-ot",
218+
"json-pack",
219+
"json-patch-multicore",
220+
"json-patch-ot",
221+
"json-patch",
222+
"json-pointer",
223+
"json-random",
224+
"json-schema",
225+
"json-size",
226+
"json-stable",
227+
"json-text",
228+
"json-type",
229+
"json-type-value",
230+
"json-walk"
231+
]
232232
}

src/json-crdt-extensions/peritext/slice/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const enum SliceTypeCon {
6969
iaside = -25, // Inline <aside>
7070
iembed = -26, // inline embed (any media, dropdown, Google Docs-like chips: date, person, file, etc.)
7171
bookmark = -27, // UI for creating a link to this slice
72+
overline = -28, // <span style="text-decoration: overline">
7273
}
7374

7475
/**
@@ -131,6 +132,7 @@ export enum SliceTypeName {
131132
iaside = SliceTypeCon.iaside,
132133
iembed = SliceTypeCon.iembed,
133134
bookmark = SliceTypeCon.bookmark,
135+
overline = SliceTypeCon.overline,
134136
}
135137

136138
export enum SliceHeaderMask {

src/json-crdt-peritext-ui/dom/CursorController.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class CursorController implements UiLifeCycles, Printable {
119119

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

122-
private readonly onFocus = (): void => {
122+
private readonly onFocus = (event: Event): void => {
123123
this.focus.next(true);
124124
};
125125

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

130130
private x = 0;
131131
private y = 0;
132-
private mouseDown: boolean = false;
132+
public readonly mouseDown = new ValueSyncStore<boolean>(false);
133133

134134
private readonly onMouseDown = (ev: MouseEvent): void => {
135+
if (!this.focus.value && this.opts.txt.editor.hasCursor()) return;
135136
const {clientX, clientY} = ev;
136137
this.x = clientX;
137138
this.y = clientY;
138139
switch (ev.detail) {
139140
case 1: {
140-
this.mouseDown = false;
141+
this.mouseDown.next(false);
141142
const at = this.posAtPoint(clientX, clientY);
142143
if (at === -1) return;
143144
this.selAnchor = at;
@@ -150,32 +151,32 @@ export class CursorController implements UiLifeCycles, Printable {
150151
ev.preventDefault();
151152
et.cursor({at, edge: 'new'});
152153
} else {
153-
this.mouseDown = true;
154+
this.mouseDown.next(true);
154155
ev.preventDefault();
155156
et.cursor({at});
156157
}
157158
break;
158159
}
159160
case 2:
160-
this.mouseDown = false;
161+
this.mouseDown.next(false);
161162
ev.preventDefault();
162163
this.opts.et.cursor({unit: 'word'});
163164
break;
164165
case 3:
165-
this.mouseDown = false;
166+
this.mouseDown.next(false);
166167
ev.preventDefault();
167168
this.opts.et.cursor({unit: 'block'});
168169
break;
169170
case 4:
170-
this.mouseDown = false;
171+
this.mouseDown.next(false);
171172
ev.preventDefault();
172173
this.opts.et.cursor({unit: 'all'});
173174
break;
174175
}
175176
};
176177

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

192193
private readonly onMouseUp = (ev: MouseEvent): void => {
193-
this.mouseDown = false;
194+
this.mouseDown.next(false);
194195
};
195196

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

244245
public toString(tab?: string): string {
245-
return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown} }`;
246+
return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown.value} }`;
246247
}
247248
}

src/json-crdt-peritext-ui/dom/DomController.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import {KeyController} from '../dom/KeyController';
66
import {CompositionController} from '../dom/CompositionController';
77
import type {PeritextEventDefaults} from '../events/PeritextEventDefaults';
88
import type {PeritextEventTarget} from '../events/PeritextEventTarget';
9-
import type {UiLifeCycles} from '../dom/types';
9+
import type {PeritextRenderingSurfaceApi, UiLifeCycles} from '../dom/types';
1010

1111
export interface DomControllerOpts {
1212
source: HTMLElement;
1313
events: PeritextEventDefaults;
1414
}
1515

16-
export class DomController implements UiLifeCycles, Printable {
16+
export class DomController implements UiLifeCycles, Printable, PeritextRenderingSurfaceApi {
1717
public readonly et: PeritextEventTarget;
1818
public readonly keys: KeyController;
1919
public readonly comp: CompositionController;
@@ -50,6 +50,12 @@ export class DomController implements UiLifeCycles, Printable {
5050
this.richText.stop();
5151
}
5252

53+
/** ----------------------------------- {@link PeritextRenderingSurfaceApi} */
54+
55+
public focus(): void {
56+
this.opts.source.focus();
57+
}
58+
5359
/** ----------------------------------------------------- {@link Printable} */
5460

5561
public toString(tab?: string): string {

src/json-crdt-peritext-ui/dom/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ export interface UiLifeCyclesRender {
1414
}
1515

1616
export type Rect = Pick<DOMRect, 'x' | 'y' | 'width' | 'height'>;
17+
18+
export interface PeritextRenderingSurfaceApi {
19+
/**
20+
* Focuses the rendering surface, so that it can receive keyboard input.
21+
*/
22+
focus(): void;
23+
}

src/json-crdt-peritext-ui/plugins/minimal/TopToolbar/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ export interface TopToolbarProps {
1212
}
1313

1414
export const TopToolbar: React.FC<TopToolbarProps> = ({ctx}) => {
15-
const pending = useSyncStore(ctx.peritext.editor.pending);
15+
const peritext = ctx.peritext;
16+
const editor = peritext.editor;
17+
const pending = useSyncStore(editor.pending);
1618

1719
if (!ctx.dom) return null;
1820

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

2123
const inlineGroupButton = (type: string | number, name: React.ReactNode) => (
2224
<Button
Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
// biome-ignore lint: React is used for JSX
21
import * as React from 'react';
32
import {rule} from 'nano-theme';
4-
import {CaretToolbar} from './CaretToolbar';
5-
import type {CaretViewProps} from '../../react/cursor/CaretView';
3+
import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar';
64
import {useToolbarPlugin} from './context';
7-
import type {PeritextEventDetailMap} from '../../events/types';
5+
import {useSyncStore, useSyncStoreOpt, useTimeout} from '../../react/hooks';
6+
import {AfterTimeout} from '../../react/util/AfterTimeout';
7+
import type {CaretViewProps} from '../../react/cursor/CaretView';
88

9-
const height = 1.9;
9+
const height = 1.8;
1010

1111
const blockClass = rule({
1212
pos: 'relative',
1313
w: '0px',
1414
h: '100%',
15-
bg: 'black',
1615
va: 'bottom',
1716
});
1817

@@ -23,9 +22,6 @@ const overClass = rule({
2322
isolation: 'isolate',
2423
us: 'none',
2524
transform: 'translateX(calc(-50% + 0px))',
26-
// w: '1px',
27-
// h: '1px',
28-
// bd: '1px solid red',
2925
});
3026

3127
export interface RenderCaretProps extends CaretViewProps {
@@ -34,17 +30,36 @@ export interface RenderCaretProps extends CaretViewProps {
3430

3531
export const RenderCaret: React.FC<RenderCaretProps> = ({children}) => {
3632
const {toolbar} = useToolbarPlugin()!;
33+
const showInlineToolbar = toolbar.showInlineToolbar;
34+
const [showCaretToolbarValue, toolbarVisibilityChangeTime] = useSyncStore(showInlineToolbar);
35+
const focus = useSyncStoreOpt(toolbar.surface.dom?.cursor.focus) || false;
36+
const doHideForCoolDown = toolbarVisibilityChangeTime + 500 > Date.now();
37+
const enableAfterCoolDown = useTimeout(500, [doHideForCoolDown]);
38+
39+
// biome-ignore lint/correctness/useExhaustiveDependencies: showInlineToolbar.next do not need to memoize
40+
const handleClose = React.useCallback(() => {
41+
setTimeout(() => {
42+
if (showInlineToolbar.value) showInlineToolbar.next([false, Date.now()]);
43+
}, 5);
44+
}, []);
45+
46+
let toolbarElement = (
47+
<CaretToolbar disabled={!enableAfterCoolDown} menu={toolbar.getCaretMenu()} onPopupClose={handleClose} />
48+
);
3749

38-
const lastEventIsCaretPositionChange =
39-
toolbar.lastEvent?.type === 'cursor' &&
40-
typeof (toolbar.lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number';
50+
if (doHideForCoolDown) {
51+
toolbarElement = <AfterTimeout ms={500}>{toolbarElement}</AfterTimeout>;
52+
}
4153

4254
return (
4355
<span className={blockClass}>
4456
{children}
45-
<span className={overClass} contentEditable={false}>
46-
{lastEventIsCaretPositionChange && <CaretToolbar />}
47-
</span>
57+
{/* <span
58+
className={overClass}
59+
contentEditable={false}
60+
>
61+
{(showCaretToolbarValue && focus) && (toolbarElement)}
62+
</span> */}
4863
</span>
4964
);
5065
};

0 commit comments

Comments
 (0)