form.mjs 21.8 KB
Newer Older
1
"use strict";
2
import "../../node_modules/jstodotxt/jsTodoExtensions.js";
3
4
import { resetModal, handleError, userData, setUserData, translations } from "../render.js";
import { _paq } from "./matomo.mjs";
Daniel Weisser's avatar
Daniel Weisser committed
5
import { RecExtension, SugarDueExtension } from "./todotxtExtensions.mjs";
6
import { generateFilterData } from "./filters.mjs";
7
import { items, item, setTodoComplete } from "./todos.mjs";
8
import { datePickerInput } from "./datePicker.mjs";
9
import { createModalJail } from "../configs/modal.config.mjs";
10
import * as recurrencePicker from "./recurrencePicker.mjs";
11

12
13
14
15
const autoCompleteContainer = document.getElementById("autoCompleteContainer");
const recurrencePickerInput = document.getElementById("recurrencePickerInput");
const modalTitle = document.getElementById("modalTitle");
const modalFormAlert = document.getElementById("modalFormAlert");
16
const modalForm = document.getElementById("modalForm");
ransome1's avatar
ransome1 committed
17
const modalFormInputLabel = document.getElementById("modalFormInputLabel");
18
19
20
21
22
23
const modalFormInputResize = document.getElementById("modalFormInputResize");
const modalBackground = document.querySelectorAll('.modal-background');
const modalClose = document.querySelectorAll('.close');
const priorityPicker = document.getElementById("priorityPicker");
const btnItemStatus = document.getElementById("btnItemStatus");

ransome1's avatar
ransome1 committed
24
modalFormInputLabel.innerHTML = translations.todoTxtSyntax;
25
26

btnItemStatus.onclick = function() {
27
  setTodoComplete(modalForm.getAttribute("data-item")).then(response => {
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
    resetModal().then(function(result) {
      console.log(result);
    }).catch(function(error) {
      handleError(error);
    });
    console.log(response);
    // trigger matomo event
    if(userData.matomoEvents) _paq.push(["trackEvent", "Form", "Click on Done/In progress"]);
  }).catch(error => {
    handleError(error);
  });
}
modalFormInputResize.onclick = function() {
  toggleInputSize(this.getAttribute("data-input-type"));
  // trigger matomo event
  if(userData.matomoEvents) _paq.push(["trackEvent", "Form", "Click on Resize"]);
}
45
document.getElementById("modalFormInput").addEventListener("keyup", event => {
46
  // do not show suggestion container if Escape has been pressed
ransome1's avatar
ransome1 committed
47
48
49
50
  if(event.key==="Escape") {
    autoCompleteContainer.classList.remove("is-active");
    return false;
  }
51
52
  modalFormInputEvent();
});
ransome1's avatar
ransome1 committed
53
54
55
56
57
58
document.getElementById("modalFormInput").onfocus = function() {
  modalForm.classList.add("is-focused");
}
document.getElementById("modalFormInput").onblur = function() {
  modalForm.classList.remove("is-focused");
}
59
modalForm.addEventListener("submit", function(event) {
60
  // intercept submit
61
  event.preventDefault();
62
63
64
65
66
67
  submitForm().then(response => {
    console.log(response);
  }).catch(error => {
    handleError(error);
  });
});
68
modalForm.addEventListener ("click", function() {
69
  // close recurrence picker if click is outside of recurrence container
70
  if(!event.target.closest("#recurrencePickerContainer") && event.target!=recurrencePickerInput) document.getElementById("recurrencePickerContainer").classList.remove("is-active")
71
});
72
73
74
75
76
77
78
79
80
81
82
83
priorityPicker.addEventListener("change", e => {
  setPriority(e.target.value).then(response => {
    console.log(response);
  }).catch(error => {
    handleError(error);
  });
});
priorityPicker.onfocus = function() {
  // close suggestion box if focus comes to priority picker
  autoCompleteContainer.classList.remove("is-active");
};

84
85
86
87
88
89
90
91
92
93
94
95
96
97
modalBackground.forEach(function(el) {
  el.onclick = function() {
    resetModal().then(function(result) {
      console.log(result);
    }).catch(function(error) {
      handleError(error);
    });
    el.parentElement.classList.remove("is-active");
    autoCompleteContainer.classList.remove("is-active");
    autoCompleteContainer.blur();
    // trigger matomo event
    if(userData.matomoEvents) _paq.push(["trackEvent", "Modal", "Click on Background"]);
  }
});
98
99
100
101
102
103
104
modalClose.forEach(function(el) {
  el.onclick = function() {
    if(el.getAttribute("data-message")) {
      // persist closed message, so it won't show again
      if(!userData.dismissedMessages.includes(el.getAttribute("data-message"))) userData.dismissedMessages.push(el.getAttribute("data-message"))
      setUserData("dismissedMessages", userData.dismissedMessages);
      // trigger matomo event
105
      if(userData.matomoEvents) _paq.push(["trackEvent", "Message", "Click on Close"]);
106
107
    } else {
      // trigger matomo event
108
      if(userData.matomoEvents) _paq.push(["trackEvent", "Modal", "Click on Close"]);
109
110
111
112
    }
    el.parentElement.parentElement.classList.remove("is-active");
  }
});
113
114
115
116
117
118
119
120
121

function getCaretPosition(inputId) {
  var content = inputId;
  if((content.selectionStart!=null)&&(content.selectionStart!=undefined)){
    var position = content.selectionStart;
    return position;
  } else {
    return false;
  }
122
}
123
124
function positionAutoCompleteContainer() {
  // Adjust position of suggestion box to input field
125
126
  let modalFormInputPosition = document.getElementById("modalFormInput").getBoundingClientRect();
  autoCompleteContainer.style.width = document.getElementById("modalFormInput").offsetWidth + "px";
ransome1's avatar
ransome1 committed
127
  autoCompleteContainer.style.top = modalFormInputPosition.top + document.getElementById("modalFormInput").offsetHeight - 40 + "px";
128
129
130
131
  autoCompleteContainer.style.left = modalFormInputPosition.left + "px";
}
function modalFormInputEvent() {
  positionAutoCompleteContainer();
132
  resizeInput(document.getElementById("modalFormInput"));
133
134
  let autoCompleteValue ="";
  let autoCompletePrefix = "";
135
  let caretPosition = getCaretPosition(document.getElementById("modalFormInput"));
136
  let autoCompleteCategory = "";
137
138
139
140
141
142
143
144
145
  if((document.getElementById("modalFormInput").value.charAt(caretPosition-2) === " " || document.getElementById("modalFormInput").value.charAt(caretPosition-2) === "\n") && (document.getElementById("modalFormInput").value.charAt(caretPosition-1) === "@" || document.getElementById("modalFormInput").value.charAt(caretPosition-1) === "+")) {
    autoCompleteValue = document.getElementById("modalFormInput").value.substr(caretPosition, document.getElementById("modalFormInput").value.lastIndexOf(" ")).split(" ").shift();
    autoCompletePrefix = document.getElementById("modalFormInput").value.charAt(caretPosition-1);
  } else if(document.getElementById("modalFormInput").value.charAt(caretPosition) === " ") {
    autoCompleteValue = document.getElementById("modalFormInput").value.substr(document.getElementById("modalFormInput").value.lastIndexOf(" ", caretPosition-1)+2).split(" ").shift();
    autoCompletePrefix = document.getElementById("modalFormInput").value.charAt(document.getElementById("modalFormInput").value.lastIndexOf(" ", caretPosition-1)+1);
  } else if(document.getElementById("modalFormInput").value.charAt(document.getElementById("modalFormInput").value.lastIndexOf(" ", caretPosition)+1) === "@" || document.getElementById("modalFormInput").value.charAt(document.getElementById("modalFormInput").value.lastIndexOf(" ", caretPosition)+1) === "+") {
    autoCompleteValue = document.getElementById("modalFormInput").value.substr(document.getElementById("modalFormInput").value.lastIndexOf(" ", caretPosition)+2).split(" ").shift();
    autoCompletePrefix = document.getElementById("modalFormInput").value.charAt(document.getElementById("modalFormInput").value.lastIndexOf(" ", caretPosition)+1);
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
  } else {
    autoCompleteContainer.classList.remove("is-active");
    autoCompleteContainer.blur();
    return false;
  }
  // suppress suggestion box if caret is at the end of word
  if(autoCompletePrefix==="+" || autoCompletePrefix==="@") {
    if(autoCompletePrefix=="+") {
      autoCompleteCategory = "projects";
    } else if(autoCompletePrefix=="@") {
      autoCompleteCategory = "contexts";
    }
    // parsed data will be passed to generate filter data and build the filter buttons
    generateFilterData(autoCompleteCategory, autoCompleteValue, autoCompletePrefix, caretPosition).then(response => {
      console.log(response);
    }).catch (error => {
      handleError(error);
    });
  } else {
    autoCompleteContainer.classList.remove("is-active");
    autoCompleteContainer.blur();
  }
}
169
function resizeInput(input) {
170
171
172
173
174
175
176
177
178
  // resizing modalFormInput
  if(input.tagName==="TEXTAREA" && input.id==="modalFormInput") {
    input.style.height="auto";
    input.style.height = input.scrollHeight+"px";
    return false;
  } else if (input.type==="text" && input.id==="modalFormInput") {
    return false;
  }
  // resizing all other input
179
180
181
182
183
184
  if(input.value) {
    input.style.width = input.value.length + 6 + "ch";
  } else if(!input.value && input.placeholder) {
    input.style.width = input.placeholder.length + 6 + "ch";
  }
}
185
186
187
188
189
190
191
192
193
194
195
196
197
function setPriority(priority) {
  try {
    const setPriorityInput = function(priority) {
      if(priority===null) {
        priorityPicker.selectedIndex = 0;
      } else {
        Array.from(priorityPicker.options).forEach(function(option) {
          if(option.value===priority) {
            priorityPicker.selectedIndex = option.index;
          }
        });
      }
    }
198
    let todo = new TodoTxtItem(document.getElementById("modalFormInput").value);
199
200
201
202
203
204
205
206
207
208
209
210
    if((priority==="down" || priority==="up") && !todo.priority) {
      todo.priority = "A";
    } else if(priority==="up" && todo.priority!="a") {
      todo.priority = String.fromCharCode(todo.priority.charCodeAt(0)-1).toUpperCase();
    } else if(priority==="down" && todo.priority!="z") {
      todo.priority = String.fromCharCode(todo.priority.charCodeAt(0)+1).toUpperCase();
    } else if(priority && priority.match(/[A-Z]/i)) {
      todo.priority = priority.toUpperCase();
    } else {
      todo.priority = null;
    }
    if(todo.priority===null || todo.priority.match(/[a-z]/i)) {
211
      document.getElementById("modalFormInput").value = todo.toString();
212
213
214
215
216
217
218
219
220
221
222
223
224
      setPriorityInput(todo.priority);
      // trigger matomo event
      if(userData.matomoEvents) _paq.push(["trackEvent", "Form", "Priority changed to: " + todo.priority]);
      return Promise.resolve("Success: Priority changed to " + todo.priority)
    }
    return Promise.resolve("Info: Priority unchanged")
  } catch(error) {
    error.functionName = setPriority.name;
    return Promise.reject(error);
  }
}
function setDueDate(days) {
  try {
225
    const todo = new TodoTxtItem(document.getElementById("modalFormInput").value, [ new DueExtension() ]);
226
227
228
229
230
231
232
233
234
235
    if(days===0) {
      todo.due = undefined;
      todo.dueString = undefined;
    } else if(days && todo.due) {
      todo.due = new Date(new Date(todo.dueString).setDate(new Date(todo.dueString).getDate() + days));
      todo.dueString = todo.due.toISOString().substr(0, 10);
    } else if(days && !todo.due) {
      todo.due = new Date(new Date().setDate(new Date().getDate() + days));
      todo.dueString = todo.due.toISOString().substr(0, 10);
    }
236
    document.getElementById("modalFormInput").value = todo.toString();
237
238
239
240
241
242
    return Promise.resolve("Success: Due date changed to " + todo.dueString)
  } catch(error) {
    error.functionName = setDueDate.name;
    return Promise.reject(error);
  }
}
243
244
function show(todo, templated) {
  try {
245
246
    // remove any previously set data-item attributes
    modalForm.removeAttribute("data-item");
247
248
    // adjust size of recurrence picker input field
    datePickerInput.value = null;
249
    recurrencePickerInput.value = null;
250
    document.getElementById("modalFormInput").value = null;
251
252
    modalFormAlert.innerHTML = null;
    modalFormAlert.parentElement.classList.remove("is-active", 'is-warning', 'is-danger');
253
    //
254
255
256
    if(todo) {
      // replace invisible multiline ascii character with new line
      // we need to check if there already is a due date in the object
257
      todo = new TodoTxtItem(todo, [ new DueExtension(), new RecExtension() ]);
258
259
260
261
262
263
      // set the priority
      setPriority(todo.priority);
      //
      if(templated === true) {
        // this is a new templated todo task
        // erase the original creation date and description
264
        todo.date = new Date();
265
        todo.text = "____________";
266
        document.getElementById("modalFormInput").value = todo.toString();
267
268
        modalTitle.innerHTML = translations.addTodo;
        // automatically select the placeholder description
269
        let selectStart = document.getElementById("modalFormInput").value.indexOf(todo.text);
270
        let selectEnd = selectStart + todo.text.length;
271
        document.getElementById("modalFormInput").setSelectionRange(selectStart, selectEnd);
272
273
        btnItemStatus.classList.remove("is-active");
      } else {
274
275
        // pass todo string to form data item
        modalForm.setAttribute("data-item", todo.toString());
276
        // this is an existing todo task to be edited
277
278
279
280
281
        // replace special char with line breaks before passing it to textarea
        if(userData.useTextarea) document.getElementById("modalFormInput").value = todo.toString().replaceAll(String.fromCharCode(16),"\r\n");
        // replace special char with space before passing it to regular input
        if(!userData.useTextarea) document.getElementById("modalFormInput").value = todo.toString().replaceAll(String.fromCharCode(16)," ");
        //document.getElementById("modalFormInput").value = todo.toString();
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
        modalTitle.innerHTML = translations.editTodo;
        btnItemStatus.classList.add("is-active");
      }
      // only show the complete button on open items
      if(todo.complete === false) {
        btnItemStatus.innerHTML = translations.done;
      } else {
        btnItemStatus.innerHTML = translations.inProgress;
      }
      // if there is a recurrence
      if(todo.rec) {
        recurrencePicker.setInput(todo.rec).then(function(result) {
          console.log(result);
        }).catch(function(error) {
          handleError(error);
        });
      }
      // if so we paste it into the input field
      if(todo.dueString) {
        datePickerInput.value = todo.dueString;
      }
    } else {
      modalTitle.innerHTML = translations.addTodo;
      btnItemStatus.classList.remove("is-active");
    }
307
308
    // switch to textarea if needed
    if(userData.useTextarea) toggleInputSize("input");
309
310
311
    // adjust size of picker inputs
    resizeInput(datePickerInput);
    resizeInput(recurrencePickerInput);
312
    resizeInput(document.getElementById("modalFormInput"));
313
314
    // create the modal jail, so tabbing won't leave modal
    createModalJail(modalForm);
315
316
317
318
    // show modal and set focus to input element
    modalForm.classList.add("is-active");
    // put focus into the input field
    document.getElementById("modalFormInput").focus();
319
320
321
322
323
324
325
326
    return Promise.resolve("Info: Show/Edit todo window opened");
  } catch (error) {
    error.functionName = show.name;
    return Promise.reject(error);
  }
}
function submitForm() {
  try {
327
328
329
330
331
332
    if(userData.file === undefined) {
      modalFormAlert.innerHTML = translations.formErrorWritingFile;
      modalFormAlert.parentElement.classList.remove("is-active", 'is-danger');
      modalFormAlert.parentElement.classList.add("is-active", 'is-warning');
      return Promise.resolve("Info: No todo.txt defined yet");
    }
333
334
    // check if there is an input in the text field, otherwise indicate it to the user
    // input value and data item are the same, nothing has changed, nothing will be written
335
    if(modalForm.getAttribute("data-item") === modalForm.elements[0].value) {
336
337
338
339
340
341
      // close and reset any modal
      resetModal().then(function(result) {
        console.log(result);
      }).catch(function(error) {
        handleError(error);
      });
342
343
      return Promise.resolve("Info: Nothing has changed, won't write anything.");
    // Edit todo
344
    } else if(modalForm.getAttribute("data-item")) {
345
346
347
348
      // get index of todo
      const index = items.objects.map(function(item) {return item.toString(); }).indexOf(modalForm.getAttribute("data-item"));
      // create a todo.txt object
      // replace new lines with spaces (https://stackoverflow.com/a/34936253)
Daniel Weisser's avatar
Daniel Weisser committed
349
      let todo = new TodoTxtItem(modalForm.elements[0].value.replaceAll(/[\r\n]+/g, String.fromCharCode(16)), [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
      // check and prevent duplicate todo
      if(items.objects.map(function(item) {return item.toString(); }).indexOf(todo.toString())!=-1) {
        modalFormAlert.innerHTML = translations.formInfoDuplicate;
        modalFormAlert.parentElement.classList.remove("is-active", 'is-danger');
        modalFormAlert.parentElement.classList.add("is-active", 'is-warning');
        return Promise.resolve("Info: Todo already exists in file, won't write duplicate");
      // check if todo text is empty
      } else if(!todo.text) {
        modalFormAlert.innerHTML = translations.formInfoIncomplete;
        modalFormAlert.parentElement.classList.remove("is-active", 'is-danger');
        modalFormAlert.parentElement.classList.add("is-active", 'is-warning');
        return Promise.resolve("Info: Todo is incomplete");
      }
      // jump to index, remove 1 item there and add the value from the input at that position
      items.objects.splice(index, 1, todo);
    // Add todo
    } else if(modalForm.getAttribute("data-item")==null && modalForm.elements[0].value!="") {
      // in case there hasn't been a passed data item, we just push the input value as a new item into the array
      // replace new lines with spaces (https://stackoverflow.com/a/34936253)
Daniel Weisser's avatar
Daniel Weisser committed
369
      let todo = new TodoTxtItem(modalForm.elements[0].value.replaceAll(/[\r\n]+/g, String.fromCharCode(16)), [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
      // we add the current date to the start date attribute of the todo.txt object
      todo.date = new Date();
      // check and prevent duplicate todo
      if(items.objects.map(function(item) {return item.toString(); }).indexOf(todo.toString())!=-1) {
        modalFormAlert.innerHTML = translations.formInfoDuplicate;
        modalFormAlert.parentElement.classList.remove("is-active", 'is-danger');
        modalFormAlert.parentElement.classList.add("is-active", 'is-warning');
        return Promise.resolve("Info: Todo already exists in file, won't write duplicate");
      // check if todo text is empty
      } else if(!todo.text) {
        modalFormAlert.innerHTML = translations.formInfoIncomplete;
        modalFormAlert.parentElement.classList.remove("is-active", 'is-danger');
        modalFormAlert.parentElement.classList.add("is-active", 'is-warning');
        return Promise.resolve("Info: Todo is incomplete");
      }
      // we build the array
      items.objects.push(todo);
      // mark the todo for anchor jump after next reload
      item.previous = todo.toString();
    } else if(modalForm.elements[0].value=="") {
      modalFormAlert.innerHTML = translations.formInfoNoInput;
      modalFormAlert.parentElement.classList.remove("is-active", 'is-danger');
      modalFormAlert.parentElement.classList.add("is-active", 'is-warning');
      return Promise.resolve("Info: Will not write empty todo");
    }
    //write the data to the file
    // a newline character is added to prevent other todo.txt apps to append new todos to the last line
    window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n", userData.file]);
398
399
400
401
402
403
    // close and reset any modal
    resetModal().then(function(result) {
      console.log(result);
    }).catch(function(error) {
      handleError(error);
    });
404
    // trigger matomo event
405
    if(userData.matomoEvents) _paq.push(["trackEvent", "Form", "Submit"]);
406
407
408
409
410
411
412
413
414
415
    return Promise.resolve("Success: Changes written to file: " + userData.file);
  // if the input field is empty, let users know
  } catch (error) {
    // if writing into file is denied throw alert
    modalFormAlert.innerHTML = translations.formErrorWritingFile + userData.file;
    modalFormAlert.parentElement.classList.add("is-active", 'is-danger');
    error.functionName = submitForm.name;
    return Promise.reject(error);
  }
}
ransome1's avatar
ransome1 committed
416

417
418
419
420
function toggleInputSize(type) {
  let newInputElement;
  switch (type) {
    case "input":
421
      newInputElement = document.createElement("textarea");
422
423
424
425
426
      modalFormInputResize.setAttribute("data-input-type", "textarea");
      modalFormInputResize.innerHTML = "<i class=\"fas fa-compress-alt\"></i>";
      setUserData("useTextarea", true);
      break;
    case "textarea":
427
      newInputElement = document.createElement("input");
428
429
430
431
432
433
434
      newInputElement.type = "text";
      modalFormInputResize.setAttribute("data-input-type", "input");
      modalFormInputResize.innerHTML = "<i class=\"fas fa-expand-alt\"></i>";
      setUserData("useTextarea", false);
      break;
  }
  newInputElement.id = "modalFormInput";
435
  newInputElement.setAttribute("tabindex", 0);
436
  newInputElement.setAttribute("class", "input is-medium");
ransome1's avatar
ransome1 committed
437
  //newInputElement.setAttribute("placeholder", translations.formTodoInputPlaceholder);
438
439
440
441
442
443
444
  // replace old element with the new one
  document.getElementById("modalFormInput").replaceWith(newInputElement);
  // replace special char with line break before passing it to textarea
  if(userData.useTextarea && modalForm.getAttribute("data-item")) document.getElementById("modalFormInput").value = document.getElementById("modalForm").getAttribute("data-item").replaceAll(String.fromCharCode(16),"\r\n");
  // replace special char with space before passing it to regular input
  if(!userData.useTextarea && modalForm.getAttribute("data-item")) document.getElementById("modalFormInput").value = document.getElementById("modalForm").getAttribute("data-item").replaceAll(String.fromCharCode(16)," ");

445
  positionAutoCompleteContainer();
446
447
  resizeInput(document.getElementById("modalFormInput"));
  document.getElementById("modalFormInput").addEventListener("keyup", () => {
448
449
    modalFormInputEvent();
  });
ransome1's avatar
ransome1 committed
450
451
452
453
454
455
  document.getElementById("modalFormInput").onfocus = function() {
    modalForm.classList.add("is-focused");
  }
  document.getElementById("modalFormInput").onblur = function() {
    modalForm.classList.remove("is-focused");
  }
456
457
  document.getElementById("modalFormInput").focus();
  createModalJail(modalForm);
458
459
}

460
461
462
463
464
465
466
467
468
469
window.onresize = function() {
  try {
    positionAutoCompleteContainer();
  } catch(error) {
    error.functionName = "window.onresize";
    handleError(error);
    return Promise.reject(error);
  }
}

470
export { show, resizeInput, setPriority, setDueDate, submitForm};