csdsolutions-csdjs
Version:
Libreria per i progetti di CSD Solutions
747 lines (624 loc) • 23.3 kB
JavaScript
import { Sortable } from './csd-lib.js';
// Layout Components
function initializeModal(element) {
if (!element) return;
const $modal = $CSD(element);
const $closeButtons = $modal.find('.csd-modal-close');
const $overlay = $modal.find('.csd-modal-overlay');
function closeModal() {
$modal.removeClass('show');
setTimeout(() => {
$modal.hide();
}, 300);
}
function openModal() {
$modal.show();
setTimeout(() => {
$modal.addClass('show');
}, 50);
}
$closeButtons.on('click', closeModal);
$overlay.on('click', closeModal);
// Store the modal instance on the element
element.csdModal = {
open: openModal,
close: closeModal
};
return element.csdModal;
}
function initializeConfirmDialog(element) {
if (!element) return;
const $dialog = $CSD(element);
const $confirmBtn = $dialog.find('.csd-confirm-btn');
const $cancelBtn = $dialog.find('.csd-cancel-btn, .csd-modal-close');
const $overlay = $dialog.find('.csd-modal-overlay');
let resolvePromise;
function closeDialog(result) {
$dialog.removeClass('show');
setTimeout(() => {
$dialog.hide();
if (resolvePromise) {
resolvePromise(result);
resolvePromise = null;
}
}, 300);
}
function openDialog() {
$dialog.show();
setTimeout(() => {
$dialog.addClass('show');
}, 50);
return new Promise(resolve => {
resolvePromise = resolve;
});
}
$confirmBtn.on('click', () => closeDialog(true));
$cancelBtn.on('click', () => closeDialog(false));
$overlay.on('click', () => closeDialog(false));
// Store the dialog instance on the element
element.csdConfirmDialog = {
open: openDialog,
close: closeDialog
};
return element.csdConfirmDialog;
}
function initializePopover(element) {
if (!element) return;
const $popover = $CSD(element);
const $trigger = $CSD($popover.data('trigger'));
const placement = $popover.data('placement') || 'bottom';
const trigger = $popover.data('trigger-event') || 'click';
const isConfirm = $popover.hasClass('csd-popover-confirm');
let resolvePromise;
let $currentPopover = null;
function createPopover() {
// Remove any existing popover
if ($currentPopover) {
$currentPopover.remove();
}
// Clone the template
$currentPopover = $popover.clone(true);
// Add to body
$CSD('body').append($currentPopover);
return $currentPopover;
}
function positionPopover($p) {
if (!$trigger.length || !$p) return;
const triggerRect = $trigger[0].getBoundingClientRect();
const popoverRect = $p[0].getBoundingClientRect();
const spacing = 12;
let top, left;
switch (placement) {
case 'top':
top = triggerRect.top - popoverRect.height - spacing;
left = triggerRect.left + (triggerRect.width / 2) - (popoverRect.width / 2);
break;
case 'bottom':
top = triggerRect.bottom + 0;
left = triggerRect.left + (triggerRect.width / 2) - (popoverRect.width / 2);
break;
case 'left':
top = triggerRect.top + (triggerRect.height / 2) - (popoverRect.height / 2);
left = triggerRect.left - popoverRect.width - spacing;
break;
case 'right':
top = triggerRect.top + (triggerRect.height / 2) - (popoverRect.height / 2);
left = triggerRect.right + spacing;
break;
}
top += window.scrollY;
left += window.scrollX;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
if (left < spacing) {
left = spacing;
} else if (left + popoverRect.width > viewportWidth - spacing) {
left = viewportWidth - popoverRect.width - spacing;
}
if (top < spacing) {
top = spacing;
} else if (top + popoverRect.height > viewportHeight - spacing) {
top = viewportHeight - popoverRect.height - spacing;
}
$p.css({
top: `${Math.round(top)}px`,
left: `${Math.round(left)}px`
});
}
function showPopover() {
// Remove any existing popovers
$CSD('.csd-popover.show').remove();
// Create new popover
const $p = createPopover();
// Show and position it
$p.addClass('show');
requestAnimationFrame(() => positionPopover($p));
// Set focus handlers
setupFocusHandlers($p);
}
function setupFocusHandlers($p) {
let isProcessing = false;
function handleConfirmAction(isConfirm) {
if (isProcessing) return;
isProcessing = true;
if (resolvePromise) {
resolvePromise(isConfirm);
resolvePromise = null;
}
window.toast.show(
isConfirm ? 'Operazione confermata' : 'Operazione annullata',
isConfirm ? 'success' : 'info'
);
hidePopover();
}
// Track focus within popover
const checkFocus = (e) => {
if (isConfirm) {
// Check if clicked element is confirm or cancel button
const isConfirmBtn = e.target.classList.contains('csd-popover-confirm-btn');
const isCancelBtn = e.target.classList.contains('csd-popover-cancel-btn');
if (isConfirmBtn) {
handleConfirmAction(true);
return;
}
if (isCancelBtn) {
handleConfirmAction(false);
return;
}
}
// For non-confirm popovers, hide when clicking outside
if (!$p[0].contains(e.target) && !$trigger[0].contains(e.target)) {
hidePopover();
}
};
// Add click handlers for confirm/cancel buttons
if (isConfirm) {
$p.find('.csd-popover-confirm-btn').on('click', () => handleConfirmAction(true));
$p.find('.csd-popover-cancel-btn').on('click', () => handleConfirmAction(false));
}
// Use both mousedown and focusin for better coverage
document.addEventListener('mousedown', checkFocus);
document.addEventListener('focusin', checkFocus);
// Cleanup when popover is hidden
const cleanup = () => {
document.removeEventListener('mousedown', checkFocus);
document.removeEventListener('focusin', checkFocus);
$p.off('hidden.csd.popover', cleanup);
if (isConfirm) {
$p.find('.csd-popover-confirm-btn').off('click');
$p.find('.csd-popover-cancel-btn').off('click');
}
};
$p.on('hidden.csd.popover', cleanup);
}
function hidePopover() {
if ($currentPopover) {
// Trigger cleanup event to remove all listeners
$currentPopover.trigger('hidden.csd.popover');
$currentPopover.removeClass('show');
setTimeout(() => {
// Remove all event listeners
$currentPopover.off();
$CSD(document).off('mousedown.csd.popover');
$CSD(document).off('focusin.csd.popover');
$CSD(window).off('resize.csd.popover scroll.csd.popover');
$currentPopover.remove();
$currentPopover = null;
}, 200); // Match transition duration
if (resolvePromise) {
resolvePromise(false);
resolvePromise = null;
}
}
}
function confirmAction() {
if (resolvePromise) {
resolvePromise(true);
resolvePromise = null;
}
hidePopover();
}
// Hide original template
$popover.remove();
// Event Bindings
if (trigger === 'hover') {
let hideTimeout;
$trigger.on('mouseenter', () => {
clearTimeout(hideTimeout);
showPopover();
});
$trigger.on('mouseleave', (e) => {
const toElement = e.relatedTarget;
if ($currentPopover && !$currentPopover[0].contains(toElement)) {
hideTimeout = setTimeout(hidePopover, 100);
}
});
// Hover handlers will be set up on the new popover when created
} else {
$trigger.on('click', (e) => {
e.stopPropagation();
if (isConfirm) {
showPopover();
return new Promise(resolve => {
resolvePromise = resolve;
});
} else {
const isVisible = $currentPopover && $currentPopover.hasClass('show');
if (isVisible) {
hidePopover();
} else {
showPopover();
}
}
});
}
// Update position on window resize and scroll
$CSD(window).on('resize.csd.popover scroll.csd.popover', () => {
if ($currentPopover && $currentPopover.hasClass('show')) {
debounce(() => positionPopover($currentPopover), 100)();
}
});
// Store the popover instance on the element
element.csdPopover = {
show: showPopover,
hide: hidePopover,
updatePosition: () => $currentPopover && positionPopover($currentPopover)
};
return element.csdPopover;
}
function initializeToast() {
const toastContainer = $CSD('<div>').addClass('csd-toast-container');
$CSD('body').append(toastContainer);
function createToast(message, type = 'info', duration = 3000) {
const toast = $CSD('<div>')
.addClass('csd-toast')
.addClass(`csd-toast-${type}`)
.text(message);
toastContainer.append(toast);
setTimeout(() => {
toast.addClass('show');
}, 50);
setTimeout(() => {
toast.removeClass('show');
setTimeout(() => {
toast.remove();
}, 300);
}, duration);
return toast;
}
return {
show: createToast
};
}
function initializeSplitter(element) {
if (!element || element.initialized) return;
const $splitter = $CSD(element);
let activeSplitter = null;
let startPosition = null;
let startSizes = null;
function updatePanelSize($panel, size) {
$panel.data('size', size);
$panel.css('flex', `0 0 ${size}%`);
}
function handleMouseMove(e) {
if (!activeSplitter) return;
const { $prevPanel, $nextPanel, isHorizontal } = activeSplitter;
const splitRect = $splitter[0].getBoundingClientRect();
const currentPosition = isHorizontal ? e.clientX : e.clientY;
const startOffset = isHorizontal ? splitRect.left : splitRect.top;
const totalSize = isHorizontal ? splitRect.width : splitRect.height;
// Calculate the position relative to the splitter
const position = currentPosition - startOffset;
const percentage = (position / totalSize) * 100;
// Calculate new sizes ensuring they sum to 100%
const totalPercentage = startSizes.prev + startSizes.next;
let newPrevSize = percentage;
let newNextSize = totalPercentage - percentage;
// Enforce minimum size (10%)
const minSize = 10;
if (newPrevSize < minSize) {
newPrevSize = minSize;
newNextSize = totalPercentage - minSize;
} else if (newNextSize < minSize) {
newNextSize = minSize;
newPrevSize = totalPercentage - minSize;
}
// Update panel sizes
updatePanelSize($prevPanel, newPrevSize);
updatePanelSize($nextPanel, newNextSize);
// Prevent text selection while dragging
e.preventDefault();
}
function handleMouseUp(e) {
if (!activeSplitter) return;
// Remove dragging class and cursor
activeSplitter.$gutter.removeClass('dragging');
$CSD('body').css('cursor', '');
// Unbind move and up handlers
$CSD(document)
.off('mousemove.csd.splitter')
.off('mouseup.csd.splitter');
// Clear state
activeSplitter = null;
startPosition = null;
startSizes = null;
// Prevent any pending events
e.preventDefault();
}
// Initialize nested splitters
$splitter.find('.csd-splitter').each(function() {
if (!this.initialized) {
initializeSplitter(this);
}
});
// Set initial sizes for panels
const $panels = $splitter.find('.csd-split-panel');
const $gutter = $splitter.find('.csd-split-gutter');
const isHorizontal = $splitter.hasClass('horizontal');
$panels.each(function() {
const $panel = $CSD(this);
if (!$panel.data('size')) {
$panel.data('size', 50); // Default to 50% each
}
updatePanelSize($panel, $panel.data('size'));
});
// Handle gutter mousedown
$gutter.on('mousedown', function(e) {
e.preventDefault();
activeSplitter = {
$gutter: $CSD(this),
$prevPanel: $CSD(this).prev('.csd-split-panel'),
$nextPanel: $CSD(this).next('.csd-split-panel'),
isHorizontal: isHorizontal
};
startPosition = isHorizontal ? e.clientX : e.clientY;
startSizes = {
prev: activeSplitter.$prevPanel.data('size'),
next: activeSplitter.$nextPanel.data('size')
};
activeSplitter.$gutter.addClass('dragging');
$CSD('body').css('cursor', isHorizontal ? 'col-resize' : 'row-resize');
// Bind move and up handlers directly to document
$CSD(document).on('mousemove', handleMouseMove)
$CSD(document).on('mouseup', handleMouseUp)
});
// Update position on window resize and scroll
$CSD(window).on('resize.csd.splitter scroll.csd.splitter', () => {
if ($splitter.find('.csd-split-panel').length) {
$splitter.find('.csd-split-panel').each(function() {
updatePanelSize($CSD(this), $CSD(this).data('size'));
});
}
});
// Mark as initialized
element.initialized = true;
// Return cleanup function
return function destroy() {
$gutter.off('.csd.splitter');
$CSD(document)
.off('mousemove')
.off('mouseup');
element.initialized = false;
};
}
function initializeList(element) {
if (!element || element.initialized) return;
const $list = $CSD(element);
const $items = $list.find('.csd-item');
// Aggiungi interazione hover e click agli items
$items.each(function(_, item) {
const $item = $CSD(item);
$item.on('click', function(e) {
// Emetti un evento custom quando un item viene cliccato
const event = new CustomEvent('csd-item-click', {
detail: {
text: $item.find('span').text(),
element: item
}
});
element.dispatchEvent(event);
});
});
element.initialized = true;
return element;
}
function initializeReorder() {
// Inizializza i gruppi
const reorderGroups = document.querySelectorAll('.csd-reorder-group');
reorderGroups.forEach(group => {
// Configura le opzioni di base per Sortable
const sortableOptions = {
handle: '.handle',
draggable: '.csd-reorder',
ghostClass: 'csd-reorder-ghost',
dragClass: 'csd-reorder-drag',
animation: 150,
// Opzioni touch specifiche
touchStartThreshold: 0,
supportPointer: true,
fallbackTolerance: 0,
delayOnTouchOnly: true,
delay: 100,
// Prevenzione scroll durante il drag
preventScroll: true,
// Feedback aptico su mobile
forceFallback: false,
// Callback
onStart: (evt) => {
evt.item.style.touchAction = 'none';
group.classList.add('reordering');
// Disabilita lo scroll del body durante il drag
document.body.style.overflow = 'hidden';
},
onEnd: (evt) => {
evt.item.style.touchAction = '';
group.classList.remove('reordering');
// Riabilita lo scroll del body
document.body.style.overflow = '';
},
onMove: (evt) => {
// Previeni lo scroll durante il movimento
evt.preventDefault && evt.preventDefault();
return true;
}
};
// Inizializza il gruppo principale
new Sortable(group, sortableOptions);
// Inizializza i sottogruppi
const groupItems = group.querySelectorAll('.csd-group-item');
groupItems.forEach(groupItem => {
new Sortable(groupItem, {
...sortableOptions,
onStart: (evt) => {
evt.item.style.touchAction = 'none';
groupItem.classList.add('reordering');
document.body.style.overflow = 'hidden';
},
onEnd: (evt) => {
evt.item.style.touchAction = '';
groupItem.classList.remove('reordering');
document.body.style.overflow = '';
}
});
});
});
}
// Initialize all splitters on document ready
$CSD(document).ready(() => {
$CSD('.csd-splitter').each(function() {
initializeSplitter(this);
});
});
// Initialize event handlers for layout components
$CSD(document).ready(() => {
// Initialize toast container if not exists
if (!$CSD('.csd-toast-container').length) {
$CSD('body').append('<div class="csd-toast-container"></div>');
}
// Toast buttons
$CSD(document).on('click', '.csd-btn-toast', function() {
const type = $CSD(this).data('type');
const message = $CSD(this).data('message');
const $toast = $CSD('<div>')
.addClass('csd-toast')
.addClass(`csd-toast-${type}`)
.html(`
<div class="csd-toast-content">
<ion-icon name="${getToastIcon(type)}"></ion-icon>
<span>${message}</span>
</div>
<button type="button" class="csd-toast-close">
<ion-icon name="close-outline"></ion-icon>
</button>
`);
const $container = $CSD('.csd-toast-container');
$container.append($toast);
// Show with animation
setTimeout(() => {
$toast.addClass('show');
}, 100);
// Auto close after 3 seconds
const autoCloseTimeout = setTimeout(() => {
closeToast($toast);
}, 2500);
// Close button handler
$toast.find('.csd-toast-close').on('click', function() {
clearTimeout(autoCloseTimeout);
closeToast($toast);
});
});
function closeToast($toast) {
$toast.removeClass('show');
setTimeout(() => {
$toast.remove();
}, 300);
}
function getToastIcon(type) {
switch(type) {
case 'success': return 'checkmark-circle-outline';
case 'warning': return 'warning-outline';
case 'error': return 'alert-circle-outline';
case 'info': return 'information-circle-outline';
default: return 'information-circle-outline';
}
}
// Initialize modals and confirm dialogs
$CSD('.csd-modal, .csd-modal-confirm').each(function() {
initializeModal(this);
});
// Modal and confirm dialog open buttons
$CSD(document).on('click', '.js-open-modal, .js-open-confirm', function(e) {
e.preventDefault();
const targetId = $CSD(this).data('target');
const $modal = $CSD(targetId);
if ($modal.length) {
$modal.show();
setTimeout(() => {
$modal.addClass('show');
}, 50);
}
});
// Modal close buttons and overlay
$CSD(document).on('click', '.csd-modal-close, .csd-modal-overlay', function(e) {
e.preventDefault();
const $modal = $CSD(this).closest('.csd-modal');
$modal.removeClass('show');
});
// Handle confirm dialog buttons
$CSD(document).on('click', '.csd-confirm-btn', function(e) {
e.preventDefault();
const $modal = $CSD(this).closest('.csd-modal-confirm');
if ($modal.length) {
console.log('Confirmed');
$modal.removeClass('show');
setTimeout(() => {
$modal.hide();
}, 300);
}
});
$CSD(document).on('click', '.csd-cancel-btn', function(e) {
e.preventDefault();
const $modal = $CSD(this).closest('.csd-modal-confirm');
if ($modal.length) {
console.log('Cancelled');
$modal.removeClass('show');
setTimeout(() => {
$modal.hide();
}, 300);
}
});
// Form submit handler
$CSD('.js-form-submit').on('click', function(e) {
e.preventDefault();
const $modal = $CSD(this).closest('.csd-modal');
const $form = $modal.find('form');
// Check form validity
if ($form[0].checkValidity()) {
// Collect form data
const formData = {};
$form.find('.csd-form-control').each(function() {
const $input = $CSD(this);
formData[$input.attr('name')] = $input.val();
});
// Handle form submission
console.log('Form data:', formData);
window.toast.show('Dati salvati con successo!', 'success');
// Clear form and close modal
$form[0].reset();
$modal.removeClass('show');
} else {
// Trigger HTML5 validation
$form[0].reportValidity();
}
});
// Handle escape key
$CSD(document).on('keydown', function(e) {
if (e.key === 'Escape') {
const $modal = $CSD('.csd-modal.show');
if ($modal.length) {
$modal.removeClass('show');
}
}
});
});
export { initializeModal, initializePopover, initializeSplitter, initializeConfirmDialog, initializeToast, initializeList, initializeReorder };