From b7f1d800efb575b3f028fafc8f99b102a3228844 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Wed, 15 Jul 2026 00:44:25 +0100 Subject: [PATCH] Release v1.1.3: playlist drag sorting, local SortableJS, and playlist UI updates --- .dockerignore | 10 +-- package.json | 2 +- src/web/public/css/admin.css | 107 ++++++++++++++++++++++++- src/web/public/js/playlist-schedule.js | 65 +++++++-------- src/web/public/js/sortable.min.js | 2 + src/web/routes/playlists/edit.js | 2 +- src/web/view.js | 4 + src/web/views/layout.hbs | 6 +- src/web/views/playlists/edit.hbs | 20 ++--- 9 files changed, 164 insertions(+), 54 deletions(-) create mode 100644 src/web/public/js/sortable.min.js diff --git a/.dockerignore b/.dockerignore index 74ce70f..c6dfaba 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,4 @@ -node_modules -npm-debug.log -uploads -.git -.gitignore -*.tmp +* +!package.json +!src/ +!src/** diff --git a/package.json b/package.json index 906530b..24e19b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "1.1.2", + "version": "1.1.3", "private": false, "description": "Pulse Signage application with MySQL and media uploads", "repository": { diff --git a/src/web/public/css/admin.css b/src/web/public/css/admin.css index 7c6c9c7..38579b8 100644 --- a/src/web/public/css/admin.css +++ b/src/web/public/css/admin.css @@ -1876,6 +1876,107 @@ table td .actions form { align-items: center; } +.playlist-order-cell { + width: 88px; + min-width: 88px; + white-space: nowrap; + vertical-align: middle; +} + +.playlist-order-cell-inner { + display: flex; + align-items: center; + gap: 8px; + min-height: 100%; +} + +.playlist-order-number { + min-width: 1.5em; + font-weight: 800; + color: var(--muted); +} + +.playlist-drag-handle { + flex: 0 0 auto; + width: 28px; + height: 28px; + padding: 0; + border: 0; + border-radius: 0; + color: var(--muted); + background: currentColor; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M12 4L8 8H16L12 4Z'/%3E%3Cpath d='M4 11H20V13H4V11Z'/%3E%3Cpath d='M4 15H20V17H4V15Z'/%3E%3Cpath d='M4 7H20V9H4V7Z'/%3E%3Cpath d='M12 20L8 16H16L12 20Z'/%3E%3C/svg%3E") center / contain no-repeat; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M12 4L8 8H16L12 4Z'/%3E%3Cpath d='M4 11H20V13H4V11Z'/%3E%3Cpath d='M4 15H20V17H4V15Z'/%3E%3Cpath d='M4 7H20V9H4V7Z'/%3E%3Cpath d='M12 20L8 16H16L12 20Z'/%3E%3C/svg%3E") center / contain no-repeat; + cursor: grab; + transition: color 160ms ease; +} + +.playlist-drag-handle { + color: var(--muted); +} + +.playlist-drag-handle:hover, +.playlist-drag-handle:focus-visible { + color: var(--primary); +} + +html[data-theme='dark'] .playlist-drag-handle { + color: var(--sidebar-text); +} + +html[data-theme='dark'] .playlist-drag-handle:hover, +html[data-theme='dark'] .playlist-drag-handle:focus-visible { + color: var(--primary); +} + +.playlist-drag-handle:active, +tr.is-dragging .playlist-drag-handle { + cursor: grabbing; +} + +tr.sortable-chosen { + opacity: 0.55; +} + +tr.sortable-ghost { + opacity: 0.32; + background: var(--primary-soft); +} + +tr.sortable-drag { + box-shadow: var(--shadow-md); + background: var(--surface); +} + +@media (min-width: 721px) { + table.table-cards tr > td:nth-child(1), + table.table-cards tr > th:nth-child(1) { + width: 1%; + white-space: nowrap; + } + + table.table-cards tr > td:nth-child(2), + table.table-cards tr > th:nth-child(2) { + width: 36%; + } + + table.table-cards tr > td:nth-child(3), + table.table-cards tr > th:nth-child(3) { + width: 36%; + } + + table.table-cards tr > td:nth-child(4), + table.table-cards tr > th:nth-child(4) { + width: 10%; + } + + table.table-cards tr > td:nth-child(5), + table.table-cards tr > th:nth-child(5) { + width: 1%; + white-space: nowrap; + } +} + .actions form { display: inline; } @@ -1911,6 +2012,11 @@ table td .actions form { margin-left: 6px; } +html[data-theme='dark'] .chip { + background: rgba(148, 163, 184, 0.16); + color: var(--sidebar-text); +} + .slide-editor-top { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); @@ -1993,7 +2099,6 @@ table td .actions form { color: #fff; border: 1px solid rgba(255, 255, 255, 0.25); background: rgba(15, 23, 42, 0.2); - backdrop-filter: blur(1px); } .slide-preview-region h1, diff --git a/src/web/public/js/playlist-schedule.js b/src/web/public/js/playlist-schedule.js index 75e3c6f..1b0588f 100644 --- a/src/web/public/js/playlist-schedule.js +++ b/src/web/public/js/playlist-schedule.js @@ -230,7 +230,6 @@ var addSlideForm = document.getElementById('playlist-add-slide-form'); var addSlideButton = document.getElementById('playlist-add-slide-button'); var addSlideSelect = document.getElementById('playlist-add-slide-select'); - var addDurationInput = document.getElementById('playlist-add-duration'); var addSlideEmpty = document.getElementById('playlist-add-slide-empty'); if (!tbody || !form) { @@ -351,9 +350,6 @@ if (addSlideSelect) { addSlideSelect.disabled = availableCount === 0; } - if (addDurationInput) { - addDurationInput.disabled = availableCount === 0; - } if (addSlideEmpty) { addSlideEmpty.style.display = availableCount === 0 ? '' : 'none'; } @@ -366,17 +362,9 @@ function updateRowOrder() { var rows = getRows(); rows.forEach(function (row, index) { - var orderCell = row.querySelector('.playlist-order-cell'); - var moveUp = row.querySelector('[data-playlist-move="up"]'); - var moveDown = row.querySelector('[data-playlist-move="down"]'); - if (orderCell) { - orderCell.textContent = String(index + 1); - } - if (moveUp) { - moveUp.disabled = index === 0; - } - if (moveDown) { - moveDown.disabled = index === rows.length - 1; + var orderNumber = row.querySelector('.playlist-order-number'); + if (orderNumber) { + orderNumber.textContent = String(index + 1); } }); syncAddSlideOptions(); @@ -423,9 +411,13 @@ row.setAttribute('data-slide-id', String(values.slide_id)); row.setAttribute('data-canvas-signature', String(values.canvas_signature || '')); row.innerHTML = '' + - '' + + '' + + '
' + + '' + + '' + + '
' + + '' + '' + values.title + '' + - '' + '' + '
' + values.summary + '
' + '' + @@ -435,9 +427,8 @@ '' + '' + '' + + '' + '
' + - '' + - '' + '' + '' + '
'; @@ -445,7 +436,6 @@ } tbody.addEventListener('click', function (event) { - var moveButton = event.target.closest('[data-playlist-move]'); var removeButton = event.target.closest('[data-playlist-remove-row]'); var scheduleButton = event.target.closest('[data-schedule-config]'); var row = event.target.closest('tr[data-playlist-slide-row]'); @@ -454,18 +444,6 @@ return; } - if (moveButton) { - event.preventDefault(); - var direction = moveButton.getAttribute('data-playlist-move'); - if (direction === 'up' && row.previousElementSibling && row.previousElementSibling.matches('[data-playlist-slide-row]')) { - tbody.insertBefore(row, row.previousElementSibling); - } else if (direction === 'down' && row.nextElementSibling && row.nextElementSibling.matches('[data-playlist-slide-row]')) { - tbody.insertBefore(row.nextElementSibling, row); - } - updateRowOrder(); - return; - } - if (removeButton) { event.preventDefault(); row.remove(); @@ -481,7 +459,26 @@ } }); - if (addSlideButton && addSlideSelect && addDurationInput) { + if (window.Sortable) { + Sortable.create(tbody, { + animation: 180, + handle: '[data-playlist-drag-handle]', + draggable: 'tr[data-playlist-slide-row]', + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + forceFallback: true, + fallbackOnBody: true, + fallbackTolerance: 3, + swapThreshold: 0.65, + invertedSwapThreshold: 0.65, + onEnd: function () { + updateRowOrder(); + } + }); + } + + if (addSlideButton && addSlideSelect) { addSlideButton.addEventListener('click', function () { var selectedOption = addSlideSelect.options[addSlideSelect.selectedIndex]; if (!selectedOption || selectedOption.disabled) { @@ -496,7 +493,7 @@ slide_id: String(selectedOption.value), canvas_signature: String(selectedOption.getAttribute('data-canvas-signature') || ''), title: selectedOption.textContent || 'Slide', - duration_seconds: Math.max(1, Number(addDurationInput.value || 10)), + duration_seconds: 10, schedule_mode: 'always', schedule_start_datetime: '', schedule_end_datetime: '', diff --git a/src/web/public/js/sortable.min.js b/src/web/public/js/sortable.min.js new file mode 100644 index 0000000..8148b9d --- /dev/null +++ b/src/web/public/js/sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.7 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function m(t){return t.host&&t!==document&&t.host.nodeType&&t.host!==t?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&g(t,e)||o&&t===n)return t}while(t!==n&&(t=m(t)))}return null}var v,b=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(b," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(b," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function D(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function E(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function jt(t){$&&$.parentNode[K]._isOutsideThisEl(t.target)}function Ht(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Rt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Ht.supportPointer&&"PointerEvent"in window&&(!u||d),emptyInsertThreshold:5};for(n in G.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Xt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Pt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?f(t,"pointerdown",this._onTapStart):(f(t,"mousedown",this._onTapStart),f(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(f(t,"dragover",this),f(t,"dragenter",this)),_t.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,N())}function Lt(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Kt(t){t.draggable=!1}function Wt(){Ot=!1}function zt(t){return setTimeout(t,0)}function Gt(t){return clearTimeout(t)}Ht.prototype={constructor:Ht,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(bt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,$):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Mt.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Mt.push(o)}}(o),!$&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||nt===l)){if(rt=j(l),lt=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return Z({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),q("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return Z({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),q("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!$&&n.parentNode===r&&(o=X(n),tt=r,Q=($=n).parentNode,et=$.nextSibling,nt=n,ct=a.group,dt={target:Ht.dragged=$,clientX:(e||t).clientX,clientY:(e||t).clientY},gt=dt.clientX-o.left,mt=dt.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,$.style["will-change"]="all",o=function(){q("delayEnded",i,{evt:t}),Ht.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!c&&i.nativeDraggable&&($.draggable=!0),i._triggerDragStart(t,e),Z({sortable:i,name:"choose",originalEvent:t}),k($,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){E($,t.trim(),Kt)}),f(l,"dragover",Ft),f(l,"mousemove",Ft),f(l,"touchmove",Ft),a.supportPointer?(f(l,"pointerup",i._onDrop),this.nativeDraggable||f(l,"pointercancel",i._onDrop)):(f(l,"mouseup",i._onDrop),f(l,"touchend",i._onDrop),f(l,"touchcancel",i._onDrop)),c&&this.nativeDraggable&&(this.options.touchStartThreshold=4,$.draggable=!0),q("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Ht.eventCanceled?this._onDrop():(a.supportPointer?(f(l,"pointerup",i._disableDelayedDrag),f(l,"pointercancel",i._disableDelayedDrag)):(f(l,"mouseup",i._disableDelayedDrag),f(l,"touchend",i._disableDelayedDrag),f(l,"touchcancel",i._disableDelayedDrag)),f(l,"mousemove",i._delayedDragTouchMoveHandler),f(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&f(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){$&&Kt($),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?f(document,"pointermove",this._onTouchMove):f(document,e?"touchmove":"mousemove",this._onTouchMove):(f($,"dragend",this),f(tt,"dragstart",this._onDragStart));try{document.selection?zt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Et=!1,tt&&$?(q("dragStarted",this,{evt:e}),this.nativeDraggable&&f(document,"dragover",jt),n=this.options,t||k($,n.dragClass,!1),k($,n.ghostClass,!0),Ht.active=this,t&&this._appendGhost(),Z({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ht){this._lastX=ht.clientX,this._lastY=ht.clientY,Yt();for(var t=document.elementFromPoint(ht.clientX,ht.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ht.clientX,ht.clientY))!==e;)e=t;if($.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:ht.clientX,clientY:ht.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=m(t=e));Bt()}},_onTouchMove:function(t){if(dt){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=J&&D(J,!0),a=J&&r&&r.a,l=J&&r&&r.d,e=Nt&&Dt&&S(Dt),a=(i.clientX-dt.clientX+o.x)/(a||1)+(e?e[0]-xt[0]:0)/(a||1),l=(i.clientY-dt.clientY+o.y)/(l||1)+(e?e[1]-xt[1]:0)/(l||1);if(!Ht.active&&!Et){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,J),e?t.clientX<_.left-10||t.clientY + {{#if (isExternalUrl this)}} + + {{else}} + + {{/if}} {{/each}} {{/if}} diff --git a/src/web/views/playlists/edit.hbs b/src/web/views/playlists/edit.hbs index ad7470f..1c231e1 100644 --- a/src/web/views/playlists/edit.hbs +++ b/src/web/views/playlists/edit.hbs @@ -29,19 +29,21 @@

Playlist items

- + {{#if playlistSlides.length}} {{#each playlistSlides}} - + - +
OrderSlideDurationScheduleActions
OrderSlideScheduleDurationActions
{{displayOrder}} +
+ + {{displayOrder}} +
+
{{title}} - -
{{scheduleSummary}}
@@ -51,10 +53,11 @@
+ +
- -
@@ -82,9 +85,6 @@ {{/each}} -