Refactor page structure: Update page classes for consistency
- Changed class from "ots-displays-page" to "ots-static-page ots-displays-page" in multiple Twig view files to standardize page layout. - Enhanced schedule-page.twig with improved calendar navigation and dropdown management. - Added global dropdown dismissal functionality to improve user experience across modals and dropdowns.
This commit is contained in:
Submodule _xibo-cms-clone deleted from 56c98da0b5
@@ -970,6 +970,104 @@ textarea:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* --- Bootstrap Tagsinput / Tokenfield inside modals --- */
|
||||
.modal-body .bootstrap-tagsinput,
|
||||
.modal-body .tagsinput,
|
||||
.modal-body .tokenfield {
|
||||
background-color: var(--modal-input-bg, var(--ots-bg)) !important;
|
||||
border: 1px solid var(--modal-input-border, var(--ots-border)) !important;
|
||||
border-radius: var(--ots-radius-sm) !important;
|
||||
color: var(--modal-input-text, var(--ots-text)) !important;
|
||||
min-height: 36px;
|
||||
padding: 4px 8px !important;
|
||||
box-shadow: none !important;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput:focus-within,
|
||||
.modal-body .tagsinput:focus-within,
|
||||
.modal-body .tokenfield:focus-within {
|
||||
border-color: var(--modal-input-focus-border, var(--ots-primary)) !important;
|
||||
box-shadow: 0 0 0 3px var(--modal-input-focus-ring, rgba(79, 140, 255, 0.2)) !important;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput input,
|
||||
.modal-body .tagsinput input,
|
||||
.modal-body .tokenfield input.token-input {
|
||||
background: transparent !important;
|
||||
color: var(--modal-input-text, var(--ots-text)) !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput input::placeholder,
|
||||
.modal-body .tagsinput input::placeholder,
|
||||
.modal-body .tokenfield input.token-input::placeholder {
|
||||
color: var(--ots-text-faint, #7f8aa3) !important;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput .tag,
|
||||
.modal-body .bootstrap-tagsinput .badge,
|
||||
.modal-body .tagsinput .tag,
|
||||
.modal-body .tokenfield .token {
|
||||
background: rgba(79, 140, 255, 0.18) !important;
|
||||
border: 1px solid rgba(79, 140, 255, 0.35) !important;
|
||||
color: var(--ots-primary, #4f8cff) !important;
|
||||
border-radius: 999px !important;
|
||||
padding: 2px 8px !important;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput .tag [data-role="remove"],
|
||||
.modal-body .bootstrap-tagsinput .badge [data-role="remove"],
|
||||
.modal-body .tokenfield .token .close {
|
||||
color: var(--ots-primary, #4f8cff) !important;
|
||||
opacity: 0.7;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput .tag [data-role="remove"]:hover,
|
||||
.modal-body .bootstrap-tagsinput .badge [data-role="remove"]:hover,
|
||||
.modal-body .tokenfield .token .close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Global bootstrap-tagsinput theming (outside modals too) */
|
||||
.bootstrap-tagsinput,
|
||||
.tagsinput,
|
||||
.tokenfield {
|
||||
background-color: var(--modal-input-bg, var(--ots-bg)) !important;
|
||||
border: 1px solid var(--modal-input-border, var(--ots-border)) !important;
|
||||
border-radius: var(--ots-radius-sm) !important;
|
||||
color: var(--modal-input-text, var(--ots-text)) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.bootstrap-tagsinput input,
|
||||
.tagsinput input,
|
||||
.tokenfield input.token-input {
|
||||
background: transparent !important;
|
||||
color: var(--modal-input-text, var(--ots-text)) !important;
|
||||
}
|
||||
|
||||
.bootstrap-tagsinput .tag,
|
||||
.bootstrap-tagsinput .badge,
|
||||
.tagsinput .tag,
|
||||
.tokenfield .token {
|
||||
background: rgba(79, 140, 255, 0.18) !important;
|
||||
border: 1px solid rgba(79, 140, 255, 0.35) !important;
|
||||
color: var(--ots-primary, #4f8cff) !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
/* --- Close button --- */
|
||||
.modal-header .close,
|
||||
.modal-header [data-dismiss="modal"] {
|
||||
@@ -1369,3 +1467,66 @@ html[style], body[style] {
|
||||
background-color: var(--color-background) !important;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
OTS UPLOAD MODAL – Dark-mode refinements
|
||||
Supplements override-styles.twig which uses CSS variables for auto theming.
|
||||
These rules guarantee the upload modal stays dark when override-dark.css loads.
|
||||
============================================================================= */
|
||||
.ots-upload-content {
|
||||
background: var(--ots-surface, #141c2b);
|
||||
border-color: var(--ots-border, #2c3a54);
|
||||
}
|
||||
.ots-upload-header {
|
||||
background: var(--ots-bg, #0b111a);
|
||||
border-bottom-color: var(--ots-border, #2c3a54);
|
||||
}
|
||||
.ots-upload-title {
|
||||
color: var(--ots-text, #e6eefb);
|
||||
}
|
||||
.ots-upload-body {
|
||||
background: var(--ots-surface, #141c2b);
|
||||
color: var(--ots-text, #e6eefb);
|
||||
}
|
||||
.ots-upload-dropzone {
|
||||
border-color: var(--ots-border, #2c3a54);
|
||||
background: rgba(79,140,255,0.03);
|
||||
}
|
||||
.ots-upload-dropzone:hover,
|
||||
.ots-upload-dropzone:focus-visible {
|
||||
border-color: var(--ots-primary, #4f8cff);
|
||||
background: rgba(79,140,255,0.07);
|
||||
}
|
||||
.ots-upload-dropzone--over {
|
||||
border-color: var(--ots-primary, #4f8cff) !important;
|
||||
background: rgba(79,140,255,0.12) !important;
|
||||
}
|
||||
.ots-upload-file-item {
|
||||
background: var(--ots-bg, #0b111a);
|
||||
border-color: var(--ots-border-soft, #243047);
|
||||
}
|
||||
.ots-upload-file-name {
|
||||
color: var(--ots-text, #e6eefb);
|
||||
}
|
||||
.ots-upload-file-meta {
|
||||
color: var(--ots-text-muted, #a9b6cc);
|
||||
}
|
||||
.ots-upload-footer {
|
||||
background: var(--ots-bg, #0b111a);
|
||||
border-top-color: var(--ots-border, #2c3a54);
|
||||
}
|
||||
.ots-upload-btn-cancel {
|
||||
color: var(--ots-text-muted, #a9b6cc);
|
||||
border-color: var(--ots-border, #2c3a54);
|
||||
}
|
||||
.ots-upload-btn-cancel:hover {
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: var(--ots-text, #e6eefb);
|
||||
}
|
||||
.ots-upload-option {
|
||||
background: rgba(79,140,255,0.04);
|
||||
border-color: rgba(79,140,255,0.08);
|
||||
color: var(--ots-text, #e6eefb);
|
||||
}
|
||||
.ots-upload-option small {
|
||||
color: var(--ots-text-muted, #a9b6cc);
|
||||
}
|
||||
|
||||
@@ -186,15 +186,31 @@ body {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Hide the rowMenu column header completely */
|
||||
th.rowMenu,
|
||||
td.rowMenu {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
width: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
/* The rowMenu th is the empty header for the per-row action dropdown.
|
||||
Keep it in flow so column count matches tbody, but make it minimal.
|
||||
The matching td (last-child) holds the action button and must stay visible.
|
||||
NOTE: No background set here — inherits from .ots-table-card .table thead th rules. */
|
||||
th.rowMenu {
|
||||
width: 40px !important;
|
||||
min-width: 40px !important;
|
||||
max-width: 40px !important;
|
||||
padding: 4px !important;
|
||||
text-align: center !important;
|
||||
box-sizing: border-box !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
/* Style the action-button td to match the header width */
|
||||
table:has(th.rowMenu) > tbody > tr > td:last-child {
|
||||
width: 40px !important;
|
||||
min-width: 40px !important;
|
||||
max-width: 40px !important;
|
||||
padding: 4px !important;
|
||||
text-align: center !important;
|
||||
box-sizing: border-box !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure floating menu children aren't clipped (scoped to floated menus only) */
|
||||
@@ -3784,14 +3800,40 @@ body.ots-sidebar-open .ots-topbar {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
|
||||
|
||||
/* Card hover behavior is handled in override-styles.twig (loaded last) */
|
||||
|
||||
/* ── .ots-static-page: kill card hover on non-dashboard pages (backup) ────── */
|
||||
.ots-static-page .dashboard-card,
|
||||
.ots-static-page .content-card,
|
||||
.ots-static-page .action-card,
|
||||
.ots-static-page .action-card--modern,
|
||||
.ots-static-page .kpi-card,
|
||||
.ots-static-page .dashboard-chart-card,
|
||||
.ots-static-page .widget,
|
||||
.ots-static-page .card,
|
||||
.ots-static-page .panel,
|
||||
.ots-static-page .media-item,
|
||||
.ots-static-page [class*="-card"],
|
||||
.ots-static-page [class*="card-"] {
|
||||
transition: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 22px 50px rgba(8, 15, 30, 0.45);
|
||||
border-color: rgba(59, 130, 246, 0.45);
|
||||
.ots-static-page .dashboard-card:hover,
|
||||
.ots-static-page .content-card:hover,
|
||||
.ots-static-page .action-card:hover,
|
||||
.ots-static-page .action-card--modern:hover,
|
||||
.ots-static-page .kpi-card:hover,
|
||||
.ots-static-page .dashboard-chart-card:hover,
|
||||
.ots-static-page .widget:hover,
|
||||
.ots-static-page .card:hover,
|
||||
.ots-static-page .panel:hover,
|
||||
.ots-static-page .media-item:hover,
|
||||
.ots-static-page [class*="-card"]:hover,
|
||||
.ots-static-page [class*="card-"]:hover {
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.dashboard-card .panel-header,
|
||||
@@ -3837,6 +3879,15 @@ body.ots-sidebar-open .ots-topbar {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: visible;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Force XiboData card variant to be transparent */
|
||||
.XiboData.card,
|
||||
.XiboData.card.py-3,
|
||||
div.XiboData {
|
||||
background-color: transparent !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.kpi-card--modern {
|
||||
@@ -3874,11 +3925,7 @@ body.ots-sidebar-open .ots-topbar {
|
||||
box-shadow: 0 14px 30px rgba(8, 15, 30, 0.35);
|
||||
}
|
||||
|
||||
.action-card--modern:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(59, 130, 246, 0.45);
|
||||
box-shadow: 0 20px 36px rgba(8, 15, 30, 0.45);
|
||||
}
|
||||
/* Hover effect moved to dashboard-page specific rules at end of file */
|
||||
|
||||
/* OTS dashboard message (theme-dashboard-message.twig) */
|
||||
.ots-dashboard-message {
|
||||
@@ -3938,13 +3985,10 @@ body.ots-sidebar-open .ots-topbar {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: all var(--transition-base);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.kpi-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 1px var(--color-primary);
|
||||
}
|
||||
/* Hover effect moved to dashboard-page specific rules at end of file */
|
||||
|
||||
.kpi-header {
|
||||
display: flex;
|
||||
@@ -4220,14 +4264,11 @@ body.ots-sidebar-open .ots-topbar {
|
||||
|
||||
/* Enhanced chart container styling */
|
||||
.dashboard-chart-card {
|
||||
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
transition: none !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25) !important;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
/* Hover effect moved to dashboard-page specific rules at end of file */
|
||||
|
||||
/* Chart action improvements */
|
||||
.dashboard-chart-actions {
|
||||
@@ -4464,16 +4505,11 @@ body.ots-sidebar-open .ots-topbar {
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
transition: all var(--transition-base);
|
||||
transition: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
/* Hover effect moved to dashboard-page specific rules at end of file */
|
||||
|
||||
.action-icon {
|
||||
width: 40px;
|
||||
@@ -4535,15 +4571,13 @@ body.ots-sidebar-open .ots-topbar {
|
||||
box-shadow: var(--ots-shadow-md, 0 8px 18px rgba(0,0,0,0.12));
|
||||
margin-bottom: 0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Strongly remove any remaining gradients / shadows inside filters */
|
||||
.ots-filter-card *,
|
||||
.ots-filter-card *::before,
|
||||
.ots-filter-card *::after {
|
||||
background-image: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@@ -4631,8 +4665,8 @@ body.ots-sidebar-open .ots-topbar {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: none;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-surface-elevated);
|
||||
}
|
||||
|
||||
.ots-filter-title {
|
||||
@@ -4669,6 +4703,7 @@ body.ots-sidebar-open .ots-topbar {
|
||||
overflow: visible;
|
||||
transition: max-height 300ms ease-out, padding 300ms ease-out;
|
||||
display: block;
|
||||
background: var(--color-surface-elevated) !important;
|
||||
}
|
||||
|
||||
.ots-filter-content.collapsed {
|
||||
@@ -4679,6 +4714,22 @@ body.ots-sidebar-open .ots-topbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Ensure FilterDiv and all nested card-body elements inherit the filter background */
|
||||
.ots-filter-card .FilterDiv,
|
||||
.ots-filter-card .card-body,
|
||||
.ots-filter-card .FilterDiv.card-body {
|
||||
background: var(--color-surface-elevated) !important;
|
||||
background-color: var(--color-surface-elevated) !important;
|
||||
}
|
||||
|
||||
/* Ensure form elements have proper container backgrounds */
|
||||
.ots-filter-card form,
|
||||
.ots-filter-card .form-inline,
|
||||
.ots-filter-card .tab-content,
|
||||
.ots-filter-card .tab-pane {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ots-filter-card .nav-tabs {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
gap: 8px;
|
||||
@@ -4846,6 +4897,7 @@ body.ots-sidebar-open .ots-topbar {
|
||||
gap: 32px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
transition: grid-template-columns 200ms ease-out;
|
||||
}
|
||||
|
||||
@@ -4912,6 +4964,7 @@ body.ots-sidebar-open .ots-topbar {
|
||||
/* DataTables controls */
|
||||
.dataTables_wrapper {
|
||||
color: var(--color-text-primary) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_length,
|
||||
@@ -4919,6 +4972,17 @@ body.ots-sidebar-open .ots-topbar {
|
||||
.dataTables_wrapper .dataTables_info,
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
color: var(--color-text-secondary) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Ensure all DataTables control wrappers and button areas have transparent backgrounds */
|
||||
.dataTables_wrapper > *,
|
||||
.dataTables_wrapper .dataTables_buttons,
|
||||
.dataTables_buttons,
|
||||
.dt-buttons,
|
||||
.buttons-collection {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input,
|
||||
@@ -4935,11 +4999,36 @@ body.ots-sidebar-open .ots-topbar {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Ensure XiboData cards are transparent */
|
||||
.XiboData.card,
|
||||
.XiboData.card.py-3,
|
||||
div.XiboData.card {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#datatable-container {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Ensure all child elements of datatable-container have transparent backgrounds */
|
||||
#datatable-container > *,
|
||||
#datatable-container .XiboData,
|
||||
#datatable-container .XiboData > *,
|
||||
#datatable-container .card,
|
||||
#datatable-container .dataTables_wrapper,
|
||||
#datatable-container .dataTables_wrapper > * {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.ots-table-card .table thead th {
|
||||
@@ -5077,6 +5166,77 @@ body.ots-light-mode .ots-table-toolbar .btn-primary {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Map and list view toggle buttons */
|
||||
#map_button,
|
||||
#list_button {
|
||||
background-color: var(--color-primary) !important;
|
||||
border-color: var(--color-primary) !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
|
||||
transition: all 0.15s ease !important;
|
||||
}
|
||||
|
||||
#map_button:hover,
|
||||
#list_button:hover {
|
||||
background-color: var(--color-primary-light) !important;
|
||||
border-color: var(--color-primary-light) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#map_button:active,
|
||||
#list_button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Active state for map/list toggle buttons */
|
||||
#map_button.active,
|
||||
#list_button.active {
|
||||
background-color: #0c7a2a !important;
|
||||
border-color: #0c7a2a !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) inset !important;
|
||||
}
|
||||
|
||||
/* Dark mode toolbar button styles - ensure consistent colors across all pages */
|
||||
.ots-table-toolbar .btn-success {
|
||||
background-color: #10b981 !important;
|
||||
border-color: #10b981 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.ots-table-toolbar .btn-success:hover {
|
||||
background-color: #059669 !important;
|
||||
border-color: #059669 !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.ots-table-toolbar .btn-primary {
|
||||
background-color: #3b82f6 !important;
|
||||
border-color: #3b82f6 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.ots-table-toolbar .btn-primary:hover {
|
||||
background-color: #2563eb !important;
|
||||
border-color: #2563eb !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.ots-table-toolbar .btn-warning {
|
||||
background-color: #f59e0b !important;
|
||||
border-color: #f59e0b !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.ots-table-toolbar .btn-warning:hover {
|
||||
background-color: #d97706 !important;
|
||||
border-color: #d97706 !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.ots-table-card .dataTables_wrapper,
|
||||
.ots-table-card .dataTables_wrapper * {
|
||||
color: #e2e8f0 !important;
|
||||
@@ -5134,6 +5294,7 @@ body.ots-light-mode .ots-table-toolbar .btn-primary {
|
||||
|
||||
.ots-map-card {
|
||||
margin-top: 16px;
|
||||
height: 600px;
|
||||
min-height: 360px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
@@ -5254,6 +5415,7 @@ body.ots-light-mode .ots-displays-page #datatable-container .XiboData table.data
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
@@ -5970,9 +6132,16 @@ textarea {
|
||||
.panel,
|
||||
.panel-body,
|
||||
.table-wrapper,
|
||||
.dataTables_wrapper,
|
||||
.dataTables_wrapper *,
|
||||
.dataTables_wrapper .dataTables_scroll,
|
||||
.dataTables_wrapper .dataTables_scroll *,
|
||||
.dataTables_wrapper .dataTables_scrollBody,
|
||||
.dataTables_wrapper .dataTables_scrollHead,
|
||||
.dataTables_wrapper .dataTables_scrollHead table {
|
||||
.dataTables_wrapper .dataTables_scrollHeadInner,
|
||||
.dataTables_wrapper .dataTables_scrollHeadInner *,
|
||||
.dataTables_wrapper .dataTables_scrollHead table,
|
||||
.dataTables_wrapper .dataTables_scrollHead table * {
|
||||
opacity: 1 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
@@ -6142,13 +6311,29 @@ legend {
|
||||
|
||||
/* Dropdowns, modals, and popovers */
|
||||
.dropdown-menu,
|
||||
.dropdown-toggle,
|
||||
.popover {
|
||||
background-color: var(--color-surface) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
}
|
||||
|
||||
/* Dropdown toggles: only style those NOT inside tables or topbar.
|
||||
Table action buttons should inherit their row background.
|
||||
Topbar nav links have their own styling. */
|
||||
.dropdown-toggle:not(table .dropdown-toggle):not(.ots-table-card .dropdown-toggle):not(.ots-topbar .dropdown-toggle):not(.navbar-nav .dropdown-toggle) {
|
||||
background-color: var(--color-surface) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
}
|
||||
|
||||
/* DataTable row action buttons — no background box */
|
||||
.ots-table-card .dropdown-toggle,
|
||||
table .btn-group .dropdown-toggle,
|
||||
.dataTables_wrapper .dropdown-toggle {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.modal,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
@@ -6622,6 +6807,20 @@ a.text-muted {
|
||||
color: var(--color-text-secondary) !important;
|
||||
}
|
||||
|
||||
/* Folder tree icons - tan color like Windows Explorer */
|
||||
#container-folder-tree .fa,
|
||||
#grid-folder-filter .fa,
|
||||
.grid-folder-tree-container .fa {
|
||||
color: #D4AF85 !important;
|
||||
font-size: 1.5em !important;
|
||||
}
|
||||
|
||||
/* Folder action button icon - match folder tree tan color */
|
||||
#folder-tree-select-folder-button .fa,
|
||||
#folder-tree-select-folder-button .fas {
|
||||
color: #D4AF85 !important;
|
||||
}
|
||||
|
||||
/* Tidy (library) button - broom icon and amber colouring */
|
||||
.btn-tidy {
|
||||
background-color: var(--color-warning) !important;
|
||||
@@ -6788,52 +6987,7 @@ body.ots-light-mode .ots-displays-page #datatable-container .XiboData table.data
|
||||
|
||||
|
||||
|
||||
/* ============================================================================
|
||||
Remove hover animations inside the page wrapper
|
||||
This prevents elements inside `#page-wrapper` from animating on hover
|
||||
while keeping other site transitions intact.
|
||||
============================================================================ */
|
||||
|
||||
#page-wrapper *:hover {
|
||||
transition: none !important;
|
||||
transform: none !important;
|
||||
animation: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#page-wrapper,
|
||||
#page-wrapper * {
|
||||
will-change: auto !important;
|
||||
}
|
||||
|
||||
/* Re-enable hover/transform/animation behavior inside dashboard areas only */
|
||||
#page-wrapper .dashboard-page *:hover,
|
||||
#page-wrapper .dashboard-card *:hover,
|
||||
#page-wrapper .dashboard-panels *:hover,
|
||||
#page-wrapper .dashboard-chart-card *:hover {
|
||||
transition: initial !important;
|
||||
transform: initial !important;
|
||||
animation: initial !important;
|
||||
box-shadow: initial !important;
|
||||
}
|
||||
|
||||
#page-wrapper .dashboard-page,
|
||||
#page-wrapper .dashboard-card,
|
||||
#page-wrapper .dashboard-panels,
|
||||
#page-wrapper .dashboard-chart-card {
|
||||
will-change: auto !important;
|
||||
}
|
||||
|
||||
/* Content cards are static – no hover animation */
|
||||
.content-card {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.content-card:hover {
|
||||
transform: none !important;
|
||||
box-shadow: 0 18px 40px rgba(8, 15, 30, 0.35) !important;
|
||||
border-color: rgba(148, 163, 184, 0.18) !important;
|
||||
}
|
||||
/* Card hover behavior is handled in override-styles.twig (loaded last) */
|
||||
|
||||
/* Ensure the very bottom of the page follows the theme variables (light/dark)
|
||||
by forcing html/body and shell containers to use the variable-driven
|
||||
@@ -7478,6 +7632,76 @@ body.ots-light-mode .modal-header .btn-close:hover {
|
||||
box-shadow: 0 0 0 3px var(--modal-input-focus-ring) !important;
|
||||
}
|
||||
|
||||
/* ---------- Bootstrap Tagsinput / Tokenfield inside modals ----------------- */
|
||||
.modal-body .bootstrap-tagsinput,
|
||||
.modal-body .tagsinput,
|
||||
.modal-body .tokenfield {
|
||||
background-color: var(--modal-input-bg) !important;
|
||||
border: 1px solid var(--modal-input-border) !important;
|
||||
border-radius: var(--ots-radius-sm) !important;
|
||||
color: var(--modal-input-text) !important;
|
||||
min-height: 36px;
|
||||
padding: 4px 8px !important;
|
||||
box-shadow: none !important;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput:focus-within,
|
||||
.modal-body .tagsinput:focus-within,
|
||||
.modal-body .tokenfield:focus-within {
|
||||
border-color: var(--modal-input-focus-border) !important;
|
||||
box-shadow: 0 0 0 3px var(--modal-input-focus-ring) !important;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput input,
|
||||
.modal-body .tagsinput input,
|
||||
.modal-body .tokenfield input.token-input {
|
||||
background: transparent !important;
|
||||
color: var(--modal-input-text) !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput input::placeholder,
|
||||
.modal-body .tagsinput input::placeholder,
|
||||
.modal-body .tokenfield input.token-input::placeholder {
|
||||
color: var(--ots-text-faint, #64748b) !important;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput .tag,
|
||||
.modal-body .bootstrap-tagsinput .badge,
|
||||
.modal-body .tagsinput .tag,
|
||||
.modal-body .tokenfield .token {
|
||||
background: rgba(79, 140, 255, 0.15) !important;
|
||||
border: 1px solid rgba(79, 140, 255, 0.3) !important;
|
||||
color: var(--ots-primary, #3b82f6) !important;
|
||||
border-radius: 999px !important;
|
||||
padding: 2px 8px !important;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput .tag [data-role="remove"],
|
||||
.modal-body .bootstrap-tagsinput .badge [data-role="remove"],
|
||||
.modal-body .tokenfield .token .close {
|
||||
color: var(--ots-primary, #3b82f6) !important;
|
||||
opacity: 0.7;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-body .bootstrap-tagsinput .tag [data-role="remove"]:hover,
|
||||
.modal-body .bootstrap-tagsinput .badge [data-role="remove"]:hover,
|
||||
.modal-body .tokenfield .token .close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ---------- Tabs inside modals --------------------------------------------- */
|
||||
.modal-body .nav-tabs {
|
||||
border-bottom: 1px solid var(--modal-border) !important;
|
||||
|
||||
@@ -664,12 +664,94 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close every open dropdown / popover on the page.
|
||||
* Covers: Bootstrap 4 native dropdowns, OTS custom dropdowns,
|
||||
* the user-menu, notification drawer, and DataTable row menus.
|
||||
*/
|
||||
function closeAllDropdowns() {
|
||||
try {
|
||||
// Row dropdown menus appended to body
|
||||
document.querySelectorAll('.ots-row-dropdown').forEach(function(m) {
|
||||
m.classList.remove('show', 'ots-row-dropdown');
|
||||
m.style.cssText = '';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.dropdown.show, .btn-group.show').forEach(function(el) {
|
||||
el.classList.remove('show');
|
||||
var m = el.querySelector('.dropdown-menu.show');
|
||||
if (m) m.classList.remove('show');
|
||||
});
|
||||
document.querySelectorAll('.dropdown-menu.show').forEach(function(m) {
|
||||
m.classList.remove('show');
|
||||
});
|
||||
document.querySelectorAll('.dropdown.active, .ots-topbar-action .dropdown.active, .ots-page-actions .dropdown.active').forEach(function(el) {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
var userMenu = document.querySelector('.ots-user-menu-body.ots-user-menu-open');
|
||||
if (userMenu) {
|
||||
userMenu.classList.remove('ots-user-menu-open');
|
||||
var userToggle = document.querySelector('#navbarUserMenu');
|
||||
if (userToggle) {
|
||||
var dd = userToggle.closest('.dropdown');
|
||||
if (dd) dd.classList.remove('active', 'show');
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('.dt-buttons.active').forEach(function(w) { w.classList.remove('active'); });
|
||||
document.querySelectorAll('.dt-button-collection.show').forEach(function(c) { c.classList.remove('show'); c.style.display = 'none'; });
|
||||
if (window.jQuery) {
|
||||
window.jQuery('.dropdown-toggle[aria-expanded="true"]').attr('aria-expanded', 'false');
|
||||
window.jQuery('.dropdown-menu.show').removeClass('show');
|
||||
window.jQuery('.dropdown.show, .btn-group.show').removeClass('show');
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up global listeners that trigger closeAllDropdowns().
|
||||
*/
|
||||
function initGlobalDropdownDismiss() {
|
||||
document.addEventListener('show.bs.modal', closeAllDropdowns, true);
|
||||
try {
|
||||
if (window.jQuery) {
|
||||
window.jQuery(document).on('show.bs.modal', closeAllDropdowns);
|
||||
window.jQuery(document).on('click', '.XiboFormButton, .XiboAjaxSubmit, .XiboFormRender', function() {
|
||||
closeAllDropdowns();
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var link = e.target.closest('.dropdown-menu a, .ots-topbar a.nav-link, .ots-topbar a.dropdown-item, .XiboFormButton');
|
||||
if (link && !e.defaultPrevented) {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', closeAllDropdowns);
|
||||
try {
|
||||
var origPush = history.pushState;
|
||||
var origReplace = history.replaceState;
|
||||
history.pushState = function() { origPush.apply(this, arguments); closeAllDropdowns(); };
|
||||
history.replaceState = function() { origReplace.apply(this, arguments); closeAllDropdowns(); };
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
var content = document.getElementById('content') || document.querySelector('.ots-content');
|
||||
if (content) {
|
||||
var contentObs = new MutationObserver(debounce(closeAllDropdowns, 80));
|
||||
contentObs.observe(content, { childList: true });
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all features when DOM is ready
|
||||
*/
|
||||
function init() {
|
||||
initSidebarToggle();
|
||||
initDropdowns();
|
||||
initGlobalDropdownDismiss();
|
||||
initSearch();
|
||||
initPageInteractions();
|
||||
initDataTables();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Applications" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage API applications and connectors." %}</p>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
try{
|
||||
var stored = localStorage.getItem('ots-theme-mode');
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
var mode = stored || (prefersLight ? 'light' : 'light');
|
||||
var mode = stored || (prefersLight ? 'light' : 'dark');
|
||||
if(mode === 'light') document.documentElement.classList.add('ots-light-mode');
|
||||
else document.documentElement.classList.remove('ots-light-mode');
|
||||
}catch(e){}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Campaigns" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your campaigns and ad campaigns." %}</p>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Commands" %}</h1>
|
||||
<p class="text-muted">{% trans "Create and manage commands for Displays." %}</p>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "DataSets" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage structured data sources." %}</p>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Dayparting" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage time-based scheduling rules." %}</p>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Displays" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your player fleet and status." %}</p>
|
||||
@@ -337,7 +337,7 @@
|
||||
</table>
|
||||
|
||||
<!-- Map -->
|
||||
<div class="row">
|
||||
<div class="row" id="map-view-container" style="display:none;">
|
||||
<div class="col-sm-12">
|
||||
<div class="map-legend" style="display:none; position: absolute; z-index: 500; right: 20px; top: 10px;">
|
||||
<div class="display-map-legend" style="font-size: 12px;">
|
||||
@@ -404,4 +404,96 @@
|
||||
{# Add page source code bundle ( JS ) #}
|
||||
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
|
||||
{# Initialize map/list view toggle AFTER all other scripts load #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
function initMapListToggle() {
|
||||
var mapBtn = document.getElementById('map_button');
|
||||
var listBtn = document.getElementById('list_button');
|
||||
var mapViewContainer = document.getElementById('map-view-container');
|
||||
// DataTables wraps the <table> in a div with id "displays_wrapper"
|
||||
var tableWrapper = document.getElementById('displays_wrapper');
|
||||
// Fallback: if DataTables hasn't wrapped it yet, target the table itself
|
||||
if (!tableWrapper) {
|
||||
tableWrapper = document.getElementById('displays');
|
||||
}
|
||||
|
||||
if (!mapBtn || !listBtn || !mapViewContainer || !tableWrapper) {
|
||||
console.warn('Map/list toggle: required elements not found:', {
|
||||
mapBtn: !!mapBtn,
|
||||
listBtn: !!listBtn,
|
||||
mapViewContainer: !!mapViewContainer,
|
||||
tableWrapper: !!tableWrapper
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Map/list toggle initialized');
|
||||
|
||||
// Show list view by default
|
||||
tableWrapper.style.display = '';
|
||||
mapViewContainer.style.display = 'none';
|
||||
listBtn.classList.add('active');
|
||||
mapBtn.classList.remove('active');
|
||||
|
||||
// Map button click handler
|
||||
mapBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('Map button clicked');
|
||||
mapViewContainer.style.display = 'block';
|
||||
tableWrapper.style.display = 'none';
|
||||
mapBtn.classList.add('active');
|
||||
listBtn.classList.remove('active');
|
||||
|
||||
// Leaflet can't size itself in a hidden container.
|
||||
// After making the map visible, tell every Leaflet map
|
||||
// instance inside it to recalculate its dimensions.
|
||||
setTimeout(function() {
|
||||
var mapEl = document.getElementById('display-map');
|
||||
if (mapEl) {
|
||||
// Leaflet stores its instance on the DOM element as _leaflet_map
|
||||
var leafletKeys = Object.keys(mapEl).filter(function(k) {
|
||||
return k.indexOf('_leaflet_map') === 0 || k === '_leaflet';
|
||||
});
|
||||
// Try the standard _leaflet_map key
|
||||
if (mapEl._leaflet_map) {
|
||||
mapEl._leaflet_map.invalidateSize();
|
||||
console.log('Leaflet invalidateSize called via _leaflet_map');
|
||||
}
|
||||
// Also try iterating over Leaflet-stamped keys
|
||||
for (var i = 0; i < leafletKeys.length; i++) {
|
||||
var inst = mapEl[leafletKeys[i]];
|
||||
if (inst && typeof inst.invalidateSize === 'function') {
|
||||
inst.invalidateSize();
|
||||
console.log('Leaflet invalidateSize called via', leafletKeys[i]);
|
||||
}
|
||||
}
|
||||
// Fallback: dispatch a resize event so Leaflet picks it up
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// List button click handler
|
||||
listBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('List button clicked');
|
||||
tableWrapper.style.display = '';
|
||||
mapViewContainer.style.display = 'none';
|
||||
listBtn.classList.add('active');
|
||||
mapBtn.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Give the page bundle time to initialize DataTables
|
||||
setTimeout(initMapListToggle, 500);
|
||||
});
|
||||
} else {
|
||||
// Give the page bundle time to initialize DataTables
|
||||
setTimeout(initMapListToggle, 500);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Display Groups" %}</h1>
|
||||
<p class="text-muted">{% trans "Organize Displays into logical groups." %}</p>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Display Settings" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage Display settings profiles." %}</p>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Fonts" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage fonts for your signage content." %}</p>
|
||||
|
||||
800
custom/otssignange/views/include-file-upload.twig
Normal file
800
custom/otssignange/views/include-file-upload.twig
Normal file
@@ -0,0 +1,800 @@
|
||||
{#
|
||||
/**
|
||||
* OTS Signage — Modern Upload Media Modal
|
||||
* Replaces the core Xibo include-file-upload.twig with a redesigned,
|
||||
* drag-and-drop, multi-file upload experience.
|
||||
*
|
||||
* Reuses the existing openUploadForm(options) API so every page
|
||||
* (library, layout, fonts, player software, dataset, etc.) keeps working
|
||||
* without any caller changes.
|
||||
*
|
||||
* Dependencies already present in Xibo: jQuery, jQuery UI, jQuery File Upload,
|
||||
* Bootstrap 4 modal, moment.js.
|
||||
*/
|
||||
#}
|
||||
|
||||
{# ── Upload Modal Markup ────────────────────────────────────────────────── #}
|
||||
<div class="modal fade ots-upload-modal" id="ots-upload-modal" tabindex="-1"
|
||||
role="dialog" aria-labelledby="ots-upload-modal-title" aria-modal="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content ots-upload-content">
|
||||
|
||||
{# Header #}
|
||||
<div class="modal-header ots-upload-header">
|
||||
<h5 class="modal-title ots-upload-title" id="ots-upload-modal-title"></h5>
|
||||
<button type="button" class="ots-upload-close" data-dismiss="modal" aria-label="Close">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Body #}
|
||||
<div class="modal-body ots-upload-body">
|
||||
|
||||
{# Tab switcher: File / URL #}
|
||||
<div class="ots-upload-tabs" id="ots-upload-tabs">
|
||||
<button type="button" class="ots-upload-tab active" data-tab="file" id="ots-tab-file">
|
||||
<i class="fas fa-file-upload"></i> File
|
||||
</button>
|
||||
<button type="button" class="ots-upload-tab" data-tab="url" id="ots-tab-url">
|
||||
<i class="fas fa-link"></i> URL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# ── FILE TAB ──────────────────────────────────────────────── #}
|
||||
<div class="ots-upload-tab-content" id="ots-upload-tab-file">
|
||||
|
||||
{# Folder selector row – shown only when options.folderSelector is true #}
|
||||
<div class="ots-upload-folder-row d-none" id="ots-upload-folder-row">
|
||||
<span class="ots-upload-folder-label" id="ots-upload-folder-label"></span>
|
||||
<button type="button" class="btn btn-sm ots-upload-folder-btn" id="ots-upload-folder-btn" title="">
|
||||
<i class="fas fa-folder-open"></i> <span id="ots-upload-folder-text"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Max file size notice #}
|
||||
<div class="ots-upload-notice d-none" id="ots-upload-size-notice"></div>
|
||||
|
||||
{# Drop-zone #}
|
||||
<form id="ots-upload-form" enctype="multipart/form-data" method="POST">
|
||||
<div class="ots-upload-dropzone" id="ots-upload-dropzone" role="button" tabindex="0"
|
||||
aria-label="Drag and drop files here or click to browse">
|
||||
<div class="ots-upload-dropzone-inner">
|
||||
<div class="ots-upload-dropzone-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<rect x="4" y="8" width="40" height="32" rx="6" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<path d="M24 30V18m0 0l-6 6m6-6l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="ots-upload-dropzone-text">
|
||||
<strong id="ots-upload-drop-label">Drop files here</strong><br>
|
||||
<span class="ots-upload-dropzone-sub">or <span class="ots-upload-browse-link">browse your computer</span></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{# File input lives outside the dropzone to avoid click-event loops #}
|
||||
<input type="file" id="ots-upload-input" name="files[]" multiple class="ots-upload-input-hidden" />
|
||||
</form>
|
||||
|
||||
{# Valid extensions badge #}
|
||||
<div class="ots-upload-ext-info d-none" id="ots-upload-ext-info"></div>
|
||||
|
||||
{# Options row (update in layouts / delete old revisions) #}
|
||||
<div class="ots-upload-options d-none" id="ots-upload-options"></div>
|
||||
|
||||
{# File list / queue #}
|
||||
<div class="ots-upload-queue d-none" id="ots-upload-queue">
|
||||
<div class="ots-upload-queue-header">
|
||||
<span class="ots-upload-queue-title">Files</span>
|
||||
<span class="ots-upload-queue-count" id="ots-upload-queue-count"></span>
|
||||
</div>
|
||||
<ul class="ots-upload-file-list" id="ots-upload-file-list"></ul>
|
||||
</div>
|
||||
|
||||
</div>{# /ots-upload-tab-file #}
|
||||
|
||||
{# ── URL TAB ───────────────────────────────────────────────── #}
|
||||
<div class="ots-upload-tab-content d-none" id="ots-upload-tab-url">
|
||||
<div class="ots-upload-url-section">
|
||||
<div class="ots-upload-url-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M17 23l6-6m-3.5.5a5 5 0 017.07 0l1.42 1.42a5 5 0 010 7.07l-2.83 2.83a5 5 0 01-7.07 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M23 17l-6 6m3.5-.5a5 5 0 00-7.07 0l-1.42-1.42a5 5 0 010-7.07l2.83-2.83a5 5 0 017.07 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="ots-upload-url-desc">Add media from an external URL</p>
|
||||
<div class="ots-upload-url-fields">
|
||||
<div class="ots-upload-url-field">
|
||||
<label for="ots-upload-url-input">URL</label>
|
||||
<input type="url" id="ots-upload-url-input" class="form-control ots-upload-url-input"
|
||||
placeholder="https://example.com/image.jpg" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" class="btn ots-upload-btn-start ots-upload-url-add" id="ots-upload-url-add">
|
||||
<i class="fas fa-plus"></i> Add to queue
|
||||
</button>
|
||||
</div>
|
||||
{# URL queue list #}
|
||||
<div class="ots-upload-queue d-none" id="ots-upload-url-queue">
|
||||
<div class="ots-upload-queue-header">
|
||||
<span class="ots-upload-queue-title">URLs</span>
|
||||
<span class="ots-upload-queue-count" id="ots-upload-url-queue-count"></span>
|
||||
</div>
|
||||
<ul class="ots-upload-file-list" id="ots-upload-url-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>{# /ots-upload-tab-url #}
|
||||
|
||||
</div>
|
||||
|
||||
{# Footer #}
|
||||
<div class="modal-footer ots-upload-footer">
|
||||
<button type="button" class="btn ots-upload-btn-cancel" data-dismiss="modal" id="ots-upload-btn-cancel">Cancel</button>
|
||||
<button type="button" class="btn ots-upload-btn-start d-none" id="ots-upload-btn-start">
|
||||
<i class="fas fa-cloud-upload-alt"></i> <span id="ots-upload-btn-start-label">Start upload</span>
|
||||
</button>
|
||||
<button type="button" class="btn ots-upload-btn-done d-none" id="ots-upload-btn-done">Done</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Upload JavaScript ──────────────────────────────────────────────────── #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
/**
|
||||
* openUploadForm(options)
|
||||
* Drop-in replacement for the core Xibo openUploadForm.
|
||||
* Keeps the same options API so existing page callers (library, layout, fonts,
|
||||
* player-software, dataset) work without modification.
|
||||
*
|
||||
* Options shape (all optional except url):
|
||||
* {
|
||||
* url: String – POST endpoint
|
||||
* title: String – modal title
|
||||
* initialisedBy: String – an identifier for the caller
|
||||
* buttons: {
|
||||
* main: { label, className, callback }
|
||||
* },
|
||||
* templateOptions: {
|
||||
* multi: Boolean – allow multiple files (default true)
|
||||
* trans: { addFiles, startUpload, cancelUpload, selectFolder, ... },
|
||||
* upload: { maxSize, maxSizeMessage, validExt, validExtensionsMessage },
|
||||
* folderSelector: Boolean,
|
||||
* currentWorkingFolderId: Number,
|
||||
* oldMediaId: Number – when replacing a media item
|
||||
* oldFolderId: Number,
|
||||
* updateInAllChecked: Boolean,
|
||||
* deleteOldRevisionsChecked: Boolean,
|
||||
* },
|
||||
* uploadDoneEvent: Function – called when all uploads finish
|
||||
* }
|
||||
*/
|
||||
window.openUploadForm = function openUploadForm(options) {
|
||||
'use strict';
|
||||
|
||||
options = options || {};
|
||||
var tOpts = options.templateOptions || {};
|
||||
var trans = tOpts.trans || {};
|
||||
var upload = tOpts.upload || {};
|
||||
var multi = tOpts.multi !== false;
|
||||
|
||||
// ── References ──
|
||||
var $modal = $('#ots-upload-modal');
|
||||
var $title = $('#ots-upload-modal-title');
|
||||
var $dropzone = $('#ots-upload-dropzone');
|
||||
var $form = $('#ots-upload-form');
|
||||
var $input = $('#ots-upload-input');
|
||||
var $queue = $('#ots-upload-queue');
|
||||
var $fileList = $('#ots-upload-file-list');
|
||||
var $queueCount= $('#ots-upload-queue-count');
|
||||
var $btnStart = $('#ots-upload-btn-start');
|
||||
var $btnDone = $('#ots-upload-btn-done');
|
||||
var $btnCancel = $('#ots-upload-btn-cancel');
|
||||
var $startLabel= $('#ots-upload-btn-start-label');
|
||||
var $folderRow = $('#ots-upload-folder-row');
|
||||
var $sizeNotice= $('#ots-upload-size-notice');
|
||||
var $extInfo = $('#ots-upload-ext-info');
|
||||
var $optionsRow= $('#ots-upload-options');
|
||||
var $dropLabel = $('#ots-upload-drop-label');
|
||||
|
||||
// ── Extra references for URL tab ──
|
||||
var $tabFile = $('#ots-tab-file');
|
||||
var $tabUrl = $('#ots-tab-url');
|
||||
var $panelFile = $('#ots-upload-tab-file');
|
||||
var $panelUrl = $('#ots-upload-tab-url');
|
||||
var $urlInput = $('#ots-upload-url-input');
|
||||
var $urlAddBtn = $('#ots-upload-url-add');
|
||||
var $urlQueue = $('#ots-upload-url-queue');
|
||||
var $urlList = $('#ots-upload-url-list');
|
||||
var $urlCount = $('#ots-upload-url-queue-count');
|
||||
var urlQueue = []; // { url, id, status, $el, xhr }
|
||||
|
||||
// ── Reset state ──
|
||||
$fileList.empty();
|
||||
$urlList.empty();
|
||||
$queue.addClass('d-none');
|
||||
$urlQueue.addClass('d-none');
|
||||
$btnStart.addClass('d-none');
|
||||
$btnDone.addClass('d-none');
|
||||
$folderRow.addClass('d-none');
|
||||
$sizeNotice.addClass('d-none');
|
||||
$extInfo.addClass('d-none');
|
||||
$optionsRow.addClass('d-none').empty();
|
||||
$input.val('');
|
||||
$urlInput.val('');
|
||||
$dropzone.removeClass('ots-upload-dropzone--over ots-upload-dropzone--has-files');
|
||||
|
||||
// Reset to file tab
|
||||
$tabFile.addClass('active');
|
||||
$tabUrl.removeClass('active');
|
||||
$panelFile.removeClass('d-none');
|
||||
$panelUrl.addClass('d-none');
|
||||
|
||||
// ── Populate UI from options ──
|
||||
$title.text(options.title || 'Upload');
|
||||
$startLabel.text(trans.startUpload || 'Start upload');
|
||||
$btnCancel.text(trans.cancelUpload || 'Cancel');
|
||||
$dropLabel.text(trans.addFiles || 'Drop files here');
|
||||
|
||||
if (!multi) {
|
||||
$input.removeAttr('multiple');
|
||||
} else {
|
||||
$input.attr('multiple', 'multiple');
|
||||
}
|
||||
|
||||
// Max file size notice
|
||||
if (upload.maxSizeMessage) {
|
||||
$sizeNotice.text(upload.maxSizeMessage).removeClass('d-none');
|
||||
}
|
||||
|
||||
// Valid extensions
|
||||
if (upload.validExt) {
|
||||
var extList = upload.validExt.replace(/\|/g, ', ');
|
||||
var extMsg = upload.validExtensionsMessage || ('Allowed: ' + extList);
|
||||
$extInfo.text(extMsg).removeClass('d-none');
|
||||
}
|
||||
|
||||
// Folder selector
|
||||
if (tOpts.folderSelector) {
|
||||
$folderRow.removeClass('d-none');
|
||||
$('#ots-upload-folder-label').text((trans.selectedFolder || 'Current Folder:'));
|
||||
$('#ots-upload-folder-text').text(trans.selectFolder || 'Select Folder');
|
||||
$('#ots-upload-folder-btn').attr('title', trans.selectFolderTitle || 'Change folder');
|
||||
|
||||
// Wire folder-selector button using the CMS's built-in folder-tree modal
|
||||
// (templates['folder-tree'], initJsTreeAjax — provided by the Xibo core)
|
||||
$('#ots-upload-folder-btn').off('click').on('click', function() {
|
||||
var modalId = 'ots-upload-folder-tree-modal';
|
||||
var containerId = 'ots-upload-folder-form-tree';
|
||||
var $ftModal = $('#' + modalId);
|
||||
|
||||
// ── First open: build the modal from the Handlebars template ──
|
||||
if ($ftModal.length === 0 && typeof templates !== 'undefined' && templates['folder-tree']) {
|
||||
var folderTreeTpl = templates['folder-tree'];
|
||||
var treeConfig = {
|
||||
container: containerId,
|
||||
modal: modalId
|
||||
};
|
||||
if (typeof translations !== 'undefined' && translations.folderTree) {
|
||||
treeConfig.trans = translations.folderTree;
|
||||
}
|
||||
$('body').append(folderTreeTpl(treeConfig));
|
||||
$ftModal = $('#' + modalId);
|
||||
|
||||
// Inject OK / Cancel footer
|
||||
var $footer = $ftModal.find('.modal-footer');
|
||||
if ($footer.length === 0) {
|
||||
$footer = $('<div class="modal-footer"></div>');
|
||||
$ftModal.find('.modal-content').append($footer);
|
||||
}
|
||||
$footer.empty().append(
|
||||
'<button type="button" class="btn btn-sm ots-upload-btn-cancel" data-dismiss="modal">Cancel</button>' +
|
||||
'<button type="button" class="btn btn-sm ots-upload-btn-start" id="ots-folder-confirm-btn">' +
|
||||
'<i class="fas fa-check"></i> OK' +
|
||||
'</button>'
|
||||
);
|
||||
|
||||
// Configure as static backdrop once
|
||||
$ftModal.modal({ backdrop: 'static', keyboard: true, show: false });
|
||||
|
||||
// Fix stacked-modal body class when this modal closes
|
||||
$ftModal.on('hidden.bs.modal', function() {
|
||||
if ($('.modal:visible').length) {
|
||||
$(document.body).addClass('modal-open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($ftModal.length === 0) {
|
||||
console.warn('Folder tree template not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Every open: reset pending selection and re-init jstree ──
|
||||
var pendingFolderId = tOpts.currentWorkingFolderId || null;
|
||||
var pendingFolderName = null;
|
||||
|
||||
// Destroy previous jstree instance so it re-initialises cleanly
|
||||
var $treeContainer = $ftModal.find('#' + containerId);
|
||||
if ($treeContainer.jstree && $treeContainer.jstree(true)) {
|
||||
try { $treeContainer.jstree('destroy'); } catch(e) {}
|
||||
}
|
||||
|
||||
// Initialise jstree
|
||||
if (typeof initJsTreeAjax === 'function') {
|
||||
initJsTreeAjax($treeContainer, 'ots-upload-form', true, 600);
|
||||
}
|
||||
|
||||
// Show the modal (works on first and subsequent opens)
|
||||
$ftModal.modal('show');
|
||||
|
||||
// Bind selection handler after the modal is visible + jstree auto-select settles
|
||||
$ftModal.off('shown.bs.modal.otsUpload').on('shown.bs.modal.otsUpload', function() {
|
||||
setTimeout(function() {
|
||||
$treeContainer.off('select_node.jstree.otsUpload')
|
||||
.on('select_node.jstree.otsUpload', function(e, data) {
|
||||
if (data && data.node) {
|
||||
pendingFolderId = data.node.id;
|
||||
pendingFolderName = data.node.text || data.node.id;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// OK button — apply selection and close
|
||||
$ftModal.find('#ots-folder-confirm-btn').off('click').on('click', function() {
|
||||
if (pendingFolderId) {
|
||||
tOpts.currentWorkingFolderId = pendingFolderId;
|
||||
$('#ots-upload-folder-text').text(pendingFolderName || pendingFolderId);
|
||||
}
|
||||
$ftModal.modal('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Done button
|
||||
var mainBtn = (options.buttons && options.buttons.main) || {};
|
||||
$btnDone.text(mainBtn.label || 'Done');
|
||||
if (mainBtn.className) {
|
||||
$btnDone.attr('class', 'btn ots-upload-btn-done d-none ' + mainBtn.className);
|
||||
}
|
||||
|
||||
// ── Internal state ──
|
||||
var fileQueue = []; // { file, id, status, $el, xhr }
|
||||
var nextId = 0;
|
||||
var uploading = false;
|
||||
var uploadCount = 0;
|
||||
var successCount= 0;
|
||||
|
||||
// ── Helper: human-readable size ──
|
||||
function humanSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// ── Helper: valid extension check ──
|
||||
function isExtAllowed(filename) {
|
||||
if (!upload.validExt) return true;
|
||||
var ext = filename.split('.').pop().toLowerCase();
|
||||
var allowed = upload.validExt.toLowerCase().split('|');
|
||||
return allowed.indexOf(ext) !== -1;
|
||||
}
|
||||
|
||||
// ── Helper: generate preview (images only) ──
|
||||
function generatePreview(file, $thumb) {
|
||||
if (file.type && file.type.indexOf('image/') === 0 && file.size < 10 * 1048576) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
$thumb.css('background-image', 'url(' + e.target.result + ')').addClass('has-preview');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
// Icon based on type
|
||||
var icon = 'fa-file';
|
||||
if (file.type && file.type.indexOf('video/') === 0) icon = 'fa-file-video';
|
||||
else if (file.type && file.type.indexOf('audio/') === 0) icon = 'fa-file-audio';
|
||||
else if (file.type && file.type.indexOf('application/pdf') === 0) icon = 'fa-file-pdf';
|
||||
else if (file.name && /\.(xlsx?|csv)$/i.test(file.name)) icon = 'fa-file-excel';
|
||||
$thumb.html('<i class="fas ' + icon + '"></i>');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add files to queue ──
|
||||
function addFiles(files) {
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var file = files[i];
|
||||
|
||||
// Multi check
|
||||
if (!multi && fileQueue.length >= 1) {
|
||||
// Replace existing file
|
||||
fileQueue = [];
|
||||
$fileList.empty();
|
||||
}
|
||||
|
||||
var id = nextId++;
|
||||
var extOk = isExtAllowed(file.name);
|
||||
var sizeOk = !upload.maxSize || file.size <= upload.maxSize;
|
||||
|
||||
var statusClass = '';
|
||||
var statusText = humanSize(file.size);
|
||||
if (!extOk) { statusClass = 'ots-upload-file--error'; statusText = 'Invalid file type'; }
|
||||
else if (!sizeOk) { statusClass = 'ots-upload-file--error'; statusText = 'File too large'; }
|
||||
|
||||
var $el = $(
|
||||
'<li class="ots-upload-file-item ' + statusClass + '" data-id="' + id + '">' +
|
||||
'<div class="ots-upload-file-thumb"></div>' +
|
||||
'<div class="ots-upload-file-info">' +
|
||||
'<span class="ots-upload-file-name">' + $('<span>').text(file.name).html() + '</span>' +
|
||||
'<span class="ots-upload-file-meta">' + statusText + '</span>' +
|
||||
'<div class="ots-upload-file-progress"><div class="ots-upload-file-progress-bar"></div></div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="ots-upload-file-remove" aria-label="Remove" title="Remove">' +
|
||||
'<i class="fas fa-times"></i>' +
|
||||
'</button>' +
|
||||
'</li>'
|
||||
);
|
||||
|
||||
generatePreview(file, $el.find('.ots-upload-file-thumb'));
|
||||
|
||||
$el.find('.ots-upload-file-remove').on('click', (function(fileId) {
|
||||
return function() { removeFile(fileId); };
|
||||
})(id));
|
||||
|
||||
$fileList.append($el);
|
||||
|
||||
fileQueue.push({
|
||||
file: file,
|
||||
id: id,
|
||||
status: (extOk && sizeOk) ? 'pending' : 'error',
|
||||
$el: $el,
|
||||
xhr: null
|
||||
});
|
||||
}
|
||||
|
||||
updateQueueUI();
|
||||
}
|
||||
|
||||
// ── Remove file ──
|
||||
function removeFile(id) {
|
||||
fileQueue = fileQueue.filter(function(f) {
|
||||
if (f.id === id) {
|
||||
f.$el.slideUp(200, function() { $(this).remove(); });
|
||||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
updateQueueUI();
|
||||
}
|
||||
|
||||
// ── Queue UI update ──
|
||||
function updateQueueUI() {
|
||||
var validFiles = fileQueue.filter(function(f) { return f.status === 'pending'; });
|
||||
var total = fileQueue.length;
|
||||
$queueCount.text(total + ' file' + (total !== 1 ? 's' : ''));
|
||||
if (total > 0) {
|
||||
$queue.removeClass('d-none');
|
||||
$dropzone.addClass('ots-upload-dropzone--has-files');
|
||||
} else {
|
||||
$queue.addClass('d-none');
|
||||
$dropzone.removeClass('ots-upload-dropzone--has-files');
|
||||
}
|
||||
// Show start button only when there are valid pending files and not already uploading
|
||||
if (validFiles.length > 0 && !uploading) {
|
||||
$btnStart.removeClass('d-none');
|
||||
} else if (!uploading) {
|
||||
$btnStart.addClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Upload all pending items (files + URLs) ──
|
||||
function startUpload() {
|
||||
var filePending = fileQueue.filter(function(f) { return f.status === 'pending'; });
|
||||
var urlPending = urlQueue.filter(function(f) { return f.status === 'pending'; });
|
||||
var allPending = filePending.concat(urlPending);
|
||||
if (allPending.length === 0) return;
|
||||
uploading = true;
|
||||
uploadCount = allPending.length;
|
||||
successCount = 0;
|
||||
|
||||
$btnStart.addClass('d-none');
|
||||
$btnCancel.text(trans.cancelUpload || 'Cancel');
|
||||
|
||||
// Upload sequentially
|
||||
var idx = 0;
|
||||
function uploadNext() {
|
||||
if (idx >= allPending.length) {
|
||||
uploading = false;
|
||||
onAllDone();
|
||||
return;
|
||||
}
|
||||
var item = allPending[idx++];
|
||||
if (item.file) {
|
||||
uploadSingle(item, uploadNext);
|
||||
} else if (item.url) {
|
||||
uploadUrlItem(item, uploadNext);
|
||||
} else {
|
||||
uploadNext();
|
||||
}
|
||||
}
|
||||
uploadNext();
|
||||
}
|
||||
|
||||
// ── Upload a single file ──
|
||||
function uploadSingle(item, callback) {
|
||||
item.status = 'uploading';
|
||||
item.$el.addClass('ots-upload-file--uploading');
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('files[]', item.file, item.file.name);
|
||||
|
||||
// Standard Xibo hidden fields
|
||||
if (tOpts.currentWorkingFolderId) formData.append('folderId', tOpts.currentWorkingFolderId);
|
||||
if (tOpts.oldMediaId) formData.append('oldMediaId', tOpts.oldMediaId);
|
||||
if (tOpts.oldFolderId) formData.append('oldFolderId', tOpts.oldFolderId);
|
||||
|
||||
// Checkboxes
|
||||
$optionsRow.find('input[type="checkbox"]').each(function() {
|
||||
formData.append($(this).attr('name'), $(this).is(':checked') ? '1' : '0');
|
||||
});
|
||||
|
||||
var $bar = item.$el.find('.ots-upload-file-progress-bar');
|
||||
var $meta = item.$el.find('.ots-upload-file-meta');
|
||||
|
||||
item.xhr = $.ajax({
|
||||
url: options.url,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
xhr: function() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable) {
|
||||
var pct = Math.round((e.loaded / e.total) * 100);
|
||||
$bar.css('width', pct + '%');
|
||||
$meta.text(pct + '%');
|
||||
}
|
||||
});
|
||||
return xhr;
|
||||
},
|
||||
success: function(response) {
|
||||
item.status = 'done';
|
||||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');
|
||||
$bar.css('width', '100%');
|
||||
$meta.text('Complete');
|
||||
successCount++;
|
||||
if (typeof options.uploadDoneEvent === 'function') {
|
||||
options.uploadDoneEvent(item.file, response);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
error: function(xhr) {
|
||||
item.status = 'error';
|
||||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--error');
|
||||
var msg = 'Upload failed';
|
||||
try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e){}
|
||||
$meta.text(msg);
|
||||
$bar.css('width', '0%');
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── All uploads finished ──
|
||||
function onAllDone() {
|
||||
$btnDone.removeClass('d-none');
|
||||
$btnStart.addClass('d-none');
|
||||
$queueCount.text(successCount + '/' + uploadCount + ' uploaded');
|
||||
}
|
||||
|
||||
// ── Drag & drop ──
|
||||
$dropzone.off('.otsUpload').on({
|
||||
'dragenter.otsUpload dragover.otsUpload': function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$dropzone.addClass('ots-upload-dropzone--over');
|
||||
},
|
||||
'dragleave.otsUpload': function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$dropzone.removeClass('ots-upload-dropzone--over');
|
||||
},
|
||||
'drop.otsUpload': function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$dropzone.removeClass('ots-upload-dropzone--over');
|
||||
var dt = e.originalEvent.dataTransfer;
|
||||
if (dt && dt.files && dt.files.length) {
|
||||
addFiles(dt.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Click to browse — use native .click() on the raw DOM element;
|
||||
// jQuery's .trigger('click') does NOT open the file picker in most browsers.
|
||||
$dropzone.off('click.otsUpload').on('click.otsUpload', function(e) {
|
||||
// Don't trigger if clicking on the remove button inside the queue
|
||||
if ($(e.target).closest('.ots-upload-file-remove').length) return;
|
||||
$input[0].click();
|
||||
});
|
||||
|
||||
// Keyboard accessibility on dropzone
|
||||
$dropzone.off('keydown.otsUpload').on('keydown.otsUpload', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
$input[0].click();
|
||||
}
|
||||
});
|
||||
|
||||
// File input change
|
||||
$input.off('change.otsUpload').on('change.otsUpload', function() {
|
||||
if (this.files && this.files.length) {
|
||||
addFiles(this.files);
|
||||
// Reset so the same file can be re-selected
|
||||
this.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Start upload button
|
||||
$btnStart.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
startUpload();
|
||||
});
|
||||
|
||||
// Done button
|
||||
$btnDone.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
if (mainBtn.callback) {
|
||||
mainBtn.callback();
|
||||
}
|
||||
$modal.modal('hide');
|
||||
});
|
||||
|
||||
// Clean up on modal close
|
||||
$modal.off('hidden.bs.modal.otsUpload').on('hidden.bs.modal.otsUpload', function() {
|
||||
// Abort any in-progress uploads
|
||||
fileQueue.forEach(function(f) {
|
||||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||||
});
|
||||
fileQueue = [];
|
||||
$fileList.empty();
|
||||
uploading = false;
|
||||
});
|
||||
|
||||
// ── Tab switching ──
|
||||
$tabFile.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
$tabFile.addClass('active');
|
||||
$tabUrl.removeClass('active');
|
||||
$panelFile.removeClass('d-none');
|
||||
$panelUrl.addClass('d-none');
|
||||
});
|
||||
$tabUrl.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
$tabUrl.addClass('active');
|
||||
$tabFile.removeClass('active');
|
||||
$panelUrl.removeClass('d-none');
|
||||
$panelFile.addClass('d-none');
|
||||
});
|
||||
|
||||
// ── URL: add to queue ──
|
||||
function addUrlToQueue(url) {
|
||||
if (!url || !url.trim()) return;
|
||||
url = url.trim();
|
||||
var id = nextId++;
|
||||
var displayName = url.length > 60 ? url.substring(0, 57) + '...' : url;
|
||||
|
||||
var $el = $(
|
||||
'<li class="ots-upload-file-item" data-id="' + id + '">' +
|
||||
'<div class="ots-upload-file-thumb"><i class="fas fa-link"></i></div>' +
|
||||
'<div class="ots-upload-file-info">' +
|
||||
'<span class="ots-upload-file-name">' + $('<span>').text(displayName).html() + '</span>' +
|
||||
'<span class="ots-upload-file-meta">Ready</span>' +
|
||||
'<div class="ots-upload-file-progress"><div class="ots-upload-file-progress-bar"></div></div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="ots-upload-file-remove" aria-label="Remove" title="Remove">' +
|
||||
'<i class="fas fa-times"></i>' +
|
||||
'</button>' +
|
||||
'</li>'
|
||||
);
|
||||
|
||||
$el.find('.ots-upload-file-remove').on('click', (function(fileId) {
|
||||
return function() { removeUrlItem(fileId); };
|
||||
})(id));
|
||||
|
||||
$urlList.append($el);
|
||||
urlQueue.push({ url: url, id: id, status: 'pending', $el: $el, xhr: null });
|
||||
updateUrlQueueUI();
|
||||
}
|
||||
|
||||
function removeUrlItem(id) {
|
||||
urlQueue = urlQueue.filter(function(f) {
|
||||
if (f.id === id) {
|
||||
f.$el.slideUp(200, function() { $(this).remove(); });
|
||||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
updateUrlQueueUI();
|
||||
}
|
||||
|
||||
function updateUrlQueueUI() {
|
||||
var pending = urlQueue.filter(function(f) { return f.status === 'pending'; });
|
||||
var total = urlQueue.length;
|
||||
$urlCount.text(total + ' URL' + (total !== 1 ? 's' : ''));
|
||||
if (total > 0) {
|
||||
$urlQueue.removeClass('d-none');
|
||||
} else {
|
||||
$urlQueue.addClass('d-none');
|
||||
}
|
||||
if (pending.length > 0 && !uploading) {
|
||||
$btnStart.removeClass('d-none');
|
||||
} else if (!uploading && fileQueue.filter(function(f) { return f.status === 'pending'; }).length === 0) {
|
||||
$btnStart.addClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
$urlAddBtn.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
addUrlToQueue($urlInput.val());
|
||||
$urlInput.val('').focus();
|
||||
});
|
||||
|
||||
// Allow Enter key in URL input to add
|
||||
$urlInput.off('keydown.otsUpload').on('keydown.otsUpload', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addUrlToQueue($urlInput.val());
|
||||
$urlInput.val('').focus();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Upload a single URL item ──
|
||||
function uploadUrlItem(item, callback) {
|
||||
item.status = 'uploading';
|
||||
item.$el.addClass('ots-upload-file--uploading');
|
||||
var $bar = item.$el.find('.ots-upload-file-progress-bar');
|
||||
var $meta = item.$el.find('.ots-upload-file-meta');
|
||||
$bar.css('width', '50%');
|
||||
$meta.text('Downloading...');
|
||||
|
||||
var postData = { url: item.url };
|
||||
if (tOpts.currentWorkingFolderId) postData.folderId = tOpts.currentWorkingFolderId;
|
||||
|
||||
item.xhr = $.ajax({
|
||||
url: options.url,
|
||||
type: 'POST',
|
||||
data: postData,
|
||||
success: function(response) {
|
||||
item.status = 'done';
|
||||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');
|
||||
$bar.css('width', '100%');
|
||||
$meta.text('Complete');
|
||||
successCount++;
|
||||
if (typeof options.uploadDoneEvent === 'function') {
|
||||
options.uploadDoneEvent(null, response);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
error: function(xhr) {
|
||||
item.status = 'error';
|
||||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--error');
|
||||
var msg = 'Upload failed';
|
||||
try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e){}
|
||||
$meta.text(msg);
|
||||
$bar.css('width', '0%');
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Clean up URL queue on modal close ──
|
||||
$modal.off('hidden.bs.modal.otsUploadUrl').on('hidden.bs.modal.otsUploadUrl', function() {
|
||||
urlQueue.forEach(function(f) {
|
||||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||||
});
|
||||
urlQueue = [];
|
||||
$urlList.empty();
|
||||
});
|
||||
|
||||
// ── Show modal ──
|
||||
$modal.modal({ backdrop: 'static', keyboard: true });
|
||||
};
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Layouts" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage and design your layouts." %}</p>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Media" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your media library." %}</p>
|
||||
@@ -140,9 +140,7 @@
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabledCount(["library.add", "library.modify"]) > 0 or settings.SETTING_LIBRARY_TIDY_ENABLED == 1 %}
|
||||
{% if currentUser.featureEnabled("library.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn" href="#" id="libraryUploadForm" title="{% trans "Add a new media item to the library" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new media item to the library via external URL" %}" href="{{ url_for("library.uploadUrl.form") }}"><i class="fa fa-link" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn" href="#" id="libraryUploadForm" title="{% trans "Add a new media item to the library" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button> {% endif %}
|
||||
{% if settings.SETTING_LIBRARY_TIDY_ENABLED == 1 and currentUser.featureEnabled("library.modify") %}
|
||||
<button class="btn btn-sm btn-warning ots-toolbar-btn XiboFormButton btn-tidy" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("library.tidy.form") }}"><i class="fa fa-broom" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
@@ -410,6 +408,15 @@
|
||||
* Media Edit form
|
||||
*/
|
||||
function mediaEditFormOpen(dialog) {
|
||||
// ── OTS: Style the edit-media modal to match the upload modal ──
|
||||
// dialog IS the .modal element (returned by bootbox.dialog())
|
||||
dialog.addClass('ots-edit-media-modal');
|
||||
|
||||
// Also apply via the global enhancer in case the class wasn't added
|
||||
if (typeof window.otsEnhanceModal === 'function') {
|
||||
window.otsEnhanceModal(dialog);
|
||||
}
|
||||
|
||||
// Create a new button
|
||||
var footer = dialog.find(".modal-footer");
|
||||
var mediaId = dialog.find("#mediaEditForm").data().mediaId;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Menu Boards" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your menu boards and content." %}</p>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Modules" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage installed modules." %}</p>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Player Versions" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage player software versions and downloads." %}</p>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Playlists" %}</h1>
|
||||
<p class="text-muted">{% trans "Create and manage content playlists." %}</p>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Resolutions" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage display resolutions." %}</p>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Schedule" %}</h1>
|
||||
<p class="text-muted">{% trans "Schedule content to your displays." %}</p>
|
||||
@@ -222,14 +222,21 @@
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="xibo-calendar-header-container col-xl-12 d-inline-flex justify-content-between">
|
||||
<div class="xibo-calendar-header text-center d-inline-flex">
|
||||
<h1 class="page-header"></h1>
|
||||
</div>
|
||||
|
||||
<div class="calendar-loading">
|
||||
<span id="calendar-progress-table" class="fa fa-spin fa-cog"></span>
|
||||
<span id="calendar-progress" class="fa fa-spin fa-cog"></span>
|
||||
<div class="xibo-calendar-header-container col-xl-12">
|
||||
<div class="ots-calendar-nav">
|
||||
<button type="button" class="ots-cal-arrow ots-cal-arrow-prev" id="ots-cal-prev" title="{% trans 'Previous' %}">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
<div class="xibo-calendar-header text-center">
|
||||
<h1 class="page-header"></h1>
|
||||
<div class="calendar-loading">
|
||||
<span id="calendar-progress-table" class="fa fa-spin fa-cog"></span>
|
||||
<span id="calendar-progress" class="fa fa-spin fa-cog"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="ots-cal-arrow ots-cal-arrow-next" id="ots-cal-next" title="{% trans 'Next' %}">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -349,4 +356,14 @@
|
||||
{# Add page source code bundle ( JS ) #}
|
||||
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/pages/schedule-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script nonce="{{ cspNonce }}">
|
||||
$(function() {
|
||||
$('#ots-cal-prev').on('click', function() {
|
||||
$('button[data-calendar-nav="prev"]').trigger('click');
|
||||
});
|
||||
$('#ots-cal-next').on('click', function() {
|
||||
$('button[data-calendar-nav="next"]').trigger('click');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Settings" %}</h1>
|
||||
<p class="text-muted">{% trans "Configure CMS settings." %}</p>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Sync Groups" %}</h1>
|
||||
<p class="text-muted">{% trans "Create and manage synchronized Display groups." %}</p>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Tags" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage content tags." %}</p>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Tasks" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage scheduled system tasks." %}</p>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Templates" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your reusable templates." %}</p>
|
||||
|
||||
@@ -1131,6 +1131,9 @@
|
||||
* (before Bootstrap sees it), prevent Bootstrap from handling it, and
|
||||
* manage show/hide/position entirely ourselves.
|
||||
*/
|
||||
// Module-level reference to row dropdown's closeMenu, set by initRowDropdowns().
|
||||
var _closeRowDropdown = null;
|
||||
|
||||
function initRowDropdowns() {
|
||||
var TOGGLE_SEL = '.dropdown-menu-container [data-toggle="dropdown"], .btn-group [data-toggle="dropdown"]';
|
||||
var activeMenu = null; // currently open menu element (in <body>)
|
||||
@@ -1209,6 +1212,9 @@
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
// Expose closeMenu so closeAllDropdowns() can reach it
|
||||
_closeRowDropdown = closeMenu;
|
||||
|
||||
// Intercept clicks in CAPTURE phase — runs BEFORE Bootstrap's handler.
|
||||
document.addEventListener('click', function(e) {
|
||||
var toggle = e.target.closest(TOGGLE_SEL);
|
||||
@@ -1263,6 +1269,121 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close every open dropdown / popover on the page.
|
||||
* Covers: Bootstrap 4 native dropdowns, OTS custom dropdowns,
|
||||
* the user-menu, notification drawer, and DataTable row menus.
|
||||
*/
|
||||
function closeAllDropdowns() {
|
||||
try {
|
||||
// 0. Close row dropdown managed by initRowDropdowns()
|
||||
if (typeof _closeRowDropdown === 'function') _closeRowDropdown();
|
||||
|
||||
// Also force-remove any stray ots-row-dropdown elements left on <body>
|
||||
document.querySelectorAll('.ots-row-dropdown').forEach(function(m) {
|
||||
m.classList.remove('show', 'ots-row-dropdown');
|
||||
m.style.cssText = '';
|
||||
});
|
||||
|
||||
// 1. Bootstrap 4 native dropdowns (.show on the wrapper or the menu)
|
||||
document.querySelectorAll('.dropdown.show, .btn-group.show').forEach(function(el) {
|
||||
el.classList.remove('show');
|
||||
var m = el.querySelector('.dropdown-menu.show');
|
||||
if (m) m.classList.remove('show');
|
||||
});
|
||||
document.querySelectorAll('.dropdown-menu.show').forEach(function(m) {
|
||||
m.classList.remove('show');
|
||||
});
|
||||
|
||||
// 2. OTS custom dropdowns that use .active
|
||||
document.querySelectorAll('.dropdown.active, .ots-topbar-action .dropdown.active, .ots-page-actions .dropdown.active').forEach(function(el) {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
|
||||
// 3. User menu (body-level floating menu)
|
||||
var userMenu = document.querySelector('.ots-user-menu-body.ots-user-menu-open');
|
||||
if (userMenu) {
|
||||
userMenu.classList.remove('ots-user-menu-open');
|
||||
var userDropdown = document.querySelector('#navbarUserMenu');
|
||||
if (userDropdown) {
|
||||
var dd = userDropdown.closest('.dropdown');
|
||||
if (dd) dd.classList.remove('active', 'show');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. DataTable button collections
|
||||
document.querySelectorAll('.dt-buttons.active').forEach(function(w) {
|
||||
w.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.dt-button-collection.show').forEach(function(c) {
|
||||
c.classList.remove('show');
|
||||
c.style.display = 'none';
|
||||
});
|
||||
|
||||
// 5. jQuery-level Bootstrap cleanup (if available)
|
||||
var jq = window.jQuery || window.$;
|
||||
if (jq) {
|
||||
jq('.dropdown-toggle[aria-expanded="true"]').attr('aria-expanded', 'false');
|
||||
jq('.dropdown-menu.show').removeClass('show');
|
||||
jq('.dropdown.show, .btn-group.show').removeClass('show');
|
||||
}
|
||||
} catch (err) {
|
||||
// never let this break the page
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up global listeners that trigger closeAllDropdowns().
|
||||
* Called once from init().
|
||||
*/
|
||||
function initGlobalDropdownDismiss() {
|
||||
// ── Close when a Bootstrap modal / dialog opens ─────────────────
|
||||
document.addEventListener('show.bs.modal', closeAllDropdowns, true);
|
||||
try {
|
||||
var jq = window.jQuery || window.$;
|
||||
if (jq) {
|
||||
jq(document).on('show.bs.modal', closeAllDropdowns);
|
||||
// Xibo opens modals when .XiboFormButton / .XiboAjaxSubmit are clicked
|
||||
jq(document).on('click', '.XiboFormButton, .XiboAjaxSubmit, .XiboFormRender', function() {
|
||||
closeAllDropdowns();
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// ── Close when any <a> inside a dropdown is clicked (page nav) ──
|
||||
document.addEventListener('click', function(e) {
|
||||
var link = e.target.closest('.dropdown-menu a, .ots-topbar a.nav-link, .ots-topbar a.dropdown-item, .XiboFormButton');
|
||||
if (link && !e.defaultPrevented) {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Close on History navigation (Xibo uses pushState for AJAX pages) ──
|
||||
window.addEventListener('popstate', closeAllDropdowns);
|
||||
// Intercept pushState / replaceState so we catch Xibo's AJAX navigation
|
||||
try {
|
||||
var origPush = history.pushState;
|
||||
var origReplace = history.replaceState;
|
||||
history.pushState = function() {
|
||||
origPush.apply(this, arguments);
|
||||
closeAllDropdowns();
|
||||
};
|
||||
history.replaceState = function() {
|
||||
origReplace.apply(this, arguments);
|
||||
closeAllDropdowns();
|
||||
};
|
||||
} catch (e) {}
|
||||
|
||||
// ── Close when main content area changes (AJAX page swap) ───────
|
||||
try {
|
||||
var content = document.getElementById('content') || document.querySelector('.ots-content');
|
||||
if (content) {
|
||||
var contentObs = new MutationObserver(debounce(closeAllDropdowns, 80));
|
||||
contentObs.observe(content, { childList: true });
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all features when DOM is ready
|
||||
*/
|
||||
@@ -1273,6 +1394,7 @@
|
||||
initThemeToggle();
|
||||
initDropdowns();
|
||||
initRowDropdowns();
|
||||
initGlobalDropdownDismiss();
|
||||
initSearch();
|
||||
initPageInteractions();
|
||||
initDataTables();
|
||||
@@ -1301,6 +1423,57 @@
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* OTS: Enhance all Xibo form modals to match the upload modal design.
|
||||
* Runs on every shown.bs.modal event and also exposed as window.otsEnhanceModal()
|
||||
* for direct invocation from form callbacks like mediaEditFormOpen.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var OTS_CLOSE_SVG = '<button type="button" class="ots-upload-close" data-dismiss="modal" aria-label="Close">' +
|
||||
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>' +
|
||||
'</button>';
|
||||
|
||||
function enhanceModal(modal) {
|
||||
var $m = window.jQuery ? window.jQuery(modal) : null;
|
||||
if (!$m || !$m.length) return;
|
||||
|
||||
// Don't re-enhance
|
||||
if ($m.data('ots-enhanced')) return;
|
||||
$m.data('ots-enhanced', true);
|
||||
|
||||
// Skip the custom upload modal (it has its own styling)
|
||||
if ($m.hasClass('ots-upload-modal') || $m.attr('id') === 'ots-upload-modal') return;
|
||||
|
||||
// Add the OTS edit modal class
|
||||
$m.addClass('ots-edit-media-modal');
|
||||
|
||||
// Replace the default close button with SVG version
|
||||
var $closeBtn = $m.find('.modal-header .close, .modal-header button[data-dismiss="modal"]:not(.ots-upload-close)');
|
||||
if ($closeBtn.length) {
|
||||
$closeBtn.first().replaceWith(OTS_CLOSE_SVG);
|
||||
}
|
||||
}
|
||||
|
||||
// Expose globally so page callbacks can invoke it directly
|
||||
window.otsEnhanceModal = enhanceModal;
|
||||
|
||||
// Hook into every modal show event
|
||||
if (window.jQuery) {
|
||||
window.jQuery(document).on('shown.bs.modal', '.modal', function() {
|
||||
enhanceModal(this);
|
||||
});
|
||||
} else {
|
||||
document.addEventListener('shown.bs.modal', function(e) {
|
||||
var modal = e.target;
|
||||
if (modal && modal.classList && modal.classList.contains('modal')) {
|
||||
enhanceModal(modal);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
})();
|
||||
|
||||
// Replace broken QR images in user profile modals with a friendly placeholder
|
||||
function initUserProfileQrFix() {
|
||||
function replaceIfEmptyDataUri(el) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Transitions" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage display transitions." %}</p>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Users" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage system users and permissions." %}</p>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-displays-page">
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "User Groups" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage user groups and permissions." %}</p>
|
||||
|
||||
Reference in New Issue
Block a user