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

13
14
15
16
const autoCompleteContainer = document.getElementById("autoCompleteContainer");
const recurrencePickerInput = document.getElementById("recurrencePickerInput");
const modalTitle = document.getElementById("modalTitle");
const modalFormAlert = document.getElementById("modalFormAlert");
17
const modalForm = document.getElementById("modalForm");
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");

24
document.getElementById("modalFormInput").placeholder = translations.formTodoInputPlaceholder;
25
26

btnItemStatus.onclick = function() {
27
28
  setTodoComplete(modalForm.getAttribute("data-item")).then(response => {
    //modalForm.classList.remove("is-active");
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    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"]);
}
46
document.getElementById("modalFormInput").addEventListener("keyup", event => {
47
48
49
50
51
  // do not show suggestion container if Escape has been pressed
  if(event.key==="Escape") return false;
  modalFormInputEvent();
});
modalForm.addEventListener("submit", function(event) {
52
  // intercept submit
53
  event.preventDefault();
54
55
56
57
58
59
  submitForm().then(response => {
    console.log(response);
  }).catch(error => {
    handleError(error);
  });
});
60
modalForm.addEventListener ("click", function() {
61
  // close recurrence picker if click is outside of recurrence container
62
  if(!event.target.closest("#recurrencePickerContainer") && event.target!=recurrencePickerInput) document.getElementById("recurrencePickerContainer").classList.remove("is-active")
63
});
64
65
66
67
68
69
70
71
72
73
74
75
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");
};

76
77
78
79
80
81
82
83
84
85
86
87
88
89
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"]);
  }
});
90
91
92
93
94
95
96
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
97
      if(userData.matomoEvents) _paq.push(["trackEvent", "Message", "Click on Close"]);
98
99
    } else {
      // trigger matomo event
100
      if(userData.matomoEvents) _paq.push(["trackEvent", "Modal", "Click on Close"]);
101
102
103
104
    }
    el.parentElement.parentElement.classList.remove("is-active");
  }
});
105
106
107
108
109
110
111
112
113

function getCaretPosition(inputId) {
  var content = inputId;
  if((content.selectionStart!=null)&&(content.selectionStart!=undefined)){
    var position = content.selectionStart;
    return position;
  } else {
    return false;
  }
114
}
115
116
function positionAutoCompleteContainer() {
  // Adjust position of suggestion box to input field
117
118
119
  let modalFormInputPosition = document.getElementById("modalFormInput").getBoundingClientRect();
  autoCompleteContainer.style.width = document.getElementById("modalFormInput").offsetWidth + "px";
  autoCompleteContainer.style.top = modalFormInputPosition.top + document.getElementById("modalFormInput").offsetHeight+2 + "px";
120
121
122
123
  autoCompleteContainer.style.left = modalFormInputPosition.left + "px";
}
function modalFormInputEvent() {
  positionAutoCompleteContainer();
124
  resizeInput(document.getElementById("modalFormInput"));
125
126
  let autoCompleteValue ="";
  let autoCompletePrefix = "";
127
  let caretPosition = getCaretPosition(document.getElementById("modalFormInput"));
128
  let autoCompleteCategory = "";
129
130
131
132
133
134
135
136
137
  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);
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
  } 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();
  }
}
161
function resizeInput(input) {
162
163
164
165
166
167
168
169
170
  // 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
171
172
173
174
175
176
  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";
  }
}
177
178
179
180
181
182
183
184
185
186
187
188
189
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;
          }
        });
      }
    }
Daniel Weisser's avatar
Daniel Weisser committed
190
    let todo = new TodoTxtItem(document.getElementById("modalFormInput").value, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
191
192
193
194
195
196
197
198
199
200
201
202
    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)) {
203
      document.getElementById("modalFormInput").value = todo.toString();
204
205
206
207
208
209
210
211
212
213
214
215
216
      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 {
Daniel Weisser's avatar
Daniel Weisser committed
217
    const todo = new TodoTxtItem(document.getElementById("modalFormInput").value, [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
218
219
220
221
222
223
224
225
226
227
    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);
    }
228
    document.getElementById("modalFormInput").value = todo.toString();
229
230
231
232
233
234
    return Promise.resolve("Success: Due date changed to " + todo.dueString)
  } catch(error) {
    error.functionName = setDueDate.name;
    return Promise.reject(error);
  }
}
235
236
function show(todo, templated) {
  try {
237
238
    // remove any previously set data-item attributes
    modalForm.removeAttribute("data-item");
239
240
    // adjust size of recurrence picker input field
    datePickerInput.value = null;
241
    recurrencePickerInput.value = null;
242
    document.getElementById("modalFormInput").value = null;
243
244
    modalFormAlert.innerHTML = null;
    modalFormAlert.parentElement.classList.remove("is-active", 'is-warning', 'is-danger');
245
    //
246
247
248
    if(todo) {
      // replace invisible multiline ascii character with new line
      // we need to check if there already is a due date in the object
249
      todo = new TodoTxtItem(todo, [ new SugarDueExtension(), new RecExtension(), new HiddenExtension() ]);
250
251
252
253
254
255
256
257
      // set the priority
      setPriority(todo.priority);
      //
      if(templated === true) {
        // this is a new templated todo task
        // erase the original creation date and description
        todo.date = null;
        todo.text = "____________";
258
        document.getElementById("modalFormInput").value = todo.toString();
259
260
        modalTitle.innerHTML = translations.addTodo;
        // automatically select the placeholder description
261
        let selectStart = document.getElementById("modalFormInput").value.indexOf(todo.text);
262
        let selectEnd = selectStart + todo.text.length;
263
        document.getElementById("modalFormInput").setSelectionRange(selectStart, selectEnd);
264
265
        btnItemStatus.classList.remove("is-active");
      } else {
266
267
        // pass todo string to form data item
        modalForm.setAttribute("data-item", todo.toString());
268
269
        // this is an existing todo task to be edited
        // put the initially passed todo to the modal data field
270
271
272
273
274
275
        //modalForm.setAttribute("data-item", todo.toString());
        // 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();
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
        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");
    }
301
302
    // switch to textarea if needed
    if(userData.useTextarea) toggleInputSize("input");
303
304
305
    // adjust size of picker inputs
    resizeInput(datePickerInput);
    resizeInput(recurrencePickerInput);
306
    resizeInput(document.getElementById("modalFormInput"));
307
308
    // create the modal jail, so tabbing won't leave modal
    createModalJail(modalForm);
309
310
311
312
    // show modal and set focus to input element
    modalForm.classList.add("is-active");
    // put focus into the input field
    document.getElementById("modalFormInput").focus();
313
314
315
316
317
318
319
320
    return Promise.resolve("Info: Show/Edit todo window opened");
  } catch (error) {
    error.functionName = show.name;
    return Promise.reject(error);
  }
}
function submitForm() {
  try {
321
322
323
324
325
326
    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");
    }
327
328
    // 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
329
    if(modalForm.getAttribute("data-item") === modalForm.elements[0].value) {
330
331
332
333
334
335
      // close and reset any modal
      resetModal().then(function(result) {
        console.log(result);
      }).catch(function(error) {
        handleError(error);
      });
336
337
      return Promise.resolve("Info: Nothing has changed, won't write anything.");
    // Edit todo
338
    } else if(modalForm.getAttribute("data-item")) {
339
340
341
342
      // 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
343
      let todo = new TodoTxtItem(modalForm.elements[0].value.replaceAll(/[\r\n]+/g, String.fromCharCode(16)), [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
      // 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
363
      let todo = new TodoTxtItem(modalForm.elements[0].value.replaceAll(/[\r\n]+/g, String.fromCharCode(16)), [ new SugarDueExtension(), new HiddenExtension(), new RecExtension() ]);
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
      // 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]);
392
393
394
395
396
397
    // close and reset any modal
    resetModal().then(function(result) {
      console.log(result);
    }).catch(function(error) {
      handleError(error);
    });
398
    // trigger matomo event
399
    if(userData.matomoEvents) _paq.push(["trackEvent", "Form", "Submit"]);
400
401
402
403
404
405
406
407
408
409
    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);
  }
}
410
411
412
413
function toggleInputSize(type) {
  let newInputElement;
  switch (type) {
    case "input":
414
      newInputElement = document.createElement("textarea");
415
416
417
418
419
      modalFormInputResize.setAttribute("data-input-type", "textarea");
      modalFormInputResize.innerHTML = "<i class=\"fas fa-compress-alt\"></i>";
      setUserData("useTextarea", true);
      break;
    case "textarea":
420
      newInputElement = document.createElement("input");
421
422
423
424
425
426
427
      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";
428
  newInputElement.setAttribute("tabindex", 0);
429
430
  newInputElement.setAttribute("class", "input is-medium");
  newInputElement.setAttribute("placeholder", translations.formTodoInputPlaceholder);
431
432
433
434
435
436
437
  // 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)," ");

438
  positionAutoCompleteContainer();
439
440
  resizeInput(document.getElementById("modalFormInput"));
  document.getElementById("modalFormInput").addEventListener("keyup", () => {
441
442
    modalFormInputEvent();
  });
443
444
  document.getElementById("modalFormInput").focus();
  createModalJail(modalForm);
445
446
}

447
448
449
450
451
452
453
454
455
456
window.onresize = function() {
  try {
    positionAutoCompleteContainer();
  } catch(error) {
    error.functionName = "window.onresize";
    handleError(error);
    return Promise.reject(error);
  }
}

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