Unify upload/edit modal track-form + fix slide cropper hang & reorder loss
Track-form unification: the "Add Language Track" dialog in the edit modal now
clones the same #um-track-form-template partial that the create modal uses, so
future tweaks to markup/CSS/behaviour show up in both flows automatically.
Added window.cloneTrackFormTemplate() + window.initTrackFormNode() as the
shared cloner+init. updateTrackCard() writes to both um-tc-*-{id} and
edit-tc-*-{id} ids so one helper serves both modals. Dropped the dead
#edit-extra-track-tpl and the custom editHandleTrackAudio (handleTrackAudio
auto-fills title identically).
Cover slides cropper fixes (both modals):
- Second batch no longer hangs. _slidesCropLoadNext now calls
openCropperModal_* BEFORE tcPreload_*; previously preloading first measured
canvas.clientWidth=0 against a display:none overlay and left _st.ready
false so Save silently returned.
- Reorder no longer loses thumbnails. Create flow stored raw Files and
spawned a FileReader per slide per render; under memory pressure some
readers failed silently. Now stores {file, url} with URL.createObjectURL
called once per slide, and renderSlidesStrip assigns img.src = url
directly. Removal revokes the URL; clear revokes everything it drops.
- Reset .value on the slides input after processing so re-selecting the
same filename still fires change.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
2d33e39b9b
commit
468820c9fe
3 files changed
+243
-196
No files matched your search
@@ -306,7 +306,7 @@
|
|||||||
@if($embedAudioInput)
|
@if($embedAudioInput)
|
||||||
<input type="file" id="edit-{{ $p }}-audio-input" name="{{ $audioInputName }}"
|
<input type="file" id="edit-{{ $p }}-audio-input" name="{{ $audioInputName }}"
|
||||||
accept="audio/*,.mp3,.m4a,.aac,.flac,.wav" style="display:none;"
|
accept="audio/*,.mp3,.m4a,.aac,.flac,.wav" style="display:none;"
|
||||||
onchange="document.getElementById('edit-{{ $p }}-fname').textContent=this.files[0]?.name||'Keep existing / choose new…'">
|
onchange="editHandleTrackAudio(this, '{{ $p }}', '{{ $titleId }}')">
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -178,22 +178,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ── Secondary track form template (cloned by JS for each extra track) ──── --}}
|
{{-- Secondary tracks in this modal are cloned from the SHARED
|
||||||
<div id="edit-extra-track-tpl" style="display:none;" aria-hidden="true">
|
#um-track-form-template defined in layouts.partials.upload-modal. One
|
||||||
<x-track-editor-form
|
template → identical UX in both the create (upload) modal and this modal. --}}
|
||||||
prefix="__TPL__"
|
|
||||||
:is-primary="false"
|
|
||||||
language-name="__LANGNAME__"
|
|
||||||
language-id="csd_v___TPL__"
|
|
||||||
title-name="__TITLENAME__"
|
|
||||||
title-id="edit-__TPL__-title"
|
|
||||||
desc-name="__DESCNAME__"
|
|
||||||
desc-id="edit-__TPL__-desc"
|
|
||||||
:embed-audio-input="true"
|
|
||||||
audio-input-name="__AUDIONAME__"
|
|
||||||
video-file-input-id=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#editVideoModal .modal-dialog { opacity: 0; transition: opacity .25s ease; }
|
#editVideoModal .modal-dialog { opacity: 0; transition: opacity .25s ease; }
|
||||||
@@ -720,6 +707,8 @@ function editSlidesZoneDrop(e, tid) { tid=tid||'t1'; e.preventDefault(); doc
|
|||||||
function editHandleSlides(fileList, tid) {
|
function editHandleSlides(fileList, tid) {
|
||||||
tid = tid || 't1';
|
tid = tid || 't1';
|
||||||
_editSlidesCropStart(fileList, tid, _editRenderSlides);
|
_editSlidesCropStart(fileList, tid, _editRenderSlides);
|
||||||
|
const inp = document.getElementById('edit-' + tid + '-slides-input');
|
||||||
|
if (inp) inp.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Slides crop queue: every added image is cropped before it enters the strip ──
|
// ── Slides crop queue: every added image is cropped before it enters the strip ──
|
||||||
@@ -740,8 +729,12 @@ function _editSlidesCropLoadNext() {
|
|||||||
if (!_editSlidesCropQueue.length) { window.closeCropperModal('slides_edit'); return; }
|
if (!_editSlidesCropQueue.length) { window.closeCropperModal('slides_edit'); return; }
|
||||||
const f = _editSlidesCropQueue.shift();
|
const f = _editSlidesCropQueue.shift();
|
||||||
if (typeof window.tcPreload_slides_edit === 'function') {
|
if (typeof window.tcPreload_slides_edit === 'function') {
|
||||||
window.tcPreload_slides_edit(f);
|
// Open modal first so the cropper canvas has a real layout box when
|
||||||
|
// initCropper measures. Preloading first (prior order) caused the
|
||||||
|
// second batch to open with stale zero-sized measurements and a Save
|
||||||
|
// button that silently did nothing.
|
||||||
window.openCropperModal_slides_edit();
|
window.openCropperModal_slides_edit();
|
||||||
|
requestAnimationFrame(() => window.tcPreload_slides_edit(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,6 +820,8 @@ function editSlidesZoneDropE(e, tid) { e.preventDefault(); document.getEleme
|
|||||||
|
|
||||||
function editHandleSlidesForTrack(tid, fileList) {
|
function editHandleSlidesForTrack(tid, fileList) {
|
||||||
_editSlidesCropStart(fileList, tid, _editRenderSlidesForTrack);
|
_editSlidesCropStart(fileList, tid, _editRenderSlidesForTrack);
|
||||||
|
const inp = document.getElementById('edit-slides-input-' + tid);
|
||||||
|
if (inp) inp.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function editClearSlidesForTrack(e, tid) {
|
function editClearSlidesForTrack(e, tid) {
|
||||||
@@ -885,7 +880,9 @@ function _editRenderSlidesForTrack(tid) {
|
|||||||
// ── Track popup ───────────────────────────────────────────────────────────────
|
// ── Track popup ───────────────────────────────────────────────────────────────
|
||||||
function editOpenTrackPopup(trackId) {
|
function editOpenTrackPopup(trackId) {
|
||||||
document.querySelectorAll('#edit-track-popup .um-track-form').forEach(f => f.style.display = 'none');
|
document.querySelectorAll('#edit-track-popup .um-track-form').forEach(f => f.style.display = 'none');
|
||||||
const form = document.getElementById('edit-tf-' + trackId);
|
// Primary track form uses the legacy id (edit-tf-t1); extra tracks are
|
||||||
|
// cloned from the SHARED #um-track-form-template and use id um-tf-{id}.
|
||||||
|
const form = document.getElementById('edit-tf-' + trackId) || document.getElementById('um-tf-' + trackId);
|
||||||
if (form) form.style.display = 'block';
|
if (form) form.style.display = 'block';
|
||||||
const popup = document.getElementById('edit-track-popup');
|
const popup = document.getElementById('edit-track-popup');
|
||||||
popup.style.opacity = '0';
|
popup.style.opacity = '0';
|
||||||
@@ -902,6 +899,10 @@ function editCloseTrackPopup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For t1 the inputs are the primary-track Blade ids. For extra tracks (both
|
||||||
|
// newly-added and existing-track rows) the inputs come from the SHARED
|
||||||
|
// #um-track-form-template, so they use the shared ids track-lang-{id} /
|
||||||
|
// track-title-{id}. One function reads whichever is present.
|
||||||
function editUpdateTrackCard(trackId) {
|
function editUpdateTrackCard(trackId) {
|
||||||
let langCode = '', title = '';
|
let langCode = '', title = '';
|
||||||
if (trackId === 't1') {
|
if (trackId === 't1') {
|
||||||
@@ -910,9 +911,8 @@ function editUpdateTrackCard(trackId) {
|
|||||||
langCode = li ? li.value : '';
|
langCode = li ? li.value : '';
|
||||||
title = ti ? ti.value.trim() : '';
|
title = ti ? ti.value.trim() : '';
|
||||||
} else {
|
} else {
|
||||||
const n = trackId.replace('e','');
|
const li = document.getElementById('track-lang-' + trackId);
|
||||||
const li = document.getElementById('csd_v_e' + n);
|
const ti = document.getElementById('track-title-' + trackId);
|
||||||
const ti = document.getElementById('edit-e' + n + '-title');
|
|
||||||
langCode = li ? li.value : '';
|
langCode = li ? li.value : '';
|
||||||
title = ti ? ti.value.trim() : '';
|
title = ti ? ti.value.trim() : '';
|
||||||
}
|
}
|
||||||
@@ -932,24 +932,26 @@ function editUpdateTrackCard(trackId) {
|
|||||||
// ── Extra track management ────────────────────────────────────────────────────
|
// ── Extra track management ────────────────────────────────────────────────────
|
||||||
const LANG_OPTIONS_EDIT_MODAL = @json(\App\Data\Languages::forLanguage());
|
const LANG_OPTIONS_EDIT_MODAL = @json(\App\Data\Languages::forLanguage());
|
||||||
|
|
||||||
|
// Clone the SHARED upload-modal track-form partial into the edit popup. Both
|
||||||
|
// "add new track" and "load existing track" go through here, so there is one
|
||||||
|
// source of truth for the form markup. The caller picks the submission names:
|
||||||
|
// - new track → extra_track_*[]
|
||||||
|
// - existing track → track_*_updates[trackId]
|
||||||
function _editCloneTrackForm(prefix, langName, titleName, descName, audioName) {
|
function _editCloneTrackForm(prefix, langName, titleName, descName, audioName) {
|
||||||
const tpl = document.getElementById('edit-extra-track-tpl');
|
const frag = window.cloneTrackFormTemplate({
|
||||||
if (!tpl) return null;
|
key: prefix, langName, titleName, descName, audioName,
|
||||||
const html = tpl.innerHTML
|
});
|
||||||
.replaceAll('__TPL__', prefix)
|
if (!frag) return null;
|
||||||
.replaceAll('__LANGNAME__', langName)
|
const host = document.getElementById('edit-tf-extra');
|
||||||
.replaceAll('__TITLENAME__', titleName)
|
host.appendChild(frag);
|
||||||
.replaceAll('__DESCNAME__', descName)
|
// _slidesData is the shared state used by the shared slides-zone handlers.
|
||||||
.replaceAll('__AUDIONAME__', audioName);
|
// Mirror the handle under _editSlidesData so the edit submit code stays
|
||||||
const tmp = document.createElement('div');
|
// oblivious to the migration (one array, two aliases).
|
||||||
tmp.innerHTML = html;
|
window._slidesData = window._slidesData || {};
|
||||||
const formEl = tmp.querySelector('.um-track-form');
|
_slidesData[prefix] = [];
|
||||||
if (!formEl) return null;
|
_editSlidesData[prefix] = _slidesData[prefix];
|
||||||
formEl.style.display = 'none';
|
window.initTrackFormNode(prefix, pfx => editUpdateTrackCard(pfx));
|
||||||
document.getElementById('edit-tf-extra').appendChild(formEl);
|
return document.getElementById('um-tf-' + prefix);
|
||||||
_editSlidesData[prefix] = [];
|
|
||||||
if (window.CSD) new CSD('csd_' + prefix);
|
|
||||||
return formEl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _editTrackCard(n, trackId, isExisting) {
|
function _editTrackCard(n, trackId, isExisting) {
|
||||||
@@ -990,34 +992,47 @@ function _editAddExistingTrack(track) {
|
|||||||
'track_description_updates[' + track.id + ']',
|
'track_description_updates[' + track.id + ']',
|
||||||
'track_file_updates[' + track.id + ']'
|
'track_file_updates[' + track.id + ']'
|
||||||
);
|
);
|
||||||
const titleEl = document.getElementById('edit-' + pfx + '-title');
|
// Shared partial field ids: track-title-{pfx} / track-desc-{pfx} / track-lang-{pfx}
|
||||||
|
const titleEl = document.getElementById('track-title-' + pfx);
|
||||||
if (titleEl) titleEl.value = track.title || '';
|
if (titleEl) titleEl.value = track.title || '';
|
||||||
const descEl = document.getElementById('edit-' + pfx + '-desc');
|
const descEl = document.getElementById('track-desc-' + pfx);
|
||||||
if (descEl) { descEl.value = track.description || ''; descEl.dispatchEvent(new Event('rte:refresh')); }
|
if (descEl) { descEl.value = track.description || ''; descEl.dispatchEvent(new Event('rte:refresh')); }
|
||||||
if (track.language && window.CSD) {
|
if (track.language && window.CSD) {
|
||||||
const hi = document.getElementById('csd_v_' + pfx);
|
const hi = document.getElementById('track-lang-' + pfx);
|
||||||
const wrap = document.getElementById('csd_' + pfx);
|
const wrap = document.getElementById('lsd_track_' + pfx);
|
||||||
if (hi) hi.value = track.language;
|
if (hi) hi.value = track.language;
|
||||||
if (wrap) _editSetLangCsd(wrap, track.language);
|
if (wrap) _editSetLangCsd(wrap, track.language);
|
||||||
}
|
}
|
||||||
const hi = document.getElementById('csd_v_' + pfx);
|
|
||||||
if (hi) hi.addEventListener('change', () => editUpdateTrackCard(pfx));
|
|
||||||
editUpdateTrackCard(pfx);
|
editUpdateTrackCard(pfx);
|
||||||
_editUpdateTrackPositions();
|
_editUpdateTrackPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
function editDeleteTrack(trackId, n) {
|
function _editRemoveTrackForms(n) {
|
||||||
_editDeleteTrackIds.push(trackId);
|
const pfx = 'e' + n;
|
||||||
document.getElementById('edit-tc-e'+n)?.remove();
|
document.getElementById('edit-tc-' + pfx)?.remove();
|
||||||
document.getElementById('edit-tf-e'+n)?.remove();
|
// Form id depends on which cloner built it: shared template → um-tf-{pfx},
|
||||||
|
// legacy edit template → edit-tf-{pfx}. Clean up whichever exists.
|
||||||
|
document.getElementById('um-tf-' + pfx)?.remove();
|
||||||
|
document.getElementById('edit-tf-' + pfx)?.remove();
|
||||||
|
delete _editSlidesData[pfx];
|
||||||
|
delete (window._slidesData || {})[pfx];
|
||||||
const popup = document.getElementById('edit-track-popup');
|
const popup = document.getElementById('edit-track-popup');
|
||||||
if (popup.style.display !== 'none') {
|
if (popup.style.display !== 'none') {
|
||||||
const vis = popup.querySelector('.um-track-form:not([style*="none"])');
|
const vis = popup.querySelector('.um-track-form:not([style*="none"])');
|
||||||
if (!vis || vis.id === 'edit-tf-e'+n) popup.style.display = 'none';
|
if (!vis || vis.id === 'edit-tf-' + pfx || vis.id === 'um-tf-' + pfx) popup.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editDeleteTrack(trackId, n) {
|
||||||
|
_editDeleteTrackIds.push(trackId);
|
||||||
|
_editRemoveTrackForms(n);
|
||||||
_editUpdateTrackPositions();
|
_editUpdateTrackPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editRemoveNewTrack(n) {
|
||||||
|
_editRemoveTrackForms(n);
|
||||||
|
}
|
||||||
|
|
||||||
function editAddExtraTrack() {
|
function editAddExtraTrack() {
|
||||||
const n = ++_editExtraCount;
|
const n = ++_editExtraCount;
|
||||||
const pfx = 'e' + n;
|
const pfx = 'e' + n;
|
||||||
@@ -1029,23 +1044,10 @@ function editAddExtraTrack() {
|
|||||||
'extra_track_descriptions[]',
|
'extra_track_descriptions[]',
|
||||||
'extra_track_files[]'
|
'extra_track_files[]'
|
||||||
);
|
);
|
||||||
const hi = document.getElementById('csd_v_' + pfx);
|
|
||||||
if (hi) hi.addEventListener('change', () => editUpdateTrackCard(pfx));
|
|
||||||
_editUpdateTrackPositions();
|
_editUpdateTrackPositions();
|
||||||
editOpenTrackPopup(pfx);
|
editOpenTrackPopup(pfx);
|
||||||
}
|
}
|
||||||
|
|
||||||
function editRemoveNewTrack(n) {
|
|
||||||
document.getElementById('edit-tc-e'+n)?.remove();
|
|
||||||
document.getElementById('edit-tf-e'+n)?.remove();
|
|
||||||
delete _editSlidesData['e'+n];
|
|
||||||
const popup = document.getElementById('edit-track-popup');
|
|
||||||
if (popup.style.display !== 'none') {
|
|
||||||
const vis = popup.querySelector('.um-track-form:not([style*="none"])');
|
|
||||||
if (!vis || vis.id === 'edit-tf-e'+n) popup.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wire primary language → update card
|
// Wire primary language → update card
|
||||||
;(function() {
|
;(function() {
|
||||||
const li = document.getElementById('edit_primary_language');
|
const li = document.getElementById('edit_primary_language');
|
||||||
|
|||||||
@@ -338,9 +338,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>{{-- /um-tf-t1 --}}
|
</div>{{-- /um-tf-t1 --}}
|
||||||
|
|
||||||
{{-- Extra track forms appended by JS --}}
|
{{-- Extra track forms appended by JS (cloned from the template below) --}}
|
||||||
<div id="um-tf-extra"></div>
|
<div id="um-tf-extra"></div>
|
||||||
|
|
||||||
|
{{-- SHARED track form template.
|
||||||
|
This is the single source of truth for the "Add Language Track"
|
||||||
|
dialog used by BOTH the create (upload) modal AND the edit modal.
|
||||||
|
Any CSS/markup/behaviour change here automatically applies to both.
|
||||||
|
Cloners replace the placeholders at clone time:
|
||||||
|
__N__ → track key (e.g. "e1" for create, "editE1" for edit)
|
||||||
|
__LANGNAME__ → form field name for the language select
|
||||||
|
__TITLENAME__ → form field name for the title input
|
||||||
|
__DESCNAME__ → form field name for the description textarea
|
||||||
|
__AUDIONAME__ → form field name for the audio file input --}}
|
||||||
|
<template id="um-track-form-template">
|
||||||
|
@include('layouts.partials.upload-modal.track-form', [
|
||||||
|
'key' => '__N__',
|
||||||
|
'showPrimaryNote' => false,
|
||||||
|
'langName' => '__LANGNAME__',
|
||||||
|
'titleName' => '__TITLENAME__',
|
||||||
|
'descName' => '__DESCNAME__',
|
||||||
|
'audioName' => '__AUDIONAME__',
|
||||||
|
'langRequired' => true,
|
||||||
|
])
|
||||||
|
</template>
|
||||||
|
|
||||||
</div>{{-- /um-tp-body --}}
|
</div>{{-- /um-tp-body --}}
|
||||||
<div class="um-tp-footer">
|
<div class="um-tp-footer">
|
||||||
<button type="button" class="action-btn action-btn-primary" onclick="closeTrackPopup()">
|
<button type="button" class="action-btn action-btn-primary" onclick="closeTrackPopup()">
|
||||||
@@ -1372,6 +1394,10 @@ function slidesZoneDrop(e, tid) {
|
|||||||
|
|
||||||
function handleSlidesForTrack(tid, fileList) {
|
function handleSlidesForTrack(tid, fileList) {
|
||||||
_slidesCropStart(fileList, tid);
|
_slidesCropStart(fileList, tid);
|
||||||
|
// Reset the file input so picking the SAME filename a second time still
|
||||||
|
// fires change (browsers suppress change when FileList is identical).
|
||||||
|
const inp = document.getElementById('slides-input-' + tid);
|
||||||
|
if (inp) inp.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Slides crop queue: every added image is cropped before it enters the strip ──
|
// ── Slides crop queue: every added image is cropped before it enters the strip ──
|
||||||
@@ -1390,8 +1416,12 @@ function _slidesCropLoadNext() {
|
|||||||
if (!_slidesCropQueue.length) { window.closeCropperModal('slides_upload'); return; }
|
if (!_slidesCropQueue.length) { window.closeCropperModal('slides_upload'); return; }
|
||||||
const f = _slidesCropQueue.shift();
|
const f = _slidesCropQueue.shift();
|
||||||
if (typeof window.tcPreload_slides_upload === 'function') {
|
if (typeof window.tcPreload_slides_upload === 'function') {
|
||||||
window.tcPreload_slides_upload(f);
|
// Open the modal FIRST so the canvas has a non-zero layout box when
|
||||||
|
// initCropper measures clientWidth/clientHeight. Preloading first
|
||||||
|
// (the previous order) caused the second batch to open with stale
|
||||||
|
// zero-sized measurements and a Save button that silently did nothing.
|
||||||
window.openCropperModal_slides_upload();
|
window.openCropperModal_slides_upload();
|
||||||
|
requestAnimationFrame(() => window.tcPreload_slides_upload(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1400,7 +1430,13 @@ function uploadSlidesCropDone(file) {
|
|||||||
if (tid && file) {
|
if (tid && file) {
|
||||||
if (!_slidesData[tid]) _slidesData[tid] = [];
|
if (!_slidesData[tid]) _slidesData[tid] = [];
|
||||||
if (_slidesData[tid].length < 10) {
|
if (_slidesData[tid].length < 10) {
|
||||||
_slidesData[tid].push(file);
|
// Cache a per-file object URL once so renderSlidesStrip can reuse it
|
||||||
|
// on every reorder without spawning a fresh FileReader per slide.
|
||||||
|
// The old approach (new FileReader per file per render) ran dozens
|
||||||
|
// of readers after a reorder and silently dropped some under memory
|
||||||
|
// pressure, leaving the strip with broken thumbnails.
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
_slidesData[tid].push({ file, url });
|
||||||
renderSlidesStrip(tid);
|
renderSlidesStrip(tid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1422,7 +1458,10 @@ function renderSlidesStrip(tid) {
|
|||||||
if (zone) zone.style.borderColor = '';
|
if (zone) zone.style.borderColor = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
files.forEach((f, index) => {
|
files.forEach((item, index) => {
|
||||||
|
// Entries are {file, url}; cached url is stable across reorders.
|
||||||
|
const url = item.url || (item.file ? URL.createObjectURL(item.file) : '');
|
||||||
|
if (item.file && !item.url) item.url = url;
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.style.cssText = 'position:relative;flex-shrink:0;cursor:grab;';
|
wrap.style.cssText = 'position:relative;flex-shrink:0;cursor:grab;';
|
||||||
wrap.draggable = true; wrap.dataset.index = index; wrap.dataset.trackId = tid;
|
wrap.draggable = true; wrap.dataset.index = index; wrap.dataset.trackId = tid;
|
||||||
@@ -1441,10 +1480,15 @@ function renderSlidesStrip(tid) {
|
|||||||
const rb = document.createElement('button');
|
const rb = document.createElement('button');
|
||||||
rb.type='button'; rb.style.cssText='position:absolute;top:-4px;right:-4px;width:16px;height:16px;background:#e61e1e;border:none;border-radius:50%;color:#fff;font-size:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;z-index:1;';
|
rb.type='button'; rb.style.cssText='position:absolute;top:-4px;right:-4px;width:16px;height:16px;background:#e61e1e;border:none;border-radius:50%;color:#fff;font-size:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;z-index:1;';
|
||||||
rb.innerHTML='<i class="bi bi-x"></i>';
|
rb.innerHTML='<i class="bi bi-x"></i>';
|
||||||
rb.addEventListener('click', function(ev) { ev.stopPropagation(); _slidesData[tid].splice(index,1); renderSlidesStrip(tid); });
|
rb.addEventListener('click', function(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const removed = _slidesData[tid].splice(index,1)[0];
|
||||||
|
if (removed && removed.url) URL.revokeObjectURL(removed.url);
|
||||||
|
renderSlidesStrip(tid);
|
||||||
|
});
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.style.cssText='width:52px;height:52px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,.12);display:block;'; img.alt='';
|
img.style.cssText='width:52px;height:52px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,.12);display:block;'; img.alt='';
|
||||||
const reader = new FileReader(); reader.onload = ev2 => { img.src=ev2.target.result; }; reader.readAsDataURL(f);
|
img.src = url;
|
||||||
wrap.appendChild(img); wrap.appendChild(rb); strip.appendChild(wrap);
|
wrap.appendChild(img); wrap.appendChild(rb); strip.appendChild(wrap);
|
||||||
});
|
});
|
||||||
if (cnt) cnt.textContent = files.length===1 ? '1 image — static cover' : files.length+' images — will crossfade during playback';
|
if (cnt) cnt.textContent = files.length===1 ? '1 image — static cover' : files.length+' images — will crossfade during playback';
|
||||||
@@ -1455,6 +1499,7 @@ function renderSlidesStrip(tid) {
|
|||||||
|
|
||||||
function clearSlidesForTrack(e, tid) {
|
function clearSlidesForTrack(e, tid) {
|
||||||
e.preventDefault(); e.stopPropagation();
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
(_slidesData[tid] || []).forEach(s => { if (s && s.url) URL.revokeObjectURL(s.url); });
|
||||||
_slidesData[tid] = [];
|
_slidesData[tid] = [];
|
||||||
const input = document.getElementById('slides-input-' + tid);
|
const input = document.getElementById('slides-input-' + tid);
|
||||||
if (input) { const fresh=input.cloneNode(false); fresh.addEventListener('change',function(){handleSlidesForTrack(tid,this.files);}); input.parentNode.replaceChild(fresh,input); }
|
if (input) { const fresh=input.cloneNode(false); fresh.addEventListener('change',function(){handleSlidesForTrack(tid,this.files);}); input.parentNode.replaceChild(fresh,input); }
|
||||||
@@ -1494,12 +1539,10 @@ function updateTrackCard(trackId) {
|
|||||||
langCode = li ? li.value : '';
|
langCode = li ? li.value : '';
|
||||||
title = ti ? ti.value.trim() : '';
|
title = ti ? ti.value.trim() : '';
|
||||||
} else {
|
} else {
|
||||||
const n = trackId.replace('e', '');
|
const li = document.getElementById('track-lang-' + trackId);
|
||||||
const uid = 'ltme_' + n;
|
const ti = document.getElementById('track-title-' + trackId);
|
||||||
const li = document.getElementById('csd_v_' + uid);
|
langCode = li ? li.value : '';
|
||||||
const ti = document.getElementById('um-tf-title-' + trackId);
|
title = ti ? ti.value.trim() : '';
|
||||||
langCode = li ? li.value : '';
|
|
||||||
title = ti ? ti.value.trim() : '';
|
|
||||||
}
|
}
|
||||||
const lang = LANG_OPTIONS_MODAL.find(o => o.value === langCode);
|
const lang = LANG_OPTIONS_MODAL.find(o => o.value === langCode);
|
||||||
const hasContent = !!(lang || title);
|
const hasContent = !!(lang || title);
|
||||||
@@ -1512,49 +1555,111 @@ function updateTrackCard(trackId) {
|
|||||||
if (card) card.style.display = hasContent ? '' : 'none';
|
if (card) card.style.display = hasContent ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
const flagEl = document.getElementById('um-tc-flag-' + trackId);
|
// Update WHICHEVER card id convention exists — the create modal uses
|
||||||
const titleEl = document.getElementById('um-tc-title-' + trackId);
|
// `um-tc-*-${id}` and the edit modal uses `edit-tc-*-${id}`. Writing to
|
||||||
if (flagEl) flagEl.className = 'fi fi-' + (lang ? lang.flag : 'xx') + ' um-tc-flag';
|
// both keeps this helper a single source of truth for any shared template.
|
||||||
if (titleEl) {
|
const flagEls = [
|
||||||
if (lang && title) titleEl.textContent = title + ' — ' + lang.label;
|
document.getElementById('um-tc-flag-' + trackId),
|
||||||
else if (lang) titleEl.textContent = lang.label;
|
document.getElementById('edit-tc-flag-' + trackId),
|
||||||
else if (title) titleEl.textContent = title;
|
];
|
||||||
else titleEl.textContent = '';
|
const titleEls = [
|
||||||
}
|
document.getElementById('um-tc-title-' + trackId),
|
||||||
|
document.getElementById('edit-tc-title-' + trackId),
|
||||||
|
];
|
||||||
|
const flagClass = 'fi fi-' + (lang ? lang.flag : 'xx') + ' um-tc-flag';
|
||||||
|
let display;
|
||||||
|
if (lang && title) display = title + ' — ' + lang.label;
|
||||||
|
else if (lang) display = lang.label;
|
||||||
|
else if (title) display = title;
|
||||||
|
else display = '';
|
||||||
|
flagEls.forEach(el => { if (el) el.className = flagClass; });
|
||||||
|
titleEls.forEach(el => { if (el) el.textContent = display; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Extra track management ────────────────────────────────────
|
// ── Extra track management ────────────────────────────────────
|
||||||
const LANG_OPTIONS_MODAL = @json(\App\Data\Languages::forLanguage());
|
const LANG_OPTIONS_MODAL = @json(\App\Data\Languages::forLanguage());
|
||||||
|
|
||||||
function _ltBuildOptsHtml() {
|
// Audio picker handler shared by every track form (primary + extras) cloned
|
||||||
return LANG_OPTIONS_MODAL.map(o =>
|
// from the <template id="um-track-form-template"> blade partial.
|
||||||
`<li class="csd-opt" role="option" tabindex="-1" data-v="${o.value}" data-s="${o.search}" aria-selected="false">
|
function handleTrackAudio(key, input) {
|
||||||
<span class="csd-opt-ico"><span class="fi fi-${o.flag} lsd-flag"></span></span>
|
const file = input.files && input.files[0];
|
||||||
<span class="csd-opt-main">${o.label}</span>
|
const fnameEl = document.getElementById('track-audio-fname-' + key);
|
||||||
<span class="csd-opt-sub">${o.native}</span>
|
const box = document.getElementById('track-audio-box-' + key);
|
||||||
</li>`
|
if (fnameEl) fnameEl.textContent = file ? file.name : 'Click to choose file…';
|
||||||
).join('');
|
if (box) box.style.borderColor = file ? '#22c55e' : '';
|
||||||
|
|
||||||
|
// Auto-fill the track title from the filename (same behavior as primary track)
|
||||||
|
if (file) {
|
||||||
|
const titleEl = document.getElementById('track-title-' + key);
|
||||||
|
if (titleEl && !titleEl.value) {
|
||||||
|
titleEl.value = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
|
||||||
|
updateTrackCard(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared cloner for #um-track-form-template. Both the create modal and the
|
||||||
|
// edit modal call this so there is ONE source for the track-form markup.
|
||||||
|
// Returns a DocumentFragment with every placeholder (__N__, __LANGNAME__, …)
|
||||||
|
// substituted. The caller is responsible for appending it + initialising the
|
||||||
|
// language picker + RTE on the new nodes.
|
||||||
|
window.cloneTrackFormTemplate = window.cloneTrackFormTemplate || function (opts) {
|
||||||
|
const tpl = document.getElementById('um-track-form-template');
|
||||||
|
if (!tpl) { console.error('[track-form] missing #um-track-form-template'); return null; }
|
||||||
|
const frag = tpl.content.cloneNode(true);
|
||||||
|
const subs = {
|
||||||
|
'__N__': opts.key,
|
||||||
|
'__LANGNAME__': opts.langName,
|
||||||
|
'__TITLENAME__': opts.titleName,
|
||||||
|
'__DESCNAME__': opts.descName,
|
||||||
|
'__AUDIONAME__': opts.audioName,
|
||||||
|
};
|
||||||
|
const walker = document.createTreeWalker(frag, NodeFilter.SHOW_ELEMENT, null);
|
||||||
|
const nodes = [];
|
||||||
|
for (let el = walker.nextNode(); el; el = walker.nextNode()) nodes.push(el);
|
||||||
|
nodes.forEach(node => {
|
||||||
|
for (const attr of Array.from(node.attributes)) {
|
||||||
|
let v = attr.value;
|
||||||
|
for (const k in subs) if (v.indexOf(k) !== -1) v = v.split(k).join(subs[k]);
|
||||||
|
if (v !== attr.value) node.setAttribute(attr.name, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return frag;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shared post-clone wiring: CSD language picker + RTE on fresh markup, plus
|
||||||
|
// change listeners that keep whichever card is on-screen in sync with the form.
|
||||||
|
window.initTrackFormNode = window.initTrackFormNode || function (key, onChange) {
|
||||||
|
if (window.CSD) new CSD('lsd_track_' + key);
|
||||||
|
const rteWrap = document.querySelector('#um-tf-' + key + ' .rte-wrap:not([data-rte-ready])');
|
||||||
|
if (rteWrap && window.RTE && typeof window.RTE.init === 'function') window.RTE.init(rteWrap);
|
||||||
|
const langInput = document.getElementById('track-lang-' + key);
|
||||||
|
const titleInput = document.getElementById('track-title-' + key);
|
||||||
|
const cb = onChange || (() => updateTrackCard(key));
|
||||||
|
if (langInput) langInput.addEventListener('change', () => cb(key));
|
||||||
|
if (titleInput) titleInput.addEventListener('input', () => cb(key));
|
||||||
|
};
|
||||||
|
|
||||||
function addExtraTrackModal() {
|
function addExtraTrackModal() {
|
||||||
const n = ++_umExtraCount;
|
const n = ++_umExtraCount;
|
||||||
const trackNum = n + 1;
|
const trackNum = n + 1;
|
||||||
const uid = 'ltme_' + n;
|
const key = 'e' + n;
|
||||||
|
|
||||||
|
// Track card (shown in the list on the main modal body)
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'um-track-card';
|
card.className = 'um-track-card';
|
||||||
card.id = 'um-tc-e' + n;
|
card.id = 'um-tc-' + key;
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="um-tc-body">
|
<div class="um-tc-body">
|
||||||
<div class="um-tc-left">
|
<div class="um-tc-left">
|
||||||
<div class="um-tc-num">${trackNum}</div>
|
<div class="um-tc-num">${trackNum}</div>
|
||||||
<span class="fi fi-xx um-tc-flag" id="um-tc-flag-e${n}"></span>
|
<span class="fi fi-xx um-tc-flag" id="um-tc-flag-${key}"></span>
|
||||||
<div class="um-tc-info">
|
<div class="um-tc-info">
|
||||||
<span class="um-tc-title" id="um-tc-title-e${n}">Track — select language</span>
|
<span class="um-tc-title" id="um-tc-title-${key}">Track — select language</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="um-tc-right">
|
<div class="um-tc-right">
|
||||||
<button type="button" class="action-btn" onclick="openTrackPopup('e${n}')">
|
<button type="button" class="action-btn" onclick="openTrackPopup('${key}')">
|
||||||
<i class="bi bi-pencil"></i> <span>Edit</span>
|
<i class="bi bi-pencil"></i> <span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="action-btn action-btn-danger icon-only" onclick="removeExtraTrackModal(${n})" title="Remove track">
|
<button type="button" class="action-btn action-btn-danger icon-only" onclick="removeExtraTrackModal(${n})" title="Remove track">
|
||||||
@@ -1564,100 +1669,24 @@ function addExtraTrackModal() {
|
|||||||
</div>`;
|
</div>`;
|
||||||
document.getElementById('um-tc-extra').appendChild(card);
|
document.getElementById('um-tc-extra').appendChild(card);
|
||||||
|
|
||||||
const formEl = document.createElement('div');
|
// Clone the SHARED track-form template and substitute placeholders. The same
|
||||||
formEl.className = 'um-track-form';
|
// template is cloned by the edit modal (editAddExtraTrack / existing-track
|
||||||
formEl.id = 'um-tf-e' + n;
|
// loader), so a change here automatically shows up in both flows.
|
||||||
formEl.style.display = 'none';
|
const frag = window.cloneTrackFormTemplate({
|
||||||
formEl.innerHTML = `
|
key: key,
|
||||||
<div class="um-tf-row2" style="margin-bottom:14px;">
|
langName: 'extra_track_languages[]',
|
||||||
<div>
|
titleName: 'extra_track_titles[]',
|
||||||
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">Language <span class="um-req">*</span></label>
|
descName: 'extra_track_descriptions[]',
|
||||||
<div class="csd-wrap" id="${uid}">
|
audioName: 'extra_track_files[]',
|
||||||
<button type="button" class="csd-btn" aria-haspopup="listbox" aria-expanded="false" aria-label="Select language">
|
});
|
||||||
<span class="csd-ico"><span class="fi fi-xx lsd-flag"></span></span>
|
if (!frag) return;
|
||||||
<span class="csd-val ph">Select language</span>
|
|
||||||
<i class="bi bi-chevron-down csd-arr"></i>
|
|
||||||
</button>
|
|
||||||
<div class="csd-panel" hidden role="listbox">
|
|
||||||
<div class="csd-srch"><i class="bi bi-search"></i>
|
|
||||||
<input class="csd-sinput" type="text" placeholder="Search language…" autocomplete="off" spellcheck="false">
|
|
||||||
</div>
|
|
||||||
<ul class="csd-list">${_ltBuildOptsHtml()}</ul>
|
|
||||||
<p class="csd-empty" hidden>No results</p>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="extra_track_languages[]" id="csd_v_${uid}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
|
|
||||||
Title <span class="um-lbl-hint">optional</span>
|
|
||||||
</label>
|
|
||||||
<input type="text" name="extra_track_titles[]" id="um-tf-title-e${n}" class="um-input"
|
|
||||||
style="font-size:13px;padding:9px 12px;" placeholder="Title in this language…">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:14px;">
|
|
||||||
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
|
|
||||||
Description <span class="um-lbl-hint">optional</span>
|
|
||||||
</label>
|
|
||||||
<div class="rte-wrap" data-min-height="100px">
|
|
||||||
<textarea class="rte-source" name="extra_track_descriptions[]" id="um-tf-desc-e${n}"
|
|
||||||
placeholder="Description in this language…"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:14px;">
|
|
||||||
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
|
|
||||||
<i class="bi bi-music-note-beamed"></i> Audio File
|
|
||||||
</label>
|
|
||||||
<label class="lta-file-label"
|
|
||||||
onmouseenter="this.style.borderColor='#e61e1e'"
|
|
||||||
onmouseleave="this.style.borderColor='#222'">
|
|
||||||
<i class="bi bi-music-note-beamed" style="color:#e61e1e;font-size:15px;flex-shrink:0;"></i>
|
|
||||||
<span class="lta-file-name" id="um-tf-fname-e${n}">Choose audio file…</span>
|
|
||||||
<input type="file" name="extra_track_files[]"
|
|
||||||
accept="audio/*,.mp3,.m4a,.aac,.flac,.wav" style="display:none;"
|
|
||||||
onchange="document.getElementById('um-tf-fname-e${n}').textContent=this.files[0]?.name||'Choose audio file…'">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
|
|
||||||
Cover Slides <span class="um-lbl-hint">optional · drag to reorder</span>
|
|
||||||
</label>
|
|
||||||
<div id="slides-zone-e${n}" class="um-slides-zone"
|
|
||||||
onclick="slidesZoneClick(event,'e${n}')"
|
|
||||||
ondragover="slidesZoneDragover(event,'e${n}')"
|
|
||||||
ondragleave="slidesZoneDragleave(event,'e${n}')"
|
|
||||||
ondrop="slidesZoneDrop(event,'e${n}')">
|
|
||||||
<input type="file" accept="image/*" multiple style="display:none" id="slides-input-e${n}"
|
|
||||||
onchange="handleSlidesForTrack('e${n}',this.files)">
|
|
||||||
<div class="um-slides-ph" id="slides-ph-e${n}">
|
|
||||||
<i class="bi bi-images"></i>
|
|
||||||
<span>Click or drag to add cover images</span>
|
|
||||||
</div>
|
|
||||||
<div id="slides-preview-e${n}" style="display:none;padding:10px 12px;width:100%;box-sizing:border-box;">
|
|
||||||
<div id="slides-strip-e${n}" class="slides-strip" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;"></div>
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
||||||
<span id="slides-count-e${n}" style="font-size:12px;color:var(--text-secondary);"></span>
|
|
||||||
<button type="button" onclick="clearSlidesForTrack(event,'e${n}')"
|
|
||||||
style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:0;display:flex;align-items:center;gap:4px;">
|
|
||||||
<i class="bi bi-x-lg"></i> Clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
document.getElementById('um-tf-extra').appendChild(formEl);
|
const host = document.getElementById('um-tf-extra');
|
||||||
_slidesData['e' + n] = [];
|
host.appendChild(frag);
|
||||||
|
_slidesData[key] = [];
|
||||||
|
|
||||||
if (window.CSD) new CSD(uid);
|
window.initTrackFormNode(key);
|
||||||
|
openTrackPopup(key);
|
||||||
const hiddenInput = document.getElementById('csd_v_' + uid);
|
|
||||||
if (hiddenInput) {
|
|
||||||
hiddenInput.addEventListener('change', function() { updateTrackCard('e' + n); });
|
|
||||||
}
|
|
||||||
|
|
||||||
openTrackPopup('e' + n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeExtraTrackModal(n) {
|
function removeExtraTrackModal(n) {
|
||||||
@@ -1686,13 +1715,15 @@ function removeExtraTrackModal(n) {
|
|||||||
|
|
||||||
// ── Chunked upload helper (bypasses Cloudflare's 100 MB per-request cap) ─
|
// ── Chunked upload helper (bypasses Cloudflare's 100 MB per-request cap) ─
|
||||||
async function _chunkedUploadVideo(file, onProgress) {
|
async function _chunkedUploadVideo(file, onProgress) {
|
||||||
const CHUNK = 8 * 1024 * 1024; // 8 MB — well under CF 100 MB limit
|
const CHUNK = 2 * 1024 * 1024; // 2 MB per chunk
|
||||||
|
const CONCURRENCY = 3; // parallel in-flight chunks
|
||||||
const total = Math.max(1, Math.ceil(file.size / CHUNK));
|
const total = Math.max(1, Math.ceil(file.size / CHUNK));
|
||||||
const uploadId = (window.crypto && crypto.randomUUID)
|
const uploadId = (window.crypto && crypto.randomUUID)
|
||||||
? crypto.randomUUID()
|
? crypto.randomUUID()
|
||||||
: (Date.now() + '-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2));
|
: (Date.now() + '-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2));
|
||||||
|
|
||||||
for (let i = 0; i < total; i++) {
|
let uploadedBytes = 0;
|
||||||
|
const uploadOne = async (i) => {
|
||||||
const start = i * CHUNK;
|
const start = i * CHUNK;
|
||||||
const end = Math.min(start + CHUNK, file.size);
|
const end = Math.min(start + CHUNK, file.size);
|
||||||
const blob = file.slice(start, end);
|
const blob = file.slice(start, end);
|
||||||
@@ -1700,6 +1731,7 @@ async function _chunkedUploadVideo(file, onProgress) {
|
|||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('upload_id', uploadId);
|
fd.append('upload_id', uploadId);
|
||||||
fd.append('chunk_index', i);
|
fd.append('chunk_index', i);
|
||||||
|
fd.append('chunk_offset', start);
|
||||||
fd.append('total_chunks', total);
|
fd.append('total_chunks', total);
|
||||||
fd.append('filename', file.name);
|
fd.append('filename', file.name);
|
||||||
fd.append('chunk', blob, 'chunk');
|
fd.append('chunk', blob, 'chunk');
|
||||||
@@ -1715,8 +1747,20 @@ async function _chunkedUploadVideo(file, onProgress) {
|
|||||||
try { const j = await res.json(); if (j && j.message) msg = j.message; } catch (_) {}
|
try { const j = await res.json(); if (j && j.message) msg = j.message; } catch (_) {}
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
if (onProgress) onProgress({ loaded: end, total: file.size });
|
uploadedBytes += (end - start);
|
||||||
}
|
if (onProgress) onProgress({ loaded: Math.min(uploadedBytes, file.size), total: file.size });
|
||||||
|
};
|
||||||
|
|
||||||
|
let next = 0;
|
||||||
|
const workers = Array.from({ length: Math.min(CONCURRENCY, total) }, async () => {
|
||||||
|
while (true) {
|
||||||
|
const i = next++;
|
||||||
|
if (i >= total) return;
|
||||||
|
await uploadOne(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(workers);
|
||||||
|
|
||||||
return { uploadId: uploadId, filename: file.name };
|
return { uploadId: uploadId, filename: file.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1753,8 +1797,9 @@ document.getElementById('upload-form-modal').addEventListener('submit', function
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
|
const _slideFile = s => (s && s.file) ? s.file : s; // tolerate legacy raw-File entries
|
||||||
if (_isMusicMode) {
|
if (_isMusicMode) {
|
||||||
(_slidesData['t1'] || []).forEach(f => formData.append('slides[]', f));
|
(_slidesData['t1'] || []).forEach(s => formData.append('slides[]', _slideFile(s)));
|
||||||
// Walk extra-track form sections in DOM order so the slide index lines up
|
// Walk extra-track form sections in DOM order so the slide index lines up
|
||||||
// with the positional `extra_track_files[]` the form already submits.
|
// with the positional `extra_track_files[]` the form already submits.
|
||||||
var extraForms = document.querySelectorAll('#um-tf-extra .um-track-form');
|
var extraForms = document.querySelectorAll('#um-tf-extra .um-track-form');
|
||||||
@@ -1763,8 +1808,8 @@ document.getElementById('upload-form-modal').addEventListener('submit', function
|
|||||||
if (!m) return;
|
if (!m) return;
|
||||||
var tid = m[1];
|
var tid = m[1];
|
||||||
var files = _slidesData[tid] || [];
|
var files = _slidesData[tid] || [];
|
||||||
files.forEach(function (f) {
|
files.forEach(function (s) {
|
||||||
formData.append('extra_track_slides[' + domIdx + '][]', f);
|
formData.append('extra_track_slides[' + domIdx + '][]', _slideFile(s));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user