Skip to content

Commit 7a3dcc6

Browse files
authored
Merge pull request #847 from streamich/peritext-block-menu
Peritext block menu
2 parents 901c4b2 + a642482 commit 7a3dcc6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2488
-614
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@
114114
"json-crdt-traces": "https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d",
115115
"json-logic-js": "^2.0.2",
116116
"nano-theme": "^1.4.3",
117-
"nice-ui": "^1.28.0",
117+
"nice-ui": "^1.29.1",
118118
"quill-delta": "^5.1.0",
119119
"react": "^18.3.1",
120120
"react-dom": "^18.3.1",
121-
"rxjs": "^7.8.1",
121+
"rxjs": "^7.8.2",
122122
"ts-jest": "^29.1.2",
123123
"ts-loader": "^9.5.1",
124124
"ts-node": "^10.9.2",

src/json-crdt-extensions/peritext/block/Block.ts

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export class Block<T = string, Attr = unknown> extends Range<T> implements IBloc
5656
return this.marker?.data() as Attr | undefined;
5757
}
5858

59+
public isLeaf(): boolean {
60+
return false;
61+
}
62+
5963
/**
6064
* Iterate through all overlay points of this block, until the next marker
6165
* (regardless if that marker is a child or not).

src/json-crdt-extensions/peritext/block/LeafBlock.ts

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export class LeafBlock<Attr = unknown> extends Block<Attr> {
1616
return str;
1717
}
1818

19+
public isLeaf(): boolean {
20+
return true;
21+
}
22+
1923
// ------------------------------------------------------------------- export
2024

2125
public toJson(): PeritextMlElement {

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {printTs, tick} from '../../../json-crdt-patch';
1+
import {printTs} from '../../../json-crdt-patch';
22
import {CursorAnchor} from '../slice/constants';
33
import {PersistedSlice} from '../slice/PersistedSlice';
44
import type {Point} from '../rga/Point';
@@ -22,6 +22,9 @@ import type {Point} from '../rga/Point';
2222
* side of the anchor is determined by the {@link Cursor#anchorSide} property.
2323
*/
2424
export class Cursor<T = string> extends PersistedSlice<T> {
25+
/**
26+
* @todo Remove getter `get` here.
27+
*/
2528
public get anchorSide(): CursorAnchor {
2629
return this.type as CursorAnchor;
2730
}

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

+97-47
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ import type {SliceRegistry} from '../registry/SliceRegistry';
2424
import type {
2525
CharIterator,
2626
CharPredicate,
27-
Position,
27+
EditorPosition,
2828
TextRangeUnit,
2929
ViewStyle,
3030
ViewRange,
3131
ViewSlice,
3232
EditorUi,
33+
EditorSelection,
3334
} from './types';
3435

3536
/**
@@ -106,11 +107,18 @@ export class Editor<T = string> implements Printable {
106107
public cursors0(): UndefIterator<Cursor<T>> {
107108
const iterator = this.txt.localSlices.iterator0();
108109
return () => {
109-
const slice = iterator();
110-
return slice instanceof Cursor ? slice : void 0;
110+
while (true) {
111+
const slice = iterator();
112+
if (slice instanceof Cursor) return slice;
113+
if (!slice) return;
114+
}
111115
};
112116
}
113117

118+
public mainCursor(): Cursor<T> | undefined {
119+
return this.cursors0()();
120+
}
121+
114122
public cursors() {
115123
return new UndefEndIter(this.cursors0());
116124
}
@@ -169,7 +177,7 @@ export class Editor<T = string> implements Printable {
169177
* the contents is removed and the cursor is set at the start of the range
170178
* as caret.
171179
*/
172-
public collapseCursor(cursor: Cursor<T>): void {
180+
public collapseCursor(cursor: Range<T>): void {
173181
this.delRange(cursor);
174182
cursor.collapseToStart();
175183
}
@@ -196,37 +204,42 @@ export class Editor<T = string> implements Printable {
196204
* Insert inline text at current cursor position. If cursor selects a range,
197205
* the range is removed and the text is inserted at the start of the range.
198206
*/
199-
public insert0(cursor: Cursor<T>, text: string): ITimespanStruct | undefined {
207+
public insert0(range: Range<T>, text: string): ITimespanStruct | undefined {
200208
if (!text) return;
201-
if (!cursor.isCollapsed()) this.delRange(cursor);
202-
const after = cursor.start.clone();
209+
if (!range.isCollapsed()) this.delRange(range);
210+
const after = range.start.clone();
203211
after.refAfter();
204212
const txt = this.txt;
205213
const textId = txt.ins(after.id, text);
206214
const span = new Timespan(textId.sid, textId.time, text.length);
207215
const shift = text.length - 1;
208216
const point = txt.point(shift ? tick(textId, shift) : textId, Anchor.After);
209-
cursor.set(point, point, CursorAnchor.Start);
217+
if (range instanceof Cursor) range.set(point, point, CursorAnchor.Start);
218+
else range.set(point);
210219
return span;
211220
}
212221

213222
/**
214223
* Inserts text at the cursor positions and collapses cursors, if necessary.
215-
* The applies any pending inline formatting to the inserted text.
224+
* Then applies any pending inline formatting to the inserted text.
216225
*/
217-
public insert(text: string): ITimespanStruct[] {
226+
public insert(text: string, ranges?: IterableIterator<Range<T>> | Range<T>[]): ITimespanStruct[] {
218227
const spans: ITimespanStruct[] = [];
219-
if (!this.hasCursor()) this.addCursor();
220-
for (let cursor: Cursor<T> | undefined, i = this.cursors0(); (cursor = i()); ) {
221-
const span = this.insert0(cursor, text);
228+
if (!ranges) {
229+
if (!this.hasCursor()) this.addCursor();
230+
ranges = this.cursors();
231+
}
232+
if (!ranges) return [];
233+
for (const range of ranges) {
234+
const span = this.insert0(range, text);
222235
if (span) spans.push(span);
223236
const pending = this.pending.value;
224237
if (pending.size) {
225238
this.pending.next(new Map());
226-
const start = cursor.start.clone();
239+
const start = range.start.clone();
227240
start.step(-text.length);
228-
const range = this.txt.range(start, cursor.end.clone());
229-
for (const [type, data] of pending) this.toggleRangeExclFmt(range, type, data);
241+
const toggleRange = this.txt.range(start, range.end.clone());
242+
for (const [type, data] of pending) this.toggleRangeExclFmt(toggleRange, type, data);
230243
}
231244
}
232245
return spans;
@@ -494,6 +507,11 @@ export class Editor<T = string> implements Printable {
494507
return point;
495508
}
496509
case 'line': {
510+
if (steps > 0) for (let i = 0; i < steps; i++) point = this.eol(point);
511+
else for (let i = 0; i < -steps; i++) point = this.bol(point);
512+
return point;
513+
}
514+
case 'vline': {
497515
if (steps > 0) for (let i = 0; i < steps; i++) point = ui?.eol?.(point, 1) ?? this.eol(point);
498516
else for (let i = 0; i < -steps; i++) point = ui?.eol?.(point, -1) ?? this.bol(point);
499517
return point;
@@ -605,18 +623,21 @@ export class Editor<T = string> implements Printable {
605623
});
606624
}
607625

608-
public selectAt(at: Position<T>, unit: TextRangeUnit | '', ui?: EditorUi<T>): void {
609-
this.cursor.set(this.point(at));
626+
public selectAt(at: EditorPosition<T>, unit: TextRangeUnit | '', ui?: EditorUi<T>): void {
627+
this.cursor.set(this.pos2point(at));
610628
if (unit) this.select(unit, ui);
611629
}
612630

613631
// --------------------------------------------------------------- formatting
614632

615-
public eraseFormatting(store: EditorSlices<T> = this.saved): void {
633+
public eraseFormatting(
634+
store: EditorSlices<T> = this.saved,
635+
selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors(),
636+
): void {
616637
const overlay = this.txt.overlay;
617-
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
638+
for (const range of selection) {
618639
overlay.refresh();
619-
const contained = overlay.findContained(cursor);
640+
const contained = overlay.findContained(range);
620641
for (const slice of contained) {
621642
if (slice instanceof PersistedSlice) {
622643
switch (slice.behavior) {
@@ -628,7 +649,7 @@ export class Editor<T = string> implements Printable {
628649
}
629650
}
630651
overlay.refresh();
631-
const overlapping = overlay.findOverlapping(cursor);
652+
const overlapping = overlay.findOverlapping(range);
632653
for (const slice of overlapping) {
633654
switch (slice.behavior) {
634655
case SliceBehavior.One:
@@ -640,11 +661,14 @@ export class Editor<T = string> implements Printable {
640661
}
641662
}
642663

643-
public clearFormatting(store: EditorSlices<T> = this.saved): void {
664+
public clearFormatting(
665+
store: EditorSlices<T> = this.saved,
666+
selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors(),
667+
): void {
644668
const overlay = this.txt.overlay;
645669
overlay.refresh();
646-
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
647-
const overlapping = overlay.findOverlapping(cursor);
670+
for (const range of selection) {
671+
const overlapping = overlay.findOverlapping(range);
648672
for (const slice of overlapping) store.del(slice.id);
649673
}
650674
}
@@ -691,18 +715,19 @@ export class Editor<T = string> implements Printable {
691715
type: CommonSliceType | string | number,
692716
data?: unknown,
693717
store: EditorSlices<T> = this.saved,
718+
selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors(),
694719
): void {
695720
// TODO: handle mutually exclusive slices (<sub>, <sub>)
696721
this.txt.overlay.refresh();
697-
CURSORS: for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
698-
if (cursor.isCollapsed()) {
722+
SELECTION: for (const range of selection) {
723+
if (range.isCollapsed()) {
699724
const pending = this.pending.value;
700725
if (pending.has(type)) pending.delete(type);
701726
else pending.set(type, data);
702727
this.pending.next(pending);
703-
continue CURSORS;
728+
continue SELECTION;
704729
}
705-
this.toggleRangeExclFmt(cursor, type, data, store);
730+
this.toggleRangeExclFmt(range, type, data, store);
706731
}
707732
}
708733

@@ -834,22 +859,27 @@ export class Editor<T = string> implements Printable {
834859
return true;
835860
}
836861

837-
public split(type?: SliceType, data?: unknown, slices: EditorSlices<T> = this.saved): void {
862+
public split(
863+
type?: SliceType,
864+
data?: unknown,
865+
selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors(),
866+
slices: EditorSlices<T> = this.saved,
867+
): void {
838868
if (type === void 0) {
839-
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
840-
this.collapseCursor(cursor);
841-
const didInsertMarker = this.splitAt(cursor.start, slices);
842-
if (didInsertMarker) cursor.move(1);
869+
for (const range of selection) {
870+
this.collapseCursor(range);
871+
const didInsertMarker = this.splitAt(range.start, slices);
872+
if (didInsertMarker && range instanceof Cursor) range.move(1);
843873
}
844874
} else {
845-
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
846-
this.collapseCursor(cursor);
875+
for (const range of selection) {
876+
this.collapseCursor(range);
847877
if (type === void 0) {
848878
// TODO: detect current block type
849879
type = CommonSliceType.p;
850880
}
851881
slices.insMarker(type, data);
852-
cursor.move(1);
882+
if (range instanceof Cursor) range.move(1);
853883
}
854884
}
855885
}
@@ -901,11 +931,11 @@ export class Editor<T = string> implements Printable {
901931
public tglMarker(
902932
type: SliceType,
903933
data?: unknown,
934+
selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors(),
904935
slices: EditorSlices<T> = this.saved,
905936
def: SliceTypeStep = SliceTypeCon.p,
906937
): void {
907-
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i())
908-
this.tglMarkerAt(cursor.start, type, data, slices, def);
938+
for (const range of selection) this.tglMarkerAt(range.start, type, data, slices, def);
909939
}
910940

911941
/**
@@ -916,15 +946,19 @@ export class Editor<T = string> implements Printable {
916946
* @param slices The slices set to use, if new marker is inserted at the start
917947
* of the document.
918948
*/
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);
949+
public updMarker(
950+
type: SliceType,
951+
data?: unknown,
952+
selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors(),
953+
slices: EditorSlices<T> = this.saved,
954+
): void {
955+
for (const range of selection) this.updMarkerAt(range.start, type, data, slices);
922956
}
923957

924-
public delMarker(): void {
958+
public delMarker(selection: Range<T>[] | IterableIterator<Range<T>> = this.cursors()): void {
925959
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);
960+
for (const range of selection) {
961+
const markerPoint = this.txt.overlay.getOrNextLowerMarker(range.start);
928962
if (markerPoint) markerPoints.add(markerPoint);
929963
}
930964
for (const markerPoint of markerPoints) {
@@ -1102,8 +1136,24 @@ export class Editor<T = string> implements Printable {
11021136

11031137
// ------------------------------------------------------------------ various
11041138

1105-
public point(at: Position<T>): Point<T> {
1106-
return typeof at === 'number' ? this.txt.pointAt(at) : Array.isArray(at) ? this.txt.pointAt(at[0], at[1]) : at;
1139+
public pos2point(at: EditorPosition<T>): Point<T> {
1140+
const txt = this.txt;
1141+
return typeof at === 'number' ? txt.pointAt(at) : Array.isArray(at) ? txt.pointAt(at[0], at[1]) : at;
1142+
}
1143+
1144+
public sel2range(at: EditorSelection<T>): [range: Range<T>, anchor: CursorAnchor] {
1145+
if (!Array.isArray(at)) return [at, CursorAnchor.End];
1146+
const [pos1, pos2] = at;
1147+
const p1 = this.pos2point(pos1);
1148+
const txt = this.txt;
1149+
if (pos2 === undefined) {
1150+
p1.refAfter();
1151+
return [txt.range(p1), CursorAnchor.End];
1152+
}
1153+
const p2 = this.pos2point(pos2);
1154+
const range = txt.rangeFromPoints(p1, p2);
1155+
const anchor: CursorAnchor = range.start === p1 ? CursorAnchor.Start : CursorAnchor.End;
1156+
return [range, anchor];
11071157
}
11081158

11091159
public end(): Point<T> {

0 commit comments

Comments
 (0)