Skip to content

Commit 89640a9

Browse files
committed
feat(json-crdt-peritext-ui): 🎸 create formal cursor state
1 parent 44f4f2e commit 89640a9

File tree

7 files changed

+66
-71
lines changed

7 files changed

+66
-71
lines changed

src/json-crdt-peritext-ui/events/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {DomClipboard} from './clipboard/DomClipboard';
44
import {create as createDataTransfer} from '../../json-crdt-extensions/peritext/transfer/create';
55
import type {Peritext} from '../../json-crdt-extensions';
66

7+
/**
8+
* @todo Move into separately importable file.
9+
*/
710
export const createEvents = (txt: Peritext) => {
811
const et = new PeritextEventTarget();
912
const clipboard: PeritextEventDefaultsOpts['clipboard'] =
@@ -15,3 +18,5 @@ export const createEvents = (txt: Peritext) => {
1518
et.defaults = defaults;
1619
return defaults;
1720
};
21+
22+
export * from './types';

src/json-crdt-peritext-ui/plugins/cursor/CursorPlugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ const h = React.createElement;
1212
* Plugin which renders the main cursor and all other current user local
1313
* cursors.
1414
*/
15-
export class CursorPlugin implements PeritextPlugin {
15+
export class CursorPlugin implements PeritextPlugin {
1616
public readonly caret: PeritextPlugin['caret'] = (props, children) => h(RenderCaret, <any>props, children);
1717
public readonly focus: PeritextPlugin['focus'] = (props, children) => h(RenderFocus, <any>props, children);
1818
public readonly anchor: PeritextPlugin['anchor'] = (props, children) => h(RenderAnchor, <any>props, children);
1919
public readonly inline: PeritextPlugin['inline'] = (props, children) => h(RenderInline, props as any, children);
2020
public readonly peritext: PeritextPlugin['peritext'] = (children, ctx) =>
21-
h(RenderPeritext, {children, ctx, plugin: this});
21+
h(RenderPeritext, {children, ctx});
2222
}
Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,16 @@
11
import * as React from 'react';
2-
import {context, type CursorPluginContextValue} from './context';
3-
import {ValueSyncStore} from '../../../util/events/sync-store';
4-
import type {ChangeDetail} from '../../events/types';
2+
import {context} from './context';
3+
import {CursorState} from './state';
54
import type {PeritextSurfaceState} from '../../web';
6-
import type {CursorPlugin} from './CursorPlugin';
75

86
export interface RenderPeritextProps {
97
ctx: PeritextSurfaceState;
10-
plugin: CursorPlugin;
118
children?: React.ReactNode;
129
}
1310

14-
export const RenderPeritext: React.FC<RenderPeritextProps> = ({ctx, plugin, children}) => {
15-
// biome-ignore lint: explicit dependency handling
16-
const value: CursorPluginContextValue = React.useMemo(
17-
() => ({
18-
ctx,
19-
plugin,
20-
score: new ValueSyncStore(0),
21-
scoreDelta: new ValueSyncStore(0),
22-
lastVisScore: new ValueSyncStore(0),
23-
}),
24-
[ctx],
25-
);
11+
export const RenderPeritext: React.FC<RenderPeritextProps> = ({ctx, children}) => {
12+
const state = React.useMemo(() => new CursorState(ctx), [ctx]);
13+
React.useEffect(() => state.start(), [state]);
2614

27-
React.useEffect(() => {
28-
const dom = ctx?.dom;
29-
if (!dom || !value) return;
30-
let lastNow: number = 0;
31-
const listener = (event: CustomEvent<ChangeDetail>) => {
32-
const now = Date.now();
33-
const timeDiff = now - lastNow;
34-
let delta = 0;
35-
switch (event.detail.ev?.type) {
36-
case 'delete':
37-
case 'insert':
38-
case 'format':
39-
case 'marker': {
40-
delta = timeDiff < 30 ? 10 : timeDiff < 70 ? 5 : timeDiff < 150 ? 2 : timeDiff <= 1000 ? 1 : -1;
41-
break;
42-
}
43-
default: {
44-
delta = timeDiff <= 1000 ? 0 : -1;
45-
break;
46-
}
47-
}
48-
if (delta) value.score.next(delta >= 0 ? value.score.value + delta : 0);
49-
value.scoreDelta.next(delta);
50-
lastNow = now;
51-
};
52-
dom.et.addEventListener('change', listener);
53-
return () => {
54-
dom.et.removeEventListener('change', listener);
55-
};
56-
}, [ctx?.dom, value]);
57-
58-
return <context.Provider value={value}>{children}</context.Provider>;
15+
return <context.Provider value={state}>{children}</context.Provider>;
5916
};
Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,5 @@
11
import * as React from 'react';
2-
import type {PeritextSurfaceState} from '../../web';
3-
import type {ValueSyncStore} from '../../../util/events/sync-store';
4-
import type {CursorPlugin} from './CursorPlugin';
5-
6-
export interface CursorPluginContextValue {
7-
ctx?: PeritextSurfaceState;
8-
9-
plugin: CursorPlugin;
10-
11-
/** Current score. */
12-
score: ValueSyncStore<number>;
13-
14-
/** By how much the score changed. */
15-
scoreDelta: ValueSyncStore<number>;
16-
17-
/** The last score that was shown to the user. */
18-
lastVisScore: ValueSyncStore<number>;
19-
}
20-
21-
export const context = React.createContext<CursorPluginContextValue>(null!);
2+
import {CursorState} from './state';
223

4+
export const context = React.createContext<CursorState>(null!);
235
export const useCursorPlugin = () => React.useContext(context);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export {CursorPlugin} from './CursorPlugin';
2+
export {useCursorPlugin} from './context';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {ValueSyncStore} from '../../../util/events/sync-store';
2+
import {ChangeDetail} from '../../events/types';
3+
import type {PeritextSurfaceState, UiLifeCycles} from '../../web';
4+
5+
export class CursorState implements UiLifeCycles {
6+
/** Current score. */
7+
public readonly score: ValueSyncStore<number> = new ValueSyncStore(0);
8+
9+
/** By how much the score changed. */
10+
public readonly scoreDelta: ValueSyncStore<number> = new ValueSyncStore(0);
11+
12+
/** The last score that was shown to the user. */
13+
public readonly lastVisScore: ValueSyncStore<number> = new ValueSyncStore(0);
14+
15+
constructor(
16+
public readonly ctx: PeritextSurfaceState,
17+
) {}
18+
19+
/* --------------------------------------------------- {@link UiLifeCycles} */
20+
public start(): () => void {
21+
const dom = this.ctx.dom;
22+
let lastNow: number = 0;
23+
const listener = (event: CustomEvent<ChangeDetail>) => {
24+
const now = Date.now();
25+
const timeDiff = now - lastNow;
26+
let delta = 0;
27+
switch (event.detail.ev?.type) {
28+
case 'delete':
29+
case 'insert':
30+
case 'format':
31+
case 'marker': {
32+
delta = timeDiff < 30 ? 10 : timeDiff < 70 ? 5 : timeDiff < 150 ? 2 : timeDiff <= 1000 ? 1 : -1;
33+
break;
34+
}
35+
default: {
36+
delta = timeDiff <= 1000 ? 0 : -1;
37+
break;
38+
}
39+
}
40+
if (delta) this.score.next(delta >= 0 ? this.score.value + delta : 0);
41+
this.scoreDelta.next(delta);
42+
lastNow = now;
43+
};
44+
dom.et.addEventListener('change', listener);
45+
return () => {
46+
dom.et.removeEventListener('change', listener);
47+
};
48+
}
49+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './react';
22
export {PeritextSurfaceState} from './state';
3+
export {UiLifeCycles, Rect} from './types';

0 commit comments

Comments
 (0)