• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marekdedic / prosemirror-remark / 13577133054

27 Feb 2025 10:29PM UTC coverage: 80.876%. First build
13577133054

Pull #482

github

web-flow
Merge f69b47264 into 8b75c17a9
Pull Request #482: Fixes for TaskListItemExtension

65 of 117 branches covered (55.56%)

2 of 8 new or added lines in 1 file covered. (25.0%)

351 of 434 relevant lines covered (80.88%)

26.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.94
/src/syntax-extensions/TaskListItemExtension.ts
1
import type { BlockContent, DefinitionContent, ListItem } from "mdast";
2
import type {
3
  DOMOutputSpec,
4
  NodeSpec,
5
  Node as ProseMirrorNode,
6
  Schema,
7
} from "prosemirror-model";
8
import type { Command, EditorState } from "prosemirror-state";
9
import type {
10
  EditorView,
11
  NodeView,
12
  NodeViewConstructor,
13
} from "prosemirror-view";
14
import type { Processor } from "unified";
15
import type { Node as UnistNode } from "unist";
16

17
import {
4✔
18
  gfmTaskListItemFromMarkdown,
19
  gfmTaskListItemToMarkdown,
20
} from "mdast-util-gfm-task-list-item";
21
import { gfmTaskListItem } from "micromark-extension-gfm-task-list-item";
4✔
22
import { InputRule } from "prosemirror-inputrules";
4✔
23
import {
4✔
24
  liftListItem,
25
  sinkListItem,
26
  splitListItem,
27
} from "prosemirror-schema-list";
28
import { createProseMirrorNode, NodeExtension } from "prosemirror-unified";
4✔
29

30
import { buildUnifiedExtension } from "../utils/buildUnifiedExtension";
4✔
31

32
class TaskListItemView implements NodeView {
33
  public readonly contentDOM: HTMLElement;
34
  public readonly dom: HTMLElement;
35

36
  public constructor(
37
    node: ProseMirrorNode,
38
    view: EditorView,
39
    getPos: () => number | undefined,
40
  ) {
41
    const checkbox = document.createElement("input");
×
42
    checkbox.setAttribute("type", "checkbox");
×
43
    checkbox.setAttribute("style", "cursor: pointer;");
×
44
    if (node.attrs["checked"] === true) {
×
45
      checkbox.setAttribute("checked", "checked");
×
46
    }
47
    checkbox.addEventListener("click", (e) => {
×
48
      const pos = getPos();
×
49
      if (pos === undefined) {
×
50
        return;
×
51
      }
52
      e.preventDefault();
×
53
      view.dispatch(
×
54
        view.state.tr.setNodeAttribute(
55
          pos,
56
          "checked",
57
          !(node.attrs["checked"] as boolean),
58
        ),
59
      );
60
    });
61

62
    const checkboxContainer = document.createElement("span");
×
63
    checkboxContainer.setAttribute("contenteditable", "false");
×
64
    checkboxContainer.setAttribute("style", "position: absolute; left: 5px;");
×
65
    checkboxContainer.appendChild(checkbox);
×
66

67
    this.contentDOM = document.createElement("span");
×
68
    this.contentDOM.setAttribute("style", "position: relative; left: 30px;");
×
69

70
    this.dom = document.createElement("li");
×
71
    this.dom.setAttribute(
×
72
      "style",
73
      "list-style-type: none; margin-left: -30px;",
74
    );
75
    this.dom.appendChild(checkboxContainer);
×
76
    this.dom.appendChild(this.contentDOM);
×
77
  }
78

79
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Inherited from the NodeView interface
80
  public stopEvent(): boolean {
81
    return true;
×
82
  }
83
}
84

85
/**
86
 * @public
87
 */
88
export class TaskListItemExtension extends NodeExtension<ListItem> {
4✔
89
  private static isAtStart(
90
    state: EditorState,
91
    view: EditorView | undefined,
92
  ): boolean {
93
    if (!state.selection.empty) {
×
94
      return false;
×
95
    }
96
    if (view !== undefined) {
×
97
      return view.endOfTextblock("backward", state);
×
98
    }
99
    return state.selection.$anchor.parentOffset > 0;
×
100
  }
101

102
  public override proseMirrorInputRules(
103
    proseMirrorSchema: Schema<string, string>,
104
  ): Array<InputRule> {
105
    return [
6✔
106
      new InputRule(/^\[([x\s]?)\][\s\S]$/u, (state, match, start) => {
NEW
107
        const resolvedPos = state.doc.resolve(start);
×
NEW
108
        const wrappingNode = resolvedPos.node(-1);
×
109
        if (wrappingNode.type.name !== "regular_list_item") {
×
110
          return null;
×
111
        }
112

NEW
113
        const regularListItemStartPos = resolvedPos.before(-1);
×
NEW
114
        const regularListItemEndPos = resolvedPos.after(-1);
×
115
        return state.tr.replaceRangeWith(
×
116
          regularListItemStartPos,
117
          regularListItemEndPos,
118
          proseMirrorSchema.nodes[this.proseMirrorNodeName()].create(
119
            { checked: match[1] === "x" },
120
            wrappingNode.content.cut(3 + match[1].length),
121
          ),
122
        );
123
      }),
124
    ];
125
  }
126

127
  public override proseMirrorKeymap(
128
    proseMirrorSchema: Schema<string, string>,
129
  ): Record<string, Command> {
130
    const nodeType = proseMirrorSchema.nodes[this.proseMirrorNodeName()];
6✔
131
    return {
6✔
132
      Backspace: (state, dispatch, view): boolean => {
133
        if (!TaskListItemExtension.isAtStart(state, view)) {
×
134
          return false;
×
135
        }
136
        const taskListItemNode = state.selection.$anchor.node(-1);
×
137
        if (taskListItemNode.type.name !== "task_list_item") {
×
138
          return false;
×
139
        }
140
        if (dispatch === undefined) {
×
141
          return true;
×
142
        }
143

NEW
144
        const startPos = state.selection.$anchor.before(-1);
×
NEW
145
        const endPos = state.selection.$anchor.after(-1);
×
146
        dispatch(
×
147
          state.tr.replaceRangeWith(
148
            startPos,
149
            endPos,
150
            proseMirrorSchema.nodes["regular_list_item"].create(
151
              {},
152
              taskListItemNode.content,
153
            ),
154
          ),
155
        );
156
        return true;
×
157
      },
158
      Enter: splitListItem(nodeType),
159
      "Shift-Tab": liftListItem(nodeType),
160
      Tab: sinkListItem(nodeType),
161
    };
162
  }
163

164
  public override proseMirrorNodeName(): string {
165
    return "task_list_item";
176✔
166
  }
167

168
  public override proseMirrorNodeSpec(): NodeSpec {
169
    return {
6✔
170
      attrs: { checked: { default: false } },
171
      content: "paragraph block*",
172
      defining: true,
173
      group: "list_item",
174
      parseDOM: [
175
        {
176
          getAttrs(dom: Node | string): false | { checked: boolean } {
177
            const checkbox = (dom as HTMLElement).firstChild;
×
178
            if (!(checkbox instanceof HTMLInputElement)) {
×
179
              return false;
×
180
            }
181
            return { checked: checkbox.checked };
×
182
          },
183
          tag: "li",
184
        },
185
      ],
186
      toDOM(node: ProseMirrorNode): DOMOutputSpec {
187
        return [
4✔
188
          "li",
189
          { style: "list-style-type: none;, margin-left: -30px;" },
190
          [
191
            "span",
192
            {
193
              contenteditable: "false",
194
              style: "position: absolute; left: 5px;",
195
            },
196
            [
197
              "input",
198
              {
199
                checked: (node.attrs["checked"] as boolean)
2!
200
                  ? "checked"
201
                  : undefined,
202
                disabled: "disabled",
203
                type: "checkbox",
204
              },
205
            ],
206
          ],
207
          ["span", { style: "position: relative; left: 30px" }, 0],
208
        ];
209
      },
210
    };
211
  }
212

213
  public override proseMirrorNodeToUnistNodes(
214
    node: ProseMirrorNode,
215
    convertedChildren: Array<BlockContent | DefinitionContent>,
216
  ): Array<ListItem> {
217
    return [
16✔
218
      {
219
        checked: node.attrs["checked"] as boolean,
220
        children: convertedChildren,
221
        type: this.unistNodeName(),
222
      },
223
    ];
224
  }
225

226
  public override proseMirrorNodeView(): NodeViewConstructor | null {
227
    return (node, view, getPos) => new TaskListItemView(node, view, getPos);
6✔
228
  }
229

230
  public override unifiedInitializationHook(
231
    processor: Processor<UnistNode, UnistNode, UnistNode, UnistNode, string>,
232
  ): Processor<UnistNode, UnistNode, UnistNode, UnistNode, string> {
233
    return processor.use(
6✔
234
      buildUnifiedExtension(
235
        [gfmTaskListItem()],
236
        [gfmTaskListItemFromMarkdown()],
237
        [gfmTaskListItemToMarkdown()],
238
      ),
239
    );
240
  }
241

242
  public override unistNodeName(): "listItem" {
243
    return "listItem";
56✔
244
  }
245

246
  public override unistNodeToProseMirrorNodes(
247
    node: ListItem,
248
    proseMirrorSchema: Schema<string, string>,
249
    convertedChildren: Array<ProseMirrorNode>,
250
  ): Array<ProseMirrorNode> {
251
    return createProseMirrorNode(
10✔
252
      this.proseMirrorNodeName(),
253
      proseMirrorSchema,
254
      convertedChildren,
255
      { checked: node.checked },
256
    );
257
  }
258

259
  public override unistToProseMirrorTest(node: UnistNode): boolean {
260
    return (
40✔
261
      node.type === this.unistNodeName() &&
30✔
262
      "checked" in node &&
263
      typeof node.checked === "boolean"
264
    );
265
  }
266
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc