@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.
- package/.babelrc +15 -0
- package/.eslintignore +3 -0
- package/.eslintrc +26 -0
- package/LICENSE +661 -0
- package/README.md +19 -0
- package/app/components/CreateTask.js +74 -0
- package/app/components/TaskRow.js +111 -0
- package/app/components/Tasks.js +197 -0
- package/app/lib/tasksManager.js +249 -0
- package/app/main.js +22 -0
- package/app/models/Task.js +58 -0
- package/app/stylesheets/main.scss +181 -0
- package/dist/dist.css +5 -0
- package/dist/dist.js +2 -0
- package/dist/dist.js.LICENSE.txt +39 -0
- package/dist/index.html +1 -0
- package/editor.index.ejs +11 -0
- package/ext.json.sample +8 -0
- package/package.json +52 -0
- package/webpack.config.js +58 -0
- package/webpack.dev.js +20 -0
- package/webpack.prod.js +11 -0
|
@@ -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
|
+
}
|