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:
ghassanandClaude Opus 4.7 committed 2026-10-02 12:18:35 +03:00
1 parent 2d33e39b9b
commit 468820c9fe
3 files changed
+243 -196

No files matched your search

@@ -178,22 +178,9 @@
</div>
</div>
{{-- ── Secondary track form template (cloned by JS for each extra track) ──── --}}
<div id="edit-extra-track-tpl" style="display:none;" aria-hidden="true">
<x-track-editor-form
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>
{{-- Secondary tracks in this modal are cloned from the SHARED
#um-track-form-template defined in layouts.partials.upload-modal. One
template → identical UX in both the create (upload) modal and this modal. --}}
<style>
#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) {
tid = tid || 't1';
_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 ──
@@ -740,8 +729,12 @@ function _editSlidesCropLoadNext() {
if (!_editSlidesCropQueue.length) { window.closeCropperModal('slides_edit'); return; }
const f = _editSlidesCropQueue.shift();
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();
requestAnimationFrame(() => window.tcPreload_slides_edit(f));
}
}
@@ -827,6 +820,8 @@ function editSlidesZoneDropE(e, tid) { e.preventDefault(); document.getEleme
function editHandleSlidesForTrack(tid, fileList) {
_editSlidesCropStart(fileList, tid, _editRenderSlidesForTrack);
const inp = document.getElementById('edit-slides-input-' + tid);
if (inp) inp.value = '';
}
function editClearSlidesForTrack(e, tid) {
@@ -885,7 +880,9 @@ function _editRenderSlidesForTrack(tid) {
// ── Track popup ───────────────────────────────────────────────────────────────
function editOpenTrackPopup(trackId) {
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';
const popup = document.getElementById('edit-track-popup');
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) {
let langCode = '', title = '';
if (trackId === 't1') {
@@ -910,9 +911,8 @@ function editUpdateTrackCard(trackId) {
langCode = li ? li.value : '';
title = ti ? ti.value.trim() : '';
} else {
const n = trackId.replace('e','');
const li = document.getElementById('csd_v_e' + n);
const ti = document.getElementById('edit-e' + n + '-title');
const li = document.getElementById('track-lang-' + trackId);
const ti = document.getElementById('track-title-' + trackId);
langCode = li ? li.value : '';
title = ti ? ti.value.trim() : '';
}
@@ -932,24 +932,26 @@ function editUpdateTrackCard(trackId) {
// ── Extra track management ────────────────────────────────────────────────────
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) {
const tpl = document.getElementById('edit-extra-track-tpl');
if (!tpl) return null;
const html = tpl.innerHTML
.replaceAll('__TPL__', prefix)
.replaceAll('__LANGNAME__', langName)
.replaceAll('__TITLENAME__', titleName)
.replaceAll('__DESCNAME__', descName)
.replaceAll('__AUDIONAME__', audioName);
const tmp = document.createElement('div');
tmp.innerHTML = html;
const formEl = tmp.querySelector('.um-track-form');
if (!formEl) return null;
formEl.style.display = 'none';
document.getElementById('edit-tf-extra').appendChild(formEl);
_editSlidesData[prefix] = [];
if (window.CSD) new CSD('csd_' + prefix);
return formEl;
const frag = window.cloneTrackFormTemplate({
key: prefix, langName, titleName, descName, audioName,
});
if (!frag) return null;
const host = document.getElementById('edit-tf-extra');
host.appendChild(frag);
// _slidesData is the shared state used by the shared slides-zone handlers.
// Mirror the handle under _editSlidesData so the edit submit code stays
// oblivious to the migration (one array, two aliases).
window._slidesData = window._slidesData || {};
_slidesData[prefix] = [];
_editSlidesData[prefix] = _slidesData[prefix];
window.initTrackFormNode(prefix, pfx => editUpdateTrackCard(pfx));
return document.getElementById('um-tf-' + prefix);
}
function _editTrackCard(n, trackId, isExisting) {
@@ -990,34 +992,47 @@ function _editAddExistingTrack(track) {
'track_description_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 || '';
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 (track.language && window.CSD) {
const hi = document.getElementById('csd_v_' + pfx);
const wrap = document.getElementById('csd_' + pfx);
const hi = document.getElementById('track-lang-' + pfx);
const wrap = document.getElementById('lsd_track_' + pfx);
if (hi) hi.value = track.language;
if (wrap) _editSetLangCsd(wrap, track.language);
}
const hi = document.getElementById('csd_v_' + pfx);
if (hi) hi.addEventListener('change', () => editUpdateTrackCard(pfx));
editUpdateTrackCard(pfx);
_editUpdateTrackPositions();
}
function editDeleteTrack(trackId, n) {
_editDeleteTrackIds.push(trackId);
document.getElementById('edit-tc-e'+n)?.remove();
document.getElementById('edit-tf-e'+n)?.remove();
function _editRemoveTrackForms(n) {
const pfx = 'e' + n;
document.getElementById('edit-tc-' + pfx)?.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');
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';
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();
}
function editRemoveNewTrack(n) {
_editRemoveTrackForms(n);
}
function editAddExtraTrack() {
const n = ++_editExtraCount;
const pfx = 'e' + n;
@@ -1029,23 +1044,10 @@ function editAddExtraTrack() {
'extra_track_descriptions[]',
'extra_track_files[]'
);
const hi = document.getElementById('csd_v_' + pfx);
if (hi) hi.addEventListener('change', () => editUpdateTrackCard(pfx));
_editUpdateTrackPositions();
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
;(function() {
const li = document.getElementById('edit_primary_language');