This commit is contained in:
Philippe Torrel
2026-10-13 11:00:26 +02:00
parent 7e46647b86
commit c41a126ea6
12 changed files with 514 additions and 34 deletions
@@ -12205,6 +12205,91 @@ blockquote {
font-family: "Gloria Hallelujah", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
}
/*!
* Toastify js 1.12.0
* https://github.com/apvarun/toastify-js
* @license MIT licensed
*
* Copyright (C) 2018 Varun A P
*/
.toastify {
padding: 12px 20px;
color: #ffffff;
display: inline-block;
box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3);
background: linear-gradient(135deg, #73a5ff, #5477f5);
position: fixed;
opacity: 0;
transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
border-radius: 2px;
cursor: pointer;
text-decoration: none;
max-width: calc(50% - 20px);
z-index: 2147483647;
}
.toastify.on {
opacity: 1;
}
.toast-close {
background: transparent;
border: 0;
color: white;
cursor: pointer;
font-family: inherit;
font-size: 1em;
opacity: 0.4;
padding: 0 5px;
}
.toastify-right {
right: 15px;
}
.toastify-left {
left: 15px;
}
.toastify-top {
top: -150px;
}
.toastify-bottom {
bottom: -150px;
}
.toastify-rounded {
border-radius: 25px;
}
.toastify-avatar {
width: 1.5em;
height: 1.5em;
margin: -7px 5px;
border-radius: 2px;
}
.toastify-center {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
max-width: -moz-fit-content;
}
@media only screen and (max-width: 360px) {
.toastify-right,
.toastify-left {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: -moz-fit-content;
max-width: fit-content;
}
}
.chuck-norris-joke {
position: relative;
width: 100%;
@@ -768,26 +768,132 @@
*/const NAME='toast';const DATA_KEY='bs.toast';const EVENT_KEY=`.${DATA_KEY}`;const EVENT_MOUSEOVER=`mouseover${EVENT_KEY}`;const EVENT_MOUSEOUT=`mouseout${EVENT_KEY}`;const EVENT_FOCUSIN=`focusin${EVENT_KEY}`;const EVENT_FOCUSOUT=`focusout${EVENT_KEY}`;const EVENT_HIDE=`hide${EVENT_KEY}`;const EVENT_HIDDEN=`hidden${EVENT_KEY}`;const EVENT_SHOW=`show${EVENT_KEY}`;const EVENT_SHOWN=`shown${EVENT_KEY}`;const CLASS_NAME_FADE='fade';const CLASS_NAME_HIDE='hide';// @deprecated - kept here only for backwards compatibility
const CLASS_NAME_SHOW='show';const CLASS_NAME_SHOWING='showing';const DefaultType={animation:'boolean',autohide:'boolean',delay:'number'};const Default={animation:true,autohide:true,delay:5000};/**
* Class definition
*/class Toast extends BaseComponent{constructor(element,config){super(element,config);this._timeout=null;this._hasMouseInteraction=false;this._hasKeyboardInteraction=false;this._setListeners();}// Getters
*/let Toast$1 = class Toast extends BaseComponent{constructor(element,config){super(element,config);this._timeout=null;this._hasMouseInteraction=false;this._hasKeyboardInteraction=false;this._setListeners();}// Getters
static get Default(){return Default;}static get DefaultType(){return DefaultType;}static get NAME(){return NAME;}// Public
show(){const showEvent=EventHandler.trigger(this._element,EVENT_SHOW);if(showEvent.defaultPrevented){return;}this._clearTimeout();if(this._config.animation){this._element.classList.add(CLASS_NAME_FADE);}const complete=()=>{this._element.classList.remove(CLASS_NAME_SHOWING);EventHandler.trigger(this._element,EVENT_SHOWN);this._maybeScheduleHide();};this._element.classList.remove(CLASS_NAME_HIDE);// @deprecated
reflow(this._element);this._element.classList.add(CLASS_NAME_SHOW,CLASS_NAME_SHOWING);this._queueCallback(complete,this._element,this._config.animation);}hide(){if(!this.isShown()){return;}const hideEvent=EventHandler.trigger(this._element,EVENT_HIDE);if(hideEvent.defaultPrevented){return;}const complete=()=>{this._element.classList.add(CLASS_NAME_HIDE);// @deprecated
this._element.classList.remove(CLASS_NAME_SHOWING,CLASS_NAME_SHOW);EventHandler.trigger(this._element,EVENT_HIDDEN);};this._element.classList.add(CLASS_NAME_SHOWING);this._queueCallback(complete,this._element,this._config.animation);}dispose(){this._clearTimeout();if(this.isShown()){this._element.classList.remove(CLASS_NAME_SHOW);}super.dispose();}isShown(){return this._element.classList.contains(CLASS_NAME_SHOW);}// Private
_maybeScheduleHide(){if(!this._config.autohide){return;}if(this._hasMouseInteraction||this._hasKeyboardInteraction){return;}this._timeout=setTimeout(()=>{this.hide();},this._config.delay);}_onInteraction(event,isInteracting){switch(event.type){case 'mouseover':case 'mouseout':{this._hasMouseInteraction=isInteracting;break;}case 'focusin':case 'focusout':{this._hasKeyboardInteraction=isInteracting;break;}}if(isInteracting){this._clearTimeout();return;}const nextElement=event.relatedTarget;if(this._element===nextElement||this._element.contains(nextElement)){return;}this._maybeScheduleHide();}_setListeners(){EventHandler.on(this._element,EVENT_MOUSEOVER,event=>this._onInteraction(event,true));EventHandler.on(this._element,EVENT_MOUSEOUT,event=>this._onInteraction(event,false));EventHandler.on(this._element,EVENT_FOCUSIN,event=>this._onInteraction(event,true));EventHandler.on(this._element,EVENT_FOCUSOUT,event=>this._onInteraction(event,false));}_clearTimeout(){clearTimeout(this._timeout);this._timeout=null;}// Static
static jQueryInterface(config){return this.each(function(){const data=Toast.getOrCreateInstance(this,config);if(typeof config==='string'){if(typeof data[config]==='undefined'){throw new TypeError(`No method named "${config}"`);}data[config](this);}});}}/**
static jQueryInterface(config){return this.each(function(){const data=Toast.getOrCreateInstance(this,config);if(typeof config==='string'){if(typeof data[config]==='undefined'){throw new TypeError(`No method named "${config}"`);}data[config](this);}});}};/**
* Data API implementation
*/enableDismissTrigger(Toast);/**
*/enableDismissTrigger(Toast$1);/**
* jQuery
*/defineJQueryPlugin(Toast);
*/defineJQueryPlugin(Toast$1);
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var toastify$1 = {exports: {}};
var toastify=toastify$1.exports;var hasRequiredToastify;function requireToastify(){if(hasRequiredToastify)return toastify$1.exports;hasRequiredToastify=1;(function(module){(function(root,factory){if(module.exports){module.exports=factory();}else {root.Toastify=factory();}})(toastify,function(global){// Object initialization
var Toastify=function(options){// Returning a new init object
return new Toastify.lib.init(options);},// Library version
version="1.12.0";// Set the default global options
Toastify.defaults={oldestFirst:true,text:"Toastify is awesome!",node:undefined,duration:3000,selector:undefined,callback:function(){},destination:undefined,newWindow:false,close:false,gravity:"toastify-top",positionLeft:false,position:'',backgroundColor:'',avatar:"",className:"",stopOnFocus:true,onClick:function(){},offset:{x:0,y:0},escapeMarkup:true,ariaLive:'polite',style:{background:''}};// Defining the prototype of the object
Toastify.lib=Toastify.prototype={toastify:version,constructor:Toastify,// Initializing the object with required parameters
init:function(options){// Verifying and validating the input object
if(!options){options={};}// Creating the options object
this.options={};this.toastElement=null;// Validating the options
this.options.text=options.text||Toastify.defaults.text;// Display message
this.options.node=options.node||Toastify.defaults.node;// Display content as node
this.options.duration=options.duration===0?0:options.duration||Toastify.defaults.duration;// Display duration
this.options.selector=options.selector||Toastify.defaults.selector;// Parent selector
this.options.callback=options.callback||Toastify.defaults.callback;// Callback after display
this.options.destination=options.destination||Toastify.defaults.destination;// On-click destination
this.options.newWindow=options.newWindow||Toastify.defaults.newWindow;// Open destination in new window
this.options.close=options.close||Toastify.defaults.close;// Show toast close icon
this.options.gravity=options.gravity==="bottom"?"toastify-bottom":Toastify.defaults.gravity;// toast position - top or bottom
this.options.positionLeft=options.positionLeft||Toastify.defaults.positionLeft;// toast position - left or right
this.options.position=options.position||Toastify.defaults.position;// toast position - left or right
this.options.backgroundColor=options.backgroundColor||Toastify.defaults.backgroundColor;// toast background color
this.options.avatar=options.avatar||Toastify.defaults.avatar;// img element src - url or a path
this.options.className=options.className||Toastify.defaults.className;// additional class names for the toast
this.options.stopOnFocus=options.stopOnFocus===undefined?Toastify.defaults.stopOnFocus:options.stopOnFocus;// stop timeout on focus
this.options.onClick=options.onClick||Toastify.defaults.onClick;// Callback after click
this.options.offset=options.offset||Toastify.defaults.offset;// toast offset
this.options.escapeMarkup=options.escapeMarkup!==undefined?options.escapeMarkup:Toastify.defaults.escapeMarkup;this.options.ariaLive=options.ariaLive||Toastify.defaults.ariaLive;this.options.style=options.style||Toastify.defaults.style;if(options.backgroundColor){this.options.style.background=options.backgroundColor;}// Returning the current object for chaining functions
return this;},// Building the DOM element
buildToast:function(){// Validating if the options are defined
if(!this.options){throw "Toastify is not initialized";}// Creating the DOM object
var divElement=document.createElement("div");divElement.className="toastify on "+this.options.className;// Positioning toast to left or right or center
if(!!this.options.position){divElement.className+=" toastify-"+this.options.position;}else {// To be depreciated in further versions
if(this.options.positionLeft===true){divElement.className+=" toastify-left";console.warn('Property `positionLeft` will be depreciated in further versions. Please use `position` instead.');}else {// Default position
divElement.className+=" toastify-right";}}// Assigning gravity of element
divElement.className+=" "+this.options.gravity;if(this.options.backgroundColor){// This is being deprecated in favor of using the style HTML DOM property
console.warn('DEPRECATION NOTICE: "backgroundColor" is being deprecated. Please use the "style.background" property.');}// Loop through our style object and apply styles to divElement
for(var property in this.options.style){divElement.style[property]=this.options.style[property];}// Announce the toast to screen readers
if(this.options.ariaLive){divElement.setAttribute('aria-live',this.options.ariaLive);}// Adding the toast message/node
if(this.options.node&&this.options.node.nodeType===Node.ELEMENT_NODE){// If we have a valid node, we insert it
divElement.appendChild(this.options.node);}else {if(this.options.escapeMarkup){divElement.innerText=this.options.text;}else {divElement.innerHTML=this.options.text;}if(this.options.avatar!==""){var avatarElement=document.createElement("img");avatarElement.src=this.options.avatar;avatarElement.className="toastify-avatar";if(this.options.position=="left"||this.options.positionLeft===true){// Adding close icon on the left of content
divElement.appendChild(avatarElement);}else {// Adding close icon on the right of content
divElement.insertAdjacentElement("afterbegin",avatarElement);}}}// Adding a close icon to the toast
if(this.options.close===true){// Create a span for close element
var closeElement=document.createElement("button");closeElement.type="button";closeElement.setAttribute("aria-label","Close");closeElement.className="toast-close";closeElement.innerHTML="✖";// Triggering the removal of toast from DOM on close click
closeElement.addEventListener("click",function(event){event.stopPropagation();this.removeElement(this.toastElement);window.clearTimeout(this.toastElement.timeOutValue);}.bind(this));//Calculating screen width
var width=window.innerWidth>0?window.innerWidth:screen.width;// Adding the close icon to the toast element
// Display on the right if screen width is less than or equal to 360px
if((this.options.position=="left"||this.options.positionLeft===true)&&width>360){// Adding close icon on the left of content
divElement.insertAdjacentElement("afterbegin",closeElement);}else {// Adding close icon on the right of content
divElement.appendChild(closeElement);}}// Clear timeout while toast is focused
if(this.options.stopOnFocus&&this.options.duration>0){var self=this;// stop countdown
divElement.addEventListener("mouseover",function(event){window.clearTimeout(divElement.timeOutValue);});// add back the timeout
divElement.addEventListener("mouseleave",function(){divElement.timeOutValue=window.setTimeout(function(){// Remove the toast from DOM
self.removeElement(divElement);},self.options.duration);});}// Adding an on-click destination path
if(typeof this.options.destination!=="undefined"){divElement.addEventListener("click",function(event){event.stopPropagation();if(this.options.newWindow===true){window.open(this.options.destination,"_blank");}else {window.location=this.options.destination;}}.bind(this));}if(typeof this.options.onClick==="function"&&typeof this.options.destination==="undefined"){divElement.addEventListener("click",function(event){event.stopPropagation();this.options.onClick();}.bind(this));}// Adding offset
if(typeof this.options.offset==="object"){var x=getAxisOffsetAValue("x",this.options);var y=getAxisOffsetAValue("y",this.options);var xOffset=this.options.position=="left"?x:"-"+x;var yOffset=this.options.gravity=="toastify-top"?y:"-"+y;divElement.style.transform="translate("+xOffset+","+yOffset+")";}// Returning the generated element
return divElement;},// Displaying the toast
showToast:function(){// Creating the DOM object for the toast
this.toastElement=this.buildToast();// Getting the root element to with the toast needs to be added
var rootElement;if(typeof this.options.selector==="string"){rootElement=document.getElementById(this.options.selector);}else if(this.options.selector instanceof HTMLElement||typeof ShadowRoot!=='undefined'&&this.options.selector instanceof ShadowRoot){rootElement=this.options.selector;}else {rootElement=document.body;}// Validating if root element is present in DOM
if(!rootElement){throw "Root element is not defined";}// Adding the DOM element
var elementToInsert=Toastify.defaults.oldestFirst?rootElement.firstChild:rootElement.lastChild;rootElement.insertBefore(this.toastElement,elementToInsert);// Repositioning the toasts in case multiple toasts are present
Toastify.reposition();if(this.options.duration>0){this.toastElement.timeOutValue=window.setTimeout(function(){// Remove the toast from DOM
this.removeElement(this.toastElement);}.bind(this),this.options.duration);// Binding `this` for function invocation
}// Supporting function chaining
return this;},hideToast:function(){if(this.toastElement.timeOutValue){clearTimeout(this.toastElement.timeOutValue);}this.removeElement(this.toastElement);},// Removing the element from the DOM
removeElement:function(toastElement){// Hiding the element
// toastElement.classList.remove("on");
toastElement.className=toastElement.className.replace(" on","");// Removing the element from DOM after transition end
window.setTimeout(function(){// remove options node if any
if(this.options.node&&this.options.node.parentNode){this.options.node.parentNode.removeChild(this.options.node);}// Remove the element from the DOM, only when the parent node was not removed before.
if(toastElement.parentNode){toastElement.parentNode.removeChild(toastElement);}// Calling the callback function
this.options.callback.call(toastElement);// Repositioning the toasts again
Toastify.reposition();}.bind(this),400);// Binding `this` for function invocation
}};// Positioning the toasts on the DOM
Toastify.reposition=function(){// Top margins with gravity
var topLeftOffsetSize={top:15,bottom:15};var topRightOffsetSize={top:15,bottom:15};var offsetSize={top:15,bottom:15};// Get all toast messages on the DOM
var allToasts=document.getElementsByClassName("toastify");var classUsed;// Modifying the position of each toast element
for(var i=0;i<allToasts.length;i++){// Getting the applied gravity
if(containsClass(allToasts[i],"toastify-top")===true){classUsed="toastify-top";}else {classUsed="toastify-bottom";}var height=allToasts[i].offsetHeight;classUsed=classUsed.substr(9,classUsed.length-1);// Spacing between toasts
var offset=15;var width=window.innerWidth>0?window.innerWidth:screen.width;// Show toast in center if screen with less than or equal to 360px
if(width<=360){// Setting the position
allToasts[i].style[classUsed]=offsetSize[classUsed]+"px";offsetSize[classUsed]+=height+offset;}else {if(containsClass(allToasts[i],"toastify-left")===true){// Setting the position
allToasts[i].style[classUsed]=topLeftOffsetSize[classUsed]+"px";topLeftOffsetSize[classUsed]+=height+offset;}else {// Setting the position
allToasts[i].style[classUsed]=topRightOffsetSize[classUsed]+"px";topRightOffsetSize[classUsed]+=height+offset;}}}// Supporting function chaining
return this;};// Helper function to get offset.
function getAxisOffsetAValue(axis,options){if(options.offset[axis]){if(isNaN(options.offset[axis])){return options.offset[axis];}else {return options.offset[axis]+'px';}}return '0px';}function containsClass(elem,yourClass){if(!elem||typeof yourClass!=="string"){return false;}else if(elem.className&&elem.className.trim().split(/\s+/gi).indexOf(yourClass)>-1){return true;}else {return false;}}// Setting up the prototype for the init object
Toastify.lib.init.prototype=Toastify.lib;// Returning the Toastify function to be assigned to the window object/module
return Toastify;});})(toastify$1);return toastify$1.exports;}
var toastifyExports = requireToastify();
var Toastify = /*@__PURE__*/getDefaultExportFromCjs(toastifyExports);
const Toast=(text='',variant='primary')=>{const getColorByName=(variant='')=>{let color;switch(variant.trim().toLowerCase()){case 'primary':color='#0d6efd';break;case 'secondary':color='#6c757d';break;case 'warning':color='#ffc720';break;case 'success':color='#13653f';break;case 'info':color='#25cff2';break;case 'error':case 'danger':color='#a52834';break;case 'light':color='#babbbc';break;case 'dark':color='#373b3e';break;default:console.warn('variant not found');color='white';}return color;};const show=()=>{Toastify({text,duration:2000,className:variant,gravity:'top',// 'top' or 'bottom'
position:'center',// `left`, `center` or `right`
stopOnFocus:true,// Prevents dismissing of toast on hover
style:{background:getColorByName(variant)}}).showToast();};return {show};};
// import 'bootstrap';
// 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'),// Modal
modalEdit:module.querySelector('.modal-edit'),formEdit:module.querySelector('.form-product-edit'),inputEditName:module.querySelector('.input-edit-name'),inputEditPrice:module.querySelector('.input-edit-price'),inputEditId:module.querySelector('.input-edit-id'),inputEditPosition:module.querySelector('.input-edit-position'),btnUpdate:module.querySelector('.button-product-update')};const SERVER_BASE_URL='http://127.0.0.1:8000';const bsModalEdit=new Modal(DOM.modalEdit,{backdrop:'static',keyboard:true});// console.log(DOM);
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'),// Modal Edit
modalEdit:module.querySelector('.modal-edit'),formEdit:module.querySelector('.form-product-edit'),inputEditName:module.querySelector('.input-edit-name'),inputEditPrice:module.querySelector('.input-edit-price'),inputEditId:module.querySelector('.input-edit-id'),inputEditPosition:module.querySelector('.input-edit-position'),btnUpdate:module.querySelector('.button-product-update'),// Modal Delete
modalDelete:module.querySelector('.modal-delete'),productName:module.querySelector('strong.product-name'),btnConfirmDelete:module.querySelector('.button-confirm-delete')};const SERVER_BASE_URL='http://127.0.0.1:8000';const bsModalEdit=new Modal(DOM.modalEdit,{backdrop:'static',keyboard:true});const bsModalDelete=new Modal(DOM.modalDelete,{backdrop:true,keyboard:true});// console.log(DOM);
// === INIT =============
const init=()=>{// Funktionsaufrufe zu beginn der Anwendung
initProducts();DOM.btnAdd.disabled=true;// Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click',onClickAdd);DOM.formEdit.addEventListener('submit',onSubmitEdit);window.addEventListener('keyup',onKeyUp);};// === EVENTHANDLER =====
DOM.btnAdd.addEventListener('click',onClickAdd);DOM.btnConfirmDelete.addEventListener('click',onClickConfirmDelete);DOM.formEdit.addEventListener('submit',onSubmitEdit);window.addEventListener('keyup',onKeyUp);};// === EVENTHANDLER =====
const onKeyUp=e=>{DOM.btnAdd.disabled=!(DOM.inputName.value!==''&&DOM.inputPrice.value!=='');// if (DOM.inputName.value !== '' && DOM.inputPrice.value !== '') {
// DOM.btnAdd.disabled = false;
// } else {
@@ -795,16 +901,19 @@
// }
};const onClickAdd=e=>{const product={name:DOM.inputName.value,//
price:Number(DOM.inputPrice.valueAsNumber)};// async API request
createProduct(product).then(data=>{console.log(data);if(data.success){loadProducts();resetInputFields();disableNonFunctionalButtons();}});// addProduct(product);
createProduct(product).then(data=>{console.log(data);if(data.success){Toast(data.msg,'success').show();loadProducts();resetInputFields();disableNonFunctionalButtons();}else {Toast(data.msg,'error').show();}});// addProduct(product);
};const onSubmitEdit=e=>{e.preventDefault();// Standardverhalten unterbinden (Modal Form nicht versenden)
const product={name:DOM.inputEditName.value,price:Number(Number(DOM.inputEditPrice.value).toFixed(2)),_id:DOM.inputEditId.value,position:parseInt(DOM.inputEditPosition.value)};// console.log(Object.fromEntries(new FormData(DOM.formEdit)));
console.log(product);DOM.btnUpdate.querySelector('i').classList.add('fa-spin');// async Fetch Request
updateProduct(product).then(data=>{console.log(data);if(data.success){DOM.btnUpdate.querySelector('i').classList.remove('fa-spin');resetInputFields();bsModalEdit.hide();loadProducts();}});};const onClickEdit=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];// btn.closest('tr'); // btn.parentNode.parentNode;
console.log(product);const icon=DOM.btnUpdate.querySelector('i');icon.classList.add('fa-spin');// async Fetch Request
updateProduct(product).then(data=>{console.log(data);if(data.success){Toast(data.msg,'success').show();icon.classList.remove('fa-spin');resetInputFields();loadProducts();bsModalEdit.hide();}else {Toast(data.msg,'error').show();}});};const onClickEdit=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];// btn.closest('tr'); // btn.parentNode.parentNode;
const id=currentRow.dataset.id;// async API request
fetchProduct(id).then(data=>{// console.log(data);
showModalEdit(data);});};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];currentRow.previousElementSibling.before(currentRow);// DOM.tbody.insertBefore(currentRow, currentRow.previousElementSibling);
const id=currentRow.dataset.id;const productName=currentRow.querySelector('.td-name').textContent.trim();console.log({id,productName});DOM.btnConfirmDelete.dataset.id=id;DOM.productName.textContent=productName;// currentRow.remove();
// currentRow.parentNode.removeChild(currentRow); // IE11 support
disableNonFunctionalButtons();};const onClickConfirmDelete=e=>{const btn=e.currentTarget;const id=btn.dataset.id;// async API request
deleteProduct(id).then(data=>{// console.log(data);
if(data.success){Toast(data.msg,'success').show();loadProducts();bsModalDelete.hide();}else {Toast(data.msg,'error').show();}});};const onClickMoveUp=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];currentRow.previousElementSibling.before(currentRow);// DOM.tbody.insertBefore(currentRow, currentRow.previousElementSibling);
disableNonFunctionalButtons();};const onClickMoveDown=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];currentRow.nextElementSibling.after(currentRow);// DOM.tbody.insertBefore(currentRow, currentRow.nextElementSibling.nextElementSibling);
disableNonFunctionalButtons();};const onMouseDownDrag=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];// btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.draggable=true;// => <tr draggable="true" >...</tr>
@@ -823,22 +932,27 @@
// BRE(A)D - Add - hinzufügen eines neuen Produkts
const createProduct=async product=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(product)});// 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;}};// HTTP - Methode - GET
return data;}catch(error){Toast(error.message||"Couldn't create product",'error').show();console.error(error);throw error;}};// HTTP - Methode - GET
// C(R)UD - Read
// (B)READ - Browse - auslesen aller Produkte
const fetchProducts=async()=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products`);// 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;}};// HTTP - Methode - GET
return data;}catch(error){Toast(error.message||"Couldn't get products",'error').show();console.error(error);throw error;}};// HTTP - Methode - GET
// C(R)UD - Read
// B(R)EAD - Read - auslesen eines Produkts
const fetchProduct=async id=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products/${id}`);// 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;}};// HTTP - Methode - PUT
return data;}catch(error){Toast(error.message||"Couldn't get product",'error').show();console.error(error);throw error;}};// HTTP - Methode - PUT
// CR(U)D - Update
// BR(E)AD - Edit - aktualisieren eines Produkts
const updateProduct=async product=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(product)});// 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 ========
return data;}catch(error){Toast(error.message||"Couldn't update product",'error').show();console.error(error);throw error;}};// HTTP - Methode - DELETE
// CRU(D) - Delete
// BREA(D) - Delete - löschen eines Produkts
const deleteProduct=async id=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products/${id}`,{method:'DELETE'});// 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){Toast(error.message||"Couldn't delete product",'error').show();console.error(error);throw error;}};// === FUNCTIONS ========
const initProducts=()=>{fetchProducts().then(products=>{products.forEach(addProduct);disableNonFunctionalButtons();});};const loadProducts=()=>{DOM.tbody.innerHTML='';fetchProducts().then(data=>{// TODO: add fade Animation
data.forEach(addProduct);disableNonFunctionalButtons();});};const addProduct=product=>{const{name,price,_id:id}=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 btnEditEl=trEl.querySelector('.button-product-edit');const btnRemoveEl=trEl.querySelector('.button-product-remove');const btnMoveUpEl=trEl.querySelector('.button-product-move-up');const btnMoveDownEl=trEl.querySelector('.button-product-move-down');// Drag 'n Drop hinzufügen.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -68,7 +68,10 @@
<i class="fas fa-pen"></i>
<span class="visually-hidden">Edit Product</span>
</button>
<button class="btn btn-sm btn-danger button-product-remove">
<button
class="btn btn-sm btn-danger button-product-remove"
data-bs-toggle="modal"
data-bs-target="#modal-product-delete">
<i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span>
</button>
@@ -85,8 +88,6 @@
</template>
<!-- ▼ Modal Edit Product ▼ -->
<!-- Button trigger modal -->
<div
class="modal fade modal-edit"
id="modal-product-edit"
@@ -142,6 +143,34 @@
</div>
<!-- ▲ /Modal Edit Product ▲ -->
<!-- ▼ Delete Modal ▼ -->
<div
class="modal fade modal-delete"
id="modal-product-delete"
tabindex="-1"
aria-labelledby="modal-product-delete-label"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modal-product-delete-label">Delete Product</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Do you really want to delete Product:<br />
<strong class="product-name">{PRODUCT_NAME}</strong>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger button-confirm-delete">Confirm to delete</button>
</div>
</div>
</div>
</div>
<!-- ▲ /Delete Modal ▲ -->
<!-- ▼ Dialog Edit Product ▼ -->
<!-- <button type="button" class="btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#slideDownDialog">
Slide down dialog
@@ -1,6 +1,6 @@
import { parents } from '../utils/helpers.js';
import { Modal } from 'bootstrap';
import Toast from './Toast';
//import * as bootstrap from 'bootstrap';
// import 'bootstrap';
@@ -22,7 +22,7 @@ const ProductManager = (el = null, options = {}) => {
btnAdd: module.querySelector('.button-product-add'),
templateRow: module.querySelector('.template-row'),
// Modal
// Modal Edit
modalEdit: module.querySelector('.modal-edit'),
formEdit: module.querySelector('.form-product-edit'),
inputEditName: module.querySelector('.input-edit-name'),
@@ -30,6 +30,11 @@ const ProductManager = (el = null, options = {}) => {
inputEditId: module.querySelector('.input-edit-id'),
inputEditPosition: module.querySelector('.input-edit-position'),
btnUpdate: module.querySelector('.button-product-update'),
// Modal Delete
modalDelete: module.querySelector('.modal-delete'),
productName: module.querySelector('strong.product-name'),
btnConfirmDelete: module.querySelector('.button-confirm-delete'),
};
const SERVER_BASE_URL = 'http://127.0.0.1:8000';
@@ -39,6 +44,11 @@ const ProductManager = (el = null, options = {}) => {
keyboard: true,
});
const bsModalDelete = new Modal(DOM.modalDelete, {
backdrop: true,
keyboard: true,
});
// console.log(DOM);
// === INIT =============
@@ -48,7 +58,7 @@ const ProductManager = (el = null, options = {}) => {
DOM.btnAdd.disabled = true;
// Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click', onClickAdd);
DOM.btnConfirmDelete.addEventListener('click', onClickConfirmDelete);
DOM.formEdit.addEventListener('submit', onSubmitEdit);
window.addEventListener('keyup', onKeyUp);
};
@@ -73,9 +83,12 @@ const ProductManager = (el = null, options = {}) => {
createProduct(product).then((data) => {
console.log(data);
if (data.success) {
Toast(data.msg, 'success').show();
loadProducts();
resetInputFields();
disableNonFunctionalButtons();
} else {
Toast(data.msg, 'error').show();
}
});
@@ -95,16 +108,19 @@ const ProductManager = (el = null, options = {}) => {
// console.log(Object.fromEntries(new FormData(DOM.formEdit)));
console.log(product);
DOM.btnUpdate.querySelector('i').classList.add('fa-spin');
const icon = DOM.btnUpdate.querySelector('i');
icon.classList.add('fa-spin');
// async Fetch Request
updateProduct(product).then((data) => {
console.log(data);
if (data.success) {
DOM.btnUpdate.querySelector('i').classList.remove('fa-spin');
Toast(data.msg, 'success').show();
icon.classList.remove('fa-spin');
resetInputFields();
bsModalEdit.hide();
loadProducts();
bsModalEdit.hide();
} else {
Toast(data.msg, 'error').show();
}
});
};
@@ -124,12 +140,36 @@ const ProductManager = (el = null, options = {}) => {
const onClickRemove = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0]; // btn.closest('tr'); // btn.parentNode.parentNode;
const id = currentRow.dataset.id;
const productName = currentRow.querySelector('.td-name').textContent.trim();
currentRow.remove();
console.log({ id, productName });
DOM.btnConfirmDelete.dataset.id = id;
DOM.productName.textContent = productName;
// currentRow.remove();
// currentRow.parentNode.removeChild(currentRow); // IE11 support
disableNonFunctionalButtons();
};
const onClickConfirmDelete = (e) => {
const btn = e.currentTarget;
const id = btn.dataset.id;
// async API request
deleteProduct(id).then((data) => {
// console.log(data);
if (data.success) {
Toast(data.msg, 'success').show();
loadProducts();
bsModalDelete.hide();
} else {
Toast(data.msg, 'error').show();
}
});
};
const onClickMoveUp = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0];
@@ -218,6 +258,7 @@ const ProductManager = (el = null, options = {}) => {
return data;
} catch (error) {
Toast(error.message || "Couldn't create product", 'error').show();
console.error(error);
throw error;
}
@@ -237,6 +278,7 @@ const ProductManager = (el = null, options = {}) => {
return data;
} catch (error) {
Toast(error.message || "Couldn't get products", 'error').show();
console.error(error);
throw error;
}
@@ -256,6 +298,7 @@ const ProductManager = (el = null, options = {}) => {
return data;
} catch (error) {
Toast(error.message || "Couldn't get product", 'error').show();
console.error(error);
throw error;
}
@@ -281,6 +324,29 @@ const ProductManager = (el = null, options = {}) => {
return data;
} catch (error) {
Toast(error.message || "Couldn't update product", 'error').show();
console.error(error);
throw error;
}
};
// HTTP - Methode - DELETE
// CRU(D) - Delete
// BREA(D) - Delete - löschen eines Produkts
const deleteProduct = async (id) => {
try {
const response = await fetch(`${SERVER_BASE_URL}/api/products/${id}`, {
method: 'DELETE',
}); // 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) {
Toast(error.message || "Couldn't delete product", 'error').show();
console.error(error);
throw error;
}
@@ -0,0 +1,59 @@
import Toastify from 'toastify-js'; // das Modul wird vom node_modules Ordner ausgelesen und gebundlet. Funktioniert nur mit JS-Bundler (rollup/esbuild)
const Toast = (text = '', variant = 'primary') => {
const getColorByName = (variant = '') => {
let color;
switch (variant.trim().toLowerCase()) {
case 'primary':
color = '#0d6efd';
break;
case 'secondary':
color = '#6c757d';
break;
case 'warning':
color = '#ffc720';
break;
case 'success':
color = '#13653f';
break;
case 'info':
color = '#25cff2';
break;
case 'error':
case 'danger':
color = '#a52834';
break;
case 'light':
color = '#babbbc';
break;
case 'dark':
color = '#373b3e';
break;
default:
console.warn('variant not found');
color = 'white';
}
return color;
};
const show = () => {
Toastify({
text,
duration: 2000,
className: variant,
gravity: 'top', // 'top' or 'bottom'
position: 'center', // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: getColorByName(variant),
},
}).showToast();
};
return {
show,
};
};
export default Toast;
@@ -1,2 +1,4 @@
// TODO: change with SASS FontAwesome Version
@import 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.css';
@import 'toastify-1.12/toastify';
@@ -0,0 +1,88 @@
/*!
* Toastify js 1.12.0
* https://github.com/apvarun/toastify-js
* @license MIT licensed
*
* Copyright (C) 2018 Varun A P
*/
.toastify {
padding: 12px 20px;
color: #ffffff;
display: inline-block;
box-shadow:
0 3px 6px -1px rgba(0, 0, 0, 0.12),
0 10px 36px -4px rgba(77, 96, 232, 0.3);
background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5);
background: linear-gradient(135deg, #73a5ff, #5477f5);
position: fixed;
opacity: 0;
transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
border-radius: 2px;
cursor: pointer;
text-decoration: none;
max-width: calc(50% - 20px);
z-index: 2147483647;
}
.toastify.on {
opacity: 1;
}
.toast-close {
background: transparent;
border: 0;
color: white;
cursor: pointer;
font-family: inherit;
font-size: 1em;
opacity: 0.4;
padding: 0 5px;
}
.toastify-right {
right: 15px;
}
.toastify-left {
left: 15px;
}
.toastify-top {
top: -150px;
}
.toastify-bottom {
bottom: -150px;
}
.toastify-rounded {
border-radius: 25px;
}
.toastify-avatar {
width: 1.5em;
height: 1.5em;
margin: -7px 5px;
border-radius: 2px;
}
.toastify-center {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
max-width: -moz-fit-content;
}
@media only screen and (max-width: 360px) {
.toastify-right,
.toastify-left {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
}
}
@@ -9,7 +9,8 @@
"version": "0.0.1",
"license": "ISC",
"dependencies": {
"bootstrap": "^5.3.8"
"bootstrap": "^5.3.8",
"toastify-js": "^1.12.0"
},
"devDependencies": {
"@babel/core": "^8.0.7",
@@ -8981,6 +8982,12 @@
"node": ">=10.13.0"
}
},
"node_modules/toastify-js": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
"integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==",
"license": "MIT"
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -34,6 +34,7 @@
"vinyl-source-stream": "^2.0.0"
},
"dependencies": {
"bootstrap": "^5.3.8"
"bootstrap": "^5.3.8",
"toastify-js": "^1.12.0"
}
}
@@ -106,7 +106,10 @@
<i class="fas fa-pen"></i>
<span class="visually-hidden">Edit Product</span>
</button>
<button class="btn btn-sm btn-danger button-product-remove">
<button
class="btn btn-sm btn-danger button-product-remove"
data-bs-toggle="modal"
data-bs-target="#modal-product-delete">
<i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span>
</button>
@@ -123,8 +126,6 @@
</template>
<!-- ▼ Modal Edit Product ▼ -->
<!-- Button trigger modal -->
<div
class="modal fade modal-edit"
id="modal-product-edit"
@@ -180,6 +181,34 @@
</div>
<!-- ▲ /Modal Edit Product ▲ -->
<!-- ▼ Delete Modal ▼ -->
<div
class="modal fade modal-delete"
id="modal-product-delete"
tabindex="-1"
aria-labelledby="modal-product-delete-label"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modal-product-delete-label">Delete Product</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Do you really want to delete Product:<br />
<strong class="product-name">{PRODUCT_NAME}</strong>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger button-confirm-delete">Confirm to delete</button>
</div>
</div>
</div>
</div>
<!-- ▲ /Delete Modal ▲ -->
<!-- ▼ Dialog Edit Product ▼ -->
<!-- <button type="button" class="btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#slideDownDialog">
Slide down dialog