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

caleb531 / workday-time-calculator / 7066859841

02 Dec 2023 12:47AM UTC coverage: 94.084% (-0.2%) from 94.33%
7066859841

push

github

caleb531
Fix compatibility with Safari 16.3 and older

See lengthy code comment for details.

376 of 407 branches covered (0.0%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 1 file covered. (100.0%)

5 existing lines in 2 files now uncovered.

2232 of 2365 relevant lines covered (94.38%)

392.43 hits per line

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

75.75
/scripts/components/editor.js
1
import m from 'mithril';
3✔
2
import Quill from 'quill';
3✔
3
import 'quill/dist/quill.snow.css';
3✔
4
import appStorage from '../models/app-storage.js';
3✔
5
import EditorAutocompleter from '../models/editor-autocompleter.js';
3✔
6

3✔
7
class EditorComponent {
3✔
8
  oninit({ attrs: { preferences, selectedDate, onSetLogContents } }) {
3✔
9
    this.preferences = preferences;
191✔
10
    this.selectedDate = selectedDate.clone();
191✔
11
    this.onSetLogContents = onSetLogContents;
191✔
12
    this.autocompleter = new EditorAutocompleter({
191✔
13
      autocompleteMode: this.preferences.autocompleteMode
191✔
14
    });
191✔
15
    this.preferences.on('change:autocompleteMode', (key, newMode) => {
191✔
16
      this.autocompleter.setMode(newMode);
6✔
17
    });
191✔
18
  }
191✔
19

3✔
20
  onupdate({ attrs: { selectedDate } }) {
3✔
21
    if (!selectedDate.isSame(this.selectedDate)) {
346✔
22
      this.selectedDate = selectedDate.clone();
12✔
23
      this.getLogContentsForSelectedDate().then((logContents) => {
12✔
24
        this.setEditorText(logContents);
12✔
25
      });
12✔
26
    }
12✔
27
  }
346✔
28

3✔
29
  // Autocomplete the shown completion, if there is one; if not, run the
3✔
30
  // designated callback as a fallback
3✔
31
  autocomplete(range, options = {}) {
3✔
32
    const completionPlaceholder = this.autocompleter.completionPlaceholder;
×
33
    if (completionPlaceholder) {
×
34
      // Perform the two atomic operations silently and then trigger the event
×
35
      // listeners all at once; this solves a race condition where the
×
36
      // text-change event fires before the tab completion operations can finish
×
37
      this.editor.insertText(
×
38
        range.index,
×
39
        completionPlaceholder + ' ',
×
40
        'silent'
×
41
      );
×
42
      this.editor.setSelection(
×
43
        range.index + completionPlaceholder.length + 1,
×
44
        0,
×
45
        'silent'
×
46
      );
×
47
      this.editor.insertText(range.index, '', 'user');
×
48
      this.resetAutocompleteInDOM();
×
49
    } else if (options.fallbackBehavior) {
×
50
      options.fallbackBehavior();
×
51
    }
×
52
  }
×
53

3✔
54
  // Clean up any leftover autocomplete placeholders in the DOM by clearing all
3✔
55
  // data-autocomplete attributes (except for those elements specified by the
3✔
56
  // optional 'excludeElements' array)
3✔
57
  resetAutocompleteInDOM({ excludeElements = [] } = {}) {
3✔
58
    this.editor.root
478✔
59
      .querySelectorAll('[data-autocomplete]')
478✔
60
      .forEach((element) => {
478✔
61
        if (!excludeElements.includes(element)) {
192!
UNCOV
62
          element.removeAttribute('data-autocomplete');
×
UNCOV
63
          element.removeAttribute('data-testid');
×
UNCOV
64
        }
×
65
      });
478✔
66
  }
478✔
67

3✔
68
  async initializeEditor(editorContainer) {
3✔
69
    this.editor = new Quill(editorContainer, {
191✔
70
      theme: 'snow',
191✔
71
      placeholder:
191✔
72
        '1. Category One\n\t\ta. 9 to 12:15\n\t\t\t\ti. Did this\n2. Category Two\n\t\ta. 12:45 to 5\n\t\t\t\ti. Did that',
191✔
73
      formats: ['list', 'indent'],
191✔
74
      modules: {
191✔
75
        toolbar: [
191✔
76
          [{ list: 'bullet' }, { list: 'ordered' }],
191✔
77
          [{ indent: '-1' }, { indent: '+1' }]
191✔
78
        ],
191✔
79
        history: {
191✔
80
          // Do not add the editor contents to the Undo history when the app
191✔
81
          // initially loads, or when the selected date changes
191✔
82
          userOnly: true
191✔
83
        },
191✔
84
        keyboard: {
191✔
85
          bindings: {
191✔
86
            // Use <tab> and shift-<tab> to indent/un-indent; these must be
191✔
87
            // defined on editor initialization rather than via
191✔
88
            // keyboard.addBinding (see
191✔
89
            // <https://github.com/quilljs/quill/issues/1647>)
191✔
90
            tab: {
191✔
91
              key: 9,
191✔
92
              handler: (range) => {
191✔
93
                this.autocomplete(range, {
×
94
                  fallbackBehavior: () => {
×
95
                    this.editor.formatLine(range, { indent: '+1' }, 'user');
×
96
                    this.autocompleter.cancel();
×
97
                  }
×
98
                });
×
99
              }
×
100
            },
191✔
101
            arrowRight: {
191✔
102
              key: 39,
191✔
103
              handler: (range) => {
191✔
104
                this.autocomplete(range, {
×
105
                  fallbackBehavior: () => {
×
106
                    if (range.length) {
×
107
                      this.editor.setSelection(
×
108
                        range.index + range.length,
×
109
                        0,
×
110
                        'user'
×
111
                      );
×
112
                    } else {
×
113
                      this.editor.setSelection(
×
114
                        range.index + range.length + 1,
×
115
                        0,
×
116
                        'user'
×
117
                      );
×
118
                    }
×
119
                  }
×
120
                });
×
121
              }
×
122
            },
191✔
123
            shiftTab: {
191✔
124
              key: 9,
191✔
125
              shiftKey: true,
191✔
126
              handler: (range) => {
191✔
127
                this.editor.formatLine(range, { indent: '-1' }, 'user');
×
128
                this.autocompleter.cancel();
×
129
              }
×
130
            },
191✔
131
            indent: {
191✔
132
              // 221 corresponds to right bracket (']')
191✔
133
              key: 221,
191✔
134
              shortKey: true,
191✔
135
              handler: (range) => {
191✔
136
                this.editor.formatLine(range, { indent: '+1' }, 'user');
×
137
                this.autocompleter.cancel();
×
138
              }
×
139
            },
191✔
140
            unIndent: {
191✔
141
              // 219 corresponds to left bracket ('[')
191✔
142
              key: 219,
191✔
143
              shortKey: true,
191✔
144
              handler: (range) => {
191✔
145
                this.editor.formatLine(range, { indent: '-1' }, 'user');
×
146
                this.autocompleter.cancel();
×
147
              }
×
148
            },
191✔
149
            escape: {
191✔
150
              key: 27,
191✔
151
              handler: () => {
191✔
152
                if (this.autocompleter.completionPlaceholder) {
×
153
                  this.autocompleter.cancel();
×
154
                  m.redraw();
×
155
                }
×
156
              }
×
157
            }
191✔
158
          }
191✔
159
        }
191✔
160
      }
191✔
161
    });
191✔
162
    this.editor.on('selection-change', () => {
191✔
163
      this.autocompleter.cancel();
172✔
164
    });
191✔
165
    this.editor.on('text-change', (delta, oldContents, source) => {
191✔
166
      if (source === 'user') {
506✔
167
        let logContents = this.editor.getContents();
303✔
168
        this.onSetLogContents(logContents);
303✔
169
        this.saveTextLog(logContents);
303✔
170
        if (delta.ops[delta.ops.length - 1]?.insert === '\n') {
303!
171
          // If user enters down to a new line, cancel the current autocomplete
×
172
          this.autocompleter.cancel();
×
173
        } else if (delta.ops[delta.ops.length - 1]?.delete >= 1) {
303✔
174
          // If the user is deleting any amount of text, then debounce the
15✔
175
          // updating of the autocomplete placeholder to prevent the placeholder
15✔
176
          // text from jittering across successive deletes (e.g. if the user
15✔
177
          // holds down the Delete key)
15✔
178
          this.autocompleter.cancel();
15✔
179
          this.autocompleter.fetchCompletions({ debounce: true });
15✔
180
        } else {
303✔
181
          // Otherwise, fetch normally
288✔
182
          this.autocompleter.fetchCompletions();
288✔
183
        }
288✔
184
        m.redraw();
303✔
185
      }
303✔
186
      // Focus the editor when the page initially loads
506✔
187
      this.editor.focus();
506✔
188
    });
191✔
189
    this.autocompleter.on('receive', (placeholder) => {
191✔
190
      const selection = window.getSelection();
231✔
191
      // Do not calculate anything if the text cursor is not active inside the
231✔
192
      // editor, or if the parent element has not been set yet
231✔
193
      if (selection.type.toLowerCase() === 'none') {
231!
194
        return;
×
195
      }
×
196
      const range = selection.getRangeAt(0);
231✔
197
      const autocompleteParentElement =
231✔
198
        range.commonAncestorContainer.parentElement;
231✔
199
      this.resetAutocompleteInDOM({
231✔
200
        // If the parent element remains the same since the last completion,
231✔
201
        // then just reuse it (i.e. don't remove and recreate it)
231✔
202
        excludeElements: [autocompleteParentElement]
231✔
203
      });
231✔
204
      autocompleteParentElement.setAttribute('data-autocomplete', placeholder);
231✔
205
      autocompleteParentElement.setAttribute(
231✔
206
        'data-testid',
231✔
207
        'log-editor-has-autocomplete-active'
231✔
208
      );
231✔
209
    });
191✔
210
    this.autocompleter.on('cancel', () => {
191✔
211
      this.resetAutocompleteInDOM();
247✔
212
    });
191✔
213
    const logContents = await this.getLogContentsForSelectedDate();
191✔
214
    this.setEditorText(logContents);
191✔
215
    this.autocompleter.setEditor(this.editor);
191✔
216
  }
191✔
217

3✔
218
  async getLogContentsForSelectedDate() {
3✔
219
    let dateStorageId = this.getSelectedDateStorageId();
203✔
220
    let logContentsPromise = appStorage.get(dateStorageId);
203✔
221
    try {
203✔
222
      return (await logContentsPromise) || this.getDefaultLogContents();
203✔
223
    } catch (error) {
203!
224
      return this.getDefaultLogContents();
×
225
    }
×
226
  }
203✔
227

3✔
228
  getSelectedDateStorageId() {
3✔
229
    return `wtc-date-${this.selectedDate.format('l')}`;
506✔
230
  }
506✔
231

3✔
232
  getDefaultLogContents() {
3✔
233
    return {
105✔
234
      ops: [
105✔
235
        {
105✔
236
          insert: '\n'
105✔
237
        }
105✔
238
      ]
105✔
239
    };
105✔
240
  }
105✔
241

3✔
242
  setEditorText(logContents, source = 'api') {
3✔
243
    this.editor.setContents(logContents, source);
203✔
244
    this.onSetLogContents(logContents);
203✔
245
    m.redraw();
203✔
246
  }
203✔
247

3✔
248
  saveTextLog(logContents) {
3✔
249
    if (logContents.ops.length === 1 && logContents.ops[0].insert === '\n') {
303✔
250
      // If the contents of the current log are empty, delete the entry from
15✔
251
      // localStorage to conserve space
15✔
252
      appStorage.remove(this.getSelectedDateStorageId(this.selectedDate));
15✔
253
    } else {
303✔
254
      appStorage.set(
288✔
255
        this.getSelectedDateStorageId(this.selectedDate),
288✔
256
        logContents
288✔
257
      );
288✔
258
    }
288✔
259
  }
303✔
260

3✔
261
  view() {
3✔
262
    return m('div.log-editor-area', [
537✔
263
      m('div.log-editor[data-testid="log-editor"]', {
537✔
264
        oncreate: (vnode) => {
537✔
265
          this.initializeEditor(vnode.dom);
191✔
266
        }
191✔
267
      })
537✔
268
    ]);
537✔
269
  }
537✔
270
}
3✔
271

3✔
272
export default EditorComponent;
3✔
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