1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-08-30 00:59:53 +02:00

update to ES2020, some refactoring and cleanups

This commit is contained in:
Morris Brodersen
2022-05-09 15:46:58 +02:00
parent b4c57030f8
commit 2fbfc5e650
26 changed files with 1507 additions and 2129 deletions

View File

@@ -1,18 +1,15 @@
/* global VT */
window.VT = window.VT || {};
VT.AppCollapsible = function (el) {
var state = {
export function AppCollapsible(el) {
const state = {
show: true,
};
el.querySelector('.bar > .toggle').addEventListener('click', function () {
update({ show: !state.show });
});
el.addEventListener('collapse', (e) => update({ show: !e.detail }));
el.appCollapsible = {
update: update,
};
el.querySelector('.bar > .toggle').addEventListener('click', () =>
update({ show: !state.show })
);
update();
function update(next) {
Object.assign(state, next);
@@ -22,8 +19,8 @@ VT.AppCollapsible = function (el) {
state.show
);
el.querySelectorAll('.body').forEach(function (el) {
el.style.height = state.show ? el.children[0].offsetHeight + 'px' : '0';
el.querySelectorAll('.body').forEach((el) => {
el.style.height = state.show ? `${el.children[0].offsetHeight}px` : '0';
});
}
};
}

View File

@@ -1,21 +1,18 @@
/* global VT */
window.VT = window.VT || {};
export function AppDraggable(el, options) {
const dragThreshold = options.dragThreshold ?? 5;
const dropRange = options.dropRange ?? 50;
const dropRangeSquared = dropRange * dropRange;
VT.AppDraggable = function (el, options) {
var dragThreshold = options.dragThreshold || 5;
var dropRange = options.dropRange || 50;
var dropRangeSquared = dropRange * dropRange;
var originX, originY;
var clientX, clientY;
var startTime;
var dragging = false;
var clicked = false;
var data;
var image;
var imageSource;
var imageX, imageY;
var currentTarget;
let originX, originY;
let clientX, clientY;
let startTime;
let dragging = false;
let clicked = false;
let data;
let image;
let imageSource;
let imageX, imageY;
let currentTarget;
el.addEventListener('touchstart', start);
el.addEventListener('mousedown', start);
@@ -23,7 +20,7 @@ VT.AppDraggable = function (el, options) {
// maybe prevent click
el.addEventListener(
'click',
function (e) {
(e) => {
if (dragging || clicked) {
e.preventDefault();
e.stopImmediatePropagation();
@@ -39,9 +36,9 @@ VT.AppDraggable = function (el, options) {
e.preventDefault();
var p = getPositionHost(e);
clientX = originX = p.clientX || p.pageX;
clientY = originY = p.clientY || p.pageY;
const p = getPositionHost(e);
clientX = originX = p.clientX ?? p.pageX;
clientY = originY = p.clientY ?? p.pageY;
startTime = Date.now();
startListening();
@@ -50,9 +47,9 @@ VT.AppDraggable = function (el, options) {
function move(e) {
e.preventDefault();
var p = getPositionHost(e);
clientX = p.clientX || p.pageX;
clientY = p.clientY || p.pageY;
const p = getPositionHost(e);
clientX = p.clientX ?? p.pageX;
clientY = p.clientY ?? p.pageY;
if (dragging) {
dispatchDrag();
@@ -60,8 +57,8 @@ VT.AppDraggable = function (el, options) {
return;
}
var deltaX = clientX - originX;
var deltaY = clientY - originY;
const deltaX = clientX - originX;
const deltaY = clientY - originY;
if (Math.abs(deltaX) < dragThreshold && Math.abs(deltaY) < dragThreshold) {
return;
@@ -92,7 +89,7 @@ VT.AppDraggable = function (el, options) {
stopListening();
requestAnimationFrame(function () {
requestAnimationFrame(() => {
clicked = false;
if (dragging) {
@@ -141,7 +138,7 @@ VT.AppDraggable = function (el, options) {
function dispatchTarget() {
if (!dragging) return;
var nextTarget = getTarget();
const nextTarget = getTarget();
if (nextTarget === currentTarget) return;
@@ -210,18 +207,18 @@ VT.AppDraggable = function (el, options) {
//
function buildDetail() {
var detail = {
el: el,
data: data,
image: image,
imageSource: imageSource,
originX: originX,
originY: originY,
clientX: clientX,
clientY: clientY,
imageX: imageX,
imageY: imageY,
setImage: function (source) {
const detail = {
el,
data,
image,
imageSource,
originX,
originY,
clientX,
clientY,
imageX,
imageY,
setImage: (source) => {
setImage(source);
detail.image = image;
},
@@ -240,21 +237,21 @@ VT.AppDraggable = function (el, options) {
image.style.position = 'fixed';
image.style.left = '0';
image.style.top = '0';
image.style.width = imageSource.offsetWidth + 'px';
image.style.height = imageSource.offsetHeight + 'px';
image.style.width = `${imageSource.offsetWidth}px`;
image.style.height = `${imageSource.offsetHeight}px`;
image.style.margin = '0';
image.style.zIndex = 9999;
image.classList.add('-dragging');
var rect = source.getBoundingClientRect();
const rect = source.getBoundingClientRect();
imageX = originX - rect.left;
imageY = originY - rect.top;
image.addEventListener('draggableDrag', function (e) {
var x = e.detail.clientX - e.detail.imageX;
var y = e.detail.clientY - e.detail.imageY;
image.addEventListener('draggableDrag', (e) => {
const x = e.detail.clientX - e.detail.imageX;
const y = e.detail.clientY - e.detail.imageY;
image.style.transition = 'none';
image.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
image.style.transform = `translate(${x}px, ${y}px)`;
});
image.addEventListener('draggableCancel', cleanUp);
@@ -278,9 +275,7 @@ VT.AppDraggable = function (el, options) {
}
function cleanUp() {
if (currentTarget) {
currentTarget.classList.remove('-drop');
}
currentTarget?.classList.remove('-drop');
removeImage();
@@ -291,27 +286,29 @@ VT.AppDraggable = function (el, options) {
}
function removeImage() {
if (image && image.parentNode) {
image.parentNode.removeChild(image);
}
image?.parentNode?.removeChild(image);
}
function getTarget() {
var candidates = [];
const candidates = [];
document.querySelectorAll(options.dropSelector).forEach(function (el) {
var rect = el.getBoundingClientRect();
var distanceSquared = pointDistanceToRectSquared(clientX, clientY, rect);
document.querySelectorAll(options.dropSelector).forEach((el) => {
const rect = el.getBoundingClientRect();
const distanceSquared = pointDistanceToRectSquared(
clientX,
clientY,
rect
);
if (distanceSquared > dropRangeSquared) return;
candidates.push({
el: el,
el,
distance2: distanceSquared,
});
});
candidates.sort(function (a, b) {
candidates.sort((a, b) => {
if (a.distance2 === 0 && b.distance2 === 0) {
// in this case, the client position is inside both rectangles
// if A contains B, B is the correct target and vice versa
@@ -327,9 +324,9 @@ VT.AppDraggable = function (el, options) {
}
function pointDistanceToRectSquared(x, y, rect) {
var dx =
const dx =
x < rect.left ? x - rect.left : x > rect.right ? x - rect.right : 0;
var dy =
const dy =
y < rect.top ? y - rect.top : y > rect.bottom ? y - rect.bottom : 0;
return dx * dx + dy * dy;
@@ -346,4 +343,4 @@ VT.AppDraggable = function (el, options) {
return e;
}
};
}

View File

@@ -1,19 +1,16 @@
/* global VT */
window.VT = window.VT || {};
VT.AppFlip = function (el, options) {
var enabled = options.initialDelay === 0;
var first;
var level = 0;
export function AppFlip(el, options) {
let enabled = options.initialDelay === 0;
let first;
let level = 0;
// enable animations only after an initial delay
setTimeout(function () {
setTimeout(() => {
enabled = true;
}, options.initialDelay || 100);
// take a snapshot before any HTML changes
// do this only for the first beforeFlip event in the current cycle
el.addEventListener('beforeFlip', function () {
el.addEventListener('beforeFlip', () => {
if (!enabled) return;
if (++level > 1) return;
@@ -22,16 +19,16 @@ VT.AppFlip = function (el, options) {
// take a snapshot after HTML changes, calculate and play animations
// do this only for the last flip event in the current cycle
el.addEventListener('flip', function () {
el.addEventListener('flip', () => {
if (!enabled) return;
if (--level > 0) return;
var last = snapshot();
var toRemove = invertForRemoval(first, last);
var toAnimate = invertForAnimation(first, last);
const last = snapshot();
const toRemove = invertForRemoval(first, last);
const toAnimate = invertForAnimation(first, last);
requestAnimationFrame(function () {
requestAnimationFrame(function () {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
remove(toRemove);
animate(toAnimate);
@@ -43,23 +40,23 @@ VT.AppFlip = function (el, options) {
// build a snapshot of the current HTML's client rectangles
// includes original transforms and hierarchy
function snapshot() {
var map = new Map();
const map = new Map();
el.querySelectorAll(options.selector).forEach(function (el) {
var key = el.dataset.key || el;
el.querySelectorAll(options.selector).forEach((el) => {
const key = el.dataset.key ?? el;
// parse original transform
// i.e. strip inverse transform using "scale(1)" marker
var transform = el.style.transform
const transform = el.style.transform
? el.style.transform.replace(/^.*scale\(1\)/, '')
: '';
map.set(key, {
key: key,
el: el,
key,
el,
rect: el.getBoundingClientRect(),
ancestor: null,
transform: transform,
transform,
});
});
@@ -69,11 +66,11 @@ VT.AppFlip = function (el, options) {
}
function resolveAncestors(map) {
map.forEach(function (entry) {
var current = entry.el.parentNode;
map.forEach((entry) => {
let current = entry.el.parentNode;
while (current && current !== el) {
var ancestor = map.get(current.dataset.key || current);
const ancestor = map.get(current.dataset.key ?? current);
if (ancestor) {
entry.ancestor = ancestor;
@@ -87,16 +84,16 @@ VT.AppFlip = function (el, options) {
// reinsert removed elements at their original position
function invertForRemoval(first, last) {
var toRemove = [];
const toRemove = [];
first.forEach(function (entry) {
first.forEach((entry) => {
if (entry.el.classList.contains('_noflip')) return;
if (!needsRemoval(entry)) return;
entry.el.style.position = 'fixed';
entry.el.style.left = entry.rect.left + 'px';
entry.el.style.top = entry.rect.top + 'px';
entry.el.style.width = entry.rect.right - entry.rect.left + 'px';
entry.el.style.left = `${entry.rect.left}px`;
entry.el.style.top = `${entry.rect.top}px`;
entry.el.style.width = `${entry.rect.right - entry.rect.left}px`;
entry.el.style.transition = 'none';
entry.el.style.transform = '';
@@ -118,9 +115,9 @@ VT.AppFlip = function (el, options) {
// set position of moved elements to their original position
// or set opacity to zero for new elements to appear nicely
function invertForAnimation(first, last) {
var toAnimate = [];
const toAnimate = [];
last.forEach(function (entry) {
last.forEach((entry) => {
if (entry.el.classList.contains('_noflip')) return;
calculate(entry);
@@ -132,13 +129,7 @@ VT.AppFlip = function (el, options) {
} else if (entry.deltaX !== 0 || entry.deltaY !== 0) {
// set inverted transform with "scale(1)" marker, see above
entry.el.style.transition = 'none';
entry.el.style.transform =
'translate(' +
entry.deltaX +
'px, ' +
entry.deltaY +
'px) scale(1) ' +
entry.transform;
entry.el.style.transform = `translate(${entry.deltaX}px, ${entry.deltaY}px) scale(1) ${entry.transform}`;
toAnimate.push(entry);
}
});
@@ -150,7 +141,7 @@ VT.AppFlip = function (el, options) {
if (entry.calculated) return;
entry.calculated = true;
var b = first.get(entry.key);
const b = first.get(entry.key);
if (b) {
entry.deltaX = b.rect.left - entry.rect.left;
@@ -172,13 +163,13 @@ VT.AppFlip = function (el, options) {
// play remove animations and remove elements after timeout
function remove(entries) {
entries.forEach(function (entry) {
entries.forEach((entry) => {
entry.el.style.transition = '';
entry.el.style.opacity = '0';
});
setTimeout(function () {
entries.forEach(function (entry) {
setTimeout(() => {
entries.forEach((entry) => {
if (entry.el.parentNode) {
entry.el.parentNode.removeChild(entry.el);
}
@@ -188,10 +179,10 @@ VT.AppFlip = function (el, options) {
// play move/appear animations
function animate(entries) {
entries.forEach(function (entry) {
entries.forEach((entry) => {
entry.el.style.transition = '';
entry.el.style.transform = entry.transform;
entry.el.style.opacity = '';
});
}
};
}

View File

@@ -1,9 +1,6 @@
/* global VT */
window.VT = window.VT || {};
VT.AppFps = function (el) {
var sampleSize = 20;
var times = [];
export function AppFps(el) {
const sampleSize = 20;
let times = [];
tick();
@@ -14,27 +11,23 @@ VT.AppFps = function (el) {
if (times.length <= sampleSize) return;
var min = Infinity;
var max = 0;
var sum = 0;
let min = Infinity;
let max = 0;
let sum = 0;
for (var i = 1; i < sampleSize + 1; ++i) {
var delta = times[i] - times[i - 1];
for (let i = 1; i < sampleSize + 1; ++i) {
const delta = times[i] - times[i - 1];
min = Math.min(min, delta);
max = Math.max(max, delta);
sum += delta;
}
var fps = (sampleSize / sum) * 1000;
const fps = (sampleSize / sum) * 1000;
el.innerText =
fps.toFixed(0) +
' fps (' +
min.toFixed(0) +
' ms - ' +
max.toFixed(0) +
' ms)';
el.innerText = `${fps.toFixed(0)} fps (${min.toFixed(0)} ms - ${max.toFixed(
0
)} ms)`;
times = [];
}
};
}

View File

@@ -1,24 +1,19 @@
/* global VT */
window.VT = window.VT || {};
export const BASE_URL =
'https://rawcdn.githack.com/primer/octicons/ff7f6eee63fa2f2d24d02e3aa76a87db48e4b6f6/icons/';
VT.AppIcon = function (el) {
const cache = {};
export function AppIcon(el) {
if (el.children.length > 0) return;
var id = el.dataset.id;
var promise = VT.AppIcon.cache[id];
const id = el.dataset.id;
let promise = cache[id];
if (!promise) {
var url = VT.AppIcon.baseUrl + id + '.svg';
promise = VT.AppIcon.cache[id] = fetch(url).then(function (r) {
return r.text();
});
promise = cache[id] = fetch(`${BASE_URL}${id}.svg`).then((r) => r.text());
}
promise.then(function (svg) {
promise.then((svg) => {
el.innerHTML = el.classList.contains('-double') ? svg + svg : svg;
});
};
VT.AppIcon.baseUrl =
'https://rawcdn.githack.com/primer/octicons/ff7f6eee63fa2f2d24d02e3aa76a87db48e4b6f6/icons/';
VT.AppIcon.cache = {};
}

View File

@@ -1,34 +1,29 @@
/* global VT */
window.VT = window.VT || {};
export function AppSortable(el, options) {
let placeholder;
let placeholderSource;
const horizontal = options.direction === 'horizontal';
let currentIndex = -1;
VT.AppSortable = function (el, options) {
var placeholder;
var placeholderSource;
var horizontal = options.direction === 'horizontal';
var currentIndex = -1;
el.addEventListener('draggableStart', (e) =>
e.detail.image.addEventListener('draggableCancel', cleanUp)
);
el.addEventListener('draggableStart', function (e) {
e.detail.image.addEventListener('draggableCancel', cleanUp);
});
el.addEventListener('draggableOver', (e) =>
maybeDispatchUpdate(calculateIndex(e.detail.image), e)
);
el.addEventListener('draggableOver', function (e) {
maybeDispatchUpdate(calculateIndex(e.detail.image), e);
});
el.addEventListener('draggableLeave', (e) => maybeDispatchUpdate(-1, e));
el.addEventListener('draggableLeave', function (e) {
maybeDispatchUpdate(-1, e);
});
el.addEventListener('draggableDrop', function (e) {
el.addEventListener('draggableDrop', (e) =>
el.dispatchEvent(
new CustomEvent('sortableDrop', {
detail: buildDetail(e),
bubbles: true,
})
);
});
)
);
el.addEventListener('sortableUpdate', function (e) {
el.addEventListener('sortableUpdate', (e) => {
if (!placeholder) {
e.detail.setPlaceholder(e.detail.originalEvent.detail.imageSource);
}
@@ -65,11 +60,11 @@ VT.AppSortable = function (el, options) {
}
function buildDetail(e) {
var detail = {
const detail = {
data: e.detail.data,
index: currentIndex,
placeholder: placeholder,
setPlaceholder: function (source) {
placeholder,
setPlaceholder: (source) => {
setPlaceholder(source);
detail.placeholder = placeholder;
},
@@ -98,14 +93,12 @@ VT.AppSortable = function (el, options) {
}
function removePlaceholder() {
if (placeholder && placeholder.parentNode) {
placeholder.parentNode.removeChild(placeholder);
}
placeholder?.parentNode?.removeChild(placeholder);
}
function removeByKey(key) {
for (var i = 0, l = el.children.length; i < l; ++i) {
var child = el.children[i];
for (let i = 0, l = el.children.length; i < l; ++i) {
const child = el.children[i];
if (child && child.dataset.key === key) {
el.removeChild(child);
@@ -116,12 +109,12 @@ VT.AppSortable = function (el, options) {
function calculateIndex(image) {
if (el.children.length === 0) return 0;
var isBefore = horizontal ? isLeft : isAbove;
var rect = image.getBoundingClientRect();
var p = 0;
const isBefore = horizontal ? isLeft : isAbove;
const rect = image.getBoundingClientRect();
let p = 0;
for (var i = 0, l = el.children.length; i < l; ++i) {
var child = el.children[i];
for (let i = 0, l = el.children.length; i < l; ++i) {
const child = el.children[i];
if (isBefore(rect, child.getBoundingClientRect())) return i - p;
if (child === placeholder) p = 1;
@@ -143,4 +136,4 @@ VT.AppSortable = function (el, options) {
rectB.left + (rectB.right - rectB.left) / 2
);
}
};
}

View File

@@ -1,49 +1,56 @@
/* global VT */
window.VT = window.VT || {};
import { AppCollapsible } from './AppCollapsible.js';
import { AppFlip } from './AppFlip.js';
import { AppFps } from './AppFps.js';
import { AppIcon } from './AppIcon.js';
import { TodoFrameCustom } from './TodoFrameCustom.js';
import { TodoFrameDays } from './TodoFrameDays.js';
import { TodoStore } from './TodoStore.js';
import { formatDateId } from './util.js';
VT.TodoApp = function (el) {
var state = {
export function TodoApp(el) {
const state = {
items: [],
customLists: [],
at: VT.formatDateId(new Date()),
at: formatDateId(new Date()),
customAt: 0,
};
el.innerHTML = [
'<header class="app-header">',
' <h1 class="title">VANILLA TODO</h1>',
' <p class="app-fps"></p>',
'</header>',
'<div class="todo-frame -days"></div>',
'<div class="app-collapsible">',
' <p class="bar">',
' <button class="app-button -circle toggle"><i class="app-icon" data-id="chevron-up-24"></i></button>',
' </p>',
' <div class="body">',
' <div class="todo-frame -custom"></div>',
' </div>',
'</div>',
'<footer class="app-footer">',
' <p>',
' VANILLA TODO &copy 2020 <a href="https://morrisbrodersen.de">Morris Brodersen</a>',
' &mdash; A case study on viable techniques for vanilla web development.',
' <a href="https://github.com/morris/vanilla-todo">About →</a>',
' </p>',
'</footer>',
].join('\n');
el.innerHTML = `
<header class="app-header">
<h1 class="title">VANILLA TODO</h1>
<p class="app-fps fps"></p>
</header>
<div class="todo-frame -days"></div>
<div class="app-collapsible">
<p class="bar">
<button class="app-button -circle toggle"><i class="app-icon" data-id="chevron-up-24"></i></button>
</p>
<div class="body">
<div class="todo-frame -custom"></div>
</div>
</div>
<footer class="app-footer">
<p>
VANILLA TODO &copy 2020-2022 <a href="https://morrisbrodersen.de">Morris Brodersen</a>
&mdash; A case study on viable techniques for vanilla web development.
<a href="https://github.com/morris/vanilla-todo">About →</a>
</p>
</footer>
`;
VT.AppFlip(el, {
AppFlip(el, {
selector: '.todo-item, .todo-item-input, .todo-day, .todo-custom-list',
removeTimeout: 200,
});
VT.TodoStore(el);
el.querySelectorAll('.app-collapsible').forEach(VT.AppCollapsible);
el.querySelectorAll('.app-icon').forEach(VT.AppIcon);
el.querySelectorAll('.app-fps').forEach(VT.AppFps);
TodoStore(el);
VT.TodoFrameDays(el.querySelector('.todo-frame.-days'));
VT.TodoFrameCustom(el.querySelector('.todo-frame.-custom'));
el.querySelectorAll('.app-collapsible').forEach(AppCollapsible);
el.querySelectorAll('.app-icon').forEach(AppIcon);
el.querySelectorAll('.app-fps').forEach(AppFps);
TodoFrameDays(el.querySelector('.todo-frame.-days'));
TodoFrameCustom(el.querySelector('.todo-frame.-custom'));
// each of these events make changes to the HTML to be animated using FLIP
// listening to them using "capture" dispatches "beforeFlip" before any changes
@@ -53,30 +60,30 @@ VT.TodoApp = function (el) {
el.addEventListener('draggableDrop', beforeFlip, true);
// some necessary work to orchestrate drag & drop with FLIP animations
el.addEventListener('draggableStart', function (e) {
el.addEventListener('draggableStart', (e) => {
e.detail.image.classList.add('_noflip');
el.appendChild(e.detail.image);
});
el.addEventListener('draggableCancel', function (e) {
el.addEventListener('draggableCancel', (e) => {
e.detail.image.classList.remove('_noflip');
update();
});
el.addEventListener('draggableDrop', function (e) {
el.addEventListener('draggableDrop', (e) => {
e.detail.image.classList.remove('_noflip');
});
el.addEventListener('sortableUpdate', function (e) {
el.addEventListener('sortableUpdate', (e) => {
e.detail.placeholder.classList.add('_noflip');
});
// dispatch "focusOther" .use-focus-other inputs if they are not active
// dispatch "focusOther" on .use-focus-other inputs if they are not active
// ensures only one edit input is active
el.addEventListener('focusin', function (e) {
el.addEventListener('focusin', (e) => {
if (!e.target.classList.contains('use-focus-other')) return;
document.querySelectorAll('.use-focus-other').forEach(function (el) {
document.querySelectorAll('.use-focus-other').forEach((el) => {
if (el === e.target) return;
el.dispatchEvent(new CustomEvent('focusOther'));
});
@@ -85,9 +92,7 @@ VT.TodoApp = function (el) {
// listen to the TodoStore's data
// this is the main update
// everything else is related to drag & drop or FLIP animations
el.addEventListener('todoData', function (e) {
update(e.detail);
});
el.addEventListener('todoData', (e) => update(e.detail));
// dispatch "flip" after HTML changes from these events
// this plays the FLIP animations
@@ -96,40 +101,27 @@ VT.TodoApp = function (el) {
el.addEventListener('draggableCancel', flip);
el.addEventListener('draggableDrop', flip);
el.todoStore.load();
el.dispatchEvent(new CustomEvent('loadStore'));
function update(next) {
Object.assign(state, next);
el.querySelector('.todo-frame.-days').todoFrameDays.update({
items: state.items,
at: state.at,
});
el.querySelectorAll('.todo-frame').forEach((el) =>
el.dispatchEvent(new CustomEvent('todoData', { detail: state }))
);
el.querySelector('.todo-frame.-custom').todoFrameCustom.update({
lists: state.customLists,
items: state.items,
at: state.customAt,
});
el.querySelectorAll('.app-collapsible').forEach(function (el) {
el.appCollapsible.update();
});
el.querySelectorAll('.app-collapsible').forEach((el) =>
el.dispatchEvent(new CustomEvent('collapse'))
);
}
function beforeFlip() {
el.dispatchEvent(
new CustomEvent('beforeFlip', {
bubbles: true,
})
);
function beforeFlip(e) {
if (e.type === 'todoData' && e.target !== el) return;
el.dispatchEvent(new CustomEvent('beforeFlip'));
}
function flip() {
el.dispatchEvent(
new CustomEvent('flip', {
bubbles: true,
})
);
el.dispatchEvent(new CustomEvent('flip'));
}
};
}

View File

@@ -1,58 +1,59 @@
/* global VT */
window.VT = window.VT || {};
import { AppDraggable } from './AppDraggable.js';
import { AppIcon } from './AppIcon.js';
import { TodoList } from './TodoList.js';
VT.TodoCustomList = function (el) {
var state = {
export function TodoCustomList(el) {
const state = {
list: null,
editing: false,
};
var startEditing = false;
var saveOnBlur = true;
let startEditing = false;
let saveOnBlur = true;
el.innerHTML = [
'<div class="header">',
' <h3 class="title"></h3>',
' <p class="form">',
' <input type="text" class="input use-focus-other">',
' <button class="app-button delete"><i class="app-icon" data-id="trashcan-16"></i></button>',
' </p>',
'</div>',
'<div class="todo-list"></div>',
].join('\n');
el.innerHTML = `
<div class="header">
<h3 class="title"></h3>
<p class="form">
<input type="text" class="input use-focus-other">
<button class="app-button delete"><i class="app-icon" data-id="trashcan-16"></i></button>
</p>
</div>
<div class="todo-list"></div>
`;
var titleEl = el.querySelector('.title');
var inputEl = el.querySelector('.input');
var deleteEl = el.querySelector('.delete');
const titleEl = el.querySelector('.title');
const inputEl = el.querySelector('.input');
const deleteEl = el.querySelector('.delete');
VT.AppDraggable(titleEl, {
AppDraggable(titleEl, {
dropSelector: '.todo-frame.-custom .container',
});
VT.TodoList(el.querySelector('.todo-list'));
el.querySelectorAll('.app-icon').forEach(VT.AppIcon);
el.querySelectorAll('.app-icon').forEach(AppIcon);
TodoList(el.querySelector('.todo-list'));
titleEl.addEventListener('click', function () {
titleEl.addEventListener('click', () => {
startEditing = true;
update({ editing: true });
});
deleteEl.addEventListener('touchstart', function () {
deleteEl.addEventListener('touchstart', () => {
saveOnBlur = false;
});
deleteEl.addEventListener('mousedown', function () {
deleteEl.addEventListener('mousedown', () => {
saveOnBlur = false;
});
inputEl.addEventListener('blur', function () {
inputEl.addEventListener('blur', () => {
if (saveOnBlur) save();
saveOnBlur = true;
});
inputEl.addEventListener('focusOther', function () {
inputEl.addEventListener('focusOther', () => {
if (state.editing) save();
});
inputEl.addEventListener('keyup', function (e) {
inputEl.addEventListener('keyup', (e) => {
switch (e.keyCode) {
case 13: // enter
save();
@@ -63,7 +64,7 @@ VT.TodoCustomList = function (el) {
}
});
deleteEl.addEventListener('click', function () {
deleteEl.addEventListener('click', () => {
if (state.list.items.length > 0) {
if (
!confirm(
@@ -82,7 +83,7 @@ VT.TodoCustomList = function (el) {
);
});
el.addEventListener('draggableStart', function (e) {
el.addEventListener('draggableStart', (e) => {
if (e.target !== titleEl) return;
e.detail.data.list = state.list;
@@ -92,25 +93,23 @@ VT.TodoCustomList = function (el) {
e.detail.setImage(el);
// override for horizontal dragging only
e.detail.image.addEventListener('draggableDrag', function (e) {
var x = e.detail.clientX - e.detail.imageX;
var y = e.detail.originY - e.detail.imageY;
e.detail.image.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
e.detail.image.addEventListener('draggableDrag', (e) => {
const x = e.detail.clientX - e.detail.imageX;
const y = e.detail.originY - e.detail.imageY;
e.detail.image.style.transform = `translate(${x}px, ${y}px)`;
});
});
el.addEventListener('addItem', function (e) {
el.addEventListener('addItem', (e) => {
e.detail.listId = state.list.id;
});
el.addEventListener('moveItem', function (e) {
el.addEventListener('moveItem', (e) => {
e.detail.listId = state.list.id;
e.detail.index = e.detail.index || 0;
e.detail.index = e.detail.index ?? 0;
});
el.todoCustomList = {
update: update,
};
el.addEventListener('todoCustomList', (e) => update({ list: e.detail }));
function save() {
el.dispatchEvent(
@@ -132,9 +131,13 @@ VT.TodoCustomList = function (el) {
titleEl.innerText = state.list.title || '...';
el.querySelector('.todo-list').todoList.update({ items: state.list.items });
el.querySelector('.todo-list > .todo-item-input').dataset.key =
'todo-item-input' + state.list.id;
el.querySelector('.todo-list').dispatchEvent(
new CustomEvent('todoItems', { detail: state.list.items })
);
el.querySelector(
'.todo-list > .todo-item-input'
).dataset.key = `todo-item-input${state.list.id}`;
el.classList.toggle('-editing', state.editing);
@@ -145,4 +148,4 @@ VT.TodoCustomList = function (el) {
startEditing = false;
}
}
};
}

View File

@@ -1,51 +1,49 @@
/* global VT */
window.VT = window.VT || {};
import { TodoList } from './TodoList.js';
import { formatDate, formatDayOfWeek } from './util.js';
VT.TodoDay = function (el) {
var state = {
export function TodoDay(el) {
const state = {
dateId: el.dataset.key,
items: [],
};
el.innerHTML = [
'<div class="header">',
' <h3 class="dayofweek"></h3>',
' <h6 class="date"></h6>',
'</div>',
'<div class="todo-list"></div>',
].join('\n');
el.innerHTML = `
<div class="header">
<h3 class="dayofweek"></h3>
<h6 class="date"></h6>
</div>
<div class="todo-list"></div>
`;
VT.TodoList(el.querySelector('.todo-list'));
TodoList(el.querySelector('.todo-list'));
el.addEventListener('addItem', function (e) {
el.addEventListener('addItem', (e) => {
e.detail.listId = state.dateId;
});
el.addEventListener('moveItem', function (e) {
el.addEventListener('moveItem', (e) => {
e.detail.listId = state.dateId;
e.detail.index = e.detail.index || 0;
e.detail.index = e.detail.index ?? 0;
});
el.todoDay = {
update: update,
};
el.addEventListener('todoDay', (e) => update(e.detail));
function update(next) {
Object.assign(state, next);
var date = new Date(state.dateId);
var today = new Date();
const date = new Date(state.dateId);
const today = new Date();
today.setHours(0, 0, 0, 0);
var tomorrow = new Date(today);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
el.classList.toggle('-past', date < today);
el.classList.toggle('-today', date >= today && date < tomorrow);
el.querySelector('.header > .dayofweek').innerText = VT.formatDayOfWeek(
date
el.querySelector('.header > .dayofweek').innerText = formatDayOfWeek(date);
el.querySelector('.header > .date').innerText = formatDate(date);
el.querySelector('.todo-list').dispatchEvent(
new CustomEvent('todoItems', { detail: state.items })
);
el.querySelector('.header > .date').innerText = VT.formatDate(date);
el.querySelector('.todo-list').todoList.update({ items: state.items });
}
};
}

View File

@@ -1,51 +1,52 @@
/* global VT */
window.VT = window.VT || {};
import { AppIcon } from './AppIcon.js';
import { AppSortable } from './AppSortable.js';
import { TodoCustomList } from './TodoCustomList.js';
VT.TodoFrameCustom = function (el) {
var state = {
lists: [],
export function TodoFrameCustom(el) {
const state = {
customLists: [],
items: [],
at: 0,
customAt: 0,
show: true,
};
el.innerHTML = [
'<div class="leftcontrols">',
' <p><button class="app-button -circle -xl back"><i class="app-icon" data-id="chevron-left-24"></i></button></p>',
'</div>',
'<div class="container"></div>',
'<div class="rightcontrols">',
' <p><button class="app-button -circle -xl forward"><i class="app-icon" data-id="chevron-right-24"></i></button></p>',
' <p><button class="app-button -circle -xl add"><i class="app-icon" data-id="plus-circle-24"></i></button></p>',
'</div>',
].join('\n');
el.innerHTML = `
<div class="leftcontrols">
<p><button class="app-button -circle -xl back"><i class="app-icon" data-id="chevron-left-24"></i></button></p>
</div>
<div class="container"></div>
<div class="rightcontrols">
<p><button class="app-button -circle -xl forward"><i class="app-icon" data-id="chevron-right-24"></i></button></p>
<p><button class="app-button -circle -xl add"><i class="app-icon" data-id="plus-circle-24"></i></button></p>
</div>
`;
VT.AppSortable(el.querySelector('.container'), { direction: 'horizontal' });
AppSortable(el.querySelector('.container'), { direction: 'horizontal' });
setTimeout(function () {
setTimeout(() => {
el.classList.add('-animated');
}, 200);
el.querySelectorAll('.app-icon').forEach(VT.AppIcon);
el.querySelectorAll('.app-icon').forEach(AppIcon);
el.querySelector('.back').addEventListener('click', function () {
el.querySelector('.back').addEventListener('click', () => {
el.dispatchEvent(
new CustomEvent('customSeek', { detail: -1, bubbles: true })
);
});
el.querySelector('.forward').addEventListener('click', function () {
el.querySelector('.forward').addEventListener('click', () => {
el.dispatchEvent(
new CustomEvent('customSeek', { detail: 1, bubbles: true })
);
});
el.querySelector('.add').addEventListener('click', function () {
el.querySelector('.add').addEventListener('click', () => {
el.dispatchEvent(new CustomEvent('addList', { detail: {}, bubbles: true }));
// TODO seek if not at end
});
el.addEventListener('sortableDrop', function (e) {
el.addEventListener('sortableDrop', (e) => {
if (!e.detail.data.list) return;
el.dispatchEvent(
@@ -59,30 +60,26 @@ VT.TodoFrameCustom = function (el) {
);
});
el.addEventListener('draggableOver', function (e) {
el.addEventListener('draggableOver', (e) => {
if (!e.detail.data.list) return;
updatePositions();
});
el.todoFrameCustom = {
update: update,
};
el.addEventListener('todoData', (e) => update(e.detail));
function update(next) {
Object.assign(state, next);
var lists = getLists();
var container = el.querySelector('.container');
var obsolete = new Set(container.children);
var childrenByKey = new Map();
const lists = getLists();
const container = el.querySelector('.container');
const obsolete = new Set(container.children);
const childrenByKey = new Map();
obsolete.forEach(function (child) {
childrenByKey.set(child.dataset.key, child);
});
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
var children = lists.map(function (list) {
var child = childrenByKey.get(list.id);
const children = lists.map((list) => {
let child = childrenByKey.get(list.id);
if (child) {
obsolete.delete(child);
@@ -90,19 +87,17 @@ VT.TodoFrameCustom = function (el) {
child = document.createElement('div');
child.className = 'card todo-custom-list';
child.dataset.key = list.id;
VT.TodoCustomList(child);
TodoCustomList(child);
}
child.todoCustomList.update({ list: list });
child.dispatchEvent(new CustomEvent('todoCustomList', { detail: list }));
return child;
});
obsolete.forEach(function (child) {
container.removeChild(child);
});
obsolete.forEach((child) => container.removeChild(child));
children.forEach(function (child, index) {
children.forEach((child, index) => {
if (child !== container.children[index]) {
container.insertBefore(child, container.children[index]);
}
@@ -113,54 +108,40 @@ VT.TodoFrameCustom = function (el) {
}
function updatePositions() {
el.querySelectorAll('.container > *').forEach(function (child, index) {
child.style.transform = 'translateX(' + (index - state.at) * 100 + '%)';
el.querySelectorAll('.container > *').forEach((child, index) => {
child.style.transform = `translateX(${(index - state.customAt) * 100}%)`;
});
}
function updateHeight() {
var height = 280;
var container = el.querySelector('.container');
let height = 280;
const container = el.querySelector('.container');
var i, l;
for (i = 0, l = container.children.length; i < l; ++i) {
for (let i = 0, l = container.children.length; i < l; ++i) {
height = Math.max(container.children[i].offsetHeight, height);
}
el.style.height = height + 50 + 'px';
el.style.height = `${height + 50}px`;
for (i = 0, l = container.children.length; i < l; ++i) {
container.children[i].style.height = height + 'px';
for (let i = 0, l = container.children.length; i < l; ++i) {
container.children[i].style.height = `${height}px`;
}
}
function getLists() {
var lists = state.lists.map(function (list) {
return {
return state.customLists
.map((list) => ({
id: list.id,
index: list.index,
title: list.title,
items: getItemsForList(list.id),
};
});
lists.sort(function (a, b) {
return a.index - b.index;
});
return lists;
}))
.sort((a, b) => a.index - b.index);
}
function getItemsForList(listId) {
var items = state.items.filter(function (item) {
return item.listId === listId;
});
items.sort(function (a, b) {
return a.index - b.index;
});
return items;
return state.items
.filter((item) => item.listId === listId)
.sort((a, b) => a.index - b.index);
}
};
}

View File

@@ -1,71 +1,66 @@
/* global VT */
window.VT = window.VT || {};
import { AppIcon } from './AppIcon.js';
import { TodoDay } from './TodoDay.js';
import { formatDateId } from './util.js';
VT.TodoFrameDays = function (el) {
var RANGE = 14;
var state = {
export function TodoFrameDays(el) {
const RANGE = 14;
const state = {
items: [],
at: VT.formatDateId(new Date()),
at: formatDateId(new Date()),
};
el.innerHTML = [
'<nav class="leftcontrols">',
' <p><button class="app-button -circle -xl backward"><i class="app-icon" data-id="chevron-left-24"></i></button></p>',
' <p><button class="app-button fastbackward"><i class="app-icon -double" data-id="chevron-left-16"></i></i></button></p>',
' <p><button class="app-button home"><i class="app-icon" data-id="home-16"></i></button></p>',
'</nav>',
'<div class="container"></div>',
'<nav class="rightcontrols">',
' <p><button class="app-button -circle -xl forward"><i class="app-icon" data-id="chevron-right-24"></i></button></p>',
' <p><button class="app-button fastforward"><i class="app-icon -double" data-id="chevron-right-16"></i></button></p>',
'</nav>',
].join('\n');
el.innerHTML = `
<nav class="leftcontrols">
<p><button class="app-button -circle -xl backward"><i class="app-icon" data-id="chevron-left-24"></i></button></p>
<p><button class="app-button fastbackward"><i class="app-icon -double" data-id="chevron-left-16"></i></i></button></p>
<p><button class="app-button home"><i class="app-icon" data-id="home-16"></i></button></p>
</nav>
<div class="container"></div>
<nav class="rightcontrols">
<p><button class="app-button -circle -xl forward"><i class="app-icon" data-id="chevron-right-24"></i></button></p>
<p><button class="app-button fastforward"><i class="app-icon -double" data-id="chevron-right-16"></i></button></p>
</nav>
`;
setTimeout(function () {
el.classList.add('-animated');
}, 200);
setTimeout(() => el.classList.add('-animated'), 200);
el.querySelectorAll('.app-icon').forEach(VT.AppIcon);
el.querySelectorAll('.app-icon').forEach(AppIcon);
el.querySelector('.backward').addEventListener('click', function () {
el.dispatchEvent(new CustomEvent('seek', { detail: -1, bubbles: true }));
});
el.querySelector('.backward').addEventListener('click', () =>
el.dispatchEvent(new CustomEvent('seek', { detail: -1, bubbles: true }))
);
el.querySelector('.forward').addEventListener('click', function () {
el.dispatchEvent(new CustomEvent('seek', { detail: 1, bubbles: true }));
});
el.querySelector('.forward').addEventListener('click', () =>
el.dispatchEvent(new CustomEvent('seek', { detail: 1, bubbles: true }))
);
el.querySelector('.fastbackward').addEventListener('click', function () {
el.dispatchEvent(new CustomEvent('seek', { detail: -5, bubbles: true }));
});
el.querySelector('.fastbackward').addEventListener('click', () =>
el.dispatchEvent(new CustomEvent('seek', { detail: -5, bubbles: true }))
);
el.querySelector('.fastforward').addEventListener('click', function () {
el.dispatchEvent(new CustomEvent('seek', { detail: 5, bubbles: true }));
});
el.querySelector('.fastforward').addEventListener('click', () =>
el.dispatchEvent(new CustomEvent('seek', { detail: 5, bubbles: true }))
);
el.querySelector('.home').addEventListener('click', function () {
el.dispatchEvent(new CustomEvent('seekHome', { bubbles: true }));
});
el.querySelector('.home').addEventListener('click', () =>
el.dispatchEvent(new CustomEvent('seekHome', { bubbles: true }))
);
el.todoFrameDays = {
update: update,
};
el.addEventListener('todoData', (e) => update(e.detail));
function update(next) {
Object.assign(state, next);
var days = getDays();
const days = getDays();
var container = el.querySelector('.container');
var obsolete = new Set(container.children);
var childrenByKey = new Map();
const container = el.querySelector('.container');
const obsolete = new Set(container.children);
const childrenByKey = new Map();
obsolete.forEach(function (child) {
childrenByKey.set(child.dataset.key, child);
});
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
var children = days.map(function (day) {
var child = childrenByKey.get(day.id);
const children = days.map((day) => {
let child = childrenByKey.get(day.id);
if (child) {
obsolete.delete(child);
@@ -73,20 +68,18 @@ VT.TodoFrameDays = function (el) {
child = document.createElement('div');
child.className = 'card todo-day';
child.dataset.key = day.id;
VT.TodoDay(child);
TodoDay(child);
}
child.todoDay.update(day);
child.style.transform = 'translateX(' + day.position * 100 + '%)';
child.dispatchEvent(new CustomEvent('todoDay', { detail: day }));
child.style.transform = `translateX(${day.position * 100}%)`;
return child;
});
obsolete.forEach(function (child) {
container.removeChild(child);
});
obsolete.forEach((child) => container.removeChild(child));
children.forEach(function (child, index) {
children.forEach((child, index) => {
if (child !== container.children[index]) {
container.insertBefore(child, container.children[index]);
}
@@ -96,26 +89,26 @@ VT.TodoFrameDays = function (el) {
}
function updateHeight() {
var height = 280;
var container = el.querySelector('.container');
let height = 280;
const container = el.querySelector('.container');
for (var i = 0, l = container.children.length; i < l; ++i) {
for (let i = 0, l = container.children.length; i < l; ++i) {
height = Math.max(container.children[i].offsetHeight, height);
}
el.style.height = height + 50 + 'px';
el.style.height = `${height + 50}px`;
}
function getDays() {
var days = [];
const days = [];
for (var i = 0; i < 2 * RANGE; ++i) {
var t = new Date(state.at);
for (let i = 0; i < 2 * RANGE; ++i) {
const t = new Date(state.at);
t.setDate(t.getDate() - RANGE + i);
var id = VT.formatDateId(t);
const id = formatDateId(t);
days.push({
id: id,
id,
items: getItemsForDay(id),
position: -RANGE + i,
});
@@ -125,14 +118,8 @@ VT.TodoFrameDays = function (el) {
}
function getItemsForDay(dateId) {
var items = state.items.filter(function (item) {
return item.listId === dateId;
});
items.sort(function (a, b) {
return a.index - b.index;
});
return items;
return state.items
.filter((item) => item.listId === dateId)
.sort((a, b) => a.index - b.index);
}
};
}

View File

@@ -1,45 +1,46 @@
/* global VT */
window.VT = window.VT || {};
import { AppDraggable } from './AppDraggable.js';
import { AppIcon } from './AppIcon.js';
VT.TodoItem = function (el) {
var state = {
export function TodoItem(el) {
const state = {
item: null,
editing: false,
};
var startEditing = false;
var saveOnBlur = true;
el.innerHTML = [
'<div class="checkbox">',
' <input type="checkbox">',
'</div>',
'<p class="label"></p>',
'<p class="form">',
' <input type="text" class="input use-focus-other">',
' <button class="app-button save"><i class="app-icon" data-id="check-16"></i></button>',
'</p>',
].join('\n');
let startEditing = false;
let saveOnBlur = true;
var checkboxEl = el.querySelector('.checkbox');
var labelEl = el.querySelector('.label');
var inputEl = el.querySelector('.input');
var saveEl = el.querySelector('.save');
el.innerHTML = `
<div class="checkbox">
<input type="checkbox">
</div>
<p class="label"></p>
<p class="form">
<input type="text" class="input use-focus-other">
<button class="app-button save"><i class="app-icon" data-id="check-16"></i></button>
</p>
`;
VT.AppDraggable(el, {
const checkboxEl = el.querySelector('.checkbox');
const labelEl = el.querySelector('.label');
const inputEl = el.querySelector('.input');
const saveEl = el.querySelector('.save');
AppDraggable(el, {
dropSelector: '.todo-list > .items',
});
el.querySelectorAll('.app-icon').forEach(VT.AppIcon);
el.querySelectorAll('.app-icon').forEach(AppIcon);
checkboxEl.addEventListener('touchstart', function () {
checkboxEl.addEventListener('touchstart', () => {
saveOnBlur = false;
});
checkboxEl.addEventListener('mousedown', function () {
checkboxEl.addEventListener('mousedown', () => {
saveOnBlur = false;
});
checkboxEl.addEventListener('click', function () {
checkboxEl.addEventListener('click', () => {
if (state.editing) save();
el.dispatchEvent(
@@ -53,12 +54,12 @@ VT.TodoItem = function (el) {
);
});
labelEl.addEventListener('click', function () {
labelEl.addEventListener('click', () => {
startEditing = true;
update({ editing: true });
});
inputEl.addEventListener('keyup', function (e) {
inputEl.addEventListener('keyup', (e) => {
switch (e.keyCode) {
case 13: // enter
save();
@@ -69,39 +70,37 @@ VT.TodoItem = function (el) {
}
});
inputEl.addEventListener('blur', function () {
inputEl.addEventListener('blur', () => {
if (saveOnBlur) save();
saveOnBlur = true;
});
inputEl.addEventListener('focusOther', function () {
inputEl.addEventListener('focusOther', () => {
if (state.editing) save();
});
saveEl.addEventListener('mousedown', function () {
saveEl.addEventListener('mousedown', () => {
saveOnBlur = false;
});
saveEl.addEventListener('click', save);
el.addEventListener('draggableStart', function (e) {
el.addEventListener('draggableStart', (e) => {
e.detail.data.item = state.item;
e.detail.data.key = state.item.id;
});
el.todoItem = {
update: update,
};
el.addEventListener('todoItem', (e) => update({ item: e.detail }));
function save() {
var label = inputEl.value.trim();
const label = inputEl.value.trim();
if (label === '') {
// deferred deletion prevents a bug at reconciliation in TodoList:
// Failed to execute 'removeChild' on 'Node': The node to be removed is
// no longer a child of this node. Perhaps it was moved in a 'blur'
// event handler?
requestAnimationFrame(function () {
requestAnimationFrame(() => {
el.dispatchEvent(
new CustomEvent('deleteItem', {
detail: state.item,
@@ -117,7 +116,7 @@ VT.TodoItem = function (el) {
new CustomEvent('saveItem', {
detail: {
item: state.item,
label: label,
label,
},
bubbles: true,
})
@@ -149,4 +148,4 @@ VT.TodoItem = function (el) {
startEditing = false;
}
}
};
}

View File

@@ -1,20 +1,19 @@
/* global VT */
window.VT = window.VT || {};
import { AppIcon } from './AppIcon.js';
VT.TodoItemInput = function (el) {
var saveOnBlur = true;
export function TodoItemInput(el) {
let saveOnBlur = true;
el.innerHTML = [
'<input type="text" class="input use-focus-other">',
'<button class="app-button save"><i class="app-icon" data-id="plus-24"></i></button>',
].join('\n');
el.innerHTML = `
<input type="text" class="input use-focus-other">
<button class="app-button save"><i class="app-icon" data-id="plus-24"></i></button>
`;
var inputEl = el.querySelector('.input');
var saveEl = el.querySelector('.save');
const inputEl = el.querySelector('.input');
const saveEl = el.querySelector('.save');
el.querySelectorAll('.app-icon').forEach(VT.AppIcon);
el.querySelectorAll('.app-icon').forEach(AppIcon);
inputEl.addEventListener('keyup', function (e) {
inputEl.addEventListener('keyup', (e) => {
switch (e.keyCode) {
case 13: // enter
save();
@@ -25,24 +24,24 @@ VT.TodoItemInput = function (el) {
}
});
inputEl.addEventListener('blur', function () {
inputEl.addEventListener('blur', () => {
if (saveOnBlur) save();
saveOnBlur = true;
});
inputEl.addEventListener('focusOther', save);
saveEl.addEventListener('mousedown', function () {
saveEl.addEventListener('mousedown', () => {
saveOnBlur = false;
});
saveEl.addEventListener('click', function () {
saveEl.addEventListener('click', () => {
save();
inputEl.focus();
});
function save() {
var label = inputEl.value.trim();
const label = inputEl.value.trim();
if (label === '') return;
@@ -50,7 +49,7 @@ VT.TodoItemInput = function (el) {
el.dispatchEvent(
new CustomEvent('addItem', {
detail: { label: label },
detail: { label },
bubbles: true,
})
);
@@ -60,4 +59,4 @@ VT.TodoItemInput = function (el) {
inputEl.value = '';
inputEl.blur();
}
};
}

View File

@@ -1,20 +1,21 @@
/* global VT */
window.VT = window.VT || {};
import { AppSortable } from './AppSortable.js';
import { TodoItem } from './TodoItem.js';
import { TodoItemInput } from './TodoItemInput.js';
VT.TodoList = function (el) {
var state = {
export function TodoList(el) {
const state = {
items: [],
};
el.innerHTML = [
'<div class="items"></div>',
'<div class="todo-item-input"></div>',
].join('\n');
el.innerHTML = `
<div class="items"></div>
<div class="todo-item-input"></div>
`;
VT.AppSortable(el.querySelector('.items'), {});
VT.TodoItemInput(el.querySelector('.todo-item-input'));
AppSortable(el.querySelector('.items'), {});
TodoItemInput(el.querySelector('.todo-item-input'));
el.addEventListener('sortableDrop', function (e) {
el.addEventListener('sortableDrop', (e) =>
el.dispatchEvent(
new CustomEvent('moveItem', {
detail: {
@@ -23,22 +24,22 @@ VT.TodoList = function (el) {
},
bubbles: true,
})
);
});
)
);
el.addEventListener('todoItems', (e) => update({ items: e.detail }));
function update(next) {
Object.assign(state, next);
var container = el.querySelector('.items');
var obsolete = new Set(container.children);
var childrenByKey = new Map();
const container = el.querySelector('.items');
const obsolete = new Set(container.children);
const childrenByKey = new Map();
obsolete.forEach(function (child) {
childrenByKey.set(child.dataset.key, child);
});
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
var children = state.items.map(function (item) {
var child = childrenByKey.get(item.id);
const children = state.items.map((item) => {
let child = childrenByKey.get(item.id);
if (child) {
obsolete.delete(child);
@@ -46,26 +47,20 @@ VT.TodoList = function (el) {
child = document.createElement('div');
child.classList.add('todo-item');
child.dataset.key = item.id;
VT.TodoItem(child);
TodoItem(child);
}
child.todoItem.update({ item: item });
child.dispatchEvent(new CustomEvent('todoItem', { detail: item }));
return child;
});
obsolete.forEach(function (child) {
container.removeChild(child);
});
obsolete.forEach((child) => container.removeChild(child));
children.forEach(function (child, index) {
children.forEach((child, index) => {
if (child !== container.children[index]) {
container.insertBefore(child, container.children[index]);
}
});
}
el.todoList = {
update: update,
};
};
}

View File

@@ -1,28 +1,30 @@
/* global VT */
window.VT = window.VT || {};
import { formatDateId, uuid } from './util.js';
VT.TodoStore = function (el) {
var state = {
export function TodoStore(el) {
const state = {
items: [],
customLists: [],
at: VT.formatDateId(new Date()),
at: formatDateId(new Date()),
customAt: 0,
};
var storeTimeout;
el.addEventListener('addItem', function (e) {
var index = 0;
let storeTimeout;
state.items.forEach(function (item) {
el.addEventListener('loadStore', load);
el.addEventListener('addItem', (e) => {
let index = 0;
for (const item of state.items) {
if (item.listId === e.detail.listId) {
index = Math.max(index, item.index + 1);
}
});
}
state.items.push({
id: VT.uuid(),
id: uuid(),
listId: e.detail.listId,
index: index,
index,
label: e.detail.label,
done: false,
});
@@ -30,71 +32,61 @@ VT.TodoStore = function (el) {
dispatch({ items: state.items });
});
el.addEventListener('checkItem', function (e) {
el.addEventListener('checkItem', (e) => {
if (e.detail.item.done === e.detail.done) return;
e.detail.item.done = e.detail.done;
dispatch({ items: state.items });
});
el.addEventListener('saveItem', function (e) {
el.addEventListener('saveItem', (e) => {
if (e.detail.item.label === e.detail.label) return;
e.detail.item.label = e.detail.label;
dispatch({ items: state.items });
});
el.addEventListener('moveItem', function (e) {
var movedItem = state.items.find(function (item) {
return item.id === e.detail.item.id;
});
el.addEventListener('moveItem', (e) => {
const movedItem = state.items.find((item) => item.id === e.detail.item.id);
var listItems = state.items.filter(function (item) {
return item.listId === e.detail.listId && item !== movedItem;
});
const listItems = state.items.filter(
(item) => item.listId === e.detail.listId && item !== movedItem
);
listItems.sort(function (a, b) {
return a.index - b.index;
});
listItems.sort((a, b) => a.index - b.index);
movedItem.listId = e.detail.listId;
listItems.splice(e.detail.index, 0, movedItem);
listItems.forEach(function (item, index) {
listItems.forEach((item, index) => {
item.index = index;
});
dispatch({ items: state.items });
});
el.addEventListener('deleteItem', function (e) {
dispatch({
items: state.items.filter(function (item) {
return item.id !== e.detail.id;
}),
});
el.addEventListener('deleteItem', (e) => {
dispatch({ items: state.items.filter((item) => item.id !== e.detail.id) });
});
el.addEventListener('addList', function (e) {
var index = 0;
el.addEventListener('addList', (e) => {
let index = 0;
state.customLists.forEach(function (customList) {
for (const customList of state.customLists) {
index = Math.max(index, customList.index + 1);
});
}
state.customLists.push({
id: VT.uuid(),
index: index,
id: uuid(),
index,
title: e.detail.title || '',
});
dispatch({ customLists: state.customLists });
});
el.addEventListener('saveList', function (e) {
var list = state.customLists.find(function (l) {
return l.id === e.detail.list.id;
});
el.addEventListener('saveList', (e) => {
const list = state.customLists.find((l) => l.id === e.detail.list.id);
if (list.title === e.detail.title) return;
@@ -103,49 +95,43 @@ VT.TodoStore = function (el) {
dispatch({ customLists: state.customLists });
});
el.addEventListener('moveList', function (e) {
var movedListIndex = state.customLists.findIndex(function (list) {
return list.id === e.detail.list.id;
});
var movedList = state.customLists[movedListIndex];
el.addEventListener('moveList', (e) => {
const movedListIndex = state.customLists.findIndex(
(list) => list.id === e.detail.list.id
);
const movedList = state.customLists[movedListIndex];
state.customLists.splice(movedListIndex, 1);
state.customLists.sort(function (a, b) {
return a.index - b.index;
});
state.customLists.sort((a, b) => a.index - b.index);
state.customLists.splice(e.detail.index, 0, movedList);
state.customLists.forEach(function (item, index) {
state.customLists.forEach((item, index) => {
item.index = index;
});
dispatch({ customLists: state.customLists });
});
el.addEventListener('deleteList', function (e) {
el.addEventListener('deleteList', (e) => {
dispatch({
customLists: state.customLists.filter(function (customList) {
return customList.id !== e.detail.id;
}),
customLists: state.customLists.filter(
(customList) => customList.id !== e.detail.id
),
});
});
el.addEventListener('seek', function (e) {
var t = new Date(state.at + ' 00:00:00');
el.addEventListener('seek', (e) => {
const t = new Date(`${state.at} 00:00:00`);
t.setDate(t.getDate() + e.detail);
dispatch({
at: VT.formatDateId(t),
});
dispatch({ at: formatDateId(t) });
});
el.addEventListener('seekHome', function () {
dispatch({
at: VT.formatDateId(new Date()),
});
});
el.addEventListener('seekHome', () =>
dispatch({ at: formatDateId(new Date()) })
);
el.addEventListener('customSeek', function (e) {
el.addEventListener('customSeek', (e) => {
dispatch({
customAt: Math.max(
0,
@@ -156,7 +142,7 @@ VT.TodoStore = function (el) {
function dispatch(next) {
Object.assign(state, next);
store();
save();
el.dispatchEvent(
new CustomEvent('todoData', {
@@ -175,24 +161,21 @@ VT.TodoStore = function (el) {
try {
dispatch(JSON.parse(localStorage.todo));
} catch (err) {
// eslint-disable-next-line no-console
console.warn(err);
}
}
function store() {
function save() {
clearTimeout(storeTimeout);
storeTimeout = setTimeout(function () {
storeTimeout = setTimeout(() => {
try {
localStorage.todo = JSON.stringify(state);
} catch (err) {
// eslint-disable-next-line no-console
console.warn(err);
}
}, 100);
}
el.todoStore = {
dispatch: dispatch,
load: load,
};
};
}

View File

@@ -1,54 +1,45 @@
/* global VT */
window.VT = window.VT || {};
VT.uuid = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
export function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
}
VT.formatDateId = function (date) {
var y = date.getFullYear();
var m = date.getMonth() + 1;
var d = date.getDate();
export function formatDateId(date) {
const y = date.getFullYear();
const m = date.getMonth() + 1;
const d = date.getDate();
const ys = y.toString().padStart(4, '0');
const ms = m.toString().padStart(2, '0');
const ds = d.toString().padStart(2, '0');
return (
y.toString().padStart(4, '0') +
'-' +
m.toString().padStart(2, '0') +
'-' +
d.toString().padStart(2, '0')
);
};
return `${ys}-${ms}-${ds}`;
}
VT.formatDate = function (date) {
return (
VT.formatMonth(date) +
' ' +
VT.formatDayOfMonth(date) +
' ' +
date.getFullYear().toString().padStart(4, '0')
);
};
export function formatDate(date) {
const m = formatMonth(date);
const d = formatDayOfMonth(date);
const y = date.getFullYear().toString().padStart(4, '0');
return `${m} ${d} ${y}`;
}
VT.formatDayOfMonth = function (date) {
var d = date.getDate();
var t = d % 10;
export function formatDayOfMonth(date) {
const d = date.getDate();
const t = d % 10;
return d === 11 || d === 12 || d === 13
? d + 'th'
? `${d}th`
: t === 1
? d + 'st'
? `${d}st`
: t === 2
? d + 'nd'
? `${d}nd`
: t === 3
? d + 'rd'
: d + 'th';
};
? `${d}rd`
: `${d}th`;
}
VT.DAY_NAMES = [
export const DAY_NAMES = [
'Sunday',
'Monday',
'Tuesday',
@@ -58,11 +49,11 @@ VT.DAY_NAMES = [
'Saturday',
];
VT.formatDayOfWeek = function (date) {
return VT.DAY_NAMES[date.getDay()];
};
export function formatDayOfWeek(date) {
return DAY_NAMES[date.getDay()];
}
VT.MONTH_NAMES = [
export const MONTH_NAMES = [
'January',
'February',
'March',
@@ -77,6 +68,6 @@ VT.MONTH_NAMES = [
'December',
];
VT.formatMonth = function (date) {
return VT.MONTH_NAMES[date.getMonth()];
};
export function formatMonth(date) {
return MONTH_NAMES[date.getMonth()];
}