retold 4.0.1 → 4.0.2
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/.claude/settings.local.json +27 -1
- package/docs/README.md +7 -6
- package/docs/_sidebar.md +34 -21
- package/docs/_topbar.md +2 -2
- package/docs/architecture/module-architecture.md +234 -0
- package/docs/{modules.md → architecture/modules.md} +25 -22
- package/docs/cover.md +2 -2
- package/docs/css/docuserve.css +6 -6
- package/docs/examples/examples.md +71 -0
- package/docs/examples/todolist/todo-list-cli-client.md +178 -0
- package/docs/examples/todolist/todo-list-console-client.md +152 -0
- package/docs/examples/todolist/todo-list-model.md +114 -0
- package/docs/examples/todolist/todo-list-server.md +128 -0
- package/docs/examples/todolist/todo-list-web-client.md +177 -0
- package/docs/examples/todolist/todo-list.md +162 -0
- package/docs/getting-started.md +8 -7
- package/docs/index.html +4 -4
- package/docs/{meadow.md → modules/meadow.md} +4 -6
- package/docs/{orator.md → modules/orator.md} +1 -0
- package/docs/{pict.md → modules/pict.md} +30 -8
- package/docs/{utility.md → modules/utility.md} +0 -9
- package/docs/retold-catalog.json +896 -2317
- package/docs/retold-keyword-index.json +162327 -120227
- package/examples/todo-list/Dockerfile +45 -0
- package/examples/todo-list/README.md +394 -0
- package/examples/todo-list/cli-client/package-lock.json +418 -0
- package/examples/todo-list/cli-client/package.json +19 -0
- package/examples/todo-list/cli-client/source/TodoCLI-CLIProgram.js +30 -0
- package/examples/todo-list/cli-client/source/TodoCLI-Run.js +3 -0
- package/examples/todo-list/cli-client/source/commands/add/TodoCLI-Command-Add.js +74 -0
- package/examples/todo-list/cli-client/source/commands/complete/TodoCLI-Command-Complete.js +84 -0
- package/examples/todo-list/cli-client/source/commands/list/TodoCLI-Command-List.js +110 -0
- package/examples/todo-list/cli-client/source/commands/remove/TodoCLI-Command-Remove.js +49 -0
- package/examples/todo-list/cli-client/source/services/TodoCLI-Service-API.js +92 -0
- package/examples/todo-list/console-client/console-client.cjs +913 -0
- package/examples/todo-list/console-client/package-lock.json +426 -0
- package/examples/todo-list/console-client/package.json +19 -0
- package/examples/todo-list/console-client/views/PictView-TUI-Header.cjs +43 -0
- package/examples/todo-list/console-client/views/PictView-TUI-Layout.cjs +58 -0
- package/examples/todo-list/console-client/views/PictView-TUI-StatusBar.cjs +41 -0
- package/examples/todo-list/console-client/views/PictView-TUI-TaskList.cjs +104 -0
- package/examples/todo-list/docker-motd.sh +36 -0
- package/examples/todo-list/docker-run.sh +2 -0
- package/examples/todo-list/docker-shell.sh +2 -0
- package/examples/todo-list/model/MeadowSchema-Task.json +152 -0
- package/examples/todo-list/model/Task-Compiled.json +25 -0
- package/examples/todo-list/model/Task.mddl +15 -0
- package/examples/todo-list/model/data/seeded_todo_events.csv +1001 -0
- package/examples/todo-list/server/database-initialization-service.cjs +273 -0
- package/examples/todo-list/server/package-lock.json +6113 -0
- package/examples/todo-list/server/package.json +19 -0
- package/examples/todo-list/server/server.cjs +138 -0
- package/examples/todo-list/web-client/css/todolist-theme.css +235 -0
- package/examples/todo-list/web-client/generate-build-config.cjs +18 -0
- package/examples/todo-list/web-client/html/index.html +18 -0
- package/examples/todo-list/web-client/package-lock.json +12030 -0
- package/examples/todo-list/web-client/package.json +43 -0
- package/examples/todo-list/web-client/source/TodoList-Application-Config.json +12 -0
- package/examples/todo-list/web-client/source/TodoList-Application.cjs +383 -0
- package/examples/todo-list/web-client/source/providers/Provider-TaskData.cjs +243 -0
- package/examples/todo-list/web-client/source/providers/Router-Config.json +32 -0
- package/examples/todo-list/web-client/source/views/View-Layout.cjs +75 -0
- package/examples/todo-list/web-client/source/views/View-TaskForm.cjs +87 -0
- package/examples/todo-list/web-client/source/views/View-TaskList.cjs +127 -0
- package/examples/todo-list/web-client/source/views/calendar/View-MonthView.cjs +293 -0
- package/examples/todo-list/web-client/source/views/calendar/View-WeekView.cjs +149 -0
- package/examples/todo-list/web-client/source/views/calendar/View-YearView.cjs +226 -0
- package/modules/Include-Retold-Module-List.sh +2 -2
- package/package.json +5 -5
- package/docs/js/pict.min.js +0 -12
- package/docs/js/pict.min.js.map +0 -1
- package/docs/pict-docuserve.min.js +0 -58
- package/docs/pict-docuserve.min.js.map +0 -1
- /package/docs/{architecture.md → architecture/architecture.md} +0 -0
- /package/docs/{fable.md → modules/fable.md} +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
|
|
3
|
+
const _ViewConfiguration =
|
|
4
|
+
{
|
|
5
|
+
ViewIdentifier: 'TodoList-Layout',
|
|
6
|
+
DefaultRenderable: 'TodoList-Layout-Shell',
|
|
7
|
+
DefaultDestinationAddress: '#TodoList-Container',
|
|
8
|
+
AutoRender: false,
|
|
9
|
+
|
|
10
|
+
CSS: /*css*/`
|
|
11
|
+
.tl-nav { display: flex; align-items: center; justify-content: space-between; background: #3D3229; color: #E8E0D4; padding: 0 1.5em; height: 54px; }
|
|
12
|
+
.tl-nav-brand { font-size: 1.25em; font-weight: 700; color: #E8E0D4; cursor: pointer; text-decoration: none; }
|
|
13
|
+
.tl-nav-links { display: flex; align-items: center; gap: 0.5em; }
|
|
14
|
+
.tl-nav-links a { color: #D4CCBE; text-decoration: none; padding: 0.4em 0.75em; cursor: pointer; border-radius: 4px; transition: color 0.15s; }
|
|
15
|
+
.tl-nav-links a:hover { color: #fff; }
|
|
16
|
+
.tl-nav-sep { color: #5E5549; margin: 0 0.15em; }
|
|
17
|
+
`,
|
|
18
|
+
|
|
19
|
+
Templates:
|
|
20
|
+
[
|
|
21
|
+
{
|
|
22
|
+
Hash: 'TodoList-Layout-Template',
|
|
23
|
+
Template: /*html*/`
|
|
24
|
+
<div class="tl-nav">
|
|
25
|
+
<a class="tl-nav-brand" onclick="{~P~}.PictApplication.navigateTo('/TaskList')">Todo List</a>
|
|
26
|
+
<div class="tl-nav-links">
|
|
27
|
+
<a onclick="{~P~}.PictApplication.navigateTo('/TaskList')">Tasks</a>
|
|
28
|
+
<span class="tl-nav-sep">|</span>
|
|
29
|
+
<a onclick="{~P~}.PictApplication.navigateTo('/WeekView')">Week</a>
|
|
30
|
+
<a onclick="{~P~}.PictApplication.navigateTo('/MonthView')">Month</a>
|
|
31
|
+
<a onclick="{~P~}.PictApplication.navigateTo('/YearView')">Year</a>
|
|
32
|
+
<span class="tl-nav-sep">|</span>
|
|
33
|
+
<a class="tl-btn tl-btn-success" onclick="{~P~}.PictApplication.addTask()">+ New Task</a>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<div id="TodoList-Content"></div>
|
|
37
|
+
`
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
Renderables:
|
|
42
|
+
[
|
|
43
|
+
{
|
|
44
|
+
RenderableHash: 'TodoList-Layout-Shell',
|
|
45
|
+
TemplateHash: 'TodoList-Layout-Template',
|
|
46
|
+
DestinationAddress: '#TodoList-Container',
|
|
47
|
+
RenderMethod: 'replace'
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
class TodoListLayoutView extends libPictView
|
|
53
|
+
{
|
|
54
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
55
|
+
{
|
|
56
|
+
super(pFable, pOptions, pServiceHash);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onAfterRender()
|
|
60
|
+
{
|
|
61
|
+
// Render the task list as the default content view
|
|
62
|
+
this.pict.views['TodoList-TaskList'].render();
|
|
63
|
+
this.pict.CSSMap.injectCSS();
|
|
64
|
+
|
|
65
|
+
if (this.pict.providers.PictRouter)
|
|
66
|
+
{
|
|
67
|
+
this.pict.providers.PictRouter.resolve();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return super.onAfterRender();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = TodoListLayoutView;
|
|
75
|
+
module.exports.default_configuration = _ViewConfiguration;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
|
|
3
|
+
const _ViewConfiguration =
|
|
4
|
+
{
|
|
5
|
+
ViewIdentifier: 'TodoList-TaskForm',
|
|
6
|
+
DefaultRenderable: 'TodoList-TaskForm-Content',
|
|
7
|
+
DefaultDestinationAddress: '#TodoList-Content',
|
|
8
|
+
DefaultTemplateRecordAddress: 'AppData.TodoList',
|
|
9
|
+
AutoRender: false,
|
|
10
|
+
|
|
11
|
+
Templates:
|
|
12
|
+
[
|
|
13
|
+
{
|
|
14
|
+
Hash: 'TodoList-TaskForm-Template',
|
|
15
|
+
Template: /*html*/`
|
|
16
|
+
<h2 class="tl-heading">{~D:Record.FormTitle~}</h2>
|
|
17
|
+
<div class="tl-form">
|
|
18
|
+
<div class="tl-form-group">
|
|
19
|
+
<label for="taskName">Name</label>
|
|
20
|
+
<input type="text" id="taskName" value="{~D:Record.SelectedTask.Name~}" placeholder="Task name" />
|
|
21
|
+
</div>
|
|
22
|
+
<div class="tl-form-group">
|
|
23
|
+
<label for="taskDescription">Description</label>
|
|
24
|
+
<textarea id="taskDescription" rows="3" placeholder="Task description">{~D:Record.SelectedTask.Description~}</textarea>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="tl-form-group">
|
|
27
|
+
<label for="taskDueDate">Due Date</label>
|
|
28
|
+
<input type="date" id="taskDueDate" value="{~D:Record.SelectedTask.DueDate~}" />
|
|
29
|
+
</div>
|
|
30
|
+
<div class="tl-form-group">
|
|
31
|
+
<label for="taskHours">Estimated Hours</label>
|
|
32
|
+
<input type="number" id="taskHours" step="0.5" min="0" value="{~D:Record.SelectedTask.LengthInHours~}" />
|
|
33
|
+
</div>
|
|
34
|
+
<div class="tl-form-group">
|
|
35
|
+
<label for="taskStatus">Status</label>
|
|
36
|
+
<select id="taskStatus">
|
|
37
|
+
<option value="Pending">Pending</option>
|
|
38
|
+
<option value="In Progress">In Progress</option>
|
|
39
|
+
<option value="Complete">Complete</option>
|
|
40
|
+
</select>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="tl-form-actions">
|
|
43
|
+
<button class="tl-btn tl-btn-primary" onclick="{~P~}.PictApplication.saveTask()">Save</button>
|
|
44
|
+
<button class="tl-btn tl-btn-default" onclick="{~P~}.PictApplication.navigateTo('/TaskList')">Cancel</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
`
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
Renderables:
|
|
52
|
+
[
|
|
53
|
+
{
|
|
54
|
+
RenderableHash: 'TodoList-TaskForm-Content',
|
|
55
|
+
TemplateHash: 'TodoList-TaskForm-Template',
|
|
56
|
+
DestinationAddress: '#TodoList-Content',
|
|
57
|
+
RenderMethod: 'replace'
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
class TodoListTaskFormView extends libPictView
|
|
63
|
+
{
|
|
64
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
65
|
+
{
|
|
66
|
+
super(pFable, pOptions, pServiceHash);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onAfterRender()
|
|
70
|
+
{
|
|
71
|
+
// Set the select element to match the current task status
|
|
72
|
+
let tmpSelectedTask = this.pict.AppData.TodoList.SelectedTask;
|
|
73
|
+
if (tmpSelectedTask && tmpSelectedTask.Status)
|
|
74
|
+
{
|
|
75
|
+
let tmpSelect = document.getElementById('taskStatus');
|
|
76
|
+
if (tmpSelect)
|
|
77
|
+
{
|
|
78
|
+
tmpSelect.value = tmpSelectedTask.Status;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return super.onAfterRender();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = TodoListTaskFormView;
|
|
87
|
+
module.exports.default_configuration = _ViewConfiguration;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
|
|
3
|
+
const _ViewConfiguration =
|
|
4
|
+
{
|
|
5
|
+
ViewIdentifier: 'TodoList-TaskList',
|
|
6
|
+
DefaultRenderable: 'TodoList-TaskList-Content',
|
|
7
|
+
DefaultDestinationAddress: '#TodoList-Content',
|
|
8
|
+
AutoRender: false,
|
|
9
|
+
|
|
10
|
+
CSS: /*css*/`
|
|
11
|
+
.tl-toolbar .tl-btn { font-size: 0.8em; padding: 0.35em 0.65em; }
|
|
12
|
+
.tl-actions { white-space: nowrap; }
|
|
13
|
+
.tl-actions .tl-btn { margin-right: 0.35em; font-size: 0.8em; padding: 0.3em 0.7em; }
|
|
14
|
+
`,
|
|
15
|
+
|
|
16
|
+
Templates:
|
|
17
|
+
[
|
|
18
|
+
{
|
|
19
|
+
Hash: 'TodoList-TaskList-Template',
|
|
20
|
+
Template: /*html*/`
|
|
21
|
+
<h2 class="tl-heading">Tasks</h2>
|
|
22
|
+
{~T:TodoList-TaskList-Toolbar~}
|
|
23
|
+
<table class="tl-table">
|
|
24
|
+
<thead>
|
|
25
|
+
<tr>
|
|
26
|
+
<th>Name</th>
|
|
27
|
+
<th>Due Date</th>
|
|
28
|
+
<th>Hours</th>
|
|
29
|
+
<th>Status</th>
|
|
30
|
+
<th>Actions</th>
|
|
31
|
+
</tr>
|
|
32
|
+
</thead>
|
|
33
|
+
<tbody>
|
|
34
|
+
{~TemplateSet:TodoList-TaskRow:AppData.TodoList.Tasks~}
|
|
35
|
+
</tbody>
|
|
36
|
+
</table>
|
|
37
|
+
`
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
Hash: 'TodoList-TaskList-Toolbar',
|
|
41
|
+
Template: /*html*/`
|
|
42
|
+
<div class="tl-toolbar">
|
|
43
|
+
<div class="tl-toolbar-group">
|
|
44
|
+
<input type="text" id="tl-search-input" placeholder="Search name or description…" onkeydown="if(event.key==='Enter'){~P~}.PictApplication.searchTasks()" />
|
|
45
|
+
<button class="tl-btn tl-btn-primary" onclick="{~P~}.PictApplication.searchTasks()">Search</button>
|
|
46
|
+
<button class="tl-btn tl-btn-default" onclick="{~P~}.PictApplication.clearSearch()">Clear</button>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="tl-toolbar-group">
|
|
49
|
+
<span class="tl-toolbar-label">Sort by</span>
|
|
50
|
+
<select id="tl-sort-order" onchange="{~P~}.PictApplication.changeSortOrder(this.value)">
|
|
51
|
+
<option value="DueDate~DESC">Due Date (newest first)</option>
|
|
52
|
+
<option value="DueDate~ASC">Due Date (oldest first)</option>
|
|
53
|
+
<option value="Name~ASC">Name (A–Z)</option>
|
|
54
|
+
<option value="Name~DESC">Name (Z–A)</option>
|
|
55
|
+
<option value="Status~ASC">Status (A–Z)</option>
|
|
56
|
+
<option value="Status~DESC">Status (Z–A)</option>
|
|
57
|
+
<option value="LengthInHours~DESC">Hours (most first)</option>
|
|
58
|
+
<option value="LengthInHours~ASC">Hours (least first)</option>
|
|
59
|
+
<option value="IDTask~DESC">Recently added</option>
|
|
60
|
+
<option value="IDTask~ASC">Oldest added</option>
|
|
61
|
+
</select>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="tl-toolbar-group">
|
|
64
|
+
<span class="tl-record-count">{~D:AppData.TodoList.Tasks.length~} of {~D:AppData.TodoList.FilteredCount~} records</span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
`
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
Hash: 'TodoList-TaskRow',
|
|
71
|
+
Template: /*html*/`
|
|
72
|
+
<tr>
|
|
73
|
+
<td>{~D:Record.Name~}</td>
|
|
74
|
+
<td>{~D:Record.DueDate~}</td>
|
|
75
|
+
<td>{~D:Record.LengthInHours~}</td>
|
|
76
|
+
<td><span class="tl-status">{~D:Record.Status~}</span></td>
|
|
77
|
+
<td class="tl-actions">
|
|
78
|
+
<button class="tl-btn tl-btn-primary" onclick="{~P~}.PictApplication.editTask({~D:Record.IDTask~})">Edit</button>
|
|
79
|
+
<button class="tl-btn tl-btn-danger" onclick="{~P~}.PictApplication.deleteTask({~D:Record.IDTask~})">Delete</button>
|
|
80
|
+
</td>
|
|
81
|
+
</tr>
|
|
82
|
+
`
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
|
|
86
|
+
Renderables:
|
|
87
|
+
[
|
|
88
|
+
{
|
|
89
|
+
RenderableHash: 'TodoList-TaskList-Content',
|
|
90
|
+
TemplateHash: 'TodoList-TaskList-Template',
|
|
91
|
+
DestinationAddress: '#TodoList-Content',
|
|
92
|
+
RenderMethod: 'replace'
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
class TodoListTaskListView extends libPictView
|
|
98
|
+
{
|
|
99
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
100
|
+
{
|
|
101
|
+
super(pFable, pOptions, pServiceHash);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onAfterRender()
|
|
105
|
+
{
|
|
106
|
+
let tmpListState = this.pict.AppData.TodoList.ListState;
|
|
107
|
+
|
|
108
|
+
// Restore the sort dropdown to reflect the current ListState
|
|
109
|
+
let tmpSortSelect = document.getElementById('tl-sort-order');
|
|
110
|
+
if (tmpSortSelect && tmpListState)
|
|
111
|
+
{
|
|
112
|
+
tmpSortSelect.value = tmpListState.SortColumn + '~' + tmpListState.SortDirection;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Restore the search input text so it survives re-renders
|
|
116
|
+
let tmpSearchInput = document.getElementById('tl-search-input');
|
|
117
|
+
if (tmpSearchInput && tmpListState)
|
|
118
|
+
{
|
|
119
|
+
tmpSearchInput.value = tmpListState.SearchText || '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return super.onAfterRender();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = TodoListTaskListView;
|
|
127
|
+
module.exports.default_configuration = _ViewConfiguration;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
|
|
3
|
+
const _ViewConfiguration =
|
|
4
|
+
{
|
|
5
|
+
ViewIdentifier: 'TodoList-MonthView',
|
|
6
|
+
DefaultRenderable: 'TodoList-MonthView-Content',
|
|
7
|
+
DefaultDestinationAddress: '#TodoList-Content',
|
|
8
|
+
AutoRender: false,
|
|
9
|
+
|
|
10
|
+
CSS: /*css*/`
|
|
11
|
+
.tl-month-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: #DDD6CA; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
|
12
|
+
.tl-month-grid-header { background: #3D3229; color: #E8E0D4; text-align: center; padding: 0.6em 0.25em; font-weight: 600; font-size: 0.85em; }
|
|
13
|
+
.tl-month-cell { background: #fff; min-height: 5em; padding: 0.4em; display: flex; flex-direction: column; }
|
|
14
|
+
.tl-month-cell-outside { background: #EAE3D8; color: #B5ADA2; }
|
|
15
|
+
.tl-month-cell-today { background: #E0EDEB; }
|
|
16
|
+
.tl-month-day-num { font-size: 0.85em; font-weight: 600; margin-bottom: 0.3em; }
|
|
17
|
+
.tl-month-counts { display: flex; gap: 0.3em; flex-wrap: wrap; margin-top: auto; }
|
|
18
|
+
.tl-month-counts .tl-cal-badge { font-size: 0.75em; }
|
|
19
|
+
`,
|
|
20
|
+
|
|
21
|
+
Templates:
|
|
22
|
+
[
|
|
23
|
+
{
|
|
24
|
+
Hash: 'TodoList-MonthView-Template',
|
|
25
|
+
Template: /*html*/`
|
|
26
|
+
<div class="tl-cal-header">
|
|
27
|
+
<h2>Month View</h2>
|
|
28
|
+
<div class="tl-cal-nav">
|
|
29
|
+
<button class="tl-btn tl-btn-default" onclick="{~P~}.PictApplication.calendarNavigate('month', -1)">← Prev</button>
|
|
30
|
+
<button class="tl-btn tl-btn-primary" onclick="{~P~}.PictApplication.calendarToday('month')">Today</button>
|
|
31
|
+
<button class="tl-btn tl-btn-default" onclick="{~P~}.PictApplication.calendarNavigate('month', 1)">Next →</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<p class="tl-cal-label">{~D:AppData.TodoList.CalendarState.MonthLabel~}</p>
|
|
35
|
+
<div id="TodoList-MonthGrid"></div>
|
|
36
|
+
<table class="tl-cal-table" style="margin-top:1.25em;">
|
|
37
|
+
<thead>
|
|
38
|
+
<tr>
|
|
39
|
+
<th>Week</th>
|
|
40
|
+
<th>Completed</th>
|
|
41
|
+
<th>Open</th>
|
|
42
|
+
<th>Total</th>
|
|
43
|
+
</tr>
|
|
44
|
+
</thead>
|
|
45
|
+
<tbody>
|
|
46
|
+
{~TemplateSet:TodoList-MonthWeekRow:AppData.TodoList.CalendarState.MonthRows~}
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
`
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
Hash: 'TodoList-MonthWeekRow',
|
|
53
|
+
Template: /*html*/`
|
|
54
|
+
<tr class="{~D:Record.TotalClass~}">
|
|
55
|
+
<td>{~D:Record.WeekLabel~}</td>
|
|
56
|
+
<td><span class="tl-cal-badge {~D:Record.CompleteBadge~}">{~D:Record.Complete~}</span></td>
|
|
57
|
+
<td><span class="tl-cal-badge {~D:Record.OpenBadge~}">{~D:Record.Open~}</span></td>
|
|
58
|
+
<td><span class="tl-cal-badge {~D:Record.TotalBadge~}">{~D:Record.Total~}</span></td>
|
|
59
|
+
</tr>
|
|
60
|
+
`
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
|
|
64
|
+
Renderables:
|
|
65
|
+
[
|
|
66
|
+
{
|
|
67
|
+
RenderableHash: 'TodoList-MonthView-Content',
|
|
68
|
+
TemplateHash: 'TodoList-MonthView-Template',
|
|
69
|
+
DestinationAddress: '#TodoList-Content',
|
|
70
|
+
RenderMethod: 'replace'
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const _MonthNamesFull = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
76
|
+
const _DayAbbr = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
77
|
+
const _MonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
78
|
+
|
|
79
|
+
class TodoListMonthView extends libPictView
|
|
80
|
+
{
|
|
81
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
82
|
+
{
|
|
83
|
+
super(pFable, pOptions, pServiceHash);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the task-count lookup and summary rows before the template renders.
|
|
88
|
+
*/
|
|
89
|
+
onBeforeRender()
|
|
90
|
+
{
|
|
91
|
+
let tmpCal = this.pict.AppData.TodoList.CalendarState;
|
|
92
|
+
let tmpAnchor = new Date(tmpCal.AnchorDate + 'T00:00:00');
|
|
93
|
+
let tmpYear = tmpAnchor.getFullYear();
|
|
94
|
+
let tmpMonth = tmpAnchor.getMonth();
|
|
95
|
+
|
|
96
|
+
tmpCal.MonthLabel = _MonthNamesFull[tmpMonth] + ' ' + tmpYear;
|
|
97
|
+
|
|
98
|
+
// Build a date->counts map from all tasks
|
|
99
|
+
let tmpAllTasks = this.pict.AppData.TodoList.AllTasks;
|
|
100
|
+
let tmpTaskMap = {};
|
|
101
|
+
for (let i = 0; i < tmpAllTasks.length; i++)
|
|
102
|
+
{
|
|
103
|
+
let tmpDate = (tmpAllTasks[i].DueDate || '').substring(0, 10);
|
|
104
|
+
if (!tmpDate)
|
|
105
|
+
{
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!tmpTaskMap[tmpDate])
|
|
109
|
+
{
|
|
110
|
+
tmpTaskMap[tmpDate] = { complete: 0, open: 0 };
|
|
111
|
+
}
|
|
112
|
+
if (tmpAllTasks[i].Status === 'Complete')
|
|
113
|
+
{
|
|
114
|
+
tmpTaskMap[tmpDate].complete++;
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
{
|
|
118
|
+
tmpTaskMap[tmpDate].open++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Store for use in onAfterRender (the grid is rendered imperatively)
|
|
123
|
+
this._taskMap = tmpTaskMap;
|
|
124
|
+
this._gridYear = tmpYear;
|
|
125
|
+
this._gridMonth = tmpMonth;
|
|
126
|
+
|
|
127
|
+
// Build weekly summary rows for the table
|
|
128
|
+
let tmpFirstDay = new Date(tmpYear, tmpMonth, 1);
|
|
129
|
+
let tmpLastDay = new Date(tmpYear, tmpMonth + 1, 0);
|
|
130
|
+
|
|
131
|
+
// Find the Monday on or before the 1st
|
|
132
|
+
let tmpStartDow = tmpFirstDay.getDay();
|
|
133
|
+
let tmpGridStart = new Date(tmpFirstDay);
|
|
134
|
+
tmpGridStart.setDate(tmpGridStart.getDate() - ((tmpStartDow + 6) % 7));
|
|
135
|
+
|
|
136
|
+
let tmpRows = [];
|
|
137
|
+
let tmpWeekStart = new Date(tmpGridStart);
|
|
138
|
+
let tmpTotalComplete = 0;
|
|
139
|
+
let tmpTotalOpen = 0;
|
|
140
|
+
|
|
141
|
+
while (tmpWeekStart <= tmpLastDay)
|
|
142
|
+
{
|
|
143
|
+
let tmpWeekComplete = 0;
|
|
144
|
+
let tmpWeekOpen = 0;
|
|
145
|
+
|
|
146
|
+
for (let d = 0; d < 7; d++)
|
|
147
|
+
{
|
|
148
|
+
let tmpDay = new Date(tmpWeekStart);
|
|
149
|
+
tmpDay.setDate(tmpWeekStart.getDate() + d);
|
|
150
|
+
let tmpDateStr = tmpDay.toISOString().substring(0, 10);
|
|
151
|
+
|
|
152
|
+
if (tmpDay.getMonth() === tmpMonth)
|
|
153
|
+
{
|
|
154
|
+
let tmpCounts = tmpTaskMap[tmpDateStr] || { complete: 0, open: 0 };
|
|
155
|
+
tmpWeekComplete += tmpCounts.complete;
|
|
156
|
+
tmpWeekOpen += tmpCounts.open;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let tmpWeekEnd = new Date(tmpWeekStart);
|
|
161
|
+
tmpWeekEnd.setDate(tmpWeekStart.getDate() + 6);
|
|
162
|
+
let tmpWeekTotal = tmpWeekComplete + tmpWeekOpen;
|
|
163
|
+
|
|
164
|
+
tmpRows.push(
|
|
165
|
+
{
|
|
166
|
+
WeekLabel: _MonthNames[tmpWeekStart.getMonth()] + ' ' + tmpWeekStart.getDate()
|
|
167
|
+
+ ' – ' + _MonthNames[tmpWeekEnd.getMonth()] + ' ' + tmpWeekEnd.getDate(),
|
|
168
|
+
Complete: tmpWeekComplete,
|
|
169
|
+
Open: tmpWeekOpen,
|
|
170
|
+
Total: tmpWeekTotal,
|
|
171
|
+
CompleteBadge: tmpWeekComplete > 0 ? 'tl-cal-badge-complete' : 'tl-cal-badge-zero',
|
|
172
|
+
OpenBadge: tmpWeekOpen > 0 ? 'tl-cal-badge-open' : 'tl-cal-badge-zero',
|
|
173
|
+
TotalBadge: tmpWeekTotal > 0 ? 'tl-cal-badge-complete' : 'tl-cal-badge-zero',
|
|
174
|
+
TotalClass: ''
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
tmpTotalComplete += tmpWeekComplete;
|
|
178
|
+
tmpTotalOpen += tmpWeekOpen;
|
|
179
|
+
|
|
180
|
+
tmpWeekStart.setDate(tmpWeekStart.getDate() + 7);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Add a totals row
|
|
184
|
+
let tmpGrandTotal = tmpTotalComplete + tmpTotalOpen;
|
|
185
|
+
tmpRows.push(
|
|
186
|
+
{
|
|
187
|
+
WeekLabel: 'Total',
|
|
188
|
+
Complete: tmpTotalComplete,
|
|
189
|
+
Open: tmpTotalOpen,
|
|
190
|
+
Total: tmpGrandTotal,
|
|
191
|
+
CompleteBadge: tmpTotalComplete > 0 ? 'tl-cal-badge-complete' : 'tl-cal-badge-zero',
|
|
192
|
+
OpenBadge: tmpTotalOpen > 0 ? 'tl-cal-badge-open' : 'tl-cal-badge-zero',
|
|
193
|
+
TotalBadge: tmpGrandTotal > 0 ? 'tl-cal-badge-complete' : 'tl-cal-badge-zero',
|
|
194
|
+
TotalClass: 'tl-cal-total'
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
tmpCal.MonthRows = tmpRows;
|
|
198
|
+
|
|
199
|
+
return super.onBeforeRender();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Render the calendar grid into the placeholder div after the template
|
|
204
|
+
* has been placed in the DOM.
|
|
205
|
+
*/
|
|
206
|
+
onAfterRender()
|
|
207
|
+
{
|
|
208
|
+
let tmpContainer = document.getElementById('TodoList-MonthGrid');
|
|
209
|
+
if (!tmpContainer)
|
|
210
|
+
{
|
|
211
|
+
return super.onAfterRender();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let tmpYear = this._gridYear;
|
|
215
|
+
let tmpMonth = this._gridMonth;
|
|
216
|
+
let tmpTaskMap = this._taskMap;
|
|
217
|
+
let tmpToday = new Date().toISOString().substring(0, 10);
|
|
218
|
+
|
|
219
|
+
let tmpFirstDay = new Date(tmpYear, tmpMonth, 1);
|
|
220
|
+
let tmpLastDay = new Date(tmpYear, tmpMonth + 1, 0);
|
|
221
|
+
|
|
222
|
+
// Find the Monday on or before the 1st
|
|
223
|
+
let tmpStartDow = tmpFirstDay.getDay();
|
|
224
|
+
let tmpGridStart = new Date(tmpFirstDay);
|
|
225
|
+
tmpGridStart.setDate(tmpGridStart.getDate() - ((tmpStartDow + 6) % 7));
|
|
226
|
+
|
|
227
|
+
let tmpHTML = '<div class="tl-month-grid">';
|
|
228
|
+
|
|
229
|
+
// Day-of-week headers (Mon–Sun)
|
|
230
|
+
for (let h = 0; h < _DayAbbr.length; h++)
|
|
231
|
+
{
|
|
232
|
+
tmpHTML += '<div class="tl-month-grid-header">' + _DayAbbr[h] + '</div>';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Calendar cells
|
|
236
|
+
let tmpCurrent = new Date(tmpGridStart);
|
|
237
|
+
let tmpSunday = new Date(tmpLastDay);
|
|
238
|
+
// Extend to fill the last row (go to next Sunday)
|
|
239
|
+
let tmpEndDow = tmpLastDay.getDay();
|
|
240
|
+
if (tmpEndDow !== 0)
|
|
241
|
+
{
|
|
242
|
+
tmpSunday.setDate(tmpLastDay.getDate() + (7 - tmpEndDow));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
while (tmpCurrent <= tmpSunday)
|
|
246
|
+
{
|
|
247
|
+
let tmpDateStr = tmpCurrent.toISOString().substring(0, 10);
|
|
248
|
+
let tmpIsOutside = tmpCurrent.getMonth() !== tmpMonth;
|
|
249
|
+
let tmpIsToday = tmpDateStr === tmpToday;
|
|
250
|
+
|
|
251
|
+
let tmpCellClass = 'tl-month-cell';
|
|
252
|
+
if (tmpIsOutside)
|
|
253
|
+
{
|
|
254
|
+
tmpCellClass += ' tl-month-cell-outside';
|
|
255
|
+
}
|
|
256
|
+
if (tmpIsToday)
|
|
257
|
+
{
|
|
258
|
+
tmpCellClass += ' tl-month-cell-today';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let tmpCounts = tmpTaskMap[tmpDateStr] || { complete: 0, open: 0 };
|
|
262
|
+
|
|
263
|
+
tmpHTML += '<div class="' + tmpCellClass + '">';
|
|
264
|
+
tmpHTML += '<div class="tl-month-day-num">' + tmpCurrent.getDate() + '</div>';
|
|
265
|
+
|
|
266
|
+
if (!tmpIsOutside && (tmpCounts.complete > 0 || tmpCounts.open > 0))
|
|
267
|
+
{
|
|
268
|
+
tmpHTML += '<div class="tl-month-counts">';
|
|
269
|
+
if (tmpCounts.complete > 0)
|
|
270
|
+
{
|
|
271
|
+
tmpHTML += '<span class="tl-cal-badge tl-cal-badge-complete">' + tmpCounts.complete + '</span>';
|
|
272
|
+
}
|
|
273
|
+
if (tmpCounts.open > 0)
|
|
274
|
+
{
|
|
275
|
+
tmpHTML += '<span class="tl-cal-badge tl-cal-badge-open">' + tmpCounts.open + '</span>';
|
|
276
|
+
}
|
|
277
|
+
tmpHTML += '</div>';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
tmpHTML += '</div>';
|
|
281
|
+
|
|
282
|
+
tmpCurrent.setDate(tmpCurrent.getDate() + 1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
tmpHTML += '</div>';
|
|
286
|
+
tmpContainer.innerHTML = tmpHTML;
|
|
287
|
+
|
|
288
|
+
return super.onAfterRender();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = TodoListMonthView;
|
|
293
|
+
module.exports.default_configuration = _ViewConfiguration;
|