@standardnotes/simple-task-editor 1.3.10

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.
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import TasksManager from '@Lib/tasksManager';
4
+
5
+ export default class CreateTask extends React.Component {
6
+
7
+ constructor(props) {
8
+ super(props);
9
+ this.state = { rawString: (this.props.unsavedTask || '') };
10
+ }
11
+
12
+ shouldComponentUpdate(nextProps) {
13
+ return nextProps.unsavedTask !== this.state.rawString;
14
+ }
15
+
16
+ componentDidUpdate(props) {
17
+ this.setState({ rawString: props.unsavedTask });
18
+ }
19
+
20
+ componentDidMount() {
21
+ if (!TasksManager.get().isMobile()) {
22
+ this.input.focus();
23
+ }
24
+ }
25
+
26
+ onTextChange = (event) => {
27
+ // save this as the current 'unsaved' task if while we're not officially saving it as an actual task yet
28
+ var rawString = event.target.value;
29
+ this.props.onUpdate(rawString);
30
+ }
31
+
32
+ handleKeyPress = (event) => {
33
+ if (event.key === 'Enter') {
34
+ var rawString = event.target.value;
35
+ this.submitTask(rawString);
36
+ }
37
+ }
38
+
39
+ submitTask(value) {
40
+ this.props.onSubmit(value);
41
+ }
42
+
43
+ render() {
44
+ let placeholderText = '';
45
+
46
+ if (TasksManager.get().showTutorial) {
47
+ placeholderText = 'Type in your task, then press Enter.';
48
+ }
49
+
50
+ const { spellcheckEnabled } = TasksManager.get();
51
+
52
+ return (
53
+ <input
54
+ className='create-task-input'
55
+ ref={(ref) => {this.input = ref;}}
56
+ placeholder={placeholderText}
57
+ type='text'
58
+ dir='auto'
59
+ value={this.props.unsavedTask}
60
+ onChange={this.onTextChange}
61
+ onKeyPress={this.handleKeyPress}
62
+ spellCheck={spellcheckEnabled}
63
+ enterKeyHint={'go'}
64
+ />
65
+ );
66
+ }
67
+
68
+ }
69
+
70
+ CreateTask.propTypes = {
71
+ unsavedTask: PropTypes.string,
72
+ onUpdate: PropTypes.func.isRequired,
73
+ onSubmit: PropTypes.func.isRequired
74
+ };
@@ -0,0 +1,111 @@
1
+ import React, { Component } from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ class TaskRow extends Component {
5
+ constructor(props) {
6
+ super(props);
7
+ this.state = { isChecked: props.task.completed, task: props.task };
8
+ }
9
+
10
+ componentDidMount() {
11
+ this.resizeTextArea(this.textArea);
12
+ }
13
+
14
+ UNSAFE_componentWillReceiveProps(newProps) {
15
+ this.setState({ task: newProps.task, isChecked: newProps.task.completed });
16
+
17
+ // Wait till after render
18
+ setTimeout(() => {
19
+ this.resizeTextArea(this.textArea);
20
+ }, 1);
21
+ }
22
+
23
+ toggleCheckboxChange = () => {
24
+ const { handleCheckboxChange } = this.props;
25
+
26
+ this.setState(({ isChecked }) => ({
27
+ isChecked: !isChecked
28
+ }));
29
+
30
+ handleCheckboxChange(this.props.task);
31
+ }
32
+
33
+ onTextChange = (event) => {
34
+ const text = event.target.value;
35
+ this.props.task.setContentString(text);
36
+ this.props.handleTextChange(this.props.task, text);
37
+
38
+ this.forceUpdate();
39
+ }
40
+
41
+ onKeyUp = (event) => {
42
+ // Delete task if empty and enter pressed
43
+ if (event.keyCode == 13) {
44
+ if (this.props.task.isEmpty()) {
45
+ this.props.deleteTask(this.props.task);
46
+ event.preventDefault();
47
+ }
48
+ }
49
+ const element = event.target;
50
+ this.resizeTextArea(element);
51
+ }
52
+
53
+ onKeyPress = (event) => {
54
+ if (event.key == 'Enter') {
55
+ // We want to disable any action on enter, since newlines are reserved
56
+ // and are how tasks are delimitted.
57
+ event.preventDefault();
58
+ }
59
+ }
60
+
61
+ resizeTextArea(textarea) {
62
+ // set to 1 first to reset scroll height in case it shrunk
63
+ textarea.style.height = '1px';
64
+ textarea.style.height = (textarea.scrollHeight)+'px';
65
+ }
66
+
67
+ render() {
68
+ const { isChecked } = this.state;
69
+ const { task, spellcheckEnabled } = this.props;
70
+
71
+ const classes = `task ${task.completed ? 'completed' : ''}`;
72
+ return (
73
+ <div className={classes}>
74
+
75
+ <label className="checkbox-container">
76
+ <input
77
+ type="checkbox"
78
+ value={task.content}
79
+ checked={isChecked}
80
+ onChange={this.toggleCheckboxChange}
81
+ className="checkbox"
82
+ spellCheck={spellcheckEnabled}
83
+ />
84
+ <span className="checkmark"></span>
85
+ </label>
86
+
87
+ <textarea
88
+ ref={(textarea) => {this.textArea = textarea;}}
89
+ value={task.content}
90
+ onChange={this.onTextChange}
91
+ onKeyUp={this.onKeyUp}
92
+ onKeyPress={this.onKeyPress}
93
+ type="text"
94
+ dir="auto"
95
+ className='task-input-textarea'
96
+ spellCheck={spellcheckEnabled}
97
+ />
98
+ </div>
99
+ );
100
+ }
101
+ }
102
+
103
+ TaskRow.propTypes = {
104
+ task: PropTypes.object.isRequired,
105
+ handleCheckboxChange: PropTypes.func.isRequired,
106
+ handleTextChange: PropTypes.func.isRequired,
107
+ deleteTask: PropTypes.func.isRequired,
108
+ spellcheckEnabled: PropTypes.bool.isRequired
109
+ };
110
+
111
+ export default TaskRow;
@@ -0,0 +1,197 @@
1
+ import React from 'react';
2
+ import TasksManager from '@Lib/tasksManager';
3
+ import TaskRow from '@Components/TaskRow';
4
+ import CreateTask from '@Components/CreateTask';
5
+ import Sortable from 'sortablejs';
6
+
7
+ export default class Tasks extends React.Component {
8
+
9
+ constructor(props) {
10
+ super(props);
11
+ this.state = { unsavedTask: '', openTasks: [], completedTasks: [] };
12
+ TasksManager.get().setDataChangeHandler(() => {
13
+ // We need TasksManager.get().isMobile() to be defined, and this handler is called once on bridge ready.
14
+ this.initiateSorting();
15
+ this.updateTasks();
16
+ });
17
+
18
+ TasksManager.get().setOnReady(() => {
19
+ let platform = TasksManager.get().getPlatform();
20
+ // add platform class to main <html> element
21
+ const root = document.documentElement;
22
+ root.className += platform;
23
+ this.setState({ ready: true });
24
+ });
25
+ }
26
+
27
+ componentDidMount() {
28
+ TasksManager.get().initiateBridge();
29
+ this.updateTasks();
30
+ }
31
+
32
+ initiateSorting() {
33
+ if (this.didInitiateSorting) {
34
+ return;
35
+ }
36
+ this.didInitiateSorting = true;
37
+
38
+ let properties = {
39
+ delay: 100, // time in milliseconds to define when the sorting should start
40
+ delayOnTouchOnly: true, // only delay if user is using touch
41
+ draggable: '.task',
42
+ dragClass: 'task-dragging',
43
+ handle: '.checkbox-container',
44
+ onEnd: this.taskCompletedDragging
45
+ };
46
+
47
+ properties['name'] = 'open-tasks';
48
+ Sortable.create(document.getElementById('open-tasks'), properties);
49
+
50
+ properties['name'] = 'completed-tasks';
51
+ Sortable.create(document.getElementById('completed-tasks'), properties);
52
+ }
53
+
54
+ updateTasks() {
55
+ this.setState(TasksManager.get().splitTasks());
56
+ }
57
+
58
+ deleteTask = (task) => {
59
+ TasksManager.get().deleteTask(task);
60
+ this.updateTasks();
61
+ }
62
+
63
+ toggleTaskStatus = (task) => {
64
+ task.toggleStatus();
65
+ if (!task.completed) {
66
+ TasksManager.get().moveTaskToTop(task);
67
+ }
68
+
69
+ setTimeout(() => {
70
+ // Allow UI to show checkmark before transferring to other list
71
+ this.taskStatusUpdated();
72
+ }, 300);
73
+ }
74
+
75
+ handleTaskTextChange = () => {
76
+ TasksManager.get().save();
77
+ }
78
+
79
+ taskStatusUpdated() {
80
+ this.updateTasks();
81
+ TasksManager.get().save();
82
+ }
83
+
84
+ taskAtIndex(list, relativeIndex) {
85
+ if (list == 0) {
86
+ return this.state.openTasks[relativeIndex];
87
+ } else {
88
+ return this.state.completedTasks[relativeIndex];
89
+ }
90
+ }
91
+
92
+ taskCompletedDragging = (evt) => {
93
+ const isSourceOpen = evt.from.id == 'open-tasks';
94
+ const isDestinationCompleted = evt.to.id == 'completed-tasks';
95
+ const isDestinationOpen = !isDestinationCompleted;
96
+ const fromIndex = evt.oldIndex;
97
+ const toIndex = evt.newIndex;
98
+
99
+ const fromTask = this.taskAtIndex(isSourceOpen ? 0 : 1, fromIndex);
100
+ const toTask = this.taskAtIndex(isDestinationOpen ? 0 : 1, toIndex);
101
+
102
+ TasksManager.get().changeTaskPosition(fromTask, toTask);
103
+ if (isDestinationCompleted) {
104
+ fromTask.markCompleted();
105
+ } else {
106
+ fromTask.markOpen();
107
+ }
108
+
109
+ this.taskStatusUpdated();
110
+ }
111
+
112
+ createTask = (rawString) => {
113
+ TasksManager.get().setUnsavedTask('');
114
+ let task = TasksManager.get().createTask(rawString);
115
+ TasksManager.get().addTask(task);
116
+ this.updateTasks();
117
+ }
118
+
119
+ saveUnsavedTask = (rawString) => {
120
+ // save current entry to task list that has not been officially saved by pressing 'enter' yet
121
+ TasksManager.get().setUnsavedTask(rawString);
122
+ TasksManager.get().save();
123
+ this.updateTasks();
124
+ }
125
+
126
+ onReopenCompleted = () => {
127
+ if (confirm('Are you sure you want to reopen completed tasks?')) {
128
+ TasksManager.get().reopenCompleted();
129
+ this.updateTasks();
130
+ }
131
+ }
132
+
133
+ onDeleteCompleted = () => {
134
+ if (confirm('Are you sure you want to delete completed tasks?')) {
135
+ TasksManager.get().deleteCompleted();
136
+ this.updateTasks();
137
+ }
138
+ }
139
+
140
+ taskRowForTask(task) {
141
+ return (
142
+ <TaskRow
143
+ task={task}
144
+ handleCheckboxChange={this.toggleTaskStatus}
145
+ handleTextChange={this.handleTaskTextChange}
146
+ deleteTask={this.deleteTask}
147
+ key={TasksManager.get().keyForTask(task)}
148
+ spellcheckEnabled={TasksManager.get().spellcheckEnabled}
149
+ />
150
+ );
151
+ }
152
+
153
+ render() {
154
+ const { unsavedTask, openTasks, completedTasks } = this.state;
155
+
156
+ return (
157
+ <>
158
+
159
+ {this.state.ready &&
160
+ <div className='task-input'>
161
+ <CreateTask
162
+ onSubmit={this.createTask}
163
+ onUpdate={this.saveUnsavedTask}
164
+ unsavedTask={unsavedTask}
165
+ />
166
+ </div>
167
+ }
168
+
169
+ <div className='task-section'>
170
+ <h3>Open Tasks</h3>
171
+ <div id="open-tasks">
172
+ {openTasks.map((task, index) => {
173
+ return this.taskRowForTask(task, index);
174
+ })}
175
+ </div>
176
+ </div>
177
+
178
+ <div className='task-section'>
179
+ <h3>Completed Tasks</h3>
180
+ <div id="completed-tasks">
181
+ {completedTasks.map((task, index) => {
182
+ return this.taskRowForTask(task, index);
183
+ })}
184
+ </div>
185
+
186
+ {completedTasks.length > 0 &&
187
+ <div>
188
+ <a className="clear-button" onClick={this.onReopenCompleted}>Reopen Completed</a>
189
+ <a className="clear-button" onClick={this.onDeleteCompleted}>Delete Completed</a>
190
+ </div>
191
+ }
192
+ </div>
193
+
194
+ </>
195
+ );
196
+ }
197
+ }
@@ -0,0 +1,249 @@
1
+ import Task from '@Models/Task';
2
+ import ComponentRelay from '@standardnotes/component-relay';
3
+
4
+ const TaskDelimitter = '\n';
5
+
6
+ export default class TasksManager {
7
+ spellcheckEnabled = true
8
+
9
+ /* Singleton */
10
+ static instance = null;
11
+ static get() {
12
+ if (this.instance === null) {
13
+ this.instance = new TasksManager();
14
+ }
15
+ return this.instance;
16
+ }
17
+
18
+ initiateBridge() {
19
+ const permissions = [
20
+ {
21
+ name: 'stream-context-item'
22
+ }
23
+ ];
24
+
25
+ this.componentRelay = new ComponentRelay({
26
+ targetWindow: window,
27
+ permissions,
28
+ onReady: () => {
29
+ this.onReady && this.onReady();
30
+ }
31
+ });
32
+
33
+ this.componentRelay.streamContextItem((note) => {
34
+ this.note = note;
35
+
36
+ if (note.isMetadataUpdate) {
37
+ return;
38
+ }
39
+
40
+ this.dataString = note.content.text;
41
+ this.unsavedTask = note.content.unsavedTask;
42
+ this.reloadData();
43
+ this.dataChangeHandler && this.dataChangeHandler(this.tasks);
44
+ this.spellcheckEnabled = JSON.stringify(note.content.spellcheck);
45
+ });
46
+ }
47
+
48
+ getPlatform() {
49
+ return this.componentRelay.platform;
50
+ }
51
+
52
+ isMobile() {
53
+ return this.componentRelay && this.componentRelay.isRunningInMobileApplication();
54
+ }
55
+
56
+ get showTutorial() {
57
+ const showTutorial = this.componentRelay.getComponentDataValueForKey('showTutorial');
58
+ return showTutorial === undefined;
59
+ }
60
+
61
+ setOnReady(onReady) {
62
+ this.onReady = onReady;
63
+ }
64
+
65
+ setDataChangeHandler(handler) {
66
+ this.dataChangeHandler = handler;
67
+ }
68
+
69
+ parseRawTasksString(string) {
70
+ if (!string) {string = '';}
71
+ const allTasks = string.split(TaskDelimitter);
72
+ return allTasks.filter((s) => {return s.replace(/ /g, '').length > 0;}).map((rawString) => {
73
+ return this.createTask(rawString);
74
+ });
75
+ }
76
+
77
+ keyForTask(task) {
78
+ return this.tasks.indexOf(task) + task.rawString;
79
+ }
80
+
81
+ reloadData() {
82
+ this.tasks = this.parseRawTasksString(this.dataString);
83
+ }
84
+
85
+ getTasks() {
86
+ if (!this.tasks) {
87
+ this.reloadData();
88
+ }
89
+ return this.tasks;
90
+ }
91
+
92
+ createTask(rawString) {
93
+ return new Task(rawString);
94
+ }
95
+
96
+ addTask(task) {
97
+ this.tasks.unshift(task);
98
+ this.save();
99
+ this.reloadData();
100
+ }
101
+
102
+ setUnsavedTask(text) {
103
+ this.unsavedTask = text;
104
+ }
105
+
106
+ completedTasks() {
107
+ return this.tasks.filter((task) => task.completed == true);
108
+ }
109
+
110
+ openTasks(tasks) {
111
+ tasks.forEach(task => {
112
+ task.markOpen();
113
+ });
114
+
115
+ this.tasks = this.categorizedTasks.openTasks.concat(tasks);
116
+ }
117
+
118
+ removeTasks(tasks) {
119
+ this.tasks = this.tasks.filter((task) => !tasks.includes(task));
120
+ }
121
+
122
+ // Splits into completed and non completed piles, and organizes them into an ordered array
123
+ splitTasks() {
124
+ let tasks = this.getTasks();
125
+ const openTasks = [], completedTasks = [];
126
+ tasks.forEach((task) => {
127
+ if (task.completed) {
128
+ completedTasks.push(task);
129
+ } else {
130
+ openTasks.push(task);
131
+ }
132
+ });
133
+
134
+ this.tasks = openTasks.concat(completedTasks);
135
+ this.categorizedTasks = {
136
+ unsavedTask: this.unsavedTask,
137
+ openTasks,
138
+ completedTasks
139
+ };
140
+
141
+ return this.categorizedTasks;
142
+ }
143
+
144
+ moveTaskToTop(task) {
145
+ this.tasks.splice(this.tasks.indexOf(task), 1);
146
+ this.tasks.unshift(task);
147
+ }
148
+
149
+ changeTaskPosition(task, taskOccupyingTargetLocation) {
150
+ const from = this.tasks.indexOf(task);
151
+ const to = this.tasks.indexOf(taskOccupyingTargetLocation);
152
+
153
+ this.tasks = this.tasks.move(from, to);
154
+ }
155
+
156
+ reopenCompleted() {
157
+ this.openTasks(this.completedTasks());
158
+ this.save();
159
+ }
160
+
161
+ deleteCompleted() {
162
+ this.removeTasks(this.completedTasks());
163
+ this.save();
164
+ }
165
+
166
+ deleteTask(task) {
167
+ this.removeTasks([task]);
168
+ this.save();
169
+ }
170
+
171
+ buildHtmlPreview() {
172
+ const { openTasks, completedTasks } = this.categorizedTasks;
173
+ const totalLength = openTasks.length + completedTasks.length;
174
+
175
+ const taskPreviewLimit = 3;
176
+ const tasksToPreview = Math.min(openTasks.length, taskPreviewLimit);
177
+
178
+ let html = '<div>';
179
+ html += `<div style="margin-top: 8px;"><strong>${completedTasks.length}/${totalLength} tasks completed</strong></div>`;
180
+ html += `<progress max="100" style="margin-top: 10px; width: 100%;" value="${(completedTasks.length/totalLength) * 100}"></progress>`;
181
+
182
+ if (tasksToPreview > 0) {
183
+ html += '<ul style=\'padding-left: 19px; margin-top: 10px;\'>';
184
+ for (let i = 0; i < tasksToPreview; i++) {
185
+ const task = openTasks[i];
186
+ html += `<li style='margin-bottom: 6px;'>${task.content}</li>`;
187
+ }
188
+ html += '</ul>';
189
+
190
+ if (openTasks.length > tasksToPreview) {
191
+ const diff = openTasks.length - tasksToPreview;
192
+ const noun = diff == 1 ? 'task' : 'tasks';
193
+ html += `<div><strong>And ${diff} other open ${noun}.</strong></div>`;
194
+ }
195
+ }
196
+
197
+ html += '</div>';
198
+
199
+ return html;
200
+ }
201
+
202
+ buildPlainPreview() {
203
+ const { openTasks, completedTasks } = this.categorizedTasks;
204
+ const totalLength = openTasks.length + completedTasks.length;
205
+
206
+ return `${completedTasks.length}/${totalLength} tasks completed.`;
207
+ }
208
+
209
+ save() {
210
+ this.dataString = this.tasks.map((task) => task.rawString).join(TaskDelimitter);
211
+
212
+ if (this.note) {
213
+ // Be sure to capture this object as a variable, as this.note may be reassigned in `streamContextItem`, so by the time
214
+ // you modify it in the presave block, it may not be the same object anymore, so the presave values will not be applied to
215
+ // the right object, and it will save incorrectly.
216
+ let note = this.note;
217
+ this.componentRelay.saveItemWithPresave(note, () => {
218
+ // required to build dynamic previews
219
+ this.splitTasks();
220
+ note.content.text = this.dataString;
221
+ note.content.unsavedTask = this.unsavedTask;
222
+ note.content.preview_html = this.buildHtmlPreview();
223
+ note.content.preview_plain = this.buildPlainPreview();
224
+ });
225
+
226
+ if (this.showTutorial) {
227
+ this.componentRelay.setComponentDataValueForKey('showTutorial', false);
228
+ }
229
+ }
230
+ }
231
+
232
+ }
233
+
234
+ Array.prototype.move = function (old_index, new_index) {
235
+ while (old_index < 0) {
236
+ old_index += this.length;
237
+ }
238
+ while (new_index < 0) {
239
+ new_index += this.length;
240
+ }
241
+ if (new_index >= this.length) {
242
+ let k = new_index - this.length;
243
+ while ((k--) + 1) {
244
+ this.push(undefined);
245
+ }
246
+ }
247
+ this.splice(new_index, 0, this.splice(old_index, 1)[0]);
248
+ return this; // for testing purposes
249
+ };
package/app/main.js ADDED
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import Tasks from '@Components/Tasks';
4
+
5
+ class App extends React.Component {
6
+ constructor(props) {
7
+ super(props);
8
+ }
9
+
10
+ render() {
11
+ return (
12
+ <div className="sn-component">
13
+ <Tasks />
14
+ </div>
15
+ );
16
+ }
17
+ }
18
+
19
+ ReactDOM.render(
20
+ <App />,
21
+ document.body.appendChild(document.createElement('div'))
22
+ );
@@ -0,0 +1,58 @@
1
+ export default class Task {
2
+
3
+ static OpenPrefix = '- [ ] ';
4
+ static AllowedPrefixes = /^- \[x\] /i;
5
+ static CompletedPrefix = '- [x] ';
6
+
7
+ constructor(rawString) {
8
+ this.rawString = rawString;
9
+
10
+ // allow both capital and lowercase X on completed when parsing
11
+ this.completed = Task.AllowedPrefixes.test(rawString);
12
+
13
+ if (!this.completed && !rawString.startsWith(Task.OpenPrefix)) {
14
+ // This is a text being created from user input, prepend open prefix
15
+ this.rawString = Task.OpenPrefix + this.rawString;
16
+ }
17
+ }
18
+
19
+ get content() {
20
+ return this.rawString.replace(Task.OpenPrefix, '').replace(Task.AllowedPrefixes, '');
21
+ }
22
+
23
+ isEmpty() {
24
+ return this.content.replace(/ /g, '').length == 0;
25
+ }
26
+
27
+ toggleStatus() {
28
+ this.completed = !this.completed;
29
+ this.updateRawString();
30
+ }
31
+
32
+ markCompleted() {
33
+ this.completed = true;
34
+ this.updateRawString();
35
+ }
36
+
37
+ markOpen() {
38
+ this.completed = false;
39
+ this.updateRawString();
40
+ }
41
+
42
+ setContentString(string) {
43
+ this.rawString = string;
44
+ if (this.completed) {
45
+ this.rawString = Task.CompletedPrefix + this.rawString;
46
+ } else {
47
+ this.rawString = Task.OpenPrefix + this.rawString;
48
+ }
49
+ }
50
+
51
+ updateRawString() {
52
+ if (this.completed) {
53
+ this.rawString = this.rawString.replace(Task.OpenPrefix, Task.CompletedPrefix);
54
+ } else {
55
+ this.rawString = this.rawString.replace(Task.AllowedPrefixes, Task.OpenPrefix);
56
+ }
57
+ }
58
+ }