<section id="my-app"></section>
* {
box-sizing: border-box;
}
body {
margin: 3em;
background-color: #f3f3f3;
}
input {
width: 320px;
padding: 25px;
}
.posRel {
position: relative;
}
.toggleAll {
position: absolute;
top: 50%;
transform: translateY(-50%) rotate(90deg);
left: 8px;
cursor: pointer
}
button {
cursor: pointer;
}
.filter {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: white;
border-top: solid 1px;
padding: 10px;
button {
background-color: transparent;
border: solid 1px;
border-color: transparent;
&.selected {
border-color: #d6b2b2;
}
}
}
::-webkit-input-placeholder{
color: #c1c1c1;
font-style: italic;
}
.todo {
max-width: 320px;
margin: 0 auto;
h1 {
text-align: center;
font-size: 2em;
color: #d6b2b2;
}
}
.list-container {
width: 100%;
}
.item {
display: flex;
align-items: center;
margin-top: 0.5em;
padding: 25px;
padding-left: 30px;
background-color: #ffffff;
position: relative;
&.itemChecked {
text-decoration: line-through;
color: #bdbdbd;
}
&:hover {
svg {
display: block;
}
}
svg {
cursor: pointer;
position: absolute;
right: 25px;
display: none;
top: 50%;
transform: translateY(-50%);
}
}
.check {
width: 15px;
height: 15px;
border-radius: 50%;
border: solid 1px;
position: absolute;
left: 7px;
cursor: pointer;
&.isChecked {
background-color: black;
}
}
.hidden {
display: none;
}
View Compiled
const vDiff = (target, source) => {
const worker = {
settings: {
original: target,
},
replace(target, source = target) {
const v = new DOMParser().parseFromString(source, 'text/html').body.childNodes
const vHTML = v[0];
if (vHTML.nodeName !== target.nodeName) {
target.parentElement.replaceChild(vHTML, target);
return;
}
this.iterate(target, vHTML);
},
iterate(targetNode, sourceNode, tOriginal) {
if (targetNode || sourceNode) {
this.checkAdditions(targetNode, sourceNode, tOriginal);
if (targetNode && sourceNode && targetNode.nodeName !== sourceNode.nodeName) {
this.checkNodeName(targetNode, sourceNode);
} else if (targetNode && sourceNode && targetNode.nodeName === sourceNode.nodeName) {
this.checkTextContent(targetNode, sourceNode);
targetNode.nodeType !== 3 && target.nodeType !== 8 && this.checkAttributes(targetNode, sourceNode);
}
}
if (targetNode && sourceNode) {
if (targetNode.childNodes && sourceNode.childNodes) {
this.settings.lengthDifferentiator = [...target.childNodes, ...sourceNode.childNodes];
} else {
this.settings.lengthDifferentiator = null;
}
Array.apply(null, this.settings.lengthDifferentiator).forEach((node, idx) => {
this.settings.lengthDifferentiator && this.iterate(targetNode.childNodes[idx], sourceNode.childNodes[idx], targetNode, sourceNode);
});
}
},
checkNodeName(targetNode, sourceNode) {
const n = sourceNode.cloneNode(true);
targetNode.parentElement.replaceChild(n, targetNode);
},
checkAttributes(targetNode, sourceNode) {
const attributes = targetNode.attributes || [];
const filteredAttrs = Object.keys(attributes).map((n) => attributes[n]);
const attributesNew = sourceNode.attributes || [];
const filteredAttrsNew = Object.keys(attributesNew).map((n) => attributesNew[n]);
filteredAttrs.forEach(o => {
return sourceNode.getAttribute(o.name) !== null ? targetNode.setAttribute(o.name, sourceNode.getAttribute(o.name)) : targetNode.removeAttribute(o.name);
});
filteredAttrsNew.forEach(a => {
return targetNode.getAttribute(a.name) !== sourceNode.getAttribute(a.name) && targetNode.setAttribute(a.name, sourceNode.getAttribute(a.name));
});
},
checkTextContent(targetNode, sourceNode) {
if (targetNode.nodeValue !== sourceNode.nodeValue) {
targetNode.textContent = sourceNode.textContent;
}
},
checkAdditions(targetNode, sourceNode, tParent = this.settings.original) {
if (sourceNode && targetNode === undefined) {
const newNode = sourceNode.cloneNode(true);
tParent.nodeType !== 3 && tParent.nodeType !== 8 && tParent.appendChild(newNode);
} else if (targetNode && sourceNode === undefined) {
targetNode.parentElement.removeChild(targetNode);
}
}
};
Object.create(worker).replace(target, source);
};
///////////////////////////////////////
const state = {
itemCounter: 0,
toDoItems: [],
filterSelect: 'all',
allChecked: false,
};
function encodeInput(v) {
return v.replace(/[\"&<>\']/g, function (a) {
return { '"': '"', '&': '&', '<': '<', '>': '>', "'": "\'" }[a];
});
}
function filterBy(filter) {
state.toDoItems.forEach(i => {
if (filter === 'all') {
i.visible = true;
} else if (filter === 'completed') {
i.visible = i.checked;
} else {
i.visible = !i.checked;
}
});
state.filterSelect = filter;
buildApp(state);
}
function addToDo(self) {
state.toDoItems.push({
val: self.value,
label: encodeInput(self.value),
id: state.itemCounter++,
checked: false,
visible: state.filterSelect !== 'completed',
});
self.value = '';
buildApp(state);
}
function checkToDo(id) {
state.toDoItems.forEach(i => {
if (i.id === id) {
i.checked = !i.checked;
i.visible = (i.checked === true && state.filterSelect !== 'active') || (i.checked === false && state.filterSelect !== 'completed');
}
});
buildApp(state);
}
function removeToDo(id) {
const newToDos = state.toDoItems.filter(i => i.id !== id);
state.toDoItems = newToDos;
buildApp(state);
}
function toggleCheckAll() {
state.allChecked = !state.allChecked;
state.toDoItems.forEach(i => {
i.checked = state.allChecked;
i.visible = (state.allChecked && state.filterSelect !== 'active') || (state.allChecked === false && state.filterSelect !== 'completed');
});
buildApp(state);
}
function editToDo(val, id) {
state.toDoItems.some(i => {
if (i.id === id && val.innerText !== i.val) {
i.val = val.innerText;
i.label = encodeInput(val.innerText);
buildApp(state);
return true;
}
return false;
});
}
function buildApp(newState = state) {
const appString = `
<section id="my-app">
<div class="todo">
<h1>todos</h1>
<div class="posRel">
<span onclick="toggleCheckAll()" class="${state.toDoItems.length ? 'toggleAll' : 'hidden'}">❯</span>
<input type="text" placeholder="What needs to be done?" onchange="addToDo(this)" />
</div>
<div class="list-container">
${newState.toDoItems.map(v => `
<div class="${v.checked ? 'itemChecked' : ''} ${v.visible ? '' : 'hidden'} item">
<div class="${v.checked ? 'isChecked' : ''} check" onClick="checkToDo(${v.id})"></div>
<svg onclick="removeToDo(${v.id})" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M3 6v18h18V6H3zm5 14a1 1 0 0 1-2 0V10a1 1 0 0 1 2 0v10zm5 0a1 1 0 0 1-2 0V10a1 1 0 0 1 2 0v10zm5 0a1 1 0 0 1-2 0V10a1 1 0 0 1 2 0v10zm4-18v2H2V2h5.711c.9 0 1.631-1.099 1.631-2h5.315c0 .901.73 2 1.631 2H22z"/>
</svg>
<span contenteditable onblur="editToDo(this, ${v.id})">${v.label}</span>
</div>
`).join('')}
${newState.toDoItems.length ? `
<div class="filter">
<p>${newState.toDoItems.filter(i => !i.checked).length} Items left</p>
<button onclick="filterBy('all')" class="${state.filterSelect === 'all' ? 'selected' : ''}">All</button>
<button onclick="filterBy('active')" class="${state.filterSelect === 'active' ? 'selected' : ''}">Active</button>
<button onclick="filterBy('completed')" class="${state.filterSelect === 'completed' ? 'selected' : ''}">Completed</button>
</div>
` : ''}
</div>
</div>
</section>
`;
vDiff(document.querySelector('#my-app'), appString);
}
buildApp();
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.