1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-01-17 04:38:14 +01: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,24 +1,19 @@
module.exports = {
extends: ['eslint:recommended', 'plugin:compat/recommended'],
globals: {
Set: 'readonly',
Map: 'readonly',
},
extends: 'eslint:recommended',
env: {
browser: true,
es2020: true,
},
parserOptions: {
ecmaVersion: 5,
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {},
settings: {
polyfills: [
'Set',
'Map',
'fetch',
'Object.assign',
'requestAnimationFrame',
'performance.now',
],
rules: {
'object-shorthand': 'error',
'prefer-arrow-callback': 'error',
'arrow-body-style': ['error', 'as-needed'],
'no-var': 'error',
'prefer-template': 'error',
'no-console': 'error',
},
};

View File

@ -1,4 +1,4 @@
Copyright 2020-2021 Morris Brodersen <mb@morrisbrodersen.de>
Copyright 2020-2022 Morris Brodersen <mb@morrisbrodersen.de>
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

184
README.md
View File

@ -9,7 +9,7 @@ with a total transfer size of **44KB** (unminified).
More importantly, it's a case study showing that **vanilla web development** is
viable in terms of [maintainability](#521-the-good),
and worthwhile in terms of [user experience](#51-user-experience)
(**100%** faster loads and **90%** less bandwidth in this case).
(**50%** less time to load and **90%** less bandwidth in this case).
**There's no custom framework invented here.**
Instead, the case study was [designed](#22-rules) to discover
@ -119,7 +119,7 @@ I came up with a set of rules to follow throughout the process:
- No build steps.
- No general-purpose utility functions related to the DOM/UI (2).
(1) This is a moving target; I used ES5 for maximum support.
(1) This is a moving target; the current version is using ES2020.
(2) These usually end up becoming a custom micro-framework,
thereby questioning why you didn't use one of the
@ -162,16 +162,12 @@ plain HTML, CSS and JS files. The HTML and CSS mostly follows
which yields an intuitive, component-oriented structure.
The stylesheets are slightly verbose.
I missed [SCSS](https://sass-lang.com/) or [LESS](http://lesscss.org/) here
I missed [SCSS](https://sass-lang.com/) here
and I think one of these is a must-have for bigger projects.
Additionally, the global CSS namespace problem is unaddressed
(see e.g. [CSS Modules](https://github.com/css-modules/css-modules)).
ES6 modules are ruled out so all JavaScript lives under
a global namespace (`VT`). This works everywhere but has some downsides
e.g. cannot be statically analyzed and may miss code completion.
Polyfills are directly fetched from [polyfill.io](https://polyfill.io/).
I've set the `nomodule` script attribute so polyfills are only fetched
for older browsers.
All JavaScript files are ES modules (`import`/`export`).
Basic code quality (code style, linting) is guided by
[Prettier](https://prettier.io), [stylelint](https://stylelint.io) and
@ -206,11 +202,11 @@ per matching element. This yields a simple mental model and synergizes
with the DOM and styles:
```
.todo-list -> VT.TodoList
.todo-list -> TodoList
scripts/TodoList.js
styles/todo-list.css
.app-collapsible -> VT.AppCollapsible
.app-collapsible -> AppCollapsible
scripts/AppCollapsible.js
styles/app-collapsible.css
@ -229,40 +225,30 @@ provide behavior and rendering for the target element.
Here's a "Hello, World!" example of mount functions:
```js
// safely initialize namespace
window.MYAPP = window.MYAPP || {};
// define mount function
// loosely mapped to ".hello-world"
MYAPP.HelloWorld = function (el) {
export function HelloWorld(el) {
// define initial state
var state = {
const state = {
title: 'Hello, World!',
description: 'An example vanilla component',
counter: 0,
};
// set rigid base HTML
// no ES6 template literals :(
el.innerHTML = [
'<h1 class="title"></h1>',
'<p class="description"></p>',
'<div class="my-counter"></div>',
].join('\n');
el.innerHTML = `
<h1 class="title"></h1>
<p class="description"></p>
<div class="my-counter"></div>
`;
// mount sub-components
el.querySelectorAll('.my-counter').forEach(MYAPP.MyCounter);
// attach event listeners
el.addEventListener('modifyCounter', function (e) {
update({ counter: state.counter + e.detail });
});
// expose public interface
// use lower-case function name
el.helloWorld = {
update: update,
};
el.addEventListener('modifyCounter', (e) =>
update({ counter: state.counter + e.detail })
);
// initial update
update();
@ -278,29 +264,30 @@ MYAPP.HelloWorld = function (el) {
el.querySelector('.description').innerText = state.description;
// pass data to sub-scomponents
el.querySelector('.my-counter').myCounter.update({
value: state.counter,
});
el.querySelector('.my-counter').dispatchEvent(
new CustomEvent('updateMyCounter', {
detail: { value: state.counter },
})
);
}
}
};
// define another component
// loosely mapped to ".my-counter"
MYAPP.MyCounter = function (el) {
export function MyCounter(el) {
// define initial state
var state = {
const state = {
value: 0,
};
// set rigid base HTML
// no ES6 template literals :(
el.innerHTML = [
'<p>',
' <span class="value"></span>',
' <button class="increment">Increment</button>',
' <button class="decrement">Decrement</button>',
'</p>',
].join('\n');
el.innerHTML = `
<p>
<span class="value"></span>
<button class="increment">Increment</button>
<button class="decrement">Decrement</button>
</p>
`;
// attach event listeners
el.querySelector('.increment').addEventListener('click', function () {
@ -325,11 +312,7 @@ MYAPP.MyCounter = function (el) {
);
});
// expose public interface
// use lower-case function name
el.myCounter = {
update: update,
};
el.addEventListener('updateMyCounter', (e) => update(e.detail));
// define idempotent update function
function update(next) {
@ -337,11 +320,11 @@ MYAPP.MyCounter = function (el) {
el.querySelector('.value').innerText = state.value;
}
};
}
// mount HelloWorld component(s)
// any <div class="hello-world"></div> in the document will be mounted
document.querySelectorAll('.hello-world').forEach(MYAPP.HelloWorld);
document.querySelectorAll('.hello-world').forEach(HelloWorld);
```
This comes with quite some boilerplate but has useful properties,
@ -352,7 +335,7 @@ For example, a mount function does not have to set any base HTML,
and may instead only set event listeners to enable some behavior.
Also note that an element can be mounted with multiple mount functions.
For example, to-do items are mounted with `VT.TodoItem` and `VT.AppDraggable`.
For example, to-do items are mounted with `TodoItem` and `AppDraggable`.
Compared to React components, mount functions provide interesting flexibility as
components and behaviors can be implemented using the same idiom and combined
@ -366,16 +349,17 @@ Reference:
#### 3.2.2. Data Flow
I found it effective to implement one-way data flow similar to React's approach.
I found it effective to implement one-way data flow similar to React's approach,
however exclusively using custom DOM events.
- **Data flows downwards** from parent components to child components
through their public interfaces (usually `update` functions).
through custom DOM events.
- **Actions flow upwards** through custom DOM events (bubbling up),
usually resulting in some parent component state change which is in turn
propagated downwards through `update` functions.
propagated downwards through data events.
The data store is factored into a separate behavior (`VT.TodoStore`).
It only receives and dispatches events, and encapsulates all of the data logic.
The data store is factored into a separate behavior (`TodoStore`).
It only receives and dispatches events and encapsulates all of the data logic.
Listening to and dispatching events is slightly verbose with standard APIs and
certainly justifies introducing helpers.
@ -424,35 +408,34 @@ amount of dynamic components efficiently. Here's a commented example
from the implementation outlining the reconciliation algorithm:
```js
/* global VT */
window.VT = window.VT || {};
VT.TodoList = function (el) {
var state = {
export function TodoList(el) {
const state = {
items: [],
};
el.innerHTML = '<div class="items"></div>';
el.innerHTML = `<div class="items"></div>`;
el.addEventListener('updateTodoList', (e) => update(e.detail));
function update(next) {
Object.assign(state, next);
var container = el.querySelector('.items');
const container = el.querySelector('.items');
// mark current children for removal
var obsolete = new Set(container.children);
const obsolete = new Set(container.children);
// map current children by data-key
var childrenByKey = new Map();
const childrenByKey = new Map();
obsolete.forEach(function (child) {
childrenByKey.set(child.getAttribute('data-key'), child);
});
obsolete.forEach((child) =>
childrenByKey.set(child.getAttribute('data-key'), child)
);
// build new list of child elements from data
var children = state.items.map(function (item) {
const children = state.items.map((item) => {
// find existing child by data-key
var child = childrenByKey.get(item.id);
let child = childrenByKey.get(item.id);
if (child) {
// if child exists, keep it
@ -466,32 +449,28 @@ VT.TodoList = function (el) {
child.setAttribute('data-key', item.id);
// mount component
VT.TodoItem(child);
TodoItem(child);
}
// update child
child.todoItem.update({ item: item });
child.dispatchEvent(
new CustomEvent('updateTodoItem', { detail: { item: item } })
);
return child;
});
// remove obsolete children
obsolete.forEach(function (child) {
container.removeChild(child);
});
obsolete.forEach((child) => container.removeChild(child));
// (re-)insert new list of children
children.forEach(function (child, index) {
children.forEach((child, index) => {
if (child !== container.children[index]) {
container.insertBefore(child, container.children[index]);
}
});
}
el.todoList = {
update: update,
};
};
}
```
It's very verbose and has lots of opportunity to introduce bugs.
@ -570,7 +549,7 @@ In particular, dragging and dropping gives proper visual feedback
when elements are reordered.
_The latter was an improvement over the original application when I started
working on the case study some weeks ago. In the meantime, the TeuxDeux
working on the case study in 2019. In the meantime, the TeuxDeux
team released an update with a much better drag & drop experience. Great job!_
One notable missing feature is Markdown support. It would be insensible
@ -632,7 +611,7 @@ and some opinionated statements based on my experience in the industry.
- Low coupling
- The result is literally just a bunch of HTML, CSS, and JS files.
All source files (HTML, CSS and JS) combine to **under 2500 lines of code**,
All source files (HTML, CSS and JS) combine to **under 2400 lines of code**,
including comments and empty lines.
For comparison, prettifying the original TeuxDeux's minified JS application
@ -645,11 +624,6 @@ I suspect a fully equivalent clone to be well below 10000 LOC, though._
- Stylesheets are a bit verbose. SCSS would help here.
- Simple components require quite some boilerplate code.
- Writing HTML templates as an array of lines is ugly (and sub-optimal).
- ES5 is generally a lot more verbose than ES6.
- Especially arrow functions, template literals,
and async/await would make the code more readable.
- ES6 modules would eliminate the need for a global namespace.
- `el.querySelectorAll(':scope ...')` is somewhat default/expected and
would justify a helper.
- Listening to and dispatching events is slightly verbose.
@ -661,6 +635,10 @@ would reduce the comparably low code size (see above) even further.
#### 5.2.3. The Bad
- Class names share a global namespace.
- Event names share a global namespace.
- Especially problematic for events that bubble up.
- No code completion in HTML strings.
- The separation between base HTML and dynamic rendering is not ideal
when compared to JSX, for example.
- JSX/virtual DOM techniques provide much better development ergonomics.
@ -672,10 +650,10 @@ would reduce the comparably low code size (see above) even further.
e.g. watch elements of selector X (at all times) and ensure the desired
behaviors are mounted once on them.
- No type safety. I've always been a proponent of dynamic languages
but since TypeScripts' type system provides the best of both worlds,
but since TypeScript's type system provides the best of both worlds,
I cannot recommend using it enough.
- We're effectively locked out of using NPM dependencies that don't provide
browser builds as we cannot use CommonJS or ES6 modules.
browser-ready builds (ES modules or UMD).
- Most frameworks handle a lot of browser inconsistencies **for free** and
continuously monitor regressions with extensive test suites.
The cost of browser testing is surely a lot higher
@ -703,6 +681,17 @@ after all:
- Rendering is idempotent and complete (React's pure `render` function).
- One-way data flow (React)
An open question is if these patterns hold for library authors.
Although not considered during the study, some observations can be made:
- The JavaScript itself would be fine to share as ES modules.
- However, event naming needs great care, as dispatching (bubbling) events
from imported behaviors can trigger parent listeners in consumer code.
- Can be mitigated by providing options to prefix or map event names.
- CSS names share a global namespace and need to be managed as well.
- Could be mitigated by prefixing as well, however making the JavaScript
a bit more complex.
## 6. Conclusion
The result of this study is a working todo application with decent UI/UX and
@ -809,9 +798,16 @@ Thanks!
## 9. Changelog
### 05/2022
- Refactored for ES2020
- Refactored for event-driven communication exclusively
- Moved original ES5-based version of the study to [/es5](./es5)
- Added assessment regarding library authoring
### 01/2021
- Add [response section](#82-response)
- Added [response section](#82-response)
### 10/2020

2096
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,9 @@
"scripts": {
"format": "prettier --write *.js 'public/scripts/*.js' 'public/styles/**/*.css'",
"format-check": "prettier --check *.js 'public/scripts/**/*.js' 'public/styles/**/*.css'",
"lint": "eslint .",
"lint-styles": "stylelint public/styles/*"
"lint": "eslint public",
"lint-styles": "stylelint public/styles/*",
"serve": "http-server public"
},
"repository": {
"type": "git",
@ -14,8 +15,9 @@
},
"keywords": [
"vanilla",
"radio",
"somafm"
"html",
"javascript",
"css"
],
"author": "Morris Brodersen <mb@morrisbrodersen.de>",
"license": "ISC",
@ -24,11 +26,12 @@
},
"homepage": "https://github.com/morris/vanilla-todo#readme",
"devDependencies": {
"eslint": "^7.17.0",
"eslint-plugin-compat": "^3.9.0",
"prettier": "^2.2.1",
"stylelint": "^13.8.0",
"stylelint-config-standard": "^20.0.0",
"eslint": "^8.14.0",
"eslint-plugin-compat": "^4.0.2",
"http-server": "^14.1.0",
"prettier": "^2.6.2",
"stylelint": "^14.8.2",
"stylelint-config-standard": "^25.0.0",
"stylelint-rscss": "^0.4.0"
}
}

View File

@ -24,30 +24,10 @@
<body>
<div class="todo-app"></div>
<script
nomodule
crossorigin
src="https://polyfill.io/v3/polyfill.min.js?features=Set%2CMap%2CObject.assign%2Cfetch%2CrequestAnimationFrame%2CNodeList.prototype.forEach%2CElement.prototype.classList%2Cperformance.now%2CNode.prototype.contains%2CElement.prototype.dataset"
></script>
<script src="scripts/AppCollapsible.js"></script>
<script src="scripts/AppDraggable.js"></script>
<script src="scripts/AppFlip.js"></script>
<script src="scripts/AppFps.js"></script>
<script src="scripts/AppIcon.js"></script>
<script src="scripts/AppSortable.js"></script>
<script src="scripts/TodoApp.js"></script>
<script src="scripts/TodoCustomList.js"></script>
<script src="scripts/TodoDay.js"></script>
<script src="scripts/TodoFrameCustom.js"></script>
<script src="scripts/TodoFrameDays.js"></script>
<script src="scripts/TodoItemInput.js"></script>
<script src="scripts/TodoItem.js"></script>
<script src="scripts/TodoList.js"></script>
<script src="scripts/TodoStore.js"></script>
<script src="scripts/util.js"></script>
<script type="module">
import { TodoApp } from './scripts/TodoApp.js';
<script>
VT.TodoApp(document.querySelector('.todo-app'));
TodoApp(document.querySelector('.todo-app'));
</script>
</body>
</html>

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()];
}

View File

@ -6,7 +6,7 @@
background: #eee;
}
.app-collapsible > .bar > .app-button:active {
.app-collapsible > .bar > .toggle:active {
background: #fff;
}

View File

@ -9,7 +9,7 @@
margin: 0;
}
.app-header > .app-fps {
.app-header > .fps {
position: absolute;
top: 10px;
right: 20px;

View File

@ -8,7 +8,7 @@
width: 1em;
height: 1em;
vertical-align: bottom;
fill: currentColor;
fill: currentcolor;
transition: transform 0.1s ease-out;
}

View File

@ -52,8 +52,8 @@
}
.todo-custom-list.-dragging {
box-shadow: 10px 0 12px -14px rgba(0, 0, 0, 0.3),
-10px 0 12px -14px rgba(0, 0, 0, 0.3);
box-shadow: 10px 0 12px -14px rgba(0, 0, 0, 30%),
-10px 0 12px -14px rgba(0, 0, 0, 30%);
background: #fff;
opacity: 0.8;
}