mirror of
https://github.com/morris/vanilla-todo.git
synced 2025-01-17 12:48:15 +01:00
update to ES2020, some refactoring and cleanups
This commit is contained in:
parent
b4c57030f8
commit
2fbfc5e650
27
.eslintrc.js
27
.eslintrc.js
@ -1,24 +1,19 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['eslint:recommended', 'plugin:compat/recommended'],
|
extends: 'eslint:recommended',
|
||||||
globals: {
|
|
||||||
Set: 'readonly',
|
|
||||||
Map: 'readonly',
|
|
||||||
},
|
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
|
es2020: true,
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 5,
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
rules: {},
|
rules: {
|
||||||
settings: {
|
'object-shorthand': 'error',
|
||||||
polyfills: [
|
'prefer-arrow-callback': 'error',
|
||||||
'Set',
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
'Map',
|
'no-var': 'error',
|
||||||
'fetch',
|
'prefer-template': 'error',
|
||||||
'Object.assign',
|
'no-console': 'error',
|
||||||
'requestAnimationFrame',
|
|
||||||
'performance.now',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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.
|
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
184
README.md
@ -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
|
More importantly, it's a case study showing that **vanilla web development** is
|
||||||
viable in terms of [maintainability](#521-the-good),
|
viable in terms of [maintainability](#521-the-good),
|
||||||
and worthwhile in terms of [user experience](#51-user-experience)
|
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.**
|
**There's no custom framework invented here.**
|
||||||
Instead, the case study was [designed](#22-rules) to discover
|
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 build steps.
|
||||||
- No general-purpose utility functions related to the DOM/UI (2).
|
- 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,
|
(2) These usually end up becoming a custom micro-framework,
|
||||||
thereby questioning why you didn't use one of the
|
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.
|
which yields an intuitive, component-oriented structure.
|
||||||
|
|
||||||
The stylesheets are slightly verbose.
|
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.
|
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
|
All JavaScript files are ES modules (`import`/`export`).
|
||||||
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.
|
|
||||||
|
|
||||||
Basic code quality (code style, linting) is guided by
|
Basic code quality (code style, linting) is guided by
|
||||||
[Prettier](https://prettier.io), [stylelint](https://stylelint.io) and
|
[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:
|
with the DOM and styles:
|
||||||
|
|
||||||
```
|
```
|
||||||
.todo-list -> VT.TodoList
|
.todo-list -> TodoList
|
||||||
scripts/TodoList.js
|
scripts/TodoList.js
|
||||||
styles/todo-list.css
|
styles/todo-list.css
|
||||||
|
|
||||||
.app-collapsible -> VT.AppCollapsible
|
.app-collapsible -> AppCollapsible
|
||||||
scripts/AppCollapsible.js
|
scripts/AppCollapsible.js
|
||||||
styles/app-collapsible.css
|
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:
|
Here's a "Hello, World!" example of mount functions:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// safely initialize namespace
|
|
||||||
window.MYAPP = window.MYAPP || {};
|
|
||||||
|
|
||||||
// define mount function
|
// define mount function
|
||||||
// loosely mapped to ".hello-world"
|
// loosely mapped to ".hello-world"
|
||||||
MYAPP.HelloWorld = function (el) {
|
export function HelloWorld(el) {
|
||||||
// define initial state
|
// define initial state
|
||||||
var state = {
|
const state = {
|
||||||
title: 'Hello, World!',
|
title: 'Hello, World!',
|
||||||
description: 'An example vanilla component',
|
description: 'An example vanilla component',
|
||||||
counter: 0,
|
counter: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// set rigid base HTML
|
// set rigid base HTML
|
||||||
// no ES6 template literals :(
|
el.innerHTML = `
|
||||||
el.innerHTML = [
|
<h1 class="title"></h1>
|
||||||
'<h1 class="title"></h1>',
|
<p class="description"></p>
|
||||||
'<p class="description"></p>',
|
<div class="my-counter"></div>
|
||||||
'<div class="my-counter"></div>',
|
`;
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
// mount sub-components
|
// mount sub-components
|
||||||
el.querySelectorAll('.my-counter').forEach(MYAPP.MyCounter);
|
el.querySelectorAll('.my-counter').forEach(MYAPP.MyCounter);
|
||||||
|
|
||||||
// attach event listeners
|
// attach event listeners
|
||||||
el.addEventListener('modifyCounter', function (e) {
|
el.addEventListener('modifyCounter', (e) =>
|
||||||
update({ counter: state.counter + e.detail });
|
update({ counter: state.counter + e.detail })
|
||||||
});
|
);
|
||||||
|
|
||||||
// expose public interface
|
|
||||||
// use lower-case function name
|
|
||||||
el.helloWorld = {
|
|
||||||
update: update,
|
|
||||||
};
|
|
||||||
|
|
||||||
// initial update
|
// initial update
|
||||||
update();
|
update();
|
||||||
@ -278,29 +264,30 @@ MYAPP.HelloWorld = function (el) {
|
|||||||
el.querySelector('.description').innerText = state.description;
|
el.querySelector('.description').innerText = state.description;
|
||||||
|
|
||||||
// pass data to sub-scomponents
|
// pass data to sub-scomponents
|
||||||
el.querySelector('.my-counter').myCounter.update({
|
el.querySelector('.my-counter').dispatchEvent(
|
||||||
value: state.counter,
|
new CustomEvent('updateMyCounter', {
|
||||||
});
|
detail: { value: state.counter },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// define another component
|
// define another component
|
||||||
// loosely mapped to ".my-counter"
|
// loosely mapped to ".my-counter"
|
||||||
MYAPP.MyCounter = function (el) {
|
export function MyCounter(el) {
|
||||||
// define initial state
|
// define initial state
|
||||||
var state = {
|
const state = {
|
||||||
value: 0,
|
value: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// set rigid base HTML
|
// set rigid base HTML
|
||||||
// no ES6 template literals :(
|
el.innerHTML = `
|
||||||
el.innerHTML = [
|
<p>
|
||||||
'<p>',
|
<span class="value"></span>
|
||||||
' <span class="value"></span>',
|
<button class="increment">Increment</button>
|
||||||
' <button class="increment">Increment</button>',
|
<button class="decrement">Decrement</button>
|
||||||
' <button class="decrement">Decrement</button>',
|
</p>
|
||||||
'</p>',
|
`;
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
// attach event listeners
|
// attach event listeners
|
||||||
el.querySelector('.increment').addEventListener('click', function () {
|
el.querySelector('.increment').addEventListener('click', function () {
|
||||||
@ -325,11 +312,7 @@ MYAPP.MyCounter = function (el) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// expose public interface
|
el.addEventListener('updateMyCounter', (e) => update(e.detail));
|
||||||
// use lower-case function name
|
|
||||||
el.myCounter = {
|
|
||||||
update: update,
|
|
||||||
};
|
|
||||||
|
|
||||||
// define idempotent update function
|
// define idempotent update function
|
||||||
function update(next) {
|
function update(next) {
|
||||||
@ -337,11 +320,11 @@ MYAPP.MyCounter = function (el) {
|
|||||||
|
|
||||||
el.querySelector('.value').innerText = state.value;
|
el.querySelector('.value').innerText = state.value;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// mount HelloWorld component(s)
|
// mount HelloWorld component(s)
|
||||||
// any <div class="hello-world"></div> in the document will be mounted
|
// 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,
|
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.
|
and may instead only set event listeners to enable some behavior.
|
||||||
|
|
||||||
Also note that an element can be mounted with multiple mount functions.
|
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
|
Compared to React components, mount functions provide interesting flexibility as
|
||||||
components and behaviors can be implemented using the same idiom and combined
|
components and behaviors can be implemented using the same idiom and combined
|
||||||
@ -366,16 +349,17 @@ Reference:
|
|||||||
|
|
||||||
#### 3.2.2. Data Flow
|
#### 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
|
- **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),
|
- **Actions flow upwards** through custom DOM events (bubbling up),
|
||||||
usually resulting in some parent component state change which is in turn
|
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`).
|
The data store is factored into a separate behavior (`TodoStore`).
|
||||||
It only receives and dispatches events, and encapsulates all of the data logic.
|
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
|
Listening to and dispatching events is slightly verbose with standard APIs and
|
||||||
certainly justifies introducing helpers.
|
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:
|
from the implementation outlining the reconciliation algorithm:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
/* global VT */
|
export function TodoList(el) {
|
||||||
window.VT = window.VT || {};
|
const state = {
|
||||||
|
|
||||||
VT.TodoList = function (el) {
|
|
||||||
var state = {
|
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
el.innerHTML = '<div class="items"></div>';
|
el.innerHTML = `<div class="items"></div>`;
|
||||||
|
|
||||||
|
el.addEventListener('updateTodoList', (e) => update(e.detail));
|
||||||
|
|
||||||
function update(next) {
|
function update(next) {
|
||||||
Object.assign(state, next);
|
Object.assign(state, next);
|
||||||
|
|
||||||
var container = el.querySelector('.items');
|
const container = el.querySelector('.items');
|
||||||
|
|
||||||
// mark current children for removal
|
// mark current children for removal
|
||||||
var obsolete = new Set(container.children);
|
const obsolete = new Set(container.children);
|
||||||
|
|
||||||
// map current children by data-key
|
// map current children by data-key
|
||||||
var childrenByKey = new Map();
|
const childrenByKey = new Map();
|
||||||
|
|
||||||
obsolete.forEach(function (child) {
|
obsolete.forEach((child) =>
|
||||||
childrenByKey.set(child.getAttribute('data-key'), child);
|
childrenByKey.set(child.getAttribute('data-key'), child)
|
||||||
});
|
);
|
||||||
|
|
||||||
// build new list of child elements from data
|
// 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
|
// find existing child by data-key
|
||||||
var child = childrenByKey.get(item.id);
|
let child = childrenByKey.get(item.id);
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
// if child exists, keep it
|
// if child exists, keep it
|
||||||
@ -466,32 +449,28 @@ VT.TodoList = function (el) {
|
|||||||
child.setAttribute('data-key', item.id);
|
child.setAttribute('data-key', item.id);
|
||||||
|
|
||||||
// mount component
|
// mount component
|
||||||
VT.TodoItem(child);
|
TodoItem(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update child
|
// update child
|
||||||
child.todoItem.update({ item: item });
|
child.dispatchEvent(
|
||||||
|
new CustomEvent('updateTodoItem', { detail: { item: item } })
|
||||||
|
);
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove obsolete children
|
// remove obsolete children
|
||||||
obsolete.forEach(function (child) {
|
obsolete.forEach((child) => container.removeChild(child));
|
||||||
container.removeChild(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
// (re-)insert new list of children
|
// (re-)insert new list of children
|
||||||
children.forEach(function (child, index) {
|
children.forEach((child, index) => {
|
||||||
if (child !== container.children[index]) {
|
if (child !== container.children[index]) {
|
||||||
container.insertBefore(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.
|
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.
|
when elements are reordered.
|
||||||
|
|
||||||
_The latter was an improvement over the original application when I started
|
_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!_
|
team released an update with a much better drag & drop experience. Great job!_
|
||||||
|
|
||||||
One notable missing feature is Markdown support. It would be insensible
|
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
|
- Low coupling
|
||||||
- The result is literally just a bunch of HTML, CSS, and JS files.
|
- 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.
|
including comments and empty lines.
|
||||||
|
|
||||||
For comparison, prettifying the original TeuxDeux's minified JS application
|
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.
|
- Stylesheets are a bit verbose. SCSS would help here.
|
||||||
- Simple components require quite some boilerplate code.
|
- 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
|
- `el.querySelectorAll(':scope ...')` is somewhat default/expected and
|
||||||
would justify a helper.
|
would justify a helper.
|
||||||
- Listening to and dispatching events is slightly verbose.
|
- 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
|
#### 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
|
- The separation between base HTML and dynamic rendering is not ideal
|
||||||
when compared to JSX, for example.
|
when compared to JSX, for example.
|
||||||
- JSX/virtual DOM techniques provide much better development ergonomics.
|
- 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
|
e.g. watch elements of selector X (at all times) and ensure the desired
|
||||||
behaviors are mounted once on them.
|
behaviors are mounted once on them.
|
||||||
- No type safety. I've always been a proponent of dynamic languages
|
- 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.
|
I cannot recommend using it enough.
|
||||||
- We're effectively locked out of using NPM dependencies that don't provide
|
- 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
|
- Most frameworks handle a lot of browser inconsistencies **for free** and
|
||||||
continuously monitor regressions with extensive test suites.
|
continuously monitor regressions with extensive test suites.
|
||||||
The cost of browser testing is surely a lot higher
|
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).
|
- Rendering is idempotent and complete (React's pure `render` function).
|
||||||
- One-way data flow (React)
|
- 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
|
## 6. Conclusion
|
||||||
|
|
||||||
The result of this study is a working todo application with decent UI/UX and
|
The result of this study is a working todo application with decent UI/UX and
|
||||||
@ -809,9 +798,16 @@ Thanks!
|
|||||||
|
|
||||||
## 9. Changelog
|
## 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
|
### 01/2021
|
||||||
|
|
||||||
- Add [response section](#82-response)
|
- Added [response section](#82-response)
|
||||||
|
|
||||||
### 10/2020
|
### 10/2020
|
||||||
|
|
||||||
|
2096
package-lock.json
generated
2096
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@ -5,8 +5,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write *.js 'public/scripts/*.js' 'public/styles/**/*.css'",
|
"format": "prettier --write *.js 'public/scripts/*.js' 'public/styles/**/*.css'",
|
||||||
"format-check": "prettier --check *.js 'public/scripts/**/*.js' 'public/styles/**/*.css'",
|
"format-check": "prettier --check *.js 'public/scripts/**/*.js' 'public/styles/**/*.css'",
|
||||||
"lint": "eslint .",
|
"lint": "eslint public",
|
||||||
"lint-styles": "stylelint public/styles/*"
|
"lint-styles": "stylelint public/styles/*",
|
||||||
|
"serve": "http-server public"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -14,8 +15,9 @@
|
|||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"vanilla",
|
"vanilla",
|
||||||
"radio",
|
"html",
|
||||||
"somafm"
|
"javascript",
|
||||||
|
"css"
|
||||||
],
|
],
|
||||||
"author": "Morris Brodersen <mb@morrisbrodersen.de>",
|
"author": "Morris Brodersen <mb@morrisbrodersen.de>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -24,11 +26,12 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/morris/vanilla-todo#readme",
|
"homepage": "https://github.com/morris/vanilla-todo#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.17.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-plugin-compat": "^3.9.0",
|
"eslint-plugin-compat": "^4.0.2",
|
||||||
"prettier": "^2.2.1",
|
"http-server": "^14.1.0",
|
||||||
"stylelint": "^13.8.0",
|
"prettier": "^2.6.2",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint": "^14.8.2",
|
||||||
|
"stylelint-config-standard": "^25.0.0",
|
||||||
"stylelint-rscss": "^0.4.0"
|
"stylelint-rscss": "^0.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,30 +24,10 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="todo-app"></div>
|
<div class="todo-app"></div>
|
||||||
|
|
||||||
<script
|
<script type="module">
|
||||||
nomodule
|
import { TodoApp } from './scripts/TodoApp.js';
|
||||||
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>
|
TodoApp(document.querySelector('.todo-app'));
|
||||||
VT.TodoApp(document.querySelector('.todo-app'));
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
/* global VT */
|
export function AppCollapsible(el) {
|
||||||
window.VT = window.VT || {};
|
const state = {
|
||||||
|
|
||||||
VT.AppCollapsible = function (el) {
|
|
||||||
var state = {
|
|
||||||
show: true,
|
show: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
el.querySelector('.bar > .toggle').addEventListener('click', function () {
|
el.addEventListener('collapse', (e) => update({ show: !e.detail }));
|
||||||
update({ show: !state.show });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.appCollapsible = {
|
el.querySelector('.bar > .toggle').addEventListener('click', () =>
|
||||||
update: update,
|
update({ show: !state.show })
|
||||||
};
|
);
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
function update(next) {
|
function update(next) {
|
||||||
Object.assign(state, next);
|
Object.assign(state, next);
|
||||||
@ -22,8 +19,8 @@ VT.AppCollapsible = function (el) {
|
|||||||
state.show
|
state.show
|
||||||
);
|
);
|
||||||
|
|
||||||
el.querySelectorAll('.body').forEach(function (el) {
|
el.querySelectorAll('.body').forEach((el) => {
|
||||||
el.style.height = state.show ? el.children[0].offsetHeight + 'px' : '0';
|
el.style.height = state.show ? `${el.children[0].offsetHeight}px` : '0';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
/* global VT */
|
export function AppDraggable(el, options) {
|
||||||
window.VT = window.VT || {};
|
const dragThreshold = options.dragThreshold ?? 5;
|
||||||
|
const dropRange = options.dropRange ?? 50;
|
||||||
|
const dropRangeSquared = dropRange * dropRange;
|
||||||
|
|
||||||
VT.AppDraggable = function (el, options) {
|
let originX, originY;
|
||||||
var dragThreshold = options.dragThreshold || 5;
|
let clientX, clientY;
|
||||||
var dropRange = options.dropRange || 50;
|
let startTime;
|
||||||
var dropRangeSquared = dropRange * dropRange;
|
let dragging = false;
|
||||||
|
let clicked = false;
|
||||||
var originX, originY;
|
let data;
|
||||||
var clientX, clientY;
|
let image;
|
||||||
var startTime;
|
let imageSource;
|
||||||
var dragging = false;
|
let imageX, imageY;
|
||||||
var clicked = false;
|
let currentTarget;
|
||||||
var data;
|
|
||||||
var image;
|
|
||||||
var imageSource;
|
|
||||||
var imageX, imageY;
|
|
||||||
var currentTarget;
|
|
||||||
|
|
||||||
el.addEventListener('touchstart', start);
|
el.addEventListener('touchstart', start);
|
||||||
el.addEventListener('mousedown', start);
|
el.addEventListener('mousedown', start);
|
||||||
@ -23,7 +20,7 @@ VT.AppDraggable = function (el, options) {
|
|||||||
// maybe prevent click
|
// maybe prevent click
|
||||||
el.addEventListener(
|
el.addEventListener(
|
||||||
'click',
|
'click',
|
||||||
function (e) {
|
(e) => {
|
||||||
if (dragging || clicked) {
|
if (dragging || clicked) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
@ -39,9 +36,9 @@ VT.AppDraggable = function (el, options) {
|
|||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
var p = getPositionHost(e);
|
const p = getPositionHost(e);
|
||||||
clientX = originX = p.clientX || p.pageX;
|
clientX = originX = p.clientX ?? p.pageX;
|
||||||
clientY = originY = p.clientY || p.pageY;
|
clientY = originY = p.clientY ?? p.pageY;
|
||||||
startTime = Date.now();
|
startTime = Date.now();
|
||||||
|
|
||||||
startListening();
|
startListening();
|
||||||
@ -50,9 +47,9 @@ VT.AppDraggable = function (el, options) {
|
|||||||
function move(e) {
|
function move(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
var p = getPositionHost(e);
|
const p = getPositionHost(e);
|
||||||
clientX = p.clientX || p.pageX;
|
clientX = p.clientX ?? p.pageX;
|
||||||
clientY = p.clientY || p.pageY;
|
clientY = p.clientY ?? p.pageY;
|
||||||
|
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
dispatchDrag();
|
dispatchDrag();
|
||||||
@ -60,8 +57,8 @@ VT.AppDraggable = function (el, options) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deltaX = clientX - originX;
|
const deltaX = clientX - originX;
|
||||||
var deltaY = clientY - originY;
|
const deltaY = clientY - originY;
|
||||||
|
|
||||||
if (Math.abs(deltaX) < dragThreshold && Math.abs(deltaY) < dragThreshold) {
|
if (Math.abs(deltaX) < dragThreshold && Math.abs(deltaY) < dragThreshold) {
|
||||||
return;
|
return;
|
||||||
@ -92,7 +89,7 @@ VT.AppDraggable = function (el, options) {
|
|||||||
|
|
||||||
stopListening();
|
stopListening();
|
||||||
|
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(() => {
|
||||||
clicked = false;
|
clicked = false;
|
||||||
|
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
@ -141,7 +138,7 @@ VT.AppDraggable = function (el, options) {
|
|||||||
function dispatchTarget() {
|
function dispatchTarget() {
|
||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
|
|
||||||
var nextTarget = getTarget();
|
const nextTarget = getTarget();
|
||||||
|
|
||||||
if (nextTarget === currentTarget) return;
|
if (nextTarget === currentTarget) return;
|
||||||
|
|
||||||
@ -210,18 +207,18 @@ VT.AppDraggable = function (el, options) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
function buildDetail() {
|
function buildDetail() {
|
||||||
var detail = {
|
const detail = {
|
||||||
el: el,
|
el,
|
||||||
data: data,
|
data,
|
||||||
image: image,
|
image,
|
||||||
imageSource: imageSource,
|
imageSource,
|
||||||
originX: originX,
|
originX,
|
||||||
originY: originY,
|
originY,
|
||||||
clientX: clientX,
|
clientX,
|
||||||
clientY: clientY,
|
clientY,
|
||||||
imageX: imageX,
|
imageX,
|
||||||
imageY: imageY,
|
imageY,
|
||||||
setImage: function (source) {
|
setImage: (source) => {
|
||||||
setImage(source);
|
setImage(source);
|
||||||
detail.image = image;
|
detail.image = image;
|
||||||
},
|
},
|
||||||
@ -240,21 +237,21 @@ VT.AppDraggable = function (el, options) {
|
|||||||
image.style.position = 'fixed';
|
image.style.position = 'fixed';
|
||||||
image.style.left = '0';
|
image.style.left = '0';
|
||||||
image.style.top = '0';
|
image.style.top = '0';
|
||||||
image.style.width = imageSource.offsetWidth + 'px';
|
image.style.width = `${imageSource.offsetWidth}px`;
|
||||||
image.style.height = imageSource.offsetHeight + 'px';
|
image.style.height = `${imageSource.offsetHeight}px`;
|
||||||
image.style.margin = '0';
|
image.style.margin = '0';
|
||||||
image.style.zIndex = 9999;
|
image.style.zIndex = 9999;
|
||||||
image.classList.add('-dragging');
|
image.classList.add('-dragging');
|
||||||
|
|
||||||
var rect = source.getBoundingClientRect();
|
const rect = source.getBoundingClientRect();
|
||||||
imageX = originX - rect.left;
|
imageX = originX - rect.left;
|
||||||
imageY = originY - rect.top;
|
imageY = originY - rect.top;
|
||||||
|
|
||||||
image.addEventListener('draggableDrag', function (e) {
|
image.addEventListener('draggableDrag', (e) => {
|
||||||
var x = e.detail.clientX - e.detail.imageX;
|
const x = e.detail.clientX - e.detail.imageX;
|
||||||
var y = e.detail.clientY - e.detail.imageY;
|
const y = e.detail.clientY - e.detail.imageY;
|
||||||
image.style.transition = 'none';
|
image.style.transition = 'none';
|
||||||
image.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
|
image.style.transform = `translate(${x}px, ${y}px)`;
|
||||||
});
|
});
|
||||||
|
|
||||||
image.addEventListener('draggableCancel', cleanUp);
|
image.addEventListener('draggableCancel', cleanUp);
|
||||||
@ -278,9 +275,7 @@ VT.AppDraggable = function (el, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanUp() {
|
function cleanUp() {
|
||||||
if (currentTarget) {
|
currentTarget?.classList.remove('-drop');
|
||||||
currentTarget.classList.remove('-drop');
|
|
||||||
}
|
|
||||||
|
|
||||||
removeImage();
|
removeImage();
|
||||||
|
|
||||||
@ -291,27 +286,29 @@ VT.AppDraggable = function (el, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeImage() {
|
function removeImage() {
|
||||||
if (image && image.parentNode) {
|
image?.parentNode?.removeChild(image);
|
||||||
image.parentNode.removeChild(image);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTarget() {
|
function getTarget() {
|
||||||
var candidates = [];
|
const candidates = [];
|
||||||
|
|
||||||
document.querySelectorAll(options.dropSelector).forEach(function (el) {
|
document.querySelectorAll(options.dropSelector).forEach((el) => {
|
||||||
var rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
var distanceSquared = pointDistanceToRectSquared(clientX, clientY, rect);
|
const distanceSquared = pointDistanceToRectSquared(
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
rect
|
||||||
|
);
|
||||||
|
|
||||||
if (distanceSquared > dropRangeSquared) return;
|
if (distanceSquared > dropRangeSquared) return;
|
||||||
|
|
||||||
candidates.push({
|
candidates.push({
|
||||||
el: el,
|
el,
|
||||||
distance2: distanceSquared,
|
distance2: distanceSquared,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
candidates.sort(function (a, b) {
|
candidates.sort((a, b) => {
|
||||||
if (a.distance2 === 0 && b.distance2 === 0) {
|
if (a.distance2 === 0 && b.distance2 === 0) {
|
||||||
// in this case, the client position is inside both rectangles
|
// in this case, the client position is inside both rectangles
|
||||||
// if A contains B, B is the correct target and vice versa
|
// 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) {
|
function pointDistanceToRectSquared(x, y, rect) {
|
||||||
var dx =
|
const dx =
|
||||||
x < rect.left ? x - rect.left : x > rect.right ? x - rect.right : 0;
|
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;
|
y < rect.top ? y - rect.top : y > rect.bottom ? y - rect.bottom : 0;
|
||||||
|
|
||||||
return dx * dx + dy * dy;
|
return dx * dx + dy * dy;
|
||||||
@ -346,4 +343,4 @@ VT.AppDraggable = function (el, options) {
|
|||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
/* global VT */
|
export function AppFlip(el, options) {
|
||||||
window.VT = window.VT || {};
|
let enabled = options.initialDelay === 0;
|
||||||
|
let first;
|
||||||
VT.AppFlip = function (el, options) {
|
let level = 0;
|
||||||
var enabled = options.initialDelay === 0;
|
|
||||||
var first;
|
|
||||||
var level = 0;
|
|
||||||
|
|
||||||
// enable animations only after an initial delay
|
// enable animations only after an initial delay
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
}, options.initialDelay || 100);
|
}, options.initialDelay || 100);
|
||||||
|
|
||||||
// take a snapshot before any HTML changes
|
// take a snapshot before any HTML changes
|
||||||
// do this only for the first beforeFlip event in the current cycle
|
// do this only for the first beforeFlip event in the current cycle
|
||||||
el.addEventListener('beforeFlip', function () {
|
el.addEventListener('beforeFlip', () => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
if (++level > 1) return;
|
if (++level > 1) return;
|
||||||
|
|
||||||
@ -22,16 +19,16 @@ VT.AppFlip = function (el, options) {
|
|||||||
|
|
||||||
// take a snapshot after HTML changes, calculate and play animations
|
// take a snapshot after HTML changes, calculate and play animations
|
||||||
// do this only for the last flip event in the current cycle
|
// do this only for the last flip event in the current cycle
|
||||||
el.addEventListener('flip', function () {
|
el.addEventListener('flip', () => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
if (--level > 0) return;
|
if (--level > 0) return;
|
||||||
|
|
||||||
var last = snapshot();
|
const last = snapshot();
|
||||||
var toRemove = invertForRemoval(first, last);
|
const toRemove = invertForRemoval(first, last);
|
||||||
var toAnimate = invertForAnimation(first, last);
|
const toAnimate = invertForAnimation(first, last);
|
||||||
|
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(() => {
|
||||||
remove(toRemove);
|
remove(toRemove);
|
||||||
animate(toAnimate);
|
animate(toAnimate);
|
||||||
|
|
||||||
@ -43,23 +40,23 @@ VT.AppFlip = function (el, options) {
|
|||||||
// build a snapshot of the current HTML's client rectangles
|
// build a snapshot of the current HTML's client rectangles
|
||||||
// includes original transforms and hierarchy
|
// includes original transforms and hierarchy
|
||||||
function snapshot() {
|
function snapshot() {
|
||||||
var map = new Map();
|
const map = new Map();
|
||||||
|
|
||||||
el.querySelectorAll(options.selector).forEach(function (el) {
|
el.querySelectorAll(options.selector).forEach((el) => {
|
||||||
var key = el.dataset.key || el;
|
const key = el.dataset.key ?? el;
|
||||||
|
|
||||||
// parse original transform
|
// parse original transform
|
||||||
// i.e. strip inverse transform using "scale(1)" marker
|
// 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\)/, '')
|
? el.style.transform.replace(/^.*scale\(1\)/, '')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
map.set(key, {
|
map.set(key, {
|
||||||
key: key,
|
key,
|
||||||
el: el,
|
el,
|
||||||
rect: el.getBoundingClientRect(),
|
rect: el.getBoundingClientRect(),
|
||||||
ancestor: null,
|
ancestor: null,
|
||||||
transform: transform,
|
transform,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,11 +66,11 @@ VT.AppFlip = function (el, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveAncestors(map) {
|
function resolveAncestors(map) {
|
||||||
map.forEach(function (entry) {
|
map.forEach((entry) => {
|
||||||
var current = entry.el.parentNode;
|
let current = entry.el.parentNode;
|
||||||
|
|
||||||
while (current && current !== el) {
|
while (current && current !== el) {
|
||||||
var ancestor = map.get(current.dataset.key || current);
|
const ancestor = map.get(current.dataset.key ?? current);
|
||||||
|
|
||||||
if (ancestor) {
|
if (ancestor) {
|
||||||
entry.ancestor = ancestor;
|
entry.ancestor = ancestor;
|
||||||
@ -87,16 +84,16 @@ VT.AppFlip = function (el, options) {
|
|||||||
|
|
||||||
// reinsert removed elements at their original position
|
// reinsert removed elements at their original position
|
||||||
function invertForRemoval(first, last) {
|
function invertForRemoval(first, last) {
|
||||||
var toRemove = [];
|
const toRemove = [];
|
||||||
|
|
||||||
first.forEach(function (entry) {
|
first.forEach((entry) => {
|
||||||
if (entry.el.classList.contains('_noflip')) return;
|
if (entry.el.classList.contains('_noflip')) return;
|
||||||
if (!needsRemoval(entry)) return;
|
if (!needsRemoval(entry)) return;
|
||||||
|
|
||||||
entry.el.style.position = 'fixed';
|
entry.el.style.position = 'fixed';
|
||||||
entry.el.style.left = entry.rect.left + 'px';
|
entry.el.style.left = `${entry.rect.left}px`;
|
||||||
entry.el.style.top = entry.rect.top + 'px';
|
entry.el.style.top = `${entry.rect.top}px`;
|
||||||
entry.el.style.width = entry.rect.right - entry.rect.left + 'px';
|
entry.el.style.width = `${entry.rect.right - entry.rect.left}px`;
|
||||||
entry.el.style.transition = 'none';
|
entry.el.style.transition = 'none';
|
||||||
entry.el.style.transform = '';
|
entry.el.style.transform = '';
|
||||||
|
|
||||||
@ -118,9 +115,9 @@ VT.AppFlip = function (el, options) {
|
|||||||
// set position of moved elements to their original position
|
// set position of moved elements to their original position
|
||||||
// or set opacity to zero for new elements to appear nicely
|
// or set opacity to zero for new elements to appear nicely
|
||||||
function invertForAnimation(first, last) {
|
function invertForAnimation(first, last) {
|
||||||
var toAnimate = [];
|
const toAnimate = [];
|
||||||
|
|
||||||
last.forEach(function (entry) {
|
last.forEach((entry) => {
|
||||||
if (entry.el.classList.contains('_noflip')) return;
|
if (entry.el.classList.contains('_noflip')) return;
|
||||||
|
|
||||||
calculate(entry);
|
calculate(entry);
|
||||||
@ -132,13 +129,7 @@ VT.AppFlip = function (el, options) {
|
|||||||
} else if (entry.deltaX !== 0 || entry.deltaY !== 0) {
|
} else if (entry.deltaX !== 0 || entry.deltaY !== 0) {
|
||||||
// set inverted transform with "scale(1)" marker, see above
|
// set inverted transform with "scale(1)" marker, see above
|
||||||
entry.el.style.transition = 'none';
|
entry.el.style.transition = 'none';
|
||||||
entry.el.style.transform =
|
entry.el.style.transform = `translate(${entry.deltaX}px, ${entry.deltaY}px) scale(1) ${entry.transform}`;
|
||||||
'translate(' +
|
|
||||||
entry.deltaX +
|
|
||||||
'px, ' +
|
|
||||||
entry.deltaY +
|
|
||||||
'px) scale(1) ' +
|
|
||||||
entry.transform;
|
|
||||||
toAnimate.push(entry);
|
toAnimate.push(entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -150,7 +141,7 @@ VT.AppFlip = function (el, options) {
|
|||||||
if (entry.calculated) return;
|
if (entry.calculated) return;
|
||||||
entry.calculated = true;
|
entry.calculated = true;
|
||||||
|
|
||||||
var b = first.get(entry.key);
|
const b = first.get(entry.key);
|
||||||
|
|
||||||
if (b) {
|
if (b) {
|
||||||
entry.deltaX = b.rect.left - entry.rect.left;
|
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
|
// play remove animations and remove elements after timeout
|
||||||
function remove(entries) {
|
function remove(entries) {
|
||||||
entries.forEach(function (entry) {
|
entries.forEach((entry) => {
|
||||||
entry.el.style.transition = '';
|
entry.el.style.transition = '';
|
||||||
entry.el.style.opacity = '0';
|
entry.el.style.opacity = '0';
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
entries.forEach(function (entry) {
|
entries.forEach((entry) => {
|
||||||
if (entry.el.parentNode) {
|
if (entry.el.parentNode) {
|
||||||
entry.el.parentNode.removeChild(entry.el);
|
entry.el.parentNode.removeChild(entry.el);
|
||||||
}
|
}
|
||||||
@ -188,10 +179,10 @@ VT.AppFlip = function (el, options) {
|
|||||||
|
|
||||||
// play move/appear animations
|
// play move/appear animations
|
||||||
function animate(entries) {
|
function animate(entries) {
|
||||||
entries.forEach(function (entry) {
|
entries.forEach((entry) => {
|
||||||
entry.el.style.transition = '';
|
entry.el.style.transition = '';
|
||||||
entry.el.style.transform = entry.transform;
|
entry.el.style.transform = entry.transform;
|
||||||
entry.el.style.opacity = '';
|
entry.el.style.opacity = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
/* global VT */
|
export function AppFps(el) {
|
||||||
window.VT = window.VT || {};
|
const sampleSize = 20;
|
||||||
|
let times = [];
|
||||||
VT.AppFps = function (el) {
|
|
||||||
var sampleSize = 20;
|
|
||||||
var times = [];
|
|
||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
@ -14,27 +11,23 @@ VT.AppFps = function (el) {
|
|||||||
|
|
||||||
if (times.length <= sampleSize) return;
|
if (times.length <= sampleSize) return;
|
||||||
|
|
||||||
var min = Infinity;
|
let min = Infinity;
|
||||||
var max = 0;
|
let max = 0;
|
||||||
var sum = 0;
|
let sum = 0;
|
||||||
|
|
||||||
for (var i = 1; i < sampleSize + 1; ++i) {
|
for (let i = 1; i < sampleSize + 1; ++i) {
|
||||||
var delta = times[i] - times[i - 1];
|
const delta = times[i] - times[i - 1];
|
||||||
min = Math.min(min, delta);
|
min = Math.min(min, delta);
|
||||||
max = Math.max(max, delta);
|
max = Math.max(max, delta);
|
||||||
sum += delta;
|
sum += delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fps = (sampleSize / sum) * 1000;
|
const fps = (sampleSize / sum) * 1000;
|
||||||
|
|
||||||
el.innerText =
|
el.innerText = `${fps.toFixed(0)} fps (${min.toFixed(0)} ms - ${max.toFixed(
|
||||||
fps.toFixed(0) +
|
0
|
||||||
' fps (' +
|
)} ms)`;
|
||||||
min.toFixed(0) +
|
|
||||||
' ms - ' +
|
|
||||||
max.toFixed(0) +
|
|
||||||
' ms)';
|
|
||||||
|
|
||||||
times = [];
|
times = [];
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,24 +1,19 @@
|
|||||||
/* global VT */
|
export const BASE_URL =
|
||||||
window.VT = window.VT || {};
|
'https://rawcdn.githack.com/primer/octicons/ff7f6eee63fa2f2d24d02e3aa76a87db48e4b6f6/icons/';
|
||||||
|
|
||||||
VT.AppIcon = function (el) {
|
const cache = {};
|
||||||
|
|
||||||
|
export function AppIcon(el) {
|
||||||
if (el.children.length > 0) return;
|
if (el.children.length > 0) return;
|
||||||
|
|
||||||
var id = el.dataset.id;
|
const id = el.dataset.id;
|
||||||
var promise = VT.AppIcon.cache[id];
|
let promise = cache[id];
|
||||||
|
|
||||||
if (!promise) {
|
if (!promise) {
|
||||||
var url = VT.AppIcon.baseUrl + id + '.svg';
|
promise = cache[id] = fetch(`${BASE_URL}${id}.svg`).then((r) => r.text());
|
||||||
promise = VT.AppIcon.cache[id] = fetch(url).then(function (r) {
|
|
||||||
return r.text();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.then(function (svg) {
|
promise.then((svg) => {
|
||||||
el.innerHTML = el.classList.contains('-double') ? svg + svg : svg;
|
el.innerHTML = el.classList.contains('-double') ? svg + svg : svg;
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
VT.AppIcon.baseUrl =
|
|
||||||
'https://rawcdn.githack.com/primer/octicons/ff7f6eee63fa2f2d24d02e3aa76a87db48e4b6f6/icons/';
|
|
||||||
VT.AppIcon.cache = {};
|
|
||||||
|
@ -1,34 +1,29 @@
|
|||||||
/* global VT */
|
export function AppSortable(el, options) {
|
||||||
window.VT = window.VT || {};
|
let placeholder;
|
||||||
|
let placeholderSource;
|
||||||
|
const horizontal = options.direction === 'horizontal';
|
||||||
|
let currentIndex = -1;
|
||||||
|
|
||||||
VT.AppSortable = function (el, options) {
|
el.addEventListener('draggableStart', (e) =>
|
||||||
var placeholder;
|
e.detail.image.addEventListener('draggableCancel', cleanUp)
|
||||||
var placeholderSource;
|
);
|
||||||
var horizontal = options.direction === 'horizontal';
|
|
||||||
var currentIndex = -1;
|
|
||||||
|
|
||||||
el.addEventListener('draggableStart', function (e) {
|
el.addEventListener('draggableOver', (e) =>
|
||||||
e.detail.image.addEventListener('draggableCancel', cleanUp);
|
maybeDispatchUpdate(calculateIndex(e.detail.image), e)
|
||||||
});
|
);
|
||||||
|
|
||||||
el.addEventListener('draggableOver', function (e) {
|
el.addEventListener('draggableLeave', (e) => maybeDispatchUpdate(-1, e));
|
||||||
maybeDispatchUpdate(calculateIndex(e.detail.image), e);
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('draggableLeave', function (e) {
|
el.addEventListener('draggableDrop', (e) =>
|
||||||
maybeDispatchUpdate(-1, e);
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('draggableDrop', function (e) {
|
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('sortableDrop', {
|
new CustomEvent('sortableDrop', {
|
||||||
detail: buildDetail(e),
|
detail: buildDetail(e),
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('sortableUpdate', function (e) {
|
el.addEventListener('sortableUpdate', (e) => {
|
||||||
if (!placeholder) {
|
if (!placeholder) {
|
||||||
e.detail.setPlaceholder(e.detail.originalEvent.detail.imageSource);
|
e.detail.setPlaceholder(e.detail.originalEvent.detail.imageSource);
|
||||||
}
|
}
|
||||||
@ -65,11 +60,11 @@ VT.AppSortable = function (el, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildDetail(e) {
|
function buildDetail(e) {
|
||||||
var detail = {
|
const detail = {
|
||||||
data: e.detail.data,
|
data: e.detail.data,
|
||||||
index: currentIndex,
|
index: currentIndex,
|
||||||
placeholder: placeholder,
|
placeholder,
|
||||||
setPlaceholder: function (source) {
|
setPlaceholder: (source) => {
|
||||||
setPlaceholder(source);
|
setPlaceholder(source);
|
||||||
detail.placeholder = placeholder;
|
detail.placeholder = placeholder;
|
||||||
},
|
},
|
||||||
@ -98,14 +93,12 @@ VT.AppSortable = function (el, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removePlaceholder() {
|
function removePlaceholder() {
|
||||||
if (placeholder && placeholder.parentNode) {
|
placeholder?.parentNode?.removeChild(placeholder);
|
||||||
placeholder.parentNode.removeChild(placeholder);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeByKey(key) {
|
function removeByKey(key) {
|
||||||
for (var i = 0, l = el.children.length; i < l; ++i) {
|
for (let i = 0, l = el.children.length; i < l; ++i) {
|
||||||
var child = el.children[i];
|
const child = el.children[i];
|
||||||
|
|
||||||
if (child && child.dataset.key === key) {
|
if (child && child.dataset.key === key) {
|
||||||
el.removeChild(child);
|
el.removeChild(child);
|
||||||
@ -116,12 +109,12 @@ VT.AppSortable = function (el, options) {
|
|||||||
function calculateIndex(image) {
|
function calculateIndex(image) {
|
||||||
if (el.children.length === 0) return 0;
|
if (el.children.length === 0) return 0;
|
||||||
|
|
||||||
var isBefore = horizontal ? isLeft : isAbove;
|
const isBefore = horizontal ? isLeft : isAbove;
|
||||||
var rect = image.getBoundingClientRect();
|
const rect = image.getBoundingClientRect();
|
||||||
var p = 0;
|
let p = 0;
|
||||||
|
|
||||||
for (var i = 0, l = el.children.length; i < l; ++i) {
|
for (let i = 0, l = el.children.length; i < l; ++i) {
|
||||||
var child = el.children[i];
|
const child = el.children[i];
|
||||||
|
|
||||||
if (isBefore(rect, child.getBoundingClientRect())) return i - p;
|
if (isBefore(rect, child.getBoundingClientRect())) return i - p;
|
||||||
if (child === placeholder) p = 1;
|
if (child === placeholder) p = 1;
|
||||||
@ -143,4 +136,4 @@ VT.AppSortable = function (el, options) {
|
|||||||
rectB.left + (rectB.right - rectB.left) / 2
|
rectB.left + (rectB.right - rectB.left) / 2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,49 +1,56 @@
|
|||||||
/* global VT */
|
import { AppCollapsible } from './AppCollapsible.js';
|
||||||
window.VT = window.VT || {};
|
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) {
|
export function TodoApp(el) {
|
||||||
var state = {
|
const state = {
|
||||||
items: [],
|
items: [],
|
||||||
customLists: [],
|
customLists: [],
|
||||||
at: VT.formatDateId(new Date()),
|
at: formatDateId(new Date()),
|
||||||
customAt: 0,
|
customAt: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
el.innerHTML = [
|
el.innerHTML = `
|
||||||
'<header class="app-header">',
|
<header class="app-header">
|
||||||
' <h1 class="title">VANILLA TODO</h1>',
|
<h1 class="title">VANILLA TODO</h1>
|
||||||
' <p class="app-fps"></p>',
|
<p class="app-fps fps"></p>
|
||||||
'</header>',
|
</header>
|
||||||
'<div class="todo-frame -days"></div>',
|
<div class="todo-frame -days"></div>
|
||||||
'<div class="app-collapsible">',
|
<div class="app-collapsible">
|
||||||
' <p class="bar">',
|
<p class="bar">
|
||||||
' <button class="app-button -circle toggle"><i class="app-icon" data-id="chevron-up-24"></i></button>',
|
<button class="app-button -circle toggle"><i class="app-icon" data-id="chevron-up-24"></i></button>
|
||||||
' </p>',
|
</p>
|
||||||
' <div class="body">',
|
<div class="body">
|
||||||
' <div class="todo-frame -custom"></div>',
|
<div class="todo-frame -custom"></div>
|
||||||
' </div>',
|
</div>
|
||||||
'</div>',
|
</div>
|
||||||
'<footer class="app-footer">',
|
<footer class="app-footer">
|
||||||
' <p>',
|
<p>
|
||||||
' VANILLA TODO © 2020 <a href="https://morrisbrodersen.de">Morris Brodersen</a>',
|
VANILLA TODO © 2020-2022 <a href="https://morrisbrodersen.de">Morris Brodersen</a>
|
||||||
' — A case study on viable techniques for vanilla web development.',
|
— A case study on viable techniques for vanilla web development.
|
||||||
' <a href="https://github.com/morris/vanilla-todo">About →</a>',
|
<a href="https://github.com/morris/vanilla-todo">About →</a>
|
||||||
' </p>',
|
</p>
|
||||||
'</footer>',
|
</footer>
|
||||||
].join('\n');
|
`;
|
||||||
|
|
||||||
VT.AppFlip(el, {
|
AppFlip(el, {
|
||||||
selector: '.todo-item, .todo-item-input, .todo-day, .todo-custom-list',
|
selector: '.todo-item, .todo-item-input, .todo-day, .todo-custom-list',
|
||||||
removeTimeout: 200,
|
removeTimeout: 200,
|
||||||
});
|
});
|
||||||
VT.TodoStore(el);
|
|
||||||
|
|
||||||
el.querySelectorAll('.app-collapsible').forEach(VT.AppCollapsible);
|
TodoStore(el);
|
||||||
el.querySelectorAll('.app-icon').forEach(VT.AppIcon);
|
|
||||||
el.querySelectorAll('.app-fps').forEach(VT.AppFps);
|
|
||||||
|
|
||||||
VT.TodoFrameDays(el.querySelector('.todo-frame.-days'));
|
el.querySelectorAll('.app-collapsible').forEach(AppCollapsible);
|
||||||
VT.TodoFrameCustom(el.querySelector('.todo-frame.-custom'));
|
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
|
// each of these events make changes to the HTML to be animated using FLIP
|
||||||
// listening to them using "capture" dispatches "beforeFlip" before any changes
|
// listening to them using "capture" dispatches "beforeFlip" before any changes
|
||||||
@ -53,30 +60,30 @@ VT.TodoApp = function (el) {
|
|||||||
el.addEventListener('draggableDrop', beforeFlip, true);
|
el.addEventListener('draggableDrop', beforeFlip, true);
|
||||||
|
|
||||||
// some necessary work to orchestrate drag & drop with FLIP animations
|
// 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');
|
e.detail.image.classList.add('_noflip');
|
||||||
el.appendChild(e.detail.image);
|
el.appendChild(e.detail.image);
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('draggableCancel', function (e) {
|
el.addEventListener('draggableCancel', (e) => {
|
||||||
e.detail.image.classList.remove('_noflip');
|
e.detail.image.classList.remove('_noflip');
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('draggableDrop', function (e) {
|
el.addEventListener('draggableDrop', (e) => {
|
||||||
e.detail.image.classList.remove('_noflip');
|
e.detail.image.classList.remove('_noflip');
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('sortableUpdate', function (e) {
|
el.addEventListener('sortableUpdate', (e) => {
|
||||||
e.detail.placeholder.classList.add('_noflip');
|
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
|
// 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;
|
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;
|
if (el === e.target) return;
|
||||||
el.dispatchEvent(new CustomEvent('focusOther'));
|
el.dispatchEvent(new CustomEvent('focusOther'));
|
||||||
});
|
});
|
||||||
@ -85,9 +92,7 @@ VT.TodoApp = function (el) {
|
|||||||
// listen to the TodoStore's data
|
// listen to the TodoStore's data
|
||||||
// this is the main update
|
// this is the main update
|
||||||
// everything else is related to drag & drop or FLIP animations
|
// everything else is related to drag & drop or FLIP animations
|
||||||
el.addEventListener('todoData', function (e) {
|
el.addEventListener('todoData', (e) => update(e.detail));
|
||||||
update(e.detail);
|
|
||||||
});
|
|
||||||
|
|
||||||
// dispatch "flip" after HTML changes from these events
|
// dispatch "flip" after HTML changes from these events
|
||||||
// this plays the FLIP animations
|
// this plays the FLIP animations
|
||||||
@ -96,40 +101,27 @@ VT.TodoApp = function (el) {
|
|||||||
el.addEventListener('draggableCancel', flip);
|
el.addEventListener('draggableCancel', flip);
|
||||||
el.addEventListener('draggableDrop', flip);
|
el.addEventListener('draggableDrop', flip);
|
||||||
|
|
||||||
el.todoStore.load();
|
el.dispatchEvent(new CustomEvent('loadStore'));
|
||||||
|
|
||||||
function update(next) {
|
function update(next) {
|
||||||
Object.assign(state, next);
|
Object.assign(state, next);
|
||||||
|
|
||||||
el.querySelector('.todo-frame.-days').todoFrameDays.update({
|
el.querySelectorAll('.todo-frame').forEach((el) =>
|
||||||
items: state.items,
|
el.dispatchEvent(new CustomEvent('todoData', { detail: state }))
|
||||||
at: state.at,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
el.querySelector('.todo-frame.-custom').todoFrameCustom.update({
|
el.querySelectorAll('.app-collapsible').forEach((el) =>
|
||||||
lists: state.customLists,
|
el.dispatchEvent(new CustomEvent('collapse'))
|
||||||
items: state.items,
|
);
|
||||||
at: state.customAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
el.querySelectorAll('.app-collapsible').forEach(function (el) {
|
|
||||||
el.appCollapsible.update();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforeFlip() {
|
function beforeFlip(e) {
|
||||||
el.dispatchEvent(
|
if (e.type === 'todoData' && e.target !== el) return;
|
||||||
new CustomEvent('beforeFlip', {
|
|
||||||
bubbles: true,
|
el.dispatchEvent(new CustomEvent('beforeFlip'));
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function flip() {
|
function flip() {
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(new CustomEvent('flip'));
|
||||||
new CustomEvent('flip', {
|
}
|
||||||
bubbles: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
@ -1,58 +1,59 @@
|
|||||||
/* global VT */
|
import { AppDraggable } from './AppDraggable.js';
|
||||||
window.VT = window.VT || {};
|
import { AppIcon } from './AppIcon.js';
|
||||||
|
import { TodoList } from './TodoList.js';
|
||||||
|
|
||||||
VT.TodoCustomList = function (el) {
|
export function TodoCustomList(el) {
|
||||||
var state = {
|
const state = {
|
||||||
list: null,
|
list: null,
|
||||||
editing: false,
|
editing: false,
|
||||||
};
|
};
|
||||||
var startEditing = false;
|
let startEditing = false;
|
||||||
var saveOnBlur = true;
|
let saveOnBlur = true;
|
||||||
|
|
||||||
el.innerHTML = [
|
el.innerHTML = `
|
||||||
'<div class="header">',
|
<div class="header">
|
||||||
' <h3 class="title"></h3>',
|
<h3 class="title"></h3>
|
||||||
' <p class="form">',
|
<p class="form">
|
||||||
' <input type="text" class="input use-focus-other">',
|
<input type="text" class="input use-focus-other">
|
||||||
' <button class="app-button delete"><i class="app-icon" data-id="trashcan-16"></i></button>',
|
<button class="app-button delete"><i class="app-icon" data-id="trashcan-16"></i></button>
|
||||||
' </p>',
|
</p>
|
||||||
'</div>',
|
</div>
|
||||||
'<div class="todo-list"></div>',
|
<div class="todo-list"></div>
|
||||||
].join('\n');
|
`;
|
||||||
|
|
||||||
var titleEl = el.querySelector('.title');
|
const titleEl = el.querySelector('.title');
|
||||||
var inputEl = el.querySelector('.input');
|
const inputEl = el.querySelector('.input');
|
||||||
var deleteEl = el.querySelector('.delete');
|
const deleteEl = el.querySelector('.delete');
|
||||||
|
|
||||||
VT.AppDraggable(titleEl, {
|
AppDraggable(titleEl, {
|
||||||
dropSelector: '.todo-frame.-custom .container',
|
dropSelector: '.todo-frame.-custom .container',
|
||||||
});
|
});
|
||||||
VT.TodoList(el.querySelector('.todo-list'));
|
el.querySelectorAll('.app-icon').forEach(AppIcon);
|
||||||
el.querySelectorAll('.app-icon').forEach(VT.AppIcon);
|
TodoList(el.querySelector('.todo-list'));
|
||||||
|
|
||||||
titleEl.addEventListener('click', function () {
|
titleEl.addEventListener('click', () => {
|
||||||
startEditing = true;
|
startEditing = true;
|
||||||
update({ editing: true });
|
update({ editing: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
deleteEl.addEventListener('touchstart', function () {
|
deleteEl.addEventListener('touchstart', () => {
|
||||||
saveOnBlur = false;
|
saveOnBlur = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
deleteEl.addEventListener('mousedown', function () {
|
deleteEl.addEventListener('mousedown', () => {
|
||||||
saveOnBlur = false;
|
saveOnBlur = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
inputEl.addEventListener('blur', function () {
|
inputEl.addEventListener('blur', () => {
|
||||||
if (saveOnBlur) save();
|
if (saveOnBlur) save();
|
||||||
saveOnBlur = true;
|
saveOnBlur = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
inputEl.addEventListener('focusOther', function () {
|
inputEl.addEventListener('focusOther', () => {
|
||||||
if (state.editing) save();
|
if (state.editing) save();
|
||||||
});
|
});
|
||||||
|
|
||||||
inputEl.addEventListener('keyup', function (e) {
|
inputEl.addEventListener('keyup', (e) => {
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
case 13: // enter
|
case 13: // enter
|
||||||
save();
|
save();
|
||||||
@ -63,7 +64,7 @@ VT.TodoCustomList = function (el) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
deleteEl.addEventListener('click', function () {
|
deleteEl.addEventListener('click', () => {
|
||||||
if (state.list.items.length > 0) {
|
if (state.list.items.length > 0) {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
@ -82,7 +83,7 @@ VT.TodoCustomList = function (el) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('draggableStart', function (e) {
|
el.addEventListener('draggableStart', (e) => {
|
||||||
if (e.target !== titleEl) return;
|
if (e.target !== titleEl) return;
|
||||||
|
|
||||||
e.detail.data.list = state.list;
|
e.detail.data.list = state.list;
|
||||||
@ -92,25 +93,23 @@ VT.TodoCustomList = function (el) {
|
|||||||
e.detail.setImage(el);
|
e.detail.setImage(el);
|
||||||
|
|
||||||
// override for horizontal dragging only
|
// override for horizontal dragging only
|
||||||
e.detail.image.addEventListener('draggableDrag', function (e) {
|
e.detail.image.addEventListener('draggableDrag', (e) => {
|
||||||
var x = e.detail.clientX - e.detail.imageX;
|
const x = e.detail.clientX - e.detail.imageX;
|
||||||
var y = e.detail.originY - e.detail.imageY;
|
const y = e.detail.originY - e.detail.imageY;
|
||||||
e.detail.image.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
|
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;
|
e.detail.listId = state.list.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('moveItem', function (e) {
|
el.addEventListener('moveItem', (e) => {
|
||||||
e.detail.listId = state.list.id;
|
e.detail.listId = state.list.id;
|
||||||
e.detail.index = e.detail.index || 0;
|
e.detail.index = e.detail.index ?? 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
el.todoCustomList = {
|
el.addEventListener('todoCustomList', (e) => update({ list: e.detail }));
|
||||||
update: update,
|
|
||||||
};
|
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
@ -132,9 +131,13 @@ VT.TodoCustomList = function (el) {
|
|||||||
|
|
||||||
titleEl.innerText = state.list.title || '...';
|
titleEl.innerText = state.list.title || '...';
|
||||||
|
|
||||||
el.querySelector('.todo-list').todoList.update({ items: state.list.items });
|
el.querySelector('.todo-list').dispatchEvent(
|
||||||
el.querySelector('.todo-list > .todo-item-input').dataset.key =
|
new CustomEvent('todoItems', { detail: state.list.items })
|
||||||
'todo-item-input' + state.list.id;
|
);
|
||||||
|
|
||||||
|
el.querySelector(
|
||||||
|
'.todo-list > .todo-item-input'
|
||||||
|
).dataset.key = `todo-item-input${state.list.id}`;
|
||||||
|
|
||||||
el.classList.toggle('-editing', state.editing);
|
el.classList.toggle('-editing', state.editing);
|
||||||
|
|
||||||
@ -145,4 +148,4 @@ VT.TodoCustomList = function (el) {
|
|||||||
startEditing = false;
|
startEditing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,51 +1,49 @@
|
|||||||
/* global VT */
|
import { TodoList } from './TodoList.js';
|
||||||
window.VT = window.VT || {};
|
import { formatDate, formatDayOfWeek } from './util.js';
|
||||||
|
|
||||||
VT.TodoDay = function (el) {
|
export function TodoDay(el) {
|
||||||
var state = {
|
const state = {
|
||||||
dateId: el.dataset.key,
|
dateId: el.dataset.key,
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
el.innerHTML = [
|
el.innerHTML = `
|
||||||
'<div class="header">',
|
<div class="header">
|
||||||
' <h3 class="dayofweek"></h3>',
|
<h3 class="dayofweek"></h3>
|
||||||
' <h6 class="date"></h6>',
|
<h6 class="date"></h6>
|
||||||
'</div>',
|
</div>
|
||||||
'<div class="todo-list"></div>',
|
<div class="todo-list"></div>
|
||||||
].join('\n');
|
`;
|
||||||
|
|
||||||
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;
|
e.detail.listId = state.dateId;
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('moveItem', function (e) {
|
el.addEventListener('moveItem', (e) => {
|
||||||
e.detail.listId = state.dateId;
|
e.detail.listId = state.dateId;
|
||||||
e.detail.index = e.detail.index || 0;
|
e.detail.index = e.detail.index ?? 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
el.todoDay = {
|
el.addEventListener('todoDay', (e) => update(e.detail));
|
||||||
update: update,
|
|
||||||
};
|
|
||||||
|
|
||||||
function update(next) {
|
function update(next) {
|
||||||
Object.assign(state, next);
|
Object.assign(state, next);
|
||||||
|
|
||||||
var date = new Date(state.dateId);
|
const date = new Date(state.dateId);
|
||||||
var today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
var tomorrow = new Date(today);
|
const tomorrow = new Date(today);
|
||||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
|
||||||
el.classList.toggle('-past', date < today);
|
el.classList.toggle('-past', date < today);
|
||||||
el.classList.toggle('-today', date >= today && date < tomorrow);
|
el.classList.toggle('-today', date >= today && date < tomorrow);
|
||||||
|
|
||||||
el.querySelector('.header > .dayofweek').innerText = VT.formatDayOfWeek(
|
el.querySelector('.header > .dayofweek').innerText = formatDayOfWeek(date);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,51 +1,52 @@
|
|||||||
/* global VT */
|
import { AppIcon } from './AppIcon.js';
|
||||||
window.VT = window.VT || {};
|
import { AppSortable } from './AppSortable.js';
|
||||||
|
import { TodoCustomList } from './TodoCustomList.js';
|
||||||
|
|
||||||
VT.TodoFrameCustom = function (el) {
|
export function TodoFrameCustom(el) {
|
||||||
var state = {
|
const state = {
|
||||||
lists: [],
|
customLists: [],
|
||||||
items: [],
|
items: [],
|
||||||
at: 0,
|
customAt: 0,
|
||||||
show: true,
|
show: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
el.innerHTML = [
|
el.innerHTML = `
|
||||||
'<div class="leftcontrols">',
|
<div class="leftcontrols">
|
||||||
' <p><button class="app-button -circle -xl back"><i class="app-icon" data-id="chevron-left-24"></i></button></p>',
|
<p><button class="app-button -circle -xl back"><i class="app-icon" data-id="chevron-left-24"></i></button></p>
|
||||||
'</div>',
|
</div>
|
||||||
'<div class="container"></div>',
|
<div class="container"></div>
|
||||||
'<div class="rightcontrols">',
|
<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 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>',
|
<p><button class="app-button -circle -xl add"><i class="app-icon" data-id="plus-circle-24"></i></button></p>
|
||||||
'</div>',
|
</div>
|
||||||
].join('\n');
|
`;
|
||||||
|
|
||||||
VT.AppSortable(el.querySelector('.container'), { direction: 'horizontal' });
|
AppSortable(el.querySelector('.container'), { direction: 'horizontal' });
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
el.classList.add('-animated');
|
el.classList.add('-animated');
|
||||||
}, 200);
|
}, 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(
|
el.dispatchEvent(
|
||||||
new CustomEvent('customSeek', { detail: -1, bubbles: true })
|
new CustomEvent('customSeek', { detail: -1, bubbles: true })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
el.querySelector('.forward').addEventListener('click', function () {
|
el.querySelector('.forward').addEventListener('click', () => {
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('customSeek', { detail: 1, bubbles: true })
|
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 }));
|
el.dispatchEvent(new CustomEvent('addList', { detail: {}, bubbles: true }));
|
||||||
// TODO seek if not at end
|
// TODO seek if not at end
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('sortableDrop', function (e) {
|
el.addEventListener('sortableDrop', (e) => {
|
||||||
if (!e.detail.data.list) return;
|
if (!e.detail.data.list) return;
|
||||||
|
|
||||||
el.dispatchEvent(
|
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;
|
if (!e.detail.data.list) return;
|
||||||
|
|
||||||
updatePositions();
|
updatePositions();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.todoFrameCustom = {
|
el.addEventListener('todoData', (e) => update(e.detail));
|
||||||
update: update,
|
|
||||||
};
|
|
||||||
|
|
||||||
function update(next) {
|
function update(next) {
|
||||||
Object.assign(state, next);
|
Object.assign(state, next);
|
||||||
|
|
||||||
var lists = getLists();
|
const lists = getLists();
|
||||||
var container = el.querySelector('.container');
|
const container = el.querySelector('.container');
|
||||||
var obsolete = new Set(container.children);
|
const obsolete = new Set(container.children);
|
||||||
var childrenByKey = new Map();
|
const childrenByKey = new Map();
|
||||||
|
|
||||||
obsolete.forEach(function (child) {
|
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
|
||||||
childrenByKey.set(child.dataset.key, child);
|
|
||||||
});
|
|
||||||
|
|
||||||
var children = lists.map(function (list) {
|
const children = lists.map((list) => {
|
||||||
var child = childrenByKey.get(list.id);
|
let child = childrenByKey.get(list.id);
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
obsolete.delete(child);
|
obsolete.delete(child);
|
||||||
@ -90,19 +87,17 @@ VT.TodoFrameCustom = function (el) {
|
|||||||
child = document.createElement('div');
|
child = document.createElement('div');
|
||||||
child.className = 'card todo-custom-list';
|
child.className = 'card todo-custom-list';
|
||||||
child.dataset.key = list.id;
|
child.dataset.key = list.id;
|
||||||
VT.TodoCustomList(child);
|
TodoCustomList(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
child.todoCustomList.update({ list: list });
|
child.dispatchEvent(new CustomEvent('todoCustomList', { detail: list }));
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
|
|
||||||
obsolete.forEach(function (child) {
|
obsolete.forEach((child) => container.removeChild(child));
|
||||||
container.removeChild(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
children.forEach(function (child, index) {
|
children.forEach((child, index) => {
|
||||||
if (child !== container.children[index]) {
|
if (child !== container.children[index]) {
|
||||||
container.insertBefore(child, container.children[index]);
|
container.insertBefore(child, container.children[index]);
|
||||||
}
|
}
|
||||||
@ -113,54 +108,40 @@ VT.TodoFrameCustom = function (el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updatePositions() {
|
function updatePositions() {
|
||||||
el.querySelectorAll('.container > *').forEach(function (child, index) {
|
el.querySelectorAll('.container > *').forEach((child, index) => {
|
||||||
child.style.transform = 'translateX(' + (index - state.at) * 100 + '%)';
|
child.style.transform = `translateX(${(index - state.customAt) * 100}%)`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHeight() {
|
function updateHeight() {
|
||||||
var height = 280;
|
let height = 280;
|
||||||
var container = el.querySelector('.container');
|
const container = el.querySelector('.container');
|
||||||
|
|
||||||
var i, l;
|
for (let i = 0, l = container.children.length; i < l; ++i) {
|
||||||
|
|
||||||
for (i = 0, l = container.children.length; i < l; ++i) {
|
|
||||||
height = Math.max(container.children[i].offsetHeight, height);
|
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) {
|
for (let i = 0, l = container.children.length; i < l; ++i) {
|
||||||
container.children[i].style.height = height + 'px';
|
container.children[i].style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLists() {
|
function getLists() {
|
||||||
var lists = state.lists.map(function (list) {
|
return state.customLists
|
||||||
return {
|
.map((list) => ({
|
||||||
id: list.id,
|
id: list.id,
|
||||||
index: list.index,
|
index: list.index,
|
||||||
title: list.title,
|
title: list.title,
|
||||||
items: getItemsForList(list.id),
|
items: getItemsForList(list.id),
|
||||||
};
|
}))
|
||||||
});
|
.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
lists.sort(function (a, b) {
|
|
||||||
return a.index - b.index;
|
|
||||||
});
|
|
||||||
|
|
||||||
return lists;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemsForList(listId) {
|
function getItemsForList(listId) {
|
||||||
var items = state.items.filter(function (item) {
|
return state.items
|
||||||
return item.listId === listId;
|
.filter((item) => item.listId === listId)
|
||||||
});
|
.sort((a, b) => a.index - b.index);
|
||||||
|
}
|
||||||
items.sort(function (a, b) {
|
|
||||||
return a.index - b.index;
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
@ -1,71 +1,66 @@
|
|||||||
/* global VT */
|
import { AppIcon } from './AppIcon.js';
|
||||||
window.VT = window.VT || {};
|
import { TodoDay } from './TodoDay.js';
|
||||||
|
import { formatDateId } from './util.js';
|
||||||
|
|
||||||
VT.TodoFrameDays = function (el) {
|
export function TodoFrameDays(el) {
|
||||||
var RANGE = 14;
|
const RANGE = 14;
|
||||||
var state = {
|
const state = {
|
||||||
items: [],
|
items: [],
|
||||||
at: VT.formatDateId(new Date()),
|
at: formatDateId(new Date()),
|
||||||
};
|
};
|
||||||
|
|
||||||
el.innerHTML = [
|
el.innerHTML = `
|
||||||
'<nav class="leftcontrols">',
|
<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 -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 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>',
|
<p><button class="app-button home"><i class="app-icon" data-id="home-16"></i></button></p>
|
||||||
'</nav>',
|
</nav>
|
||||||
'<div class="container"></div>',
|
<div class="container"></div>
|
||||||
'<nav class="rightcontrols">',
|
<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 -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>',
|
<p><button class="app-button fastforward"><i class="app-icon -double" data-id="chevron-right-16"></i></button></p>
|
||||||
'</nav>',
|
</nav>
|
||||||
].join('\n');
|
`;
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(() => el.classList.add('-animated'), 200);
|
||||||
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.querySelector('.backward').addEventListener('click', () =>
|
||||||
el.dispatchEvent(new CustomEvent('seek', { detail: -1, bubbles: true }));
|
el.dispatchEvent(new CustomEvent('seek', { detail: -1, bubbles: true }))
|
||||||
});
|
);
|
||||||
|
|
||||||
el.querySelector('.forward').addEventListener('click', function () {
|
el.querySelector('.forward').addEventListener('click', () =>
|
||||||
el.dispatchEvent(new CustomEvent('seek', { detail: 1, bubbles: true }));
|
el.dispatchEvent(new CustomEvent('seek', { detail: 1, bubbles: true }))
|
||||||
});
|
);
|
||||||
|
|
||||||
el.querySelector('.fastbackward').addEventListener('click', function () {
|
el.querySelector('.fastbackward').addEventListener('click', () =>
|
||||||
el.dispatchEvent(new CustomEvent('seek', { detail: -5, bubbles: true }));
|
el.dispatchEvent(new CustomEvent('seek', { detail: -5, bubbles: true }))
|
||||||
});
|
);
|
||||||
|
|
||||||
el.querySelector('.fastforward').addEventListener('click', function () {
|
el.querySelector('.fastforward').addEventListener('click', () =>
|
||||||
el.dispatchEvent(new CustomEvent('seek', { detail: 5, bubbles: true }));
|
el.dispatchEvent(new CustomEvent('seek', { detail: 5, bubbles: true }))
|
||||||
});
|
);
|
||||||
|
|
||||||
el.querySelector('.home').addEventListener('click', function () {
|
el.querySelector('.home').addEventListener('click', () =>
|
||||||
el.dispatchEvent(new CustomEvent('seekHome', { bubbles: true }));
|
el.dispatchEvent(new CustomEvent('seekHome', { bubbles: true }))
|
||||||
});
|
);
|
||||||
|
|
||||||
el.todoFrameDays = {
|
el.addEventListener('todoData', (e) => update(e.detail));
|
||||||
update: update,
|
|
||||||
};
|
|
||||||
|
|
||||||
function update(next) {
|
function update(next) {
|
||||||
Object.assign(state, next);
|
Object.assign(state, next);
|
||||||
|
|
||||||
var days = getDays();
|
const days = getDays();
|
||||||
|
|
||||||
var container = el.querySelector('.container');
|
const container = el.querySelector('.container');
|
||||||
var obsolete = new Set(container.children);
|
const obsolete = new Set(container.children);
|
||||||
var childrenByKey = new Map();
|
const childrenByKey = new Map();
|
||||||
|
|
||||||
obsolete.forEach(function (child) {
|
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
|
||||||
childrenByKey.set(child.dataset.key, child);
|
|
||||||
});
|
|
||||||
|
|
||||||
var children = days.map(function (day) {
|
const children = days.map((day) => {
|
||||||
var child = childrenByKey.get(day.id);
|
let child = childrenByKey.get(day.id);
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
obsolete.delete(child);
|
obsolete.delete(child);
|
||||||
@ -73,20 +68,18 @@ VT.TodoFrameDays = function (el) {
|
|||||||
child = document.createElement('div');
|
child = document.createElement('div');
|
||||||
child.className = 'card todo-day';
|
child.className = 'card todo-day';
|
||||||
child.dataset.key = day.id;
|
child.dataset.key = day.id;
|
||||||
VT.TodoDay(child);
|
TodoDay(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
child.todoDay.update(day);
|
child.dispatchEvent(new CustomEvent('todoDay', { detail: day }));
|
||||||
child.style.transform = 'translateX(' + day.position * 100 + '%)';
|
child.style.transform = `translateX(${day.position * 100}%)`;
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
|
|
||||||
obsolete.forEach(function (child) {
|
obsolete.forEach((child) => container.removeChild(child));
|
||||||
container.removeChild(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
children.forEach(function (child, index) {
|
children.forEach((child, index) => {
|
||||||
if (child !== container.children[index]) {
|
if (child !== container.children[index]) {
|
||||||
container.insertBefore(child, container.children[index]);
|
container.insertBefore(child, container.children[index]);
|
||||||
}
|
}
|
||||||
@ -96,26 +89,26 @@ VT.TodoFrameDays = function (el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateHeight() {
|
function updateHeight() {
|
||||||
var height = 280;
|
let height = 280;
|
||||||
var container = el.querySelector('.container');
|
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);
|
height = Math.max(container.children[i].offsetHeight, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
el.style.height = height + 50 + 'px';
|
el.style.height = `${height + 50}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDays() {
|
function getDays() {
|
||||||
var days = [];
|
const days = [];
|
||||||
|
|
||||||
for (var i = 0; i < 2 * RANGE; ++i) {
|
for (let i = 0; i < 2 * RANGE; ++i) {
|
||||||
var t = new Date(state.at);
|
const t = new Date(state.at);
|
||||||
t.setDate(t.getDate() - RANGE + i);
|
t.setDate(t.getDate() - RANGE + i);
|
||||||
var id = VT.formatDateId(t);
|
const id = formatDateId(t);
|
||||||
|
|
||||||
days.push({
|
days.push({
|
||||||
id: id,
|
id,
|
||||||
items: getItemsForDay(id),
|
items: getItemsForDay(id),
|
||||||
position: -RANGE + i,
|
position: -RANGE + i,
|
||||||
});
|
});
|
||||||
@ -125,14 +118,8 @@ VT.TodoFrameDays = function (el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getItemsForDay(dateId) {
|
function getItemsForDay(dateId) {
|
||||||
var items = state.items.filter(function (item) {
|
return state.items
|
||||||
return item.listId === dateId;
|
.filter((item) => item.listId === dateId)
|
||||||
});
|
.sort((a, b) => a.index - b.index);
|
||||||
|
}
|
||||||
items.sort(function (a, b) {
|
|
||||||
return a.index - b.index;
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
@ -1,45 +1,46 @@
|
|||||||
/* global VT */
|
import { AppDraggable } from './AppDraggable.js';
|
||||||
window.VT = window.VT || {};
|
import { AppIcon } from './AppIcon.js';
|
||||||
|
|
||||||
VT.TodoItem = function (el) {
|
export function TodoItem(el) {
|
||||||
var state = {
|
const state = {
|
||||||
item: null,
|
item: null,
|
||||||
editing: false,
|
editing: false,
|
||||||
};
|
};
|
||||||
var startEditing = false;
|
|
||||||
var saveOnBlur = true;
|
|
||||||
|
|
||||||
el.innerHTML = [
|
let startEditing = false;
|
||||||
'<div class="checkbox">',
|
let saveOnBlur = true;
|
||||||
' <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');
|
|
||||||
|
|
||||||
var checkboxEl = el.querySelector('.checkbox');
|
el.innerHTML = `
|
||||||
var labelEl = el.querySelector('.label');
|
<div class="checkbox">
|
||||||
var inputEl = el.querySelector('.input');
|
<input type="checkbox">
|
||||||
var saveEl = el.querySelector('.save');
|
</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',
|
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;
|
saveOnBlur = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
checkboxEl.addEventListener('mousedown', function () {
|
checkboxEl.addEventListener('mousedown', () => {
|
||||||
saveOnBlur = false;
|
saveOnBlur = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
checkboxEl.addEventListener('click', function () {
|
checkboxEl.addEventListener('click', () => {
|
||||||
if (state.editing) save();
|
if (state.editing) save();
|
||||||
|
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
@ -53,12 +54,12 @@ VT.TodoItem = function (el) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
labelEl.addEventListener('click', function () {
|
labelEl.addEventListener('click', () => {
|
||||||
startEditing = true;
|
startEditing = true;
|
||||||
update({ editing: true });
|
update({ editing: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
inputEl.addEventListener('keyup', function (e) {
|
inputEl.addEventListener('keyup', (e) => {
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
case 13: // enter
|
case 13: // enter
|
||||||
save();
|
save();
|
||||||
@ -69,39 +70,37 @@ VT.TodoItem = function (el) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
inputEl.addEventListener('blur', function () {
|
inputEl.addEventListener('blur', () => {
|
||||||
if (saveOnBlur) save();
|
if (saveOnBlur) save();
|
||||||
saveOnBlur = true;
|
saveOnBlur = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
inputEl.addEventListener('focusOther', function () {
|
inputEl.addEventListener('focusOther', () => {
|
||||||
if (state.editing) save();
|
if (state.editing) save();
|
||||||
});
|
});
|
||||||
|
|
||||||
saveEl.addEventListener('mousedown', function () {
|
saveEl.addEventListener('mousedown', () => {
|
||||||
saveOnBlur = false;
|
saveOnBlur = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
saveEl.addEventListener('click', save);
|
saveEl.addEventListener('click', save);
|
||||||
|
|
||||||
el.addEventListener('draggableStart', function (e) {
|
el.addEventListener('draggableStart', (e) => {
|
||||||
e.detail.data.item = state.item;
|
e.detail.data.item = state.item;
|
||||||
e.detail.data.key = state.item.id;
|
e.detail.data.key = state.item.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
el.todoItem = {
|
el.addEventListener('todoItem', (e) => update({ item: e.detail }));
|
||||||
update: update,
|
|
||||||
};
|
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
var label = inputEl.value.trim();
|
const label = inputEl.value.trim();
|
||||||
|
|
||||||
if (label === '') {
|
if (label === '') {
|
||||||
// deferred deletion prevents a bug at reconciliation in TodoList:
|
// deferred deletion prevents a bug at reconciliation in TodoList:
|
||||||
// Failed to execute 'removeChild' on 'Node': The node to be removed is
|
// 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'
|
// no longer a child of this node. Perhaps it was moved in a 'blur'
|
||||||
// event handler?
|
// event handler?
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(() => {
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('deleteItem', {
|
new CustomEvent('deleteItem', {
|
||||||
detail: state.item,
|
detail: state.item,
|
||||||
@ -117,7 +116,7 @@ VT.TodoItem = function (el) {
|
|||||||
new CustomEvent('saveItem', {
|
new CustomEvent('saveItem', {
|
||||||
detail: {
|
detail: {
|
||||||
item: state.item,
|
item: state.item,
|
||||||
label: label,
|
label,
|
||||||
},
|
},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
})
|
})
|
||||||
@ -149,4 +148,4 @@ VT.TodoItem = function (el) {
|
|||||||
startEditing = false;
|
startEditing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
/* global VT */
|
import { AppIcon } from './AppIcon.js';
|
||||||
window.VT = window.VT || {};
|
|
||||||
|
|
||||||
VT.TodoItemInput = function (el) {
|
export function TodoItemInput(el) {
|
||||||
var saveOnBlur = true;
|
let saveOnBlur = true;
|
||||||
|
|
||||||
el.innerHTML = [
|
el.innerHTML = `
|
||||||
'<input type="text" class="input use-focus-other">',
|
<input type="text" class="input use-focus-other">
|
||||||
'<button class="app-button save"><i class="app-icon" data-id="plus-24"></i></button>',
|
<button class="app-button save"><i class="app-icon" data-id="plus-24"></i></button>
|
||||||
].join('\n');
|
`;
|
||||||
|
|
||||||
var inputEl = el.querySelector('.input');
|
const inputEl = el.querySelector('.input');
|
||||||
var saveEl = el.querySelector('.save');
|
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) {
|
switch (e.keyCode) {
|
||||||
case 13: // enter
|
case 13: // enter
|
||||||
save();
|
save();
|
||||||
@ -25,24 +24,24 @@ VT.TodoItemInput = function (el) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
inputEl.addEventListener('blur', function () {
|
inputEl.addEventListener('blur', () => {
|
||||||
if (saveOnBlur) save();
|
if (saveOnBlur) save();
|
||||||
saveOnBlur = true;
|
saveOnBlur = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
inputEl.addEventListener('focusOther', save);
|
inputEl.addEventListener('focusOther', save);
|
||||||
|
|
||||||
saveEl.addEventListener('mousedown', function () {
|
saveEl.addEventListener('mousedown', () => {
|
||||||
saveOnBlur = false;
|
saveOnBlur = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
saveEl.addEventListener('click', function () {
|
saveEl.addEventListener('click', () => {
|
||||||
save();
|
save();
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
var label = inputEl.value.trim();
|
const label = inputEl.value.trim();
|
||||||
|
|
||||||
if (label === '') return;
|
if (label === '') return;
|
||||||
|
|
||||||
@ -50,7 +49,7 @@ VT.TodoItemInput = function (el) {
|
|||||||
|
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('addItem', {
|
new CustomEvent('addItem', {
|
||||||
detail: { label: label },
|
detail: { label },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -60,4 +59,4 @@ VT.TodoItemInput = function (el) {
|
|||||||
inputEl.value = '';
|
inputEl.value = '';
|
||||||
inputEl.blur();
|
inputEl.blur();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
/* global VT */
|
import { AppSortable } from './AppSortable.js';
|
||||||
window.VT = window.VT || {};
|
import { TodoItem } from './TodoItem.js';
|
||||||
|
import { TodoItemInput } from './TodoItemInput.js';
|
||||||
|
|
||||||
VT.TodoList = function (el) {
|
export function TodoList(el) {
|
||||||
var state = {
|
const state = {
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
el.innerHTML = [
|
el.innerHTML = `
|
||||||
'<div class="items"></div>',
|
<div class="items"></div>
|
||||||
'<div class="todo-item-input"></div>',
|
<div class="todo-item-input"></div>
|
||||||
].join('\n');
|
`;
|
||||||
|
|
||||||
VT.AppSortable(el.querySelector('.items'), {});
|
AppSortable(el.querySelector('.items'), {});
|
||||||
VT.TodoItemInput(el.querySelector('.todo-item-input'));
|
TodoItemInput(el.querySelector('.todo-item-input'));
|
||||||
|
|
||||||
el.addEventListener('sortableDrop', function (e) {
|
el.addEventListener('sortableDrop', (e) =>
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('moveItem', {
|
new CustomEvent('moveItem', {
|
||||||
detail: {
|
detail: {
|
||||||
@ -23,22 +24,22 @@ VT.TodoList = function (el) {
|
|||||||
},
|
},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
el.addEventListener('todoItems', (e) => update({ items: e.detail }));
|
||||||
|
|
||||||
function update(next) {
|
function update(next) {
|
||||||
Object.assign(state, next);
|
Object.assign(state, next);
|
||||||
|
|
||||||
var container = el.querySelector('.items');
|
const container = el.querySelector('.items');
|
||||||
var obsolete = new Set(container.children);
|
const obsolete = new Set(container.children);
|
||||||
var childrenByKey = new Map();
|
const childrenByKey = new Map();
|
||||||
|
|
||||||
obsolete.forEach(function (child) {
|
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
|
||||||
childrenByKey.set(child.dataset.key, child);
|
|
||||||
});
|
|
||||||
|
|
||||||
var children = state.items.map(function (item) {
|
const children = state.items.map((item) => {
|
||||||
var child = childrenByKey.get(item.id);
|
let child = childrenByKey.get(item.id);
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
obsolete.delete(child);
|
obsolete.delete(child);
|
||||||
@ -46,26 +47,20 @@ VT.TodoList = function (el) {
|
|||||||
child = document.createElement('div');
|
child = document.createElement('div');
|
||||||
child.classList.add('todo-item');
|
child.classList.add('todo-item');
|
||||||
child.dataset.key = item.id;
|
child.dataset.key = item.id;
|
||||||
VT.TodoItem(child);
|
TodoItem(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
child.todoItem.update({ item: item });
|
child.dispatchEvent(new CustomEvent('todoItem', { detail: item }));
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
|
|
||||||
obsolete.forEach(function (child) {
|
obsolete.forEach((child) => container.removeChild(child));
|
||||||
container.removeChild(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
children.forEach(function (child, index) {
|
children.forEach((child, index) => {
|
||||||
if (child !== container.children[index]) {
|
if (child !== container.children[index]) {
|
||||||
container.insertBefore(child, container.children[index]);
|
container.insertBefore(child, container.children[index]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
el.todoList = {
|
|
||||||
update: update,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -1,28 +1,30 @@
|
|||||||
/* global VT */
|
import { formatDateId, uuid } from './util.js';
|
||||||
window.VT = window.VT || {};
|
|
||||||
|
|
||||||
VT.TodoStore = function (el) {
|
export function TodoStore(el) {
|
||||||
var state = {
|
const state = {
|
||||||
items: [],
|
items: [],
|
||||||
customLists: [],
|
customLists: [],
|
||||||
at: VT.formatDateId(new Date()),
|
at: formatDateId(new Date()),
|
||||||
customAt: 0,
|
customAt: 0,
|
||||||
};
|
};
|
||||||
var storeTimeout;
|
|
||||||
|
|
||||||
el.addEventListener('addItem', function (e) {
|
let storeTimeout;
|
||||||
var index = 0;
|
|
||||||
|
|
||||||
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) {
|
if (item.listId === e.detail.listId) {
|
||||||
index = Math.max(index, item.index + 1);
|
index = Math.max(index, item.index + 1);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
state.items.push({
|
state.items.push({
|
||||||
id: VT.uuid(),
|
id: uuid(),
|
||||||
listId: e.detail.listId,
|
listId: e.detail.listId,
|
||||||
index: index,
|
index,
|
||||||
label: e.detail.label,
|
label: e.detail.label,
|
||||||
done: false,
|
done: false,
|
||||||
});
|
});
|
||||||
@ -30,71 +32,61 @@ VT.TodoStore = function (el) {
|
|||||||
dispatch({ items: state.items });
|
dispatch({ items: state.items });
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('checkItem', function (e) {
|
el.addEventListener('checkItem', (e) => {
|
||||||
if (e.detail.item.done === e.detail.done) return;
|
if (e.detail.item.done === e.detail.done) return;
|
||||||
|
|
||||||
e.detail.item.done = e.detail.done;
|
e.detail.item.done = e.detail.done;
|
||||||
dispatch({ items: state.items });
|
dispatch({ items: state.items });
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('saveItem', function (e) {
|
el.addEventListener('saveItem', (e) => {
|
||||||
if (e.detail.item.label === e.detail.label) return;
|
if (e.detail.item.label === e.detail.label) return;
|
||||||
|
|
||||||
e.detail.item.label = e.detail.label;
|
e.detail.item.label = e.detail.label;
|
||||||
dispatch({ items: state.items });
|
dispatch({ items: state.items });
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('moveItem', function (e) {
|
el.addEventListener('moveItem', (e) => {
|
||||||
var movedItem = state.items.find(function (item) {
|
const movedItem = state.items.find((item) => item.id === e.detail.item.id);
|
||||||
return item.id === e.detail.item.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
var listItems = state.items.filter(function (item) {
|
const listItems = state.items.filter(
|
||||||
return item.listId === e.detail.listId && item !== movedItem;
|
(item) => item.listId === e.detail.listId && item !== movedItem
|
||||||
});
|
);
|
||||||
|
|
||||||
listItems.sort(function (a, b) {
|
listItems.sort((a, b) => a.index - b.index);
|
||||||
return a.index - b.index;
|
|
||||||
});
|
|
||||||
|
|
||||||
movedItem.listId = e.detail.listId;
|
movedItem.listId = e.detail.listId;
|
||||||
listItems.splice(e.detail.index, 0, movedItem);
|
listItems.splice(e.detail.index, 0, movedItem);
|
||||||
|
|
||||||
listItems.forEach(function (item, index) {
|
listItems.forEach((item, index) => {
|
||||||
item.index = index;
|
item.index = index;
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({ items: state.items });
|
dispatch({ items: state.items });
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('deleteItem', function (e) {
|
el.addEventListener('deleteItem', (e) => {
|
||||||
dispatch({
|
dispatch({ items: state.items.filter((item) => item.id !== e.detail.id) });
|
||||||
items: state.items.filter(function (item) {
|
|
||||||
return item.id !== e.detail.id;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('addList', function (e) {
|
el.addEventListener('addList', (e) => {
|
||||||
var index = 0;
|
let index = 0;
|
||||||
|
|
||||||
state.customLists.forEach(function (customList) {
|
for (const customList of state.customLists) {
|
||||||
index = Math.max(index, customList.index + 1);
|
index = Math.max(index, customList.index + 1);
|
||||||
});
|
}
|
||||||
|
|
||||||
state.customLists.push({
|
state.customLists.push({
|
||||||
id: VT.uuid(),
|
id: uuid(),
|
||||||
index: index,
|
index,
|
||||||
title: e.detail.title || '',
|
title: e.detail.title || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({ customLists: state.customLists });
|
dispatch({ customLists: state.customLists });
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('saveList', function (e) {
|
el.addEventListener('saveList', (e) => {
|
||||||
var list = state.customLists.find(function (l) {
|
const list = state.customLists.find((l) => l.id === e.detail.list.id);
|
||||||
return l.id === e.detail.list.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (list.title === e.detail.title) return;
|
if (list.title === e.detail.title) return;
|
||||||
|
|
||||||
@ -103,49 +95,43 @@ VT.TodoStore = function (el) {
|
|||||||
dispatch({ customLists: state.customLists });
|
dispatch({ customLists: state.customLists });
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('moveList', function (e) {
|
el.addEventListener('moveList', (e) => {
|
||||||
var movedListIndex = state.customLists.findIndex(function (list) {
|
const movedListIndex = state.customLists.findIndex(
|
||||||
return list.id === e.detail.list.id;
|
(list) => list.id === e.detail.list.id
|
||||||
});
|
);
|
||||||
var movedList = state.customLists[movedListIndex];
|
const movedList = state.customLists[movedListIndex];
|
||||||
|
|
||||||
state.customLists.splice(movedListIndex, 1);
|
state.customLists.splice(movedListIndex, 1);
|
||||||
state.customLists.sort(function (a, b) {
|
state.customLists.sort((a, b) => a.index - b.index);
|
||||||
return a.index - b.index;
|
|
||||||
});
|
|
||||||
state.customLists.splice(e.detail.index, 0, movedList);
|
state.customLists.splice(e.detail.index, 0, movedList);
|
||||||
|
|
||||||
state.customLists.forEach(function (item, index) {
|
state.customLists.forEach((item, index) => {
|
||||||
item.index = index;
|
item.index = index;
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({ customLists: state.customLists });
|
dispatch({ customLists: state.customLists });
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('deleteList', function (e) {
|
el.addEventListener('deleteList', (e) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
customLists: state.customLists.filter(function (customList) {
|
customLists: state.customLists.filter(
|
||||||
return customList.id !== e.detail.id;
|
(customList) => customList.id !== e.detail.id
|
||||||
}),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('seek', function (e) {
|
el.addEventListener('seek', (e) => {
|
||||||
var t = new Date(state.at + ' 00:00:00');
|
const t = new Date(`${state.at} 00:00:00`);
|
||||||
t.setDate(t.getDate() + e.detail);
|
t.setDate(t.getDate() + e.detail);
|
||||||
|
|
||||||
dispatch({
|
dispatch({ at: formatDateId(t) });
|
||||||
at: VT.formatDateId(t),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('seekHome', function () {
|
el.addEventListener('seekHome', () =>
|
||||||
dispatch({
|
dispatch({ at: formatDateId(new Date()) })
|
||||||
at: VT.formatDateId(new Date()),
|
);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('customSeek', function (e) {
|
el.addEventListener('customSeek', (e) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
customAt: Math.max(
|
customAt: Math.max(
|
||||||
0,
|
0,
|
||||||
@ -156,7 +142,7 @@ VT.TodoStore = function (el) {
|
|||||||
|
|
||||||
function dispatch(next) {
|
function dispatch(next) {
|
||||||
Object.assign(state, next);
|
Object.assign(state, next);
|
||||||
store();
|
save();
|
||||||
|
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('todoData', {
|
new CustomEvent('todoData', {
|
||||||
@ -175,24 +161,21 @@ VT.TodoStore = function (el) {
|
|||||||
try {
|
try {
|
||||||
dispatch(JSON.parse(localStorage.todo));
|
dispatch(JSON.parse(localStorage.todo));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function store() {
|
function save() {
|
||||||
clearTimeout(storeTimeout);
|
clearTimeout(storeTimeout);
|
||||||
|
|
||||||
storeTimeout = setTimeout(function () {
|
storeTimeout = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
localStorage.todo = JSON.stringify(state);
|
localStorage.todo = JSON.stringify(state);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
el.todoStore = {
|
|
||||||
dispatch: dispatch,
|
|
||||||
load: load,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -1,54 +1,45 @@
|
|||||||
/* global VT */
|
export function uuid() {
|
||||||
window.VT = window.VT || {};
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = (Math.random() * 16) | 0,
|
||||||
VT.uuid = function () {
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
||||||
var r = (Math.random() * 16) | 0,
|
|
||||||
v = c == 'x' ? r : (r & 0x3) | 0x8;
|
v = c == 'x' ? r : (r & 0x3) | 0x8;
|
||||||
return v.toString(16);
|
return v.toString(16);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
VT.formatDateId = function (date) {
|
export function formatDateId(date) {
|
||||||
var y = date.getFullYear();
|
const y = date.getFullYear();
|
||||||
var m = date.getMonth() + 1;
|
const m = date.getMonth() + 1;
|
||||||
var d = date.getDate();
|
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 (
|
return `${ys}-${ms}-${ds}`;
|
||||||
y.toString().padStart(4, '0') +
|
}
|
||||||
'-' +
|
|
||||||
m.toString().padStart(2, '0') +
|
|
||||||
'-' +
|
|
||||||
d.toString().padStart(2, '0')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
VT.formatDate = function (date) {
|
export function formatDate(date) {
|
||||||
return (
|
const m = formatMonth(date);
|
||||||
VT.formatMonth(date) +
|
const d = formatDayOfMonth(date);
|
||||||
' ' +
|
const y = date.getFullYear().toString().padStart(4, '0');
|
||||||
VT.formatDayOfMonth(date) +
|
return `${m} ${d} ${y}`;
|
||||||
' ' +
|
}
|
||||||
date.getFullYear().toString().padStart(4, '0')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
VT.formatDayOfMonth = function (date) {
|
export function formatDayOfMonth(date) {
|
||||||
var d = date.getDate();
|
const d = date.getDate();
|
||||||
var t = d % 10;
|
const t = d % 10;
|
||||||
|
|
||||||
return d === 11 || d === 12 || d === 13
|
return d === 11 || d === 12 || d === 13
|
||||||
? d + 'th'
|
? `${d}th`
|
||||||
: t === 1
|
: t === 1
|
||||||
? d + 'st'
|
? `${d}st`
|
||||||
: t === 2
|
: t === 2
|
||||||
? d + 'nd'
|
? `${d}nd`
|
||||||
: t === 3
|
: t === 3
|
||||||
? d + 'rd'
|
? `${d}rd`
|
||||||
: d + 'th';
|
: `${d}th`;
|
||||||
};
|
}
|
||||||
|
|
||||||
VT.DAY_NAMES = [
|
export const DAY_NAMES = [
|
||||||
'Sunday',
|
'Sunday',
|
||||||
'Monday',
|
'Monday',
|
||||||
'Tuesday',
|
'Tuesday',
|
||||||
@ -58,11 +49,11 @@ VT.DAY_NAMES = [
|
|||||||
'Saturday',
|
'Saturday',
|
||||||
];
|
];
|
||||||
|
|
||||||
VT.formatDayOfWeek = function (date) {
|
export function formatDayOfWeek(date) {
|
||||||
return VT.DAY_NAMES[date.getDay()];
|
return DAY_NAMES[date.getDay()];
|
||||||
};
|
}
|
||||||
|
|
||||||
VT.MONTH_NAMES = [
|
export const MONTH_NAMES = [
|
||||||
'January',
|
'January',
|
||||||
'February',
|
'February',
|
||||||
'March',
|
'March',
|
||||||
@ -77,6 +68,6 @@ VT.MONTH_NAMES = [
|
|||||||
'December',
|
'December',
|
||||||
];
|
];
|
||||||
|
|
||||||
VT.formatMonth = function (date) {
|
export function formatMonth(date) {
|
||||||
return VT.MONTH_NAMES[date.getMonth()];
|
return MONTH_NAMES[date.getMonth()];
|
||||||
};
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
background: #eee;
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-collapsible > .bar > .app-button:active {
|
.app-collapsible > .bar > .toggle:active {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header > .app-fps {
|
.app-header > .fps {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
fill: currentColor;
|
fill: currentcolor;
|
||||||
transition: transform 0.1s ease-out;
|
transition: transform 0.1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,8 +52,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.todo-custom-list.-dragging {
|
.todo-custom-list.-dragging {
|
||||||
box-shadow: 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, 0.3);
|
-10px 0 12px -14px rgba(0, 0, 0, 30%);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user