diff --git a/shopping_app/static/css/split/layout.css b/shopping_app/static/css/split/layout.css index 02a4c72..0ca8276 100644 --- a/shopping_app/static/css/split/layout.css +++ b/shopping_app/static/css/split/layout.css @@ -1382,6 +1382,14 @@ input[type="checkbox"].form-check-input, min-width: 0; overflow-wrap: break-word; word-break: normal; + appearance: none; + background: transparent; + border: 0; + padding: 0; + margin: 0; + text-align: left; + font: inherit; + color: inherit; } .shopping-item-text .info-line { @@ -2284,3 +2292,35 @@ body:not(.sorting-active) .drag-handle { color: rgba(255,255,255,.66); line-height: 1.35; } + + +.endpoint-view_list .shopping-item-name[data-item-menu-trigger], +.endpoint-list .shopping-item-name[data-item-menu-trigger] { + cursor: pointer; +} + +.endpoint-view_list .shopping-item-name[data-item-menu-trigger]:hover, +.endpoint-view_list .shopping-item-name[data-item-menu-trigger]:focus-visible, +.endpoint-list .shopping-item-name[data-item-menu-trigger]:hover, +.endpoint-list .shopping-item-name[data-item-menu-trigger]:focus-visible { + text-decoration: none; +} + +#desktopItemMenu { + position: fixed; + z-index: 1200; + min-width: 10rem; + display: grid; + gap: .35rem; + padding: .45rem; + border: 1px solid rgba(255, 255, 255, .12); + border-radius: .9rem; + background: rgba(18, 20, 24, .96); + box-shadow: 0 16px 38px rgba(0, 0, 0, .34); + backdrop-filter: blur(10px); +} + +#desktopItemMenu[hidden] { + display: none !important; +} + diff --git a/shopping_app/static/js/desktop_item_menu.js b/shopping_app/static/js/desktop_item_menu.js new file mode 100644 index 0000000..8dde2ed --- /dev/null +++ b/shopping_app/static/js/desktop_item_menu.js @@ -0,0 +1,126 @@ +(function () { + const DESKTOP_QUERY = '(min-width: 992px) and (pointer: fine)'; + + function isDesktopOwnerList() { + return window.matchMedia(DESKTOP_QUERY).matches + && !window.IS_SHARE + && ( + document.body.classList.contains('endpoint-view_list') + || document.body.classList.contains('endpoint-list') + ); + } + + function getMenu() { + return document.getElementById('desktopItemMenu'); + } + + function hideDesktopItemMenu() { + const menu = getMenu(); + if (!menu) return; + menu.hidden = true; + delete menu.dataset.itemId; + delete menu.dataset.itemName; + delete menu.dataset.itemQuantity; + } + + function positionMenu(menu, clickX, clickY) { + const gap = 14; + menu.style.left = '0px'; + menu.style.top = '0px'; + menu.hidden = false; + + const rect = menu.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let left = clickX - (rect.width / 2); + let top = clickY - rect.height - gap; + + if (top < 12) { + top = Math.min(viewportHeight - rect.height - 12, clickY + gap); + } + + left = Math.max(12, Math.min(left, viewportWidth - rect.width - 12)); + top = Math.max(12, Math.min(top, viewportHeight - rect.height - 12)); + + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; + } + + function showDesktopItemMenu(trigger, event) { + const menu = getMenu(); + if (!menu) return; + + menu.dataset.itemId = trigger.dataset.itemId || ''; + menu.dataset.itemName = trigger.dataset.itemName || ''; + menu.dataset.itemQuantity = trigger.dataset.itemQuantity || '1'; + + let clickX = event.clientX || 0; + let clickY = event.clientY || 0; + + if (!clickX && !clickY) { + const rect = trigger.getBoundingClientRect(); + clickX = rect.left + (rect.width / 2); + clickY = rect.top; + } + + positionMenu(menu, clickX, clickY); + } + + document.addEventListener('click', function (event) { + const menu = getMenu(); + const trigger = event.target.closest('[data-item-menu-trigger="true"]'); + + if (trigger && isDesktopOwnerList() && !trigger.disabled) { + event.preventDefault(); + event.stopPropagation(); + showDesktopItemMenu(trigger, event); + return; + } + + if (menu && !menu.hidden && !event.target.closest('#desktopItemMenu')) { + hideDesktopItemMenu(); + } + }); + + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape') { + hideDesktopItemMenu(); + } + }); + + ['scroll', 'resize'].forEach(function (eventName) { + window.addEventListener(eventName, hideDesktopItemMenu, true); + }); + + document.addEventListener('DOMContentLoaded', function () { + const menu = getMenu(); + if (!menu) return; + + menu.addEventListener('click', function (event) { + const actionButton = event.target.closest('[data-menu-action]'); + if (!actionButton) return; + + const itemId = parseInt(menu.dataset.itemId || '', 10); + const itemName = menu.dataset.itemName || ''; + const itemQuantity = parseInt(menu.dataset.itemQuantity || '1', 10) || 1; + + if (!itemId) { + hideDesktopItemMenu(); + return; + } + + if (actionButton.dataset.menuAction === 'edit') { + openEditItemModal(event, itemId, itemName, itemQuantity); + } + + if (actionButton.dataset.menuAction === 'delete') { + deleteItem(itemId); + } + + hideDesktopItemMenu(); + }); + }); + + window.hideDesktopItemMenu = hideDesktopItemMenu; +})(); diff --git a/shopping_app/static/js/functions.js b/shopping_app/static/js/functions.js index 09fc220..b5a71a8 100644 --- a/shopping_app/static/js/functions.js +++ b/shopping_app/static/js/functions.js @@ -385,8 +385,9 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal const isOwner = window.IS_OWNER === true || window.IS_OWNER === 'true'; const isArchived = window.IS_ARCHIVED === true || window.IS_ARCHIVED === 'true'; - const safeName = escapeHtml(item.name || ''); - const nameForEdit = JSON.stringify(String(item.name || '')); + const rawName = String(item.name || ''); + const safeName = escapeHtml(rawName); + const nameForEdit = JSON.stringify(rawName); const quantity = Number.isInteger(item.quantity) ? item.quantity : parseInt(item.quantity, 10) || 1; const quantityBadge = quantity > 1 ? `x${quantity}` @@ -417,6 +418,15 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal const iconBtn = 'btn btn-outline-light btn-sm shopping-action-btn'; const wideBtn = 'btn btn-outline-light btn-sm shopping-action-btn shopping-action-btn--wide'; + const itemNameHtml = canEditListItem + ? `` + : `${safeName}`; let actionButtons = ''; if (canEditListItem) { @@ -454,7 +464,7 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal
- ${safeName} +${itemNameHtml} ${quantityBadge} ${infoHtml}
diff --git a/shopping_app/templates/list.html b/shopping_app/templates/list.html index 23501ee..086497d 100644 --- a/shopping_app/templates/list.html +++ b/shopping_app/templates/list.html @@ -125,7 +125,13 @@
- {{ item.name }} + {% if item.quantity and item.quantity > 1 %} x{{ item.quantity }} {% endif %} @@ -166,6 +172,11 @@ {% endfor %} + +