Skip to content

Commit 3cd6657

Browse files
authored
Merge pull request #845 from streamich/peritext-block-rendering-improvements
Peritext block rendering improvements
2 parents ec1cf9a + 846824c commit 3cd6657

File tree

17 files changed

+231
-90
lines changed

17 files changed

+231
-90
lines changed

src/json-crdt-extensions/peritext/editor/Editor.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {printTree} from 'tree-dump/lib/printTree';
77
import {createRegistry} from '../registry/registry';
88
import {PersistedSlice} from '../slice/PersistedSlice';
99
import {stringify} from '../../../json-text/stringify';
10-
import {CommonSliceType, type SliceTypeSteps, type SliceType} from '../slice';
10+
import {CommonSliceType, type SliceTypeSteps, type SliceType, type SliceTypeStep} from '../slice';
1111
import {isLetter, isPunctuation, isWhitespace, stepsEqual} from './util';
1212
import {ValueSyncStore} from '../../../util/events/sync-store';
1313
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
@@ -854,6 +854,85 @@ export class Editor<T = string> implements Printable {
854854
}
855855
}
856856

857+
public setStartMarker(type: SliceType, data?: unknown, slices: EditorSlices<T> = this.saved): MarkerSlice<T> {
858+
const after = this.txt.pointStart() ?? this.txt.pointAbsStart();
859+
after.refAfter();
860+
if (Array.isArray(type) && type.length === 1) type = type[0];
861+
return slices.slices.insMarkerAfter(after.id, type, data);
862+
}
863+
864+
public tglMarkerAt(
865+
point: Point<T>,
866+
type: SliceType,
867+
data?: unknown,
868+
slices: EditorSlices<T> = this.saved,
869+
def: SliceTypeStep = SliceTypeCon.p,
870+
): void {
871+
const overlay = this.txt.overlay;
872+
const markerPoint = overlay.getOrNextLowerMarker(point);
873+
if (markerPoint) {
874+
const marker = markerPoint.marker;
875+
const tag = marker.tag();
876+
if (!Array.isArray(type)) type = [type];
877+
const typeTag = type[type.length - 1];
878+
if (tag === typeTag) type = [...type.slice(0, -1), def];
879+
if (Array.isArray(type) && type.length === 1) type = type[0];
880+
marker.update({type});
881+
} else this.setStartMarker(type, data, slices);
882+
}
883+
884+
public updMarkerAt(point: Point<T>, type: SliceType, data?: unknown, slices: EditorSlices<T> = this.saved): void {
885+
const overlay = this.txt.overlay;
886+
const markerPoint = overlay.getOrNextLowerMarker(point);
887+
if (markerPoint) {
888+
const marker = markerPoint.marker;
889+
if (Array.isArray(type) && type.length === 1) type = type[0];
890+
marker.update({type});
891+
} else this.setStartMarker(type, data, slices);
892+
}
893+
894+
/**
895+
* Toggle the type of a block split between the slice type and the default
896+
* (paragraph) block type.
897+
*
898+
* @param type Slice type to toggle.
899+
* @param data Custom data of the slice.
900+
*/
901+
public tglMarker(
902+
type: SliceType,
903+
data?: unknown,
904+
slices: EditorSlices<T> = this.saved,
905+
def: SliceTypeStep = SliceTypeCon.p,
906+
): void {
907+
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i())
908+
this.tglMarkerAt(cursor.start, type, data, slices, def);
909+
}
910+
911+
/**
912+
* Update the type of a block split at all cursor positions.
913+
*
914+
* @param type Slice type to set.
915+
* @param data Custom data of the slice.
916+
* @param slices The slices set to use, if new marker is inserted at the start
917+
* of the document.
918+
*/
919+
public updMarker(type: SliceType, data?: unknown, slices: EditorSlices<T> = this.saved): void {
920+
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i())
921+
this.updMarkerAt(cursor.start, type, data, slices);
922+
}
923+
924+
public delMarker(): void {
925+
const markerPoints = new Set<MarkerOverlayPoint<T>>();
926+
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
927+
const markerPoint = this.txt.overlay.getOrNextLowerMarker(cursor.start);
928+
if (markerPoint) markerPoints.add(markerPoint);
929+
}
930+
for (const markerPoint of markerPoints) {
931+
const boundary = markerPoint.marker.boundary();
932+
this.delRange(boundary);
933+
}
934+
}
935+
857936
// ---------------------------------------------------------- export / import
858937

859938
public export(range: Range<T>): ViewRange {

src/json-crdt-extensions/peritext/overlay/Overlay.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,16 @@ export class Overlay<T = string> implements Printable, Stateful {
496496
return op instanceof MarkerOverlayPoint && op.id.time === id.time && op.id.sid === id.sid;
497497
}
498498

499+
public skipMarkers(point: Point<T>, direction: -1 | 1): boolean {
500+
while (true) {
501+
const isMarker = this.isMarker(point.id);
502+
if (!isMarker) return true;
503+
const end = point.step(direction);
504+
if (end) break;
505+
}
506+
return false;
507+
}
508+
499509
// ----------------------------------------------------------------- Stateful
500510

501511
public hash: number = 0;
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1+
import {Anchor} from '../rga/constants';
12
import {PersistedSlice} from './PersistedSlice';
3+
import type {Range} from '../rga/Range';
24

35
/**
46
* Represents a block split in the text, i.e. it is a *marker* that shows
57
* where a block was split. Markers also insert one "\n" new line character.
68
* Both marker ends are attached to the "before" anchor fo the "\n" new line
79
* character, i.e. it is *collapsed* to the "before" anchor.
810
*/
9-
export class MarkerSlice<T = string> extends PersistedSlice<T> {}
11+
export class MarkerSlice<T = string> extends PersistedSlice<T> {
12+
/**
13+
* Returns the {@link Range} which exactly contains the block boundary of this
14+
* marker.
15+
*/
16+
public boundary(): Range<T> {
17+
const start = this.start;
18+
const end = start.clone();
19+
end.anchor = Anchor.After;
20+
return this.txt.range(start, end);
21+
}
22+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {s} from '../../../json-crdt-patch';
2121
import type {VecNode} from '../../../json-crdt/nodes';
2222
import type {ITimestampStruct} from '../../../json-crdt-patch/clock';
2323
import type {ArrChunk} from '../../../json-crdt/nodes';
24-
import type {MutableSlice, SliceView, SliceType, SliceUpdateParams, SliceTypeSteps} from './types';
24+
import type {MutableSlice, SliceView, SliceType, SliceUpdateParams, SliceTypeSteps, SliceTypeStep} from './types';
2525
import type {Stateful} from '../types';
2626
import type {Printable} from 'tree-dump/lib/types';
2727
import type {AbstractRga} from '../../../json-crdt/nodes/rga';
@@ -109,6 +109,11 @@ export class PersistedSlice<T = string> extends Range<T> implements MutableSlice
109109
public behavior: SliceBehavior;
110110
public type: SliceType;
111111

112+
public tag(): SliceTypeStep {
113+
const type = this.type;
114+
return Array.isArray(type) ? type[type.length - 1] : type;
115+
}
116+
112117
public typeSteps(): SliceTypeSteps {
113118
const type = this.type ?? SliceTypeCon.p;
114119
return Array.isArray(type) ? type : [type];

src/json-crdt-peritext-ui/__demos__/components/App.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {CursorPlugin} from '../../plugins/cursor';
77
import {ToolbarPlugin} from '../../plugins/toolbar';
88
import {DebugPlugin} from '../../plugins/debug';
99
import {BlocksPlugin} from '../../plugins/blocks';
10-
import {ValueSyncStore} from '../../../util/events/sync-store';
10+
import {DebugState} from '../../plugins/debug/state';
1111

1212
const markdown =
1313
'The German __automotive sector__ is in the process of *cutting ' +
@@ -37,11 +37,11 @@ export const App: React.FC = () => {
3737
}, [model]);
3838

3939
const plugins = React.useMemo(() => {
40-
const debugEnabled = new ValueSyncStore(false);
40+
const debugState = new DebugState();
4141
const cursorPlugin = new CursorPlugin();
42-
const toolbarPlugin = new ToolbarPlugin({debug: debugEnabled});
42+
const toolbarPlugin = new ToolbarPlugin({debug: debugState});
4343
const blocksPlugin = new BlocksPlugin();
44-
const debugPlugin = new DebugPlugin({enabled: debugEnabled});
44+
const debugPlugin = new DebugPlugin({state: debugState});
4545
return [cursorPlugin, toolbarPlugin, blocksPlugin, debugPlugin];
4646
}, []);
4747

src/json-crdt-peritext-ui/events/defaults/PeritextEventDefaults.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,13 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
4545
if (!pos) return;
4646
const currLine = ui.getLineInfo(point);
4747
if (!currLine) return;
48-
const lineEdgeX = currLine[0][1].x;
49-
const relX = pos[0] - lineEdgeX;
48+
const x = pos[0];
5049
const iterations = Math.abs(steps);
5150
let nextPoint = point;
5251
for (let i = 0; i < iterations; i++) {
53-
const nextLine = steps > 0 ? ui.getNextLineInfo(currLine) : ui.getPrevLineInfo(currLine);
52+
const nextLine = steps > 0 ? ui.getNextLineInfo(currLine) : ui.getNextLineInfo(currLine, -1);
5453
if (!nextLine) break;
55-
nextPoint = ui.findPointAtRelX(relX, nextLine);
54+
nextPoint = ui.findPointAtX(x, nextLine);
5655
if (!nextPoint) break;
5756
if (point.anchor === Anchor.Before) nextPoint.refBefore();
5857
else nextPoint.refAfter();
@@ -187,17 +186,22 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
187186

188187
public readonly marker = (event: CustomEvent<events.MarkerDetail>) => {
189188
const {action, type, data} = event.detail;
189+
const editor = this.txt.editor;
190190
switch (action) {
191191
case 'ins': {
192-
this.txt.editor.split(type, data);
192+
editor.split(type, data);
193193
break;
194194
}
195195
case 'tog': {
196-
const marker = this.txt.overlay.getOrNextLowerMarker(this.txt.editor.cursor.start);
197-
if (marker) {
198-
marker.marker.update({type});
199-
}
200-
console.log('togggling..', marker);
196+
if (type !== void 0) editor.tglMarker(type, data);
197+
break;
198+
}
199+
case 'upd': {
200+
if (type !== void 0) editor.updMarker(type, data);
201+
break;
202+
}
203+
case 'del': {
204+
editor.delMarker();
201205
break;
202206
}
203207
}

src/json-crdt-peritext-ui/events/defaults/ui/UiHandle.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ export class UiHandle {
4343
return [x, rect];
4444
}
4545

46-
public findPointAtRelX(relX: number, line: UiLineInfo): Point<string> {
47-
const lineRect = line[0][1];
48-
const lineX = lineRect.x;
46+
public findPointAtX(targetX: number, line: UiLineInfo): Point<string> {
4947
let point = line[0][0].clone();
5048
const curr = point;
5149
let bestDiff = 1e9;
@@ -55,8 +53,7 @@ export class UiHandle {
5553
const pointX = this.pointX(curr);
5654
if (!pointX) break;
5755
const [x] = pointX;
58-
const currRelX = x - lineX;
59-
const diff = Math.abs(currRelX - relX);
56+
const diff = Math.abs(x - targetX);
6057
if (diff <= bestDiff) {
6158
bestDiff = diff;
6259
point = curr.clone();
@@ -104,17 +101,12 @@ export class UiHandle {
104101
return [left, right];
105102
}
106103

107-
public getPrevLineInfo(line: UiLineInfo): UiLineInfo | undefined {
108-
const [[left]] = line;
109-
if (left.isStart()) return;
110-
const point = left.copy((p) => p.step(-1));
111-
return this.getLineInfo(point);
112-
}
113-
114-
public getNextLineInfo(line: UiLineInfo): UiLineInfo | undefined {
115-
const [, [right]] = line;
116-
if (right.viewPos() >= this.txt.str.length()) return;
117-
const point = right.copy((p) => p.step(1));
104+
public getNextLineInfo(line: UiLineInfo, direction: 1 | -1 = 1): UiLineInfo | undefined {
105+
const edge = line[direction > 0 ? 1 : 0][0];
106+
if (edge.viewPos() >= this.txt.str.length()) return;
107+
const point = edge.copy((p) => p.step(direction));
108+
const success = this.txt.overlay.skipMarkers(point, direction);
109+
if (!success) return;
118110
return this.getLineInfo(point);
119111
}
120112
}

src/json-crdt-peritext-ui/plugins/debug/DebugPlugin.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import * as React from 'react';
22
import {RenderInline} from './RenderInline';
33
import {RenderBlock} from './RenderBlock';
4+
import {RenderCaret} from './RenderCaret';
45
import {RenderPeritext, type RenderPeritextProps} from './RenderPeritext';
56
import type {PeritextPlugin} from '../../react/types';
6-
import {RenderCaret} from './RenderCaret';
77

8-
export interface DebugPluginOpts extends Pick<RenderPeritextProps, 'enabled'> {}
8+
export interface DebugPluginOpts extends Pick<RenderPeritextProps, 'state'> {}
99

1010
export class DebugPlugin implements PeritextPlugin {
1111
constructor(protected readonly opts: DebugPluginOpts = {}) {}

src/json-crdt-peritext-ui/plugins/debug/RenderBlock.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export interface RenderBlockProps extends BlockViewProps {
2121

2222
export const RenderBlock: React.FC<RenderBlockProps> = ({block, hash, children}) => {
2323
const ctx = useDebugCtx();
24-
const enabled = useSyncStore(ctx.enabled);
24+
const enabled = useSyncStore(ctx.state.enabled);
25+
const showSliceOutlines = useSyncStore(ctx.state.showSliceOutlines);
26+
const showSliceInfo = useSyncStore(ctx.state.showSliceInfo);
2527

2628
if (!enabled) return children;
2729

@@ -30,10 +32,12 @@ export const RenderBlock: React.FC<RenderBlockProps> = ({block, hash, children})
3032

3133
return (
3234
<div style={{position: 'relative'}}>
33-
<div contentEditable={false} className={labelContainerClass} onMouseDown={(e) => e.preventDefault()}>
34-
<DebugLabel right={hash.toString(36)}>{block.path.map((type) => formatType(type)).join('.')}</DebugLabel>
35-
</div>
36-
<div style={{outline: '1px dotted blue'}}>{children}</div>
35+
{showSliceInfo && (
36+
<div contentEditable={false} className={labelContainerClass} onMouseDown={(e) => e.preventDefault()}>
37+
<DebugLabel right={hash.toString(36)}>{block.path.map((type) => formatType(type)).join('.')}</DebugLabel>
38+
</div>
39+
)}
40+
{showSliceOutlines ? <div style={{outline: '1px dotted blue'}}>{children}</div> : children}
3741
</div>
3842
);
3943
};

0 commit comments

Comments
 (0)