Skip to content

Commit 0c2a65d

Browse files
committed
feat(json-crdt-peritext-ui): 🎸 improve "format" event interface and default impl
1 parent fde45ea commit 0c2a65d

File tree

9 files changed

+93
-65
lines changed

9 files changed

+93
-65
lines changed

‎src/json-crdt-peritext-ui/events/PeritextEventTarget.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,16 @@ export class PeritextEventTarget extends SubscriptionEventTarget<PeritextEventMa
8383
}
8484
}
8585

86-
public format(type: FormatDetail['type'], behavior?: FormatDetail['behavior'], data?: FormatDetail['data']): void;
86+
public format(action: FormatDetail['action'], type: FormatDetail['type'], stack?: FormatDetail['stack'], data?: FormatDetail['data']): void;
8787
public format(detail: FormatDetail): void;
8888
public format(
89-
a: FormatDetail | FormatDetail['type'],
90-
behavior?: FormatDetail['behavior'],
89+
a: FormatDetail | FormatDetail['action'],
90+
type?: FormatDetail['type'],
91+
stack?: FormatDetail['stack'],
9192
data?: FormatDetail['data'],
9293
): void {
9394
const detail: FormatDetail =
94-
typeof a === 'object' && !Array.isArray(a) ? (a as FormatDetail) : ({type: a, behavior, data} as FormatDetail);
95+
typeof a === 'object' && !Array.isArray(a) ? (a as FormatDetail) : ({action: a, type, stack, data} as FormatDetail);
9596
this.dispatch('format', detail);
9697
}
9798

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

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -202,27 +202,38 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
202202
public readonly format = ({detail}: CustomEvent<events.FormatDetail>) => {
203203
const selection = [...this.getSelSet(detail)];
204204
this.moveSelSet(selection, detail);
205-
const {type: tag, store = 'saved', behavior = 'one', data} = detail;
205+
const {action, type: tag, store = 'saved'} = detail;
206206
const editor = this.txt.editor;
207207
const slices: EditorSlices = store === 'saved' ? editor.saved : store === 'extra' ? editor.extra : editor.local;
208-
switch (behavior) {
209-
case 'many': {
208+
switch (action) {
209+
case 'ins':
210+
case 'tog': {
211+
const {stack = 'one', data} = detail;
210212
if (tag === undefined) throw new Error('TYPE_REQUIRED');
211-
slices.insStack(tag, data, selection);
213+
switch (stack) {
214+
case 'many': {
215+
slices.insStack(tag, data, selection);
216+
break;
217+
}
218+
case 'one': {
219+
if (action === 'ins') slices.insOne(tag, data, selection);
220+
else editor.toggleExclFmt(tag, data, slices, selection);
221+
break;
222+
}
223+
case 'erase': {
224+
slices.insOne(tag, data, selection);
225+
break;
226+
}
227+
}
212228
break;
213229
}
214-
case 'one': {
215-
if (tag === undefined) throw new Error('TYPE_REQUIRED');
216-
editor.toggleExclFmt(tag, data, slices, selection);
230+
case 'del': {
231+
editor.clearFormatting(slices, selection);
217232
break;
218233
}
219234
case 'erase': {
220235
if (tag === undefined) editor.eraseFormatting(slices, selection);
221-
else slices.insErase(tag, data, selection);
222-
break;
223-
}
224-
case 'clear': {
225-
editor.clearFormatting(slices, selection);
236+
else slices.insErase(tag, detail.data, selection);
226237
break;
227238
}
228239
}

‎src/json-crdt-peritext-ui/events/types.ts‎

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,23 @@ export interface CursorDetail extends RangeEventDetail {
261261
* Event dispatched to insert an inline rich-text annotation into the document.
262262
*/
263263
export interface FormatDetail extends RangeEventDetail {
264+
/**
265+
* The action to perform.
266+
*
267+
* - The `'ins'` action inserts a new annotation into the document.
268+
* - The `'tog'` action toggles the annotation on or off, for annotations of
269+
* stack behavior `'one'`. For other annotations, it works the same as
270+
* the `'ins'` action.
271+
* - The `'del'` action removes all annotations that intersect with
272+
* any part of the selection set.
273+
* - The `'erase'` action tries to "erase" all annotations that intersect with
274+
* any part of the selection set. It works by deleting all annotations which
275+
* are contained. For annotations, which partially intersect with the
276+
* selection set, a corresponding slice with "erase" stacking behavior is
277+
* inserted, which logically removes the annotation from the document.
278+
*/
279+
action: 'ins' | 'tog' | 'del' | 'erase';
280+
264281
/**
265282
* Type of the annotation. The type is used to determine the visual style of
266283
* the annotation, for example, the type `'bold'` may render the text in bold.
@@ -290,23 +307,22 @@ export interface FormatDetail extends RangeEventDetail {
290307
data?: unknown;
291308

292309
/**
293-
* Specifies the behavior of the annotation. If `'many'`, the annotation of
294-
* this type will be stacked on top of each other, and all of them will be
295-
* applied to the text, with the last annotation on top. If `'one'`,
296-
* the annotation is not stacked, only one such annotation can be applied per
297-
* character. The `'erase'` behavior is used to remove the `'many`' or
298-
* `'one'` annotation from the the given range.
299-
*
300-
* The special `'clear'` behavior is used to remove all annotations
301-
* that intersect with any part of any of the cursors in the document. Usage:
302-
*
303-
* ```js
304-
* {type: 'clear'}
305-
* ```
310+
* Specifies the stacking behavior of the annotation.
311+
*
312+
* - If `'many'`, the annotation of this type will be stacked on top of each
313+
* other, and all of them will be applied to the text, with the last
314+
* annotation on top.
315+
* - If `'one'`, the annotation is not stacked, only one such annotation can
316+
* be applied per character. The last annotation "wins", i.e. the last
317+
* annotation in the document will be applied to the text.
318+
* - The `'erase'` behavior is used to logically remove the `'many`' or
319+
* `'one'` annotation from the the given range. It works by logically
320+
* "erasing" all `'many'` or `'one'` annotations with the same `type`,
321+
* which were applied to the given range before.
306322
*
307323
* @default 'one'
308324
*/
309-
behavior?: 'one' | 'many' | 'erase' | 'clear';
325+
stack?: 'one' | 'many' | 'erase';
310326

311327
/**
312328
* The slice set where the annotation will be stored. `'saved'` is the main

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const TopToolbar: React.FC<TopToolbarProps> = ({ctx}) => {
2323

2424
const inlineGroupButton = (type: string | number, name: React.ReactNode) => (
2525
<Button
26-
onClick={() => ctx.dom?.et.format(type)}
26+
onClick={() => ctx.dom?.et.format('tog', type)}
2727
onMouseDown={(e) => e.preventDefault()}
2828
active={(complete.has(type) && !pending?.has(type)) || (!complete.has(type) && pending?.has(type))}
2929
>
@@ -66,14 +66,14 @@ export const TopToolbar: React.FC<TopToolbarProps> = ({ctx}) => {
6666
{inlineGroupButton(CommonSliceType.bookmark, 'Bookmark')}
6767
<ButtonSeparator />
6868
{button('Blue', () => {
69-
ctx.dom?.et.format(CommonSliceType.col, 'one', '#07f');
69+
ctx.dom?.et.format('tog', CommonSliceType.col, 'one', '#07f');
7070
})}
7171
<ButtonSeparator />
7272
{button('Erase', () => {
73-
ctx.dom?.et.format({behavior: 'erase'});
73+
ctx.dom?.et.format({action: 'erase'});
7474
})}
7575
{button('Clear', () => {
76-
ctx.dom?.et.format({behavior: 'clear'});
76+
ctx.dom?.et.format({action: 'del'});
7777
})}
7878
{button('Delete block split', () => {
7979
ctx.dom?.et.marker({action: 'del'});

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const TopToolbar: React.FC<TopToolbarProps> = ({ctx}) => {
2626

2727
const inlineGroupButton = (type: string | number, name: React.ReactNode) => (
2828
<Button
29-
onClick={() => ctx.dom?.et.format(type)}
29+
onClick={() => ctx.dom?.et.format('tog', type)}
3030
onMouseDown={(e) => e.preventDefault()}
3131
active={(complete.has(type) && !pending?.has(type)) || (!complete.has(type) && pending?.has(type))}
3232
>
@@ -69,14 +69,14 @@ export const TopToolbar: React.FC<TopToolbarProps> = ({ctx}) => {
6969
{inlineGroupButton(CommonSliceType.bookmark, 'Bookmark')}
7070
<ButtonSeparator />
7171
{button('Blue', () => {
72-
ctx.dom?.et.format(CommonSliceType.col, 'one', '#07f');
72+
ctx.dom?.et.format('tog', CommonSliceType.col, 'one', '#07f');
7373
})}
7474
<ButtonSeparator />
7575
{button('Erase', () => {
76-
ctx.dom?.et.format({behavior: 'erase'});
76+
ctx.dom?.et.format({action: 'erase'});
7777
})}
7878
{button('Clear', () => {
79-
ctx.dom?.et.format({behavior: 'clear'});
79+
ctx.dom?.et.format({action: 'del'});
8080
})}
8181
{button('Delete block split', () => {
8282
ctx.dom?.et.marker({action: 'del'});

‎src/json-crdt-peritext-ui/plugins/toolbar/state/ToolbarState.tsx‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export class ToolbarState implements UiLifeCycles {
232232
right: () => <Sidetip small>⌘ B</Sidetip>,
233233
keys: ['⌘', 'b'],
234234
onSelect: () => {
235-
et.format(CommonSliceType.b);
235+
et.format('tog', CommonSliceType.b);
236236
},
237237
},
238238
{
@@ -243,7 +243,7 @@ export class ToolbarState implements UiLifeCycles {
243243
right: () => <Sidetip small>⌘ I</Sidetip>,
244244
keys: ['⌘', 'i'],
245245
onSelect: () => {
246-
et.format(CommonSliceType.i);
246+
et.format('tog', CommonSliceType.i);
247247
},
248248
},
249249
{
@@ -252,36 +252,36 @@ export class ToolbarState implements UiLifeCycles {
252252
right: () => <Sidetip small>⌘ U</Sidetip>,
253253
keys: ['⌘', 'u'],
254254
onSelect: () => {
255-
et.format(CommonSliceType.u);
255+
et.format('tog', CommonSliceType.u);
256256
},
257257
},
258258
{
259259
name: 'Strikethrough',
260260
// icon: () => <Iconista width={15} height={15} set="radix" icon="strikethrough" />,
261261
icon: () => <Iconista width={16} height={16} set="tabler" icon="strikethrough" />,
262262
onSelect: () => {
263-
et.format(CommonSliceType.s);
263+
et.format('tog', CommonSliceType.s);
264264
},
265265
},
266266
{
267267
name: 'Overline',
268268
icon: () => <Iconista width={16} height={16} set="tabler" icon="overline" />,
269269
onSelect: () => {
270-
et.format(CommonSliceType.overline);
270+
et.format('tog', CommonSliceType.overline);
271271
},
272272
},
273273
{
274274
name: 'Highlight',
275275
icon: () => <Iconista width={16} height={16} set="tabler" icon="highlight" />,
276276
onSelect: () => {
277-
et.format(CommonSliceType.mark);
277+
et.format('tog', CommonSliceType.mark);
278278
},
279279
},
280280
{
281281
name: 'Classified',
282282
icon: () => <Iconista width={16} height={16} set="tabler" icon="lock-password" />,
283283
onSelect: () => {
284-
et.format(CommonSliceType.spoiler);
284+
et.format('tog', CommonSliceType.spoiler);
285285
},
286286
},
287287
],
@@ -298,49 +298,49 @@ export class ToolbarState implements UiLifeCycles {
298298
name: 'Code',
299299
icon: () => <Iconista width={16} height={16} set="tabler" icon="code" />,
300300
onSelect: () => {
301-
et.format(CommonSliceType.code);
301+
et.format('tog', CommonSliceType.code);
302302
},
303303
},
304304
{
305305
name: 'Math',
306306
icon: () => <Iconista width={16} height={16} set="tabler" icon="math-integral-x" />,
307307
onSelect: () => {
308-
et.format(CommonSliceType.math);
308+
et.format('tog', CommonSliceType.math);
309309
},
310310
},
311311
{
312312
name: 'Superscript',
313313
icon: () => <Iconista width={16} height={16} set="tabler" icon="superscript" />,
314314
onSelect: () => {
315-
et.format(CommonSliceType.sup);
315+
et.format('tog', CommonSliceType.sup);
316316
},
317317
},
318318
{
319319
name: 'Subscript',
320320
icon: () => <Iconista width={16} height={16} set="tabler" icon="subscript" />,
321321
onSelect: () => {
322-
et.format(CommonSliceType.sub);
322+
et.format('tog', CommonSliceType.sub);
323323
},
324324
},
325325
{
326326
name: 'Keyboard key',
327327
icon: () => <Iconista width={16} height={16} set="lucide" icon="keyboard" />,
328328
onSelect: () => {
329-
et.format(CommonSliceType.kbd);
329+
et.format('tog', CommonSliceType.kbd);
330330
},
331331
},
332332
{
333333
name: 'Insertion',
334334
icon: () => <Iconista width={16} height={16} set="tabler" icon="pencil-plus" />,
335335
onSelect: () => {
336-
et.format(CommonSliceType.ins);
336+
et.format('tog', CommonSliceType.ins);
337337
},
338338
},
339339
{
340340
name: 'Deletion',
341341
icon: () => <Iconista width={16} height={16} set="tabler" icon="pencil-minus" />,
342342
onSelect: () => {
343-
et.format(CommonSliceType.del);
343+
et.format('tog', CommonSliceType.del);
344344
},
345345
},
346346
],
@@ -443,7 +443,7 @@ export class ToolbarState implements UiLifeCycles {
443443
danger: true,
444444
icon: () => <Iconista width={16} height={16} set="tabler" icon="eraser" />,
445445
onSelect: () => {
446-
et.format({behavior: 'erase'});
446+
et.format({action: 'erase'});
447447
},
448448
},
449449
{
@@ -452,7 +452,7 @@ export class ToolbarState implements UiLifeCycles {
452452
more: true,
453453
icon: () => <Iconista width={16} height={16} set="tabler" icon="trash" />,
454454
onSelect: () => {
455-
et.format({behavior: 'clear'});
455+
et.format({action: 'del'});
456456
},
457457
},
458458
],

‎src/json-crdt-peritext-ui/plugins/toolbar/state/formattings.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export class NewFormatting<Node extends ObjNode = ObjNode> extends RangeFormatti
8787
if (!data || typeof data !== 'object') return;
8888
if (!data.title) delete data.title;
8989
const et = state.surface.events.et;
90-
et.format(this.behavior.tag, 'many', data);
90+
et.format('tog', this.behavior.tag, 'many', data);
9191
et.cursor({move: [['focus', 'char', 0, true]]});
9292
};
9393
}

‎src/json-crdt-peritext-ui/web/dom/InputController.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,32 +154,32 @@ export class InputController implements UiLifeCycles {
154154
// case 'historyRedo': {}
155155
case 'formatBold': {
156156
event.preventDefault();
157-
et.format(SliceTypeCon.b);
157+
et.format('tog', SliceTypeCon.b);
158158
break;
159159
}
160160
case 'formatItalic': {
161161
event.preventDefault();
162-
et.format(SliceTypeCon.i);
162+
et.format('tog', SliceTypeCon.i);
163163
break;
164164
}
165165
case 'formatUnderline': {
166166
event.preventDefault();
167-
et.format(SliceTypeCon.u);
167+
et.format('tog', SliceTypeCon.u);
168168
break;
169169
}
170170
case 'formatStrikeThrough': {
171171
event.preventDefault();
172-
et.format(SliceTypeCon.s);
172+
et.format('tog', SliceTypeCon.s);
173173
break;
174174
}
175175
case 'formatSuperscript': {
176176
event.preventDefault();
177-
et.format(SliceTypeCon.sup);
177+
et.format('tog', SliceTypeCon.sup);
178178
break;
179179
}
180180
case 'formatSubscript': {
181181
event.preventDefault();
182-
et.format(SliceTypeCon.sub);
182+
et.format('tog', SliceTypeCon.sub);
183183
break;
184184
}
185185
// case 'formatJustifyFull': { // make the current selection fully justified

‎src/json-crdt-peritext-ui/web/dom/RichTextController.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,23 @@ export class RichTextController implements UiLifeCycles {
1818
switch (key) {
1919
case 'b':
2020
event.preventDefault();
21-
et.format(CommonSliceType.b);
21+
et.format('tog', CommonSliceType.b);
2222
return;
2323
case 'i':
2424
event.preventDefault();
25-
et.format(CommonSliceType.i);
25+
et.format('tog', CommonSliceType.i);
2626
return;
2727
case 'u':
2828
event.preventDefault();
29-
et.format(CommonSliceType.u);
29+
et.format('tog', CommonSliceType.u);
3030
return;
3131
}
3232
}
3333
if (event.metaKey && event.shiftKey) {
3434
switch (key) {
3535
case 'x':
3636
event.preventDefault();
37-
et.format(CommonSliceType.s);
37+
et.format('tog', CommonSliceType.s);
3838
return;
3939
}
4040
}

0 commit comments

Comments
 (0)