@dxos/react-ui-editor 0.3.11-main.346d280 → 0.3.11-main.3a8a0ef

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,57 +2,39 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import {
6
- EditorView,
7
- Decoration,
8
- WidgetType,
9
- MatchDecorator,
10
- ViewPlugin,
11
- type DecorationSet,
12
- type ViewUpdate,
13
- } from '@codemirror/view';
14
-
15
- // TODO(burdon): Reconcile with theme.
16
- const styles = EditorView.baseTheme({
17
- '& .cm-task-item': {
18
- paddingLeft: '4px',
19
- paddingRight: '8px',
20
- },
21
- });
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { RangeSetBuilder } from '@codemirror/state';
7
+ import { EditorView, Decoration, WidgetType, ViewPlugin, type DecorationSet, type ViewUpdate } from '@codemirror/view';
22
8
 
23
9
  class CheckboxWidget extends WidgetType {
24
- constructor(
25
- private _checked: boolean,
26
- private readonly _indent: number,
27
- private readonly _onCheck?: (check: boolean) => void,
28
- ) {
10
+ constructor(private _checked: boolean) {
29
11
  super();
30
12
  }
31
13
 
32
14
  override eq(other: this) {
33
- return this._checked === other._checked && this._indent === other._indent;
15
+ return this._checked === other._checked;
34
16
  }
35
17
 
36
18
  override toDOM(view: EditorView) {
37
- const wrap = document.createElement('span');
38
- wrap.className = 'cm-task-item';
39
- wrap.setAttribute('aria-hidden', 'true');
40
- wrap.style.setProperty('margin-left', this._indent * 24 + 'px');
41
-
42
- const input = wrap.appendChild(document.createElement('input'));
19
+ const input = document.createElement('input');
43
20
  input.type = 'checkbox';
44
21
  input.checked = this._checked;
45
- if (!this._onCheck) {
22
+ if (view.state.readOnly) {
46
23
  input.setAttribute('disabled', 'true');
47
- }
48
- if (this._onCheck) {
49
- input.onchange = (event: Event) => {
50
- this._onCheck?.((event.target as any).checked);
51
- return true;
24
+ } else {
25
+ input.onmousedown = (event: Event) => {
26
+ const pos = view.posAtDOM(input);
27
+ const text = view.state.sliceDoc(pos, pos + 3);
28
+ if (text === (this._checked ? '[x]' : '[ ]')) {
29
+ view.dispatch({
30
+ changes: { from: pos + 1, to: pos + 2, insert: this._checked ? ' ' : 'x' },
31
+ });
32
+ event.preventDefault();
33
+ }
52
34
  };
53
35
  }
54
36
 
55
- return wrap;
37
+ return input;
56
38
  }
57
39
 
58
40
  override ignoreEvent() {
@@ -60,60 +42,54 @@ class CheckboxWidget extends WidgetType {
60
42
  }
61
43
  }
62
44
 
45
+ const checkedDeco = Decoration.replace({ widget: new CheckboxWidget(true) });
46
+ const uncheckedDeco = Decoration.replace({ widget: new CheckboxWidget(false) });
47
+
63
48
  export type TasklistOptions = {};
64
49
 
65
50
  export const tasklist = (options: TasklistOptions = {}) => {
66
- // TODO(burdon): Matcher isn't as precise as syntax tree.
67
- // Allows for indents to be greater than AST would allow.
68
- const taskMatcher = new MatchDecorator({
69
- regexp: /^(\s*)- \[([ xX])\]\s/g,
70
- decoration: (match, view, pos) => {
71
- const indent = Math.floor(match[1].length / 2);
72
- const checked = match[2] === 'x' || match[2] === 'X';
73
- return Decoration.replace({
74
- widget: new CheckboxWidget(
75
- checked,
76
- indent,
77
- view.state.readOnly
78
- ? undefined
79
- : (checked) => {
80
- const idx = pos + match[0].indexOf('[') + 1;
81
- view.dispatch({
82
- changes: {
83
- from: idx,
84
- to: idx + 1,
85
- insert: checked ? 'x' : ' ',
86
- },
87
- // TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
88
- selection: {
89
- anchor: idx + 3,
90
- },
91
- });
92
- },
93
- ),
94
- });
95
- },
96
- });
97
-
98
- const tasks = ViewPlugin.fromClass(
51
+ return ViewPlugin.fromClass(
99
52
  class {
100
- tasks: DecorationSet;
53
+ decorations: DecorationSet;
54
+
101
55
  constructor(view: EditorView) {
102
- this.tasks = taskMatcher.createDeco(view);
56
+ this.decorations = buildDecorations(view);
103
57
  }
104
58
 
105
59
  update(update: ViewUpdate) {
106
- this.tasks = taskMatcher.updateDeco(update, this.tasks);
60
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
61
+ this.decorations = buildDecorations(update.view);
62
+ }
107
63
  }
108
64
  },
109
65
  {
110
- decorations: (value) => value.tasks,
66
+ decorations: (v) => v.decorations,
111
67
  provide: (plugin) =>
112
68
  EditorView.atomicRanges.of((view) => {
113
- return view.plugin(plugin)?.tasks || Decoration.none;
69
+ return view.plugin(plugin)?.decorations || Decoration.none;
114
70
  }),
115
71
  },
116
72
  );
73
+ };
74
+
75
+ const buildDecorations = (view: EditorView): DecorationSet => {
76
+ const builder = new RangeSetBuilder<Decoration>();
77
+ const { state } = view;
78
+ const cursor = state.selection.main.head;
79
+
80
+ for (const { from, to } of view.visibleRanges) {
81
+ syntaxTree(state).iterate({
82
+ enter: (node) => {
83
+ // Check if cursor is inside text.
84
+ if (node.name === 'TaskMarker' && (cursor < node.from || cursor > node.to)) {
85
+ const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
86
+ builder.add(node.from, node.to, checked ? checkedDeco : uncheckedDeco);
87
+ }
88
+ },
89
+ from,
90
+ to,
91
+ });
92
+ }
117
93
 
118
- return [tasks, styles];
94
+ return builder.finish();
119
95
  };
@@ -12,7 +12,7 @@ import { type YText } from '@dxos/text-model';
12
12
  describe('YJS', () => {
13
13
  // https://docs.yjs.dev/api/shared-types/y.text
14
14
  // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
15
- test('absolute position', () => {
15
+ test.skip('absolute position', () => {
16
16
  const obj = new TextObject('hello world');
17
17
  const index = obj.text.indexOf('world');
18
18
  const relPos = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(obj.content as YText, index));
@@ -160,14 +160,6 @@ export const defaultTheme: ThemeStyles = {
160
160
  textDecoration: 'none',
161
161
  },
162
162
 
163
- //
164
- // widgets
165
- //
166
- '.cm-widgetBuffer': {
167
- display: 'none',
168
- height: 0,
169
- },
170
-
171
163
  //
172
164
  // table
173
165
  //