Release v1.1.3: playlist drag sorting, local SortableJS, and playlist UI updates
This commit is contained in:
1 parent
b0649d838e
commit
b7f1d800ef
9 files changed
+164
-54
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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 = '' +
|
||||
'<td class="playlist-order-cell"></td>' +
|
||||
'<td class="playlist-order-cell" data-label="Order">' +
|
||||
'<div class="playlist-order-cell-inner">' +
|
||||
'<button type="button" class="playlist-drag-handle" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder"></button>' +
|
||||
'<span class="playlist-order-number"></span>' +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'<td>' + values.title + '<input type="hidden" name="slide_id[]" value="' + values.slide_id + '" /></td>' +
|
||||
'<td><input name="duration_seconds[]" type="number" min="1" value="' + values.duration_seconds + '" required /></td>' +
|
||||
'<td>' +
|
||||
'<div class="playlist-schedule-summary">' + values.summary + '</div>' +
|
||||
'<input type="hidden" name="schedule_mode[]" value="' + values.schedule_mode + '" />' +
|
||||
@@ -435,9 +427,8 @@
|
||||
'<input type="hidden" name="schedule_end_time[]" value="' + values.schedule_end_time + '" />' +
|
||||
'<input type="hidden" name="schedule_days_json[]" value="' + values.schedule_days_json + '" />' +
|
||||
'</td>' +
|
||||
'<td><input name="duration_seconds[]" type="number" min="1" value="' + values.duration_seconds + '" required /></td>' +
|
||||
'<td><div class="actions playlist-item-actions">' +
|
||||
'<button type="button" class="secondary" data-playlist-move="up">Up</button>' +
|
||||
'<button type="button" class="secondary" data-playlist-move="down">Down</button>' +
|
||||
'<button type="button" class="schedule-config-button" disabled>Schedule</button>' +
|
||||
'<button type="button" class="danger" data-playlist-remove-row>Remove</button>' +
|
||||
'</div></td>';
|
||||
@@ -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: '',
|
||||
|
||||
Vendored
+2
@@ -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;n++)o[n]=t[n];return o}function i(t,e,n){return(e=function(t){t=function(t,e){if("object"!=typeof t||!t)return t;var n=t[Symbol.toPrimitive];if(void 0===n)return("string"===e?String:Number)(t);e=n.call(t,e||"default");if("object"!=typeof e)return e;throw new TypeError("@@toPrimitive must return a primitive value.")}(t,"string");return"symbol"==typeof t?t:t+""}(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function a(){return(a=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)!{}.hasOwnProperty.call(o,n)||(t[n]=o[n])}return t}).apply(null,arguments)}function r(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?r(Object(n),!0).forEach(function(t){i(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):r(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function l(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};var n,o={};for(n in t)if({}.hasOwnProperty.call(t,n)){if(-1!==e.indexOf(n))continue;o[n]=t[n]}return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],-1===e.indexOf(n)&&{}.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function e(t){return function(t){if(Array.isArray(t))return o(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return o(t,e);var n={}.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?o(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(t){return(s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),c=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),d=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),h={capture:!1,passive:!1};function f(t,e,n){t.addEventListener(e,n,!y&&h)}function p(t,e,n){t.removeEventListener(e,n,!y&&h)}function g(t,e){if(e&&(">"===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<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBouLine truncated
|
||||
@@ -110,6 +110,6 @@ module.exports = function renderPlaylistEditPage(playlist, data, message, curren
|
||||
selectableSlides: selectableSlides,
|
||||
playlistCanvasSignature: playlistCanvasSignature,
|
||||
playlistCanvasMismatch: playlistCanvasMismatch,
|
||||
scripts: ['js/playlist-schedule.js']
|
||||
scripts: ['js/sortable.min.js', 'js/playlist-schedule.js']
|
||||
});
|
||||
};
|
||||
@@ -10,6 +10,10 @@ Handlebars.registerHelper('eq', function (left, right) {
|
||||
return left === right;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('isExternalUrl', function (value) {
|
||||
return /^https?:\/\//i.test(String(value || ''));
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('playerUrl', function (slug) {
|
||||
const base = (process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || 'http://localhost:3001').replace(/\/$/, '');
|
||||
return `${base}/screen/${encodeURIComponent(slug)}`;
|
||||
|
||||
@@ -105,7 +105,11 @@
|
||||
{{/if}}
|
||||
{{#if scripts.length}}
|
||||
{{#each scripts}}
|
||||
<script src="/assets/{{this}}"></script>
|
||||
{{#if (isExternalUrl this)}}
|
||||
<script src="{{this}}"></script>
|
||||
{{else}}
|
||||
<script src="/assets/{{this}}"></script>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</body>
|
||||
|
||||
@@ -29,19 +29,21 @@
|
||||
<div class="card">
|
||||
<h3>Playlist items</h3>
|
||||
<table class="table-cards">
|
||||
<thead><tr><th>Order</th><th>Slide</th><th>Duration</th><th>Schedule</th><th>Actions</th></tr></thead>
|
||||
<thead><tr><th>Order</th><th>Slide</th><th>Schedule</th><th>Duration</th><th>Actions</th></tr></thead>
|
||||
<tbody id="playlist-items-body">
|
||||
{{#if playlistSlides.length}}
|
||||
{{#each playlistSlides}}
|
||||
<tr data-playlist-slide-row data-row-key="existing-{{id}}" data-slide-id="{{slide_id}}" data-canvas-signature="{{canvasSignature}}">
|
||||
<td class="playlist-order-cell" data-label="Order">{{displayOrder}}</td>
|
||||
<td class="playlist-order-cell" data-label="Order">
|
||||
<div class="playlist-order-cell-inner">
|
||||
<button type="button" class="playlist-drag-handle" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder"></button>
|
||||
<span class="playlist-order-number">{{displayOrder}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Slide">
|
||||
{{title}}
|
||||
<input type="hidden" name="slide_id[]" value="{{slide_id}}" />
|
||||
</td>
|
||||
<td data-label="Duration">
|
||||
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required />
|
||||
</td>
|
||||
<td data-label="Schedule">
|
||||
<div class="playlist-schedule-summary">{{scheduleSummary}}</div>
|
||||
<input type="hidden" name="schedule_mode[]" value="{{schedule_mode}}" />
|
||||
@@ -51,10 +53,11 @@
|
||||
<input type="hidden" name="schedule_end_time[]" value="{{schedule_end_time}}" />
|
||||
<input type="hidden" name="schedule_days_json[]" value="{{schedule_days_json}}" />
|
||||
</td>
|
||||
<td data-label="Duration">
|
||||
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required />
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions playlist-item-actions">
|
||||
<button type="button" class="secondary" data-playlist-move="up" {{#if isFirst}}disabled{{/if}}>Up</button>
|
||||
<button type="button" class="secondary" data-playlist-move="down" {{#if isLast}}disabled{{/if}}>Down</button>
|
||||
<button type="button" class="schedule-config-button" data-schedule-config="/admin/playlists/{{../playlist.id}}/slides/{{id}}/config" data-schedule-config-row="existing-{{id}}">Schedule</button>
|
||||
<button type="button" class="danger" data-playlist-remove-row>Remove</button>
|
||||
</div>
|
||||
@@ -82,9 +85,6 @@
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<label style="width: 160px;">Visible for (seconds)
|
||||
<input id="playlist-add-duration" type="number" min="1" value="10" required />
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" id="playlist-add-slide-button">Add slide</button>
|
||||
</form>
|
||||
|
||||
Reference in new issue
Block a user