This commit is contained in:
Philippe Torrel
2026-10-08 14:26:06 +02:00
parent a064006966
commit ebe9ab0e34
30 changed files with 1450 additions and 0 deletions
@@ -0,0 +1,40 @@
*,
html {
box-sizing: border-box;
}
html,
body {
padding: 0;
margin: 0;
height: 100%;
}
/* Mobile first */
.product-manager table tfoot .row > * {
margin-bottom: 0.5rem;
}
@media (min-width: 992px) {
.product-manager table tfoot .row > * {
margin-bottom: 0;
}
}
.product-manager table td {
vertical-align: middle;
}
.product-manager table tbody:empty::after {
content: 'loading products...';
color: #999;
padding: 1rem;
}
.product-manager table td button {
margin: 0 3px;
}
.product-manager table .th-actions {
width: 185px;
}
@@ -0,0 +1,264 @@
'use strict';
(() => {
// === DOM & VARS =======
const module = document.querySelector('.product-manager') || console.warn('Product Manager Element not found');
if (!module) return;
const DOM = {
module,
table: module.querySelector('.table-products'),
tbody: module.querySelector('tbody'),
inputName: module.querySelector('.input-product-name'),
inputPrice: module.querySelector('.input-product-price'),
btnAdd: module.querySelector('.button-product-add'),
templateRow: module.querySelector('.template-row'),
};
console.log(DOM);
// === INIT =============
const init = () => {
// Funktionsaufrufe zu beginn der Anwendung
initProducts();
// Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click', onClickAdd);
};
// === EVENTHANDLER =====
const onClickAdd = (e) => {
const product = {
name: DOM.inputName.value, //
price: Number(DOM.inputPrice.valueAsNumber),
};
addProduct(product);
disableNonFunctionalButtons();
};
const onClickRemove = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0]; // btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.remove();
// currentRow.parentNode.removeChild(currentRow); // IE11 support
disableNonFunctionalButtons();
};
const onClickMoveUp = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0];
DOM.tbody.insertBefore(currentRow, currentRow.previousElementSibling);
disableNonFunctionalButtons();
};
const onClickMoveDown = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0];
DOM.tbody.insertBefore(currentRow, currentRow.nextElementSibling.nextElementSibling);
disableNonFunctionalButtons();
};
// === XHR/FETCH ========
const fetchProducts = async () => {
try {
const response = await fetch('/data/products.json'); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
console.error(error);
throw error;
}
};
// === FUNCTIONS ========
const initProducts = () => {
fetchProducts().then((products) => {
products.forEach(addProduct);
disableNonFunctionalButtons();
});
// PRODUCTS.forEach((product) => {
// addProduct(product);
// });
// PRODUCTS.forEach(addProduct);
// disableNonFunctionalButtons();
};
const addProduct = (product) => {
const { name, price } = product;
// https://developer.mozilla.org/de/docs/Web/HTML/Reference/Elements/template#anwendungshinweise
const trEl = DOM.templateRow.content.firstElementChild.cloneNode(true);
const tdNameEl = trEl.querySelector('.td-name');
const tdPriceEl = trEl.querySelector('.td-price');
const btnRemoveEl = trEl.querySelector('.button-product-remove');
const btnMoveUpEl = trEl.querySelector('.button-product-move-up');
const btnMoveDownEl = trEl.querySelector('.button-product-move-down');
tdNameEl.textContent = name;
tdPriceEl.textContent = Number(price).toFixed(2);
btnRemoveEl.addEventListener('click', onClickRemove);
btnMoveUpEl.addEventListener('click', onClickMoveUp);
btnMoveDownEl.addEventListener('click', onClickMoveDown);
// const tdNameEl = createTd(name, 'td-name');
// const tdPriceEl = createTd(price, 'td-price');
// const tdActionsEl = createTdWithActions();
// const trEl = createTr(tdNameEl, tdPriceEl, tdActionsEl);
DOM.tbody.appendChild(trEl);
};
// const createTdWithActions = () => {
// const td = document.createElement('td');
// td.classList.add('td-actions');
// const btnRemove = createBtn({
// classNames: ['btn-danger', 'button-product-remove'],
// iconName: 'trash-can',
// label: 'Remove product',
// clickHandler: onClickRemove,
// });
// const btnMoveUp = createBtn({
// classNames: ['btn-secondary', 'button-product-move-up'],
// iconName: 'chevron-up',
// label: 'Move up',
// clickHandler: onClickMoveUp,
// });
// const btnMoveDown = createBtn({
// classNames: ['btn-secondary', 'button-product-move-down'],
// iconName: 'chevron-down',
// label: 'Move down',
// clickHandler: onClickMoveDown,
// });
// td.append(btnRemove, btnMoveUp, btnMoveDown);
// return td;
// };
// const createBtnRemove = () => {
// const btn = document.createElement('button');
// const icon = document.createElement('i');
// const span = document.createElement('span');
// btn.classList.add('btn', 'btn-danger', 'button-product-remove');
// icon.classList.add('fas', 'fa-trash-can');
// span.classList.add('visually-hidden');
// span.textContent = 'Remove Product';
// btn.addEventListener('click', onClickRemove);
// btn.appendChild(icon);
// btn.appendChild(span);
// return btn;
// };
// const createBtnMoveUp = () => {
// const btn = document.createElement('button');
// const icon = document.createElement('i');
// const span = document.createElement('span');
// btn.classList.add('btn', 'btn-secondary', 'button-product-move-up');
// icon.classList.add('fas', 'fa-chevron-up');
// span.classList.add('visually-hidden');
// span.textContent = 'Move up';
// btn.addEventListener('click', onClickMoveUp);
// btn.appendChild(icon);
// btn.appendChild(span);
// return btn;
// };
// const createBtnMoveDown = () => {
// const btn = document.createElement('button');
// const icon = document.createElement('i');
// const span = document.createElement('span');
// btn.classList.add('btn', 'btn-secondary', 'button-product-move-down');
// icon.classList.add('fas', 'fa-chevron-down');
// span.classList.add('visually-hidden');
// span.textContent = 'Move down';
// btn.addEventListener('click', onClickMoveDown);
// btn.appendChild(icon);
// btn.appendChild(span);
// return btn;
// };
// const createBtn = (options = {}) => {
// const {
// classNames = ['btn-primary'],
// iconName = 'circle',
// label,
// clickHandler = (e) => {
// console.log('no click action');
// },
// } = options;
// const btn = document.createElement('button');
// const icon = document.createElement('i');
// const span = document.createElement('span');
// btn.classList.add('btn', ...classNames);
// icon.classList.add('fas', `fa-${iconName}`);
// span.classList.add('visually-hidden');
// span.textContent = label;
// btn.addEventListener('click', clickHandler);
// btn.appendChild(icon);
// btn.appendChild(span);
// return btn;
// };
// const createTd = (text, ...classNames) => {
// const td = document.createElement('td');
// td.classList.add(...classNames);
// td.textContent = text;
// return td;
// };
// const createTr = (...tds) => {
// const tr = document.createElement('tr');
// // tds.forEach((td) => {
// // tr.appendChild(td);
// // });
// tr.append(...tds);
// return tr;
// };
const disableNonFunctionalButtons = () => {
DOM.tbody.querySelectorAll('tr').forEach((tr) => {
tr.querySelector('.button-product-move-up').disabled = !tr.previousElementSibling;
tr.querySelector('.button-product-move-down').disabled = !tr.nextElementSibling;
});
};
// Helper
function parents(el, selector) {
const parents = [];
while ((el = el.parentNode) && el !== document) {
if (!selector || el.matches(selector)) parents.push(el);
}
return parents;
}
init();
})();
@@ -0,0 +1,5 @@
const PRODUCTS = [
{ name: '3Doodler 3D Printing Pen', price: 29.99 },
{ name: 'Powerstation 5- E. Maximus Chargus', price: 44.95 },
{ name: '8-Bit Legendary Hero Heat-Change Mug', price: 6.99 },
];
@@ -0,0 +1,5 @@
[
{ "name": "3Doodler 3D Printing Pen", "price": 29.99 },
{ "name": "Powerstation 5- E. Maximus Chargus", "price": 44.95 },
{ "name": "8-Bit Legendary Hero Heat-Change Mug", "price": 6.99 }
]
@@ -0,0 +1,90 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Productmanager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.css" />
<link rel="stylesheet" href="assets/css/main.css" />
<script src="data/products.js"></script>
<script src="assets/js/main.js" defer></script>
</head>
<body>
<main>
<!-- ▼ Product Manager ▼ -->
<div class="product-manager">
<div class="container py-5">
<h1>Productmanager</h1>
<!-- table.table.table-striped.table-products>thead.table-dark>tr>th*2 -->
<table class="table table-striped table-products">
<thead class="table-dark">
<tr>
<th class="th-name">Name</th>
<th class="th-price">Price in &euro;</th>
<th class="th-actions">Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3">
<div class="row">
<div class="col-12 col-md-6 col-lg-6">
<input
type="text"
name="product-name"
class="form-control input-product-name"
placeholder="Insert product name"
aria-label="Product name" />
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="input-group">
<span class="input-group-text">€</span>
<input
type="number"
name="prodcut-price"
class="form-control input-product-price"
placeholder="00.00"
aria-label="Product price"
min="0"
step="0.01" />
</div>
</div>
<div class="col-12 col-md-6 col-lg">
<button class="btn btn-dark button-product-add">
<i class="fas fa-plus"></i>
Add Product
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<template class="template-row">
<tr>
<td class="td-name">{PRODUCT_NAME}</td>
<td class="td-price">{PRODUCT_PRICE}</td>
<td class="td-actions">
<button class="btn btn-danger button-product-remove">
<i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span>
</button>
<button class="btn btn-secondary button-product-move-up">
<i class="fas fa-chevron-up"></i>
<span class="visually-hidden">Move up</span>
</button>
<button class="btn btn-secondary button-product-move-down">
<i class="fas fa-chevron-down"></i>
<span class="visually-hidden">Move down</span>
</button>
</td>
</tr>
</template>
</div>
<!-- ▲ /Product Manager ▲ -->
</main>
</body>
</html>
@@ -0,0 +1,11 @@
{
"name": "product-manager-tpl-json",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "npx http-server -p 3000 -c-1"
},
"keywords": [],
"type": "module"
}
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Node.outerHTML vs Node.innerHTML</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<main>
<div class="container py-5">
<h1>
<a href="https://developer.mozilla.org/de/docs/Web/API/Element/outerHTML">Node.outerHTML</a> vs
<em><a href="https://developer.mozilla.org/de/docs/Web/API/Element/innerHTML">Node.innerHTML</a></em>
</h1>
</div>
</main>
<script>
'use strict';
const $ = (qs) => document.querySelector(qs);
const $$ = (qs) => Array.from(document.querySelectorAll(qs));
console.log($('h1').innerHTML); //=> Node.innerHTML <em>Node.innerHTML</em>
console.log($('h1').outerHTML); //=> <h1>Node.outerHTML vs <em>Node.innerHTML</em></h1>
</script>
</body>
</html>
@@ -0,0 +1,39 @@
// 'use strict'; // nicht mehr notwendig, da JS über type="module" in den strikten Modus gesetzt wird.
import '../../node_modules/jquery/dist/jquery.js';
// import $ from 'https://esm.sh/jquery';
import _ from 'https://esm.sh/lodash';
import EigenerName from './modules/Example.js'; // WICHTIG mit Dateiendung, wenn kein JS Bundler im Einsatz
import { Example as ExampleRenamed, Example2, TAX_RATE } from './modules/examples.js';
import Examples from './modules/examples.js';
(() => {
// === DOM & VARS =======
const DOM = {};
// === INIT =============
const init = () => {
console.log('init...');
EigenerName();
console.log($('h1').css({ color: 'tomato' }));
console.log(_.shuffle([1, 2, 3, 4, 5]));
Examples.method();
Examples.method2();
console.log(Examples.eigenschaft); // => 'wert'
ExampleRenamed();
Example2();
console.log(TAX_RATE); // => 1.19
};
// === EVENTHANDLER =====
// === XHR/FETCH ========
// === FUNCTIONS ========
init();
})();
@@ -0,0 +1,9 @@
const Example = () => {
console.log('init Example');
};
// Freigabe in ES6 // import Example from './Example.js'
export default Example;
// commonJS ("alte, gängige" Node, die es nicht Browserseitig gibt) -> require('./Example')
// exports.Example = Example;
@@ -0,0 +1,21 @@
export const Example = () => {
console.log('Example init aus "examples"');
};
export function Example2() {
console.log('Example2 init aus "examples"');
}
export const TAX_RATE = 1.19;
export const DB_NAME = 'todos_db';
const text = 'Ich bin ein Text';
// Default Objekt als Freigabe
export default {
method: Example,
method2: Example2,
eigenschaft: 'Wert',
// text:text
text, // Kurzschreibweise für text: text
};
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Verwendung von Import - Module in (ES6)</title>
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
<style>
@import url('https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css');
</style>
<script src="assets/js/main.js" type="module"></script>
</head>
<body>
<main>
<div class="container py-5">
<h1>Verwendung von Import - Module in (ES6)</h1>
<p>
Die statische
<a href="https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Statements/import">import</a
>-Statement Deklaration wird verwendet, um schreibgeschützte, dynamische Bindings zu importieren, die von
einem anderen Modul exportiert werden. Die importierten Bindings werden dynamische Bindings genannt, weil sie
durch das Modul, das das Binding exportiert, aktualisiert werden, aber nicht durch das importierende Modul neu
zugewiesen werden können.
</p>
<p>
Um die import-Deklaration in einer Quelldatei zu verwenden, muss die Datei zur Laufzeit als Modul
interpretiert werden. In HTML geschieht dies, indem <strong>type="module"</strong> zum
<strong>&lt;script&gt;</strong>-Tag hinzugefügt wird. Module werden automatisch im Strict Mode interpretiert.
</p>
<p>
Es gibt auch eine funktionsähnliche dynamische import(), die keine Skripte des Typs
<strong>type="module"</strong> erfordert.
</p>
<hr />
<p>Um die Modul-Schreibweise in JS zu verwenden, müssen folgende Punkte berücksichtigt werden:</p>
<ul class="list list-group">
<li class="list-group-item">Script Tag benötigt Modulangabe mit <strong>type="module"</strong></li>
<li class="list-group-item">
Kommunikation läuft über über das HTTP-Protokoll (HTTP-Kommunikation lokal über Server)
</li>
<li class="list-group-item">
Eingebundene Module müssen mit Dateiendung (.js) angegeben werden (komplette Pfadangabe).
</li>
</ul>
</div>
</main>
<script>
'use strict';
</script>
</body>
</html>
@@ -0,0 +1,21 @@
{
"name": "04_es6-module",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "04_es6-module",
"version": "1.0.0",
"dependencies": {
"jquery": "^4.0.0"
}
},
"node_modules/jquery": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-4.0.0.tgz",
"integrity": "sha512-TXCHVR3Lb6TZdtw1l3RTLf8RBWVGexdxL6AC8/e0xZKEpBflBsjh9/8LXw+dkNFuOyW9B7iB3O1sP7hS0Kiacg==",
"license": "MIT"
}
}
}
@@ -0,0 +1,14 @@
{
"name": "04_es6-module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "npx http-server -p 3000 -c-1"
},
"keywords": [],
"type": "commonjs",
"dependencies": {
"jquery": "^4.0.0"
}
}
@@ -0,0 +1,40 @@
*,
html {
box-sizing: border-box;
}
html,
body {
padding: 0;
margin: 0;
height: 100%;
}
/* Mobile first */
.product-manager table tfoot .row > * {
margin-bottom: 0.5rem;
}
@media (min-width: 992px) {
.product-manager table tfoot .row > * {
margin-bottom: 0;
}
}
.product-manager table td {
vertical-align: middle;
}
.product-manager table tbody:empty::after {
content: 'loading products...';
color: #999;
padding: 1rem;
}
.product-manager table td button {
margin: 0 3px;
}
.product-manager table .th-actions {
width: 185px;
}
@@ -0,0 +1,23 @@
import ProductManager from './modules/ProductManager.js';
(() => {
// === DOM & VARS =======
const DOM = {
productManagers: Array.from(document.querySelectorAll('.product-manager')),
};
// === INIT =============
const init = () => {
DOM.productManagers.forEach((el) => {
ProductManager(el);
});
};
// === EVENTHANDLER =====
// === XHR/FETCH ========
// === FUNCTIONS ========
init();
})();
@@ -0,0 +1,160 @@
import { parents } from '../utils/helpers.js';
// Factory Function
const ProductManager = (el = null, options = {}) => {
// === DOM & VARS =======
const module = el || document.querySelector('.product-manager') || console.warn('Product Manager Element not found');
if (!module) return;
const DOM = {
module,
table: module.querySelector('.table-products'),
tbody: module.querySelector('tbody'),
inputName: module.querySelector('.input-product-name'),
inputPrice: module.querySelector('.input-product-price'),
btnAdd: module.querySelector('.button-product-add'),
templateRow: module.querySelector('.template-row'),
};
console.log(DOM);
// === INIT =============
const init = () => {
// Funktionsaufrufe zu beginn der Anwendung
initProducts();
// Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click', onClickAdd);
};
// === EVENTHANDLER =====
const onClickAdd = (e) => {
const product = {
name: DOM.inputName.value, //
price: Number(DOM.inputPrice.valueAsNumber),
};
addProduct(product);
disableNonFunctionalButtons();
};
const onClickRemove = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0]; // btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.remove();
// currentRow.parentNode.removeChild(currentRow); // IE11 support
disableNonFunctionalButtons();
};
const onClickMoveUp = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0];
DOM.tbody.insertBefore(currentRow, currentRow.previousElementSibling);
disableNonFunctionalButtons();
};
const onClickMoveDown = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0];
DOM.tbody.insertBefore(currentRow, currentRow.nextElementSibling.nextElementSibling);
disableNonFunctionalButtons();
};
// === XHR/FETCH ========
const fetchProducts = async () => {
try {
const response = await fetch('/data/products.json'); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
console.error(error);
throw error;
}
};
// === FUNCTIONS ========
const initProducts = () => {
fetchProducts().then((products) => {
products.forEach(addProduct);
disableNonFunctionalButtons();
});
};
const addProduct = (product) => {
const { name, price } = product;
// https://developer.mozilla.org/de/docs/Web/HTML/Reference/Elements/template#anwendungshinweise
const trEl = DOM.templateRow.content.firstElementChild.cloneNode(true);
const tdNameEl = trEl.querySelector('.td-name');
const tdPriceEl = trEl.querySelector('.td-price');
const btnRemoveEl = trEl.querySelector('.button-product-remove');
const btnMoveUpEl = trEl.querySelector('.button-product-move-up');
const btnMoveDownEl = trEl.querySelector('.button-product-move-down');
tdNameEl.textContent = name;
tdPriceEl.textContent = Number(price).toFixed(2);
btnRemoveEl.addEventListener('click', onClickRemove);
btnMoveUpEl.addEventListener('click', onClickMoveUp);
btnMoveDownEl.addEventListener('click', onClickMoveDown);
DOM.tbody.appendChild(trEl);
};
// const createBtn = (options = {}) => {
// const {
// classNames = ['btn-primary'],
// iconName = 'circle',
// label,
// clickHandler = (e) => {
// console.log('no click action');
// },
// } = options;
// const btn = document.createElement('button');
// const icon = document.createElement('i');
// const span = document.createElement('span');
// btn.classList.add('btn', ...classNames);
// icon.classList.add('fas', `fa-${iconName}`);
// span.classList.add('visually-hidden');
// span.textContent = label;
// btn.addEventListener('click', clickHandler);
// btn.appendChild(icon);
// btn.appendChild(span);
// return btn;
// };
const disableNonFunctionalButtons = () => {
DOM.tbody.querySelectorAll('tr').forEach((tr) => {
tr.querySelector('.button-product-move-up').disabled = !tr.previousElementSibling;
tr.querySelector('.button-product-move-down').disabled = !tr.nextElementSibling;
});
};
// Helper
// function parents(el, selector) {
// const parents = [];
// while ((el = el.parentNode) && el !== document) {
// if (!selector || el.matches(selector)) parents.push(el);
// }
// return parents;
// }
init();
return {
element: DOM.module,
init,
};
};
export default ProductManager;
@@ -0,0 +1,32 @@
// Helper
export function parents(el, selector) {
const parents = [];
while ((el = el.parentNode) && el !== document) {
if (!selector || el.matches(selector)) parents.push(el);
}
return parents;
}
export function addClass(el, className) {
if (Array.isArray(className)) {
el.classList.add(...className);
} else {
el.classList.add(className);
}
return el;
}
export function removeClass(el, className) {
if (Array.isArray(className)) {
el.classList.remove(...className);
} else {
el.classList.remove(className);
}
return el;
}
export default {
parents,
addClass,
removeClass,
};
@@ -0,0 +1,5 @@
const PRODUCTS = [
{ name: '3Doodler 3D Printing Pen', price: 29.99 },
{ name: 'Powerstation 5- E. Maximus Chargus', price: 44.95 },
{ name: '8-Bit Legendary Hero Heat-Change Mug', price: 6.99 },
];
@@ -0,0 +1,5 @@
[
{ "name": "3Doodler 3D Printing Pen", "price": 29.99 },
{ "name": "Powerstation 5- E. Maximus Chargus", "price": 44.95 },
{ "name": "8-Bit Legendary Hero Heat-Change Mug", "price": 6.99 }
]
@@ -0,0 +1,90 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Productmanager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.css" />
<link rel="stylesheet" href="assets/css/main.css" />
<script src="data/products.js"></script>
<script src="assets/js/main.js" type="module"></script>
</head>
<body>
<main>
<!-- ▼ Product Manager ▼ -->
<div class="product-manager">
<div class="container py-5">
<h1>Productmanager</h1>
<!-- table.table.table-striped.table-products>thead.table-dark>tr>th*2 -->
<table class="table table-striped table-products">
<thead class="table-dark">
<tr>
<th class="th-name">Name</th>
<th class="th-price">Price in &euro;</th>
<th class="th-actions">Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3">
<div class="row">
<div class="col-12 col-md-6 col-lg-6">
<input
type="text"
name="product-name"
class="form-control input-product-name"
placeholder="Insert product name"
aria-label="Product name" />
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="input-group">
<span class="input-group-text">€</span>
<input
type="number"
name="prodcut-price"
class="form-control input-product-price"
placeholder="00.00"
aria-label="Product price"
min="0"
step="0.01" />
</div>
</div>
<div class="col-12 col-md-6 col-lg">
<button class="btn btn-dark button-product-add">
<i class="fas fa-plus"></i>
Add Product
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<template class="template-row">
<tr>
<td class="td-name">{PRODUCT_NAME}</td>
<td class="td-price">{PRODUCT_PRICE}</td>
<td class="td-actions">
<button class="btn btn-danger button-product-remove">
<i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span>
</button>
<button class="btn btn-secondary button-product-move-up">
<i class="fas fa-chevron-up"></i>
<span class="visually-hidden">Move up</span>
</button>
<button class="btn btn-secondary button-product-move-down">
<i class="fas fa-chevron-down"></i>
<span class="visually-hidden">Move down</span>
</button>
</td>
</tr>
</template>
</div>
<!-- ▲ /Product Manager ▲ -->
</main>
</body>
</html>
@@ -0,0 +1,11 @@
{
"name": "product-manager-tpl-json",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "npx http-server -p 3000 -c-1"
},
"keywords": [],
"type": "module"
}
@@ -0,0 +1,105 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drag 'n Drop Example</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" />
<style>
.box {
position: absolute;
z-index: 2;
width: 100px;
height: 100px;
background-color: tomato;
border: 10px solid #222;
}
.target-box {
position: absolute;
z-index: 1;
bottom: 1rem;
width: 200px;
height: 200px;
background-color: orange;
left: 50%;
transform: translateX(-50%);
border: 10px solid #222;
}
</style>
</head>
<body>
<main>
<div class="container py-5">
<h1>Drag 'n Drop Example</h1>
<hr />
<div class="box" id="source-box"></div>
<div class="target-box"></div>
</div>
</main>
<script>
'use strict';
const box = document.querySelector('.box');
const targetBox = document.querySelector('.target-box');
// MouseEvent
box.addEventListener('mousedown', (e) => {
console.log('MouseEvent: ', e);
const el = e.currentTarget;
el.draggable = true; // => <div class="box" draggable="true">...</div>
});
box.addEventListener('mouseup', (e) => {
console.log('MouseEvent: ', e);
const el = e.currentTarget;
el.draggable = false; // => <div class="box" draggable="false">...</div>
});
// DragEvent
box.addEventListener('dragstart', (e) => {
console.log('DragEvent: ', e);
console.log('DRAG_START');
const sourceEl = e.currentTarget;
const idx = [...sourceEl.parentNode.children].indexOf(sourceEl); // => Trick um schnell den Index (die Position) eines Elementes herauzufinden.
const className = sourceEl.classList.value; // => 'box'
const id = sourceEl.id;
console.log({ idx, className, id });
// e.dataTransfer.setData('text/plain', idx);
e.dataTransfer.setData('application/json', JSON.stringify({ idx, className, id }));
});
// NOTWENDIG
targetBox.addEventListener('dragover', (e) => {
e.preventDefault(); // Standardverhalten unterbinden. Notwendig, damit 'drop'-Event grieft
});
targetBox.addEventListener('drop', (e) => {
console.log('DragEvent: ', e);
console.log('DROP');
const { idx, id, className } = JSON.parse(e.dataTransfer.getData('application/json'));
// Beispiel Veränderung von sourceElment
const sourceEl = document.querySelector('.box');
const newIdx = [...sourceEl.parentNode.children].indexOf(sourceEl);
console.log({ idx, className, newIdx, id });
document.querySelector(`.${className}`).style.backgroundColor = 'limegreen';
// document.querySelector(`.container > *:nth-child(${idx})`).style.width = `${targetBox.offsetWidth}px`;
document.querySelector(`.${className}`).style.width = `${targetBox.offsetWidth}px`;
document.getElementById(id).style.height = `${targetBox.offsetHeight}px`;
document.querySelector(`.${className}`).style.top = `${targetBox.getBoundingClientRect().top}px`;
document.querySelector(`.${className}`).style.left = `${targetBox.getBoundingClientRect().left}px`;
});
</script>
</body>
</html>
@@ -0,0 +1,43 @@
*,
html {
box-sizing: border-box;
}
html,
body {
padding: 0;
margin: 0;
height: 100%;
}
/* Mobile first */
.product-manager table tfoot .row > * {
margin-bottom: 0.5rem;
}
@media (min-width: 992px) {
.product-manager table tfoot .row > * {
margin-bottom: 0;
}
}
.product-manager table td {
vertical-align: middle;
}
.product-manager table tbody:empty::after {
content: 'loading products...';
color: #999;
padding: 1rem;
}
.product-manager table td button {
margin: 0 3px;
}
.product-manager table .th-actions {
width: 185px;
}
.product-manager table .th-dragger {
width: 50px;
}
@@ -0,0 +1,23 @@
import ProductManager from './modules/ProductManager.js';
(() => {
// === DOM & VARS =======
const DOM = {
productManagers: Array.from(document.querySelectorAll('.product-manager')),
};
// === INIT =============
const init = () => {
DOM.productManagers.forEach((el) => {
ProductManager(el);
});
};
// === EVENTHANDLER =====
// === XHR/FETCH ========
// === FUNCTIONS ========
init();
})();
@@ -0,0 +1,163 @@
import { parents } from '../utils/helpers.js';
// Factory Function
const ProductManager = (el = null, options = {}) => {
// === DOM & VARS =======
const module = el || document.querySelector('.product-manager') || console.warn('Product Manager Element not found');
if (!module) return;
const DOM = {
module,
table: module.querySelector('.table-products'),
tbody: module.querySelector('tbody'),
inputName: module.querySelector('.input-product-name'),
inputPrice: module.querySelector('.input-product-price'),
btnAdd: module.querySelector('.button-product-add'),
templateRow: module.querySelector('.template-row'),
};
console.log(DOM);
// === INIT =============
const init = () => {
// Funktionsaufrufe zu beginn der Anwendung
initProducts();
// Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click', onClickAdd);
};
// === EVENTHANDLER =====
const onClickAdd = (e) => {
const product = {
name: DOM.inputName.value, //
price: Number(DOM.inputPrice.valueAsNumber),
};
addProduct(product);
disableNonFunctionalButtons();
};
const onClickRemove = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0]; // btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.remove();
// currentRow.parentNode.removeChild(currentRow); // IE11 support
disableNonFunctionalButtons();
};
const onClickMoveUp = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0];
DOM.tbody.insertBefore(currentRow, currentRow.previousElementSibling);
disableNonFunctionalButtons();
};
const onClickMoveDown = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0];
DOM.tbody.insertBefore(currentRow, currentRow.nextElementSibling.nextElementSibling);
disableNonFunctionalButtons();
};
// === XHR/FETCH ========
const fetchProducts = async () => {
try {
const response = await fetch('/data/products.json'); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
console.error(error);
throw error;
}
};
// === FUNCTIONS ========
const initProducts = () => {
fetchProducts().then((products) => {
products.forEach(addProduct);
disableNonFunctionalButtons();
});
};
const addProduct = (product) => {
const { name, price } = product;
// https://developer.mozilla.org/de/docs/Web/HTML/Reference/Elements/template#anwendungshinweise
const trEl = DOM.templateRow.content.firstElementChild.cloneNode(true);
const tdNameEl = trEl.querySelector('.td-name');
const tdPriceEl = trEl.querySelector('.td-price');
const btnDragEl = trEl.querySelector('.button-product-drag');
const btnRemoveEl = trEl.querySelector('.button-product-remove');
const btnMoveUpEl = trEl.querySelector('.button-product-move-up');
const btnMoveDownEl = trEl.querySelector('.button-product-move-down');
// TODO: Optional Drag 'n Drop hinzufügen.
tdNameEl.textContent = name;
tdPriceEl.textContent = Number(price).toFixed(2);
btnRemoveEl.addEventListener('click', onClickRemove);
btnMoveUpEl.addEventListener('click', onClickMoveUp);
btnMoveDownEl.addEventListener('click', onClickMoveDown);
DOM.tbody.appendChild(trEl);
};
// const createBtn = (options = {}) => {
// const {
// classNames = ['btn-primary'],
// iconName = 'circle',
// label,
// clickHandler = (e) => {
// console.log('no click action');
// },
// } = options;
// const btn = document.createElement('button');
// const icon = document.createElement('i');
// const span = document.createElement('span');
// btn.classList.add('btn', ...classNames);
// icon.classList.add('fas', `fa-${iconName}`);
// span.classList.add('visually-hidden');
// span.textContent = label;
// btn.addEventListener('click', clickHandler);
// btn.appendChild(icon);
// btn.appendChild(span);
// return btn;
// };
const disableNonFunctionalButtons = () => {
DOM.tbody.querySelectorAll('tr').forEach((tr) => {
tr.querySelector('.button-product-move-up').disabled = !tr.previousElementSibling;
tr.querySelector('.button-product-move-down').disabled = !tr.nextElementSibling;
});
};
// Helper
// function parents(el, selector) {
// const parents = [];
// while ((el = el.parentNode) && el !== document) {
// if (!selector || el.matches(selector)) parents.push(el);
// }
// return parents;
// }
init();
return {
element: DOM.module,
init,
};
};
export default ProductManager;
@@ -0,0 +1,32 @@
// Helper
export function parents(el, selector) {
const parents = [];
while ((el = el.parentNode) && el !== document) {
if (!selector || el.matches(selector)) parents.push(el);
}
return parents;
}
export function addClass(el, className) {
if (Array.isArray(className)) {
el.classList.add(...className);
} else {
el.classList.add(className);
}
return el;
}
export function removeClass(el, className) {
if (Array.isArray(className)) {
el.classList.remove(...className);
} else {
el.classList.remove(className);
}
return el;
}
export default {
parents,
addClass,
removeClass,
};
@@ -0,0 +1,5 @@
const PRODUCTS = [
{ name: '3Doodler 3D Printing Pen', price: 29.99 },
{ name: 'Powerstation 5- E. Maximus Chargus', price: 44.95 },
{ name: '8-Bit Legendary Hero Heat-Change Mug', price: 6.99 },
];
@@ -0,0 +1,5 @@
[
{ "name": "3Doodler 3D Printing Pen", "price": 29.99 },
{ "name": "Powerstation 5- E. Maximus Chargus", "price": 44.95 },
{ "name": "8-Bit Legendary Hero Heat-Change Mug", "price": 6.99 }
]
@@ -0,0 +1,97 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Productmanager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.css" />
<link rel="stylesheet" href="assets/css/main.css" />
<script src="data/products.js"></script>
<script src="assets/js/main.js" type="module"></script>
</head>
<body>
<main>
<!-- ▼ Product Manager ▼ -->
<div class="product-manager">
<div class="container py-5">
<h1>Productmanager</h1>
<!-- table.table.table-striped.table-products>thead.table-dark>tr>th*2 -->
<table class="table table-striped table-products">
<thead class="table-dark">
<tr>
<th class="th-dragger">#</th>
<th class="th-name">Name</th>
<th class="th-price">Price in &euro;</th>
<th class="th-actions">Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="4">
<div class="row">
<div class="col-12 col-md-6 col-lg-6">
<input
type="text"
name="product-name"
class="form-control input-product-name"
placeholder="Insert product name"
aria-label="Product name" />
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="input-group">
<span class="input-group-text">€</span>
<input
type="number"
name="prodcut-price"
class="form-control input-product-price"
placeholder="00.00"
aria-label="Product price"
min="0"
step="0.01" />
</div>
</div>
<div class="col-12 col-md-6 col-lg">
<button class="btn btn-dark button-product-add">
<i class="fas fa-plus"></i>
Add Product
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<template class="template-row">
<tr>
<td class="td-dragger">
<button class="btn btn-default button-product-drag">
<i class="fas fa-grip-vertical"></i>
<span class="visually-hidden">Drag Product</span>
</button>
</td>
<td class="td-name">{PRODUCT_NAME}</td>
<td class="td-price">{PRODUCT_PRICE}</td>
<td class="td-actions">
<button class="btn btn-danger button-product-remove">
<i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span>
</button>
<button class="btn btn-secondary button-product-move-up">
<i class="fas fa-chevron-up"></i>
<span class="visually-hidden">Move up</span>
</button>
<button class="btn btn-secondary button-product-move-down">
<i class="fas fa-chevron-down"></i>
<span class="visually-hidden">Move down</span>
</button>
</td>
</tr>
</template>
</div>
<!-- ▲ /Product Manager ▲ -->
</main>
</body>
</html>
@@ -0,0 +1,11 @@
{
"name": "product-manager-tpl-json",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "npx http-server -p 3000 -c-1"
},
"keywords": [],
"type": "module"
}