import * as dicomViewer from "./dicomViewer.js" window.dicomViewer = dicomViewer; window.marked_answers = { "correct": [], "half-correct": [], "incorrect": [], } window.control_pressed = false; $(document).ready(function () { // Select all text input fields and textareas with the clear button clearableTextAreaSetup(); document.body.addEventListener("htmx:responseError", function(e) { let el = document.getElementById("htmx-error"); if (!el) { // No global error container available; log and return to avoid JS errors console.error('htmx:responseError but no #htmx-error element present', e.detail); return; } var error; if (e.detail.pathInfo.requestPath.includes("cimar") && e.detail.xhr.status == 403) { error = "Cimar permission error, please login and refresh the page."; //document.getElementById("cimar-login-form").focus(); htmx.trigger("#cimar-login-needed", "cimar-login-needed"); } else { error = e.detail.xhr.response; } el.innerHTML = error; el.classList.remove("hidden"); }); $("table thead th input").click((e) => { let status = e.currentTarget.checked; $("table tbody input").prop("checked", status); }) //$(".answer-list li").each(function (index, element) { // $(element).append($("G")); //}); // Legacy per-element click handler: skip on the new mark2 page which // manages clicks via a delegated handler (it uses #answer-list). This // prevents duplicate/colliding handlers when mark2 is rendered. if (!document.getElementById('answer-list')) { $(".answer-list span.answer").each(function (index, element) { console.log(element); $(element).click(function (e) { var classes = ['answer correct', 'answer half-correct', 'answer incorrect']; $(element).each(function () { let mark = classes[($.inArray(this.className, classes) + 1) % classes.length]; this.className = mark this.dataset.newmark = mark.split(" ")[1] }); prepAnswerData(); }); }); } prepAnswerData(); if ($(".post-form").length > 0) { $(".post-form").get(0).addEventListener("submit", function (e) { //if ($(".not-marked").length > 0 && e.submitter.name != "skip") { // e.preventDefault(); // before the code // alert("Ensure all answers are marked first"); //} }); } let dicom_images = document.getElementsByClassName("dicom-image"); if (dicom_images.length) { for (let element of dicom_images) { setUpDicom3d(element) } } let dicom_images_legacy = document.getElementsByClassName("dicom-image-legacy"); if (dicom_images_legacy.length) { for (let element of dicom_images_legacy) { setUpDicomLegacy(element) } } loadDicomViewer(); if ($("#question-mark-list").length) { $(".show-all-button").click(() => { $("#question-mark-list li").show(); }); $(".show-unmarked-button").click(() => { $("#question-mark-list li").each((n, el) => { // Can't seem to get django to output this as a int.... if (parseInt(el.dataset.markcount) < 1) { $(el).hide(); } }) }); } // $(document).keydown(keyDownHandler); // $(document).keyup(keyUpHandler); $("#button-select-add-exam").click(function (evt) { // Find selected objects var jqxhr = $.get(evt.target.dataset.exam_list_url, function (data) { console.log(data); $("#exam-options").empty(); data.forEach((obj, n) => { $("#exam-options").append($(document.createElement('button')).prop({ type: 'button', innerHTML: obj.name, class: '', }).data("exam-id", obj.id).click((evt) => { addToExam(evt) })) }) }) .done(function () { //alert( "second success" ); }) .fail(function () { //alert( "error" ); }) .always(function () { //alert( "finished" ); }); return; function addToExam(evt) { let ids = [] $("tbody input:checked").each((n, el) => { ids.push(el.value) }) // if no question selected if (ids.length < 1) { toastr.info('No question selected.'); return } let post_url = document.getElementById("button-select-add-exam").dataset.exam_json_edit_url; let type = document.getElementById("button-select-add-exam").dataset.type; let csrf = document.getElementById("button-select-add-exam").dataset.csrf; $.ajax({ url: post_url, data: { csrfmiddlewaretoken: csrf, add_exam_questions: JSON.stringify(ids), type: type, exam_id: $(evt.target).data("exam-id"), }, type: "POST", dataType: "json", }) // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly .done(function (data) { console.log(data); if (data.status == "success") { toastr.info('Questions added to exams.') } }) .always(function () { console.log('[Done]'); $("#exam-options").empty(); }) } }); }); window.loadDicomViewerEvent = new Event('loadDicomViewer'); window.addEventListener('loadDicomViewer', function (e) { console.log("LoadDicomViewer event", e.detail) let images = []; //console.log($("#image_form_set img")); $(e.detail).each((n, el) => { images.push($(el).attr("src")); }); //console.log("listen2", images) loadDicomViewer(images); }, false); window.loadDicomViewerEvent = new Event('loadDicomViewerUrls'); window.addEventListener('loadDicomViewerUrls', function (e) { console.log("LoadDicomViewer unls event", e.detail) // Skip loading old viewer if using new dv3d viewer (set on new_uploads page) if (window.skipLoadDicomViewer) { console.log("Skipping old loadDicomViewer - using dv3d instead"); return; } loadDicomViewer(e.detail.images, e.detail.annotations); }, false); function clearableTextAreaSetup() { const textFields = document.querySelectorAll(".position-relative input[type='text'], .position-relative textarea"); textFields.forEach((field) => { const wrapper = field.closest(".position-relative"); if (!wrapper) return; const clearButton = wrapper.querySelector(".clear-input-button"); if (!clearButton) return; // Function to toggle the visibility of the clear button const toggleClearButton = () => { if (field.value.trim() !== "" && wrapper.matches(":hover")) { clearButton.style.display = "inline-block"; // Show the button } else { clearButton.style.display = "none"; // Hide the button } }; // Initial check toggleClearButton(); // Add event listeners to monitor changes in the input field or textarea field.addEventListener("input", toggleClearButton); // Add hover event listeners to the wrapper wrapper.addEventListener("mouseenter", toggleClearButton); wrapper.addEventListener("mouseleave", toggleClearButton); // Add click event to clear the field or textarea clearButton.addEventListener("click", function () { field.value = ""; // Clear the field toggleClearButton(); }); }); } function loadDicomViewer(images_to_load, annotations_to_load) { // Skip loading old viewer if using new dv3d viewer (set on new_uploads page) if (window.skipLoadDicomViewer) { console.log("Skipping old loadDicomViewer - using dv3d instead"); return; } console.log("loadDicomViewer", images_to_load); let single_dicom = document.getElementById("single-dicom-viewer"); if (single_dicom) { console.log("Load single dicom viewer"); $(single_dicom).empty() let images = single_dicom.dataset.images; if (images_to_load != undefined) { images = images_to_load; //} else if (images.indexOf(",") > 0) { } else if (images == undefined || images == "") { // No images to load return } else { images = JSON.parse(images); } let annotations = single_dicom.dataset.annotations; if (annotations_to_load != undefined) { annotations = annotations_to_load; } else if (annotations && annotations.indexOf(",") > 0) { annotations = JSON.parse(annotations); } else { annotations = undefined; } let load_as_stack; if (images.length > 5) { load_as_stack = true; } else { load_as_stack = false; } if (images) { dicomViewer.loadCornerstone($(single_dicom), null, images, annotations, load_as_stack); } } } window.loadDicomViewer = loadDicomViewer function prepAnswerData() { //$("#id_correct").val($("li.correct").map(function() { // ans = $(this).text(); // window.marked_answers["correct"].push(ans); // return ans //}).get().join('--//--')); //$("#id_half_correct").val($("li.half-correct").map(function() { // ans = $(this).text(); // window.marked_answers["half-correct"].push(ans); // return ans; //}).get().join('--//--')); //$("#id_incorrect").val($("li.incorrect").map(function() { // ans = $(this).text(); // window.marked_answers["incorrect"].push(ans); // return ans; //}).get().join('--//--')); window.marked_answers["correct"] = []; window.marked_answers["half-correct"] = []; window.marked_answers["incorrect"] = []; $("li span.correct").map(function () { let ans = $(this).text(); window.marked_answers["correct"].push(ans); }) $("li span.half-correct").map(function () { let ans = $(this).text(); window.marked_answers["half-correct"].push(ans); }) $("li span.incorrect").map(function () { let ans = $(this).text(); window.marked_answers["incorrect"].push(ans); }) $("#id_marked_answers").val(JSON.stringify(window.marked_answers)); } function loadJsonToolStateOnCurrentImage(element, json) { let el = element; const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager; const c = cornerstone.getEnabledElement(el); let image_id = c.image.imageId; let tool_state_no_id = JSON.parse(json); let tool_state = {}; tool_state[image_id] = tool_state_no_id; toolStateManager.restoreToolState(tool_state); cornerstone.reset(el); return tool_state; } // Called by HTMX after a fragment swap; initializes any dicom-image-legacy viewers window.initAnatomyFragment = function(fragmentRoot) { console.log("initAnatomyFragment", fragmentRoot); try { const root = fragmentRoot && fragmentRoot.nodeType ? fragmentRoot : (fragmentRoot && fragmentRoot.detail && fragmentRoot.detail.target) ? fragmentRoot.detail.target : document; const imgs = (root && root.querySelectorAll) ? root.querySelectorAll('.dicom-image-legacy') : []; imgs.forEach((el) => { try { // Avoid double initialization if (el.dataset && (el.dataset.dicomInitialised === '1' || el.dataset.dicomInitialised === 'true')) return; if (typeof window.setUpDicomLegacy === 'function') { window.setUpDicomLegacy(el); if (el.dataset) el.dataset.dicomInitialised = '1'; } } catch (err) { console.warn('initAnatomyFragment: setUpDicomLegacy failed for element', el, err); } }); } catch (err) { console.warn('initAnatomyFragment error', err); } } // Global fallback: listen for HTMX swaps and initialise any viewers inside document.addEventListener('htmx:afterSwap', function (e) { try { const root = e && e.target ? e.target : (e && e.detail && e.detail.target ? e.detail.target : document); if (window.initAnatomyFragment) window.initAnatomyFragment(root); } catch (err) { console.warn('htmx afterSwap listener error', err); } }); async function setUpDicomLegacy(element) { console.log("setUpDicom (original)", element); $(element).bind('contextmenu', function (e) { return false; }); $(element).dblclick((evt) => { element.requestFullscreen(); }) cornerstoneBase64ImageLoader.external.cornerstone = cornerstone; cornerstoneWebImageLoader.external.cornerstone = cornerstone; cornerstoneWADOImageLoader.external.cornerstone = cornerstone; cornerstoneTools.init(); const PanTool = cornerstoneTools.PanTool; const ZoomTool = cornerstoneTools.ZoomTool; const ZoomMouseWheelTool = cornerstoneTools.ZoomMouseWheelTool; const WwwcTool = cornerstoneTools.WwwcTool; const WwwcRegionTool = cornerstoneTools.WwwcRegionTool; const RotateTool = cornerstoneTools.RotateTool; const StackScrollTool = cornerstoneTools.StackScrollTool; const MagnifyTool = cornerstoneTools.MagnifyTool; const ArrowAnnotateTool = cornerstoneTools.ArrowAnnotateTool; const images = element.dataset.url.split(","); const annotation = element.dataset.annotations; //console.log("Dicom - load imageId: ", imageIds); //for (let index = 0; index < imageIds.length; index++) { // if (imageIds[index].endsWith("dcm")) { // imageIds[index] = "wadouri:" + imageIds[index]; // } //} let imageIds = []; for (let i = 0; i < images.length; i++) { let data_url = images[i]; // check stack type if (data_url.startsWith("data:image")) { let imageId = "base64://" + data_url.split(",")[1]; imageIds.push(imageId); } else if (data_url.startsWith("base64://") || data_url.startsWith("wadouri:")) { imageIds.push(data_url); // Treat application/octet-stream as if they are dicoms } else if (data_url.startsWith("data:application/dicom") || data_url.startsWith("data:application/octet-stream")) { //stack = stack.split(";")[1]; let dfile = await urltoFile(data_url, "dicom", "application/dicom"); const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add( dfile ); loadAnnotation(imageId, annotation); imageIds.push(imageId); //cornerstone.loadImage(imageId).then(function(image) { // tempFunction(image); //}); } else { let url; // This doesn't seem to have any benefit //if (data_url.startsWith("http")) { // url = data_url; //} else { // url = window.location.href.replace(/\/\#\/?$/, '') + "/" + data_url //} url = data_url; if (url.endsWith("dcm")) { url = "wadouri:" + url; } // if there is no extension treat it as a dicom if (/(?:\/|^)[^.\/]+$/.test(url)) { url = "wadouri:" + url; } imageIds.push(url); } } cornerstone.enable(element); cornerstone.loadAndCacheImage(imageIds[0]).then(function (image) { cornerstone.displayImage(element, image); cornerstoneTools.addToolForElement(element, PanTool); cornerstoneTools.addToolForElement(element, ZoomTool); cornerstoneTools.addToolForElement(element, ZoomMouseWheelTool); cornerstoneTools.addToolForElement(element, WwwcTool); cornerstoneTools.addToolForElement(element, WwwcRegionTool); cornerstoneTools.addToolForElement(element, RotateTool); cornerstoneTools.addToolForElement(element, StackScrollTool); cornerstoneTools.addToolForElement(element, MagnifyTool); cornerstoneTools.addToolForElement(element, ArrowAnnotateTool, { configuration: { getTextCallback: () => {}, changeTextCallback: () => {}, allowEmptyLabel: true, renderDashed: false, drawHandles: false, drawHandlesOnHover: true, }, }); // Enable our tools // Avoid incorrect aspect ratio cornerstoneTools.setToolActiveForElement(element, "Pan", { mouseButtonMask: 1 }); cornerstoneTools.setToolActiveForElement(element, "Wwwc", { mouseButtonMask: 2 }); cornerstoneTools.setToolActiveForElement(element, "ZoomMouseWheel", { mouseButtonMask: 3 }); cornerstoneTools.setToolActiveForElement(element, "Zoom", { mouseButtonMask: 4 }); if (element.dataset.edit_annotation == "true") { cornerstoneTools.setToolActiveForElement(element, "ArrowAnnotate", { mouseButtonMask: 2 }); } else { cornerstoneTools.setToolEnabledForElement(element, "ArrowAnnotate"); } if (element.dataset.annotations) { loadJsonToolStateOnCurrentImage(element, element.dataset.annotations) } cornerstone.resize(element); }).catch((err, err2) => { console.log(err); }); } console.log("setUpDicom defined"); // keep reference to the existing 2D setup window.setUpDicomLegacy = setUpDicomLegacy; async function setUpDicom3d(element) { console.log("setUpDicom3d (cornerstone3d) for", element); // parse images from dataset let raw = element.dataset.url || ""; if (!raw) return; let images; try { images = JSON.parse(raw); } catch (e) { images = raw.split(",").map(s => s.trim()).filter(Boolean); } if (!images || images.length === 0) return; // ensure element has a viewport-like appearance element.style.width = element.style.width || "500px"; element.style.height = element.style.height || "500px"; element.style.position = element.style.position || "relative"; element.innerHTML = ""; // clear // Disable right-click context menu on the element element.addEventListener('contextmenu', function (e) { e.preventDefault(); }); element.addEventListener('dblclick', function (e) { if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } }); // create a unique rendering engine / viewport ids per element const renderingEngineId = `rendingEngine`; const viewportId = `1`; try { // dynamic import of cornerstone3D core + dicom loader const coreModule = window.cornerstone3d const dicomLoaderModule = window.cornerstoneDICOMImageLoader const toolsModule = window.cornerstone3dTools; await coreModule.init(); await dicomLoaderModule.init(); await toolsModule.init(); const { RenderingEngine, Enums } = coreModule; const { chunkManager } = dicomLoaderModule; // may exist for some setups // create engine and enable a Stack viewport const renderingEngine = new RenderingEngine(renderingEngineId); console.log(renderingEngine) const viewportInput = { viewportId, type: Enums.ViewportType.STACK, element, //viewportType: Enums.ViewportType.STACK, // Stack viewport for slice stacks }; try { renderingEngine.enableElement(viewportInput); } catch (err) { // Some bundlings / integrations (dv3d wrapper) may reject the provided // viewport type (e.g. "Viewport is not a valid type"). In that case // fall back to the 2D cornerstone loader which is more permissive. console.warn('renderingEngine.enableElement failed, falling back to 2D loader:', err); try { dicomViewer.loadCornerstone($(element), null, images, undefined, images.length > 5); element._cornerstone3d = { fallback: 'cornerstone2d', imageCount: images.length }; return element._cornerstone3d; } catch (err2) { console.error('2D fallback after enableElement failure also failed:', err2); throw err; } } const viewport = renderingEngine.getViewport(viewportId); // --- Register basic interaction tools (best-effort with @cornerstonejs/tools) --- // dynamic import so bundlers/CDNs provide the package // prefer module import, fall back to global if present const { ToolGroupManager } = toolsModule; toolsModule.addTool(toolsModule.PanTool) toolsModule.addTool(toolsModule.WindowLevelTool) //toolsModule.addTool(toolsModule.StackScrollTool) toolsModule.addTool(toolsModule.ZoomTool) const toolGroupId = `toolgroup-${viewportId}`; console.log("ToolGroup id", toolGroupId); console.log("ToolGroupManager", ToolGroupManager); if (!ToolGroupManager.getToolGroup(toolGroupId)) { ToolGroupManager.createToolGroup(toolGroupId); } const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); toolGroup.addTool(toolsModule.PanTool.toolName); toolGroup.addTool(toolsModule.WindowLevelTool.toolName); //toolGroup.addTool(toolsModule.StackScrollTool.toolName); toolGroup.addTool(toolsModule.ZoomTool.toolName); // use actual tool names if available, else fall back to common strings //const stackName = (StackScrollTool && (StackScrollTool.toolName || StackScrollTool.name)) || 'StackScroll'; //toolGroup.setToolEnabled(toolsModule.PanTool.toolName, { bindings: [{ mouseButton: 1 }] }); toolGroup.setToolActive(toolsModule.PanTool.toolName, { bindings: [{ mouseButton: 1 }] }); toolGroup.setToolActive(toolsModule.WindowLevelTool.toolName, { bindings: [{ mouseButton: 2 }] }); toolGroup.setToolActive(toolsModule.ZoomTool.toolName, { bindings: [{ mouseButton: 4 }] }); toolGroup.setToolActive(toolsModule.ZoomTool.toolName, { bindings: [{ mouseButton: 524288 }] }); toolGroup.addViewport(viewportId, renderingEngineId); window.toolGroup = toolGroup; // expose for debugging // Prepare imageIds: ensure remote URLs are usable by dicom-image-loader (wadouri:) // but avoid forcing wadouri: for standard web images (png/jpg/etc) because // the dicom parser will fail when given non-DICOM files. For such images // return the raw URL so the appropriate web-image loader can handle them. // If any image looks like a plain web image (png/jpg/etc) prefer the // existing 2D cornerstone loader which handles http(s) image schemes. // This avoids a situation where the 3D dicom loader has no handler for // the 'https' scheme and throws "No image loader found for scheme 'https'". const looksLikeWebImage = (u) => { if (!u) return false; try { u = u.trim(); } catch (e) {} if (u.startsWith('data:image')) return true; if (u.match(/\.(png|jpe?g|gif|bmp|webp)$/i)) return true; return false; }; if (images.some(looksLikeWebImage)) { // Fallback: use the 2D cornerstone loader which has web-image handling // and is more tolerant of raw http(s) URLs. We reuse the project's // dicomViewer helper (it expects a jQuery element). console.warn('setUpDicom3d: contains web-image(s) — falling back to 2D loader'); try { // Remove any partially-created dv3d DOM and ensure the element is a // clean container for the 2D viewer. This ensures the `.canvas-panel` // appended by `loadCornerstone` overlays exactly the element area. try { element.innerHTML = ""; } catch (e) { $(element).empty(); } element.style.position = element.style.position || 'relative'; // annotations are not available in this scope in the 3d code path; pass // undefined for annotations and let the loader handle it. dicomViewer.loadCornerstone($(element), null, images, undefined, images.length > 5); // store a lightweight marker so callers can inspect what happened element._cornerstone3d = { fallback: 'cornerstone2d', imageCount: images.length }; return element._cornerstone3d; } catch (err) { console.warn('Fallback 2D loader failed; will continue to try 3D loader', err); // fallthrough to attempt 3D loading below } } const imageIds = images.map((url) => { url = url.trim(); // if it's already a scheme we pass through if (/^(wadouri:|base64:|data:|http:|https:)/i.test(url)) return url; // default: treat as wadouri (likely a DICOM file) return `wadouri:${url}`; }); // If dicom-image-loader provides a metadata cache factory, pre-cache metadata if (typeof dicomLoaderModule.createImageIdsAndCacheMetaData === "function") { try { // attempt to call helper to cache meta-data for the first image (best-effort) await dicomLoaderModule.createImageIdsAndCacheMetaData({ imageIds, }); } catch (e) { // not fatal — continue console.warn("createImageIdsAndCacheMetaData failed (continuing):", e); } } try { await viewport.setStack(imageIds, 0); } catch (err) { // If the 3D loader fails because it doesn't know how to load 'https' // URLs, fallback to the 2D cornerstone loader which handles web images. console.warn('viewport.setStack failed, attempting 2D fallback:', err); try { try { element.innerHTML = ""; } catch (e) { $(element).empty(); } element.style.position = element.style.position || 'relative'; dicomViewer.loadCornerstone($(element), null, images, undefined, images.length > 5); element._cornerstone3d = { fallback: 'cornerstone2d', imageCount: images.length }; return element._cornerstone3d; } catch (err2) { console.error('2D fallback also failed:', err2); throw err; // rethrow original error } } viewport.render(); // store references for debugging/use elsewhere element._cornerstone3d = { renderingEngineId, viewportId, renderingEngine, viewport, imageIds, }; console.log("setUpDicom3d complete", element._cornerstone3d); return element._cornerstone3d; } catch (err) { console.error("setUpDicom3d failed:", err); // per request: no fallback here — rethrow so caller can handle if desired throw err; } } // expose globally like existing code expects window.setUpDicom = setUpDicom3d; function create_popup_window(url, title) { newwindow = window.open(url, title, 'height=800,width=600'); if (window.focus) { newwindow.focus() } return false; } window.create_popup_window = create_popup_window function delete_multiple(url, csrf_token) { let answer_ids = []; $("tbody input:checked").each((n, el) => { answer_ids.push(el.value); }) if (confirm(`Delete ${answer_ids.length} answers?`)) { $.ajax({ url: url, data: { csrfmiddlewaretoken: csrf_token, answer_ids: JSON.stringify(answer_ids) // true if checked else false }, type: "POST", dataType: "json", }) // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly .done(function (data) { console.log(data); if (data.status == "success") { toastr.info('Deleted.') } // show some message according to the response. // For eg. A message box showing that the status has been changed }) .always(function () { console.log('[Done]'); }) } } window.delete_multiple = delete_multiple function getTimeRemaining(endtime) { const total = Date.parse(endtime) - Date.parse(new Date()); const seconds = Math.floor((total / 1000) % 60); const minutes = Math.floor((total / 1000 / 60) % 60); const hours = Math.floor((total / (1000 * 60 * 60)) % 24); //const days = Math.floor(total / (1000 * 60 * 60 * 24)); return { total, //days, hours, minutes, seconds }; } function initializeClock(id, endtime) { const clock = document.getElementById(id); clock.style.display = "block" const daysSpan = clock.querySelector('.days'); const hoursSpan = clock.querySelector('.hours'); const minutesSpan = clock.querySelector('.minutes'); const secondsSpan = clock.querySelector('.seconds'); function updateClock() { const t = getTimeRemaining(endtime); //daysSpan.innerHTML = t.days; hoursSpan.innerHTML = ('0' + t.hours).slice(-2); minutesSpan.innerHTML = ('0' + t.minutes).slice(-2); secondsSpan.innerHTML = ('0' + t.seconds).slice(-2); if (t.total <= 0) { clearInterval(timeinterval); timeup.style.display = "block" clock.style.display = "none" } } const timeinterval = setInterval(updateClock, 1000); updateClock(); } window.initializeClock = initializeClock // Unwrap single-root

elements from TinyMCE content before any form submit. // This prevents a single wrapping

from being sent to the server while // preserving other markup. We use a delegated submit listener so it works // for dynamically-added forms as well (e.g. admin inlines). document.addEventListener('submit', function(evt) { try { var form = evt.target; if (!form || !form.querySelector) return; // Find all tinyMCE textareas inside the form var areas = form.querySelectorAll('textarea.tinymce'); areas.forEach(function(area) { var id = area.id; if (!id) return; var ed = (typeof tinyMCE !== 'undefined') ? tinyMCE.get(id) : null; if (!ed) return; var html = (ed.getContent && typeof ed.getContent === 'function') ? ed.getContent({format: 'html'}) : area.value; if (!html) return; // Use a temporary container to inspect top-level elements safely var tmp = document.createElement('div'); tmp.innerHTML = html.trim(); // If there is exactly one top-level element and it is a

, unwrap it if (tmp.childElementCount === 1 && tmp.firstElementChild && tmp.firstElementChild.tagName.toLowerCase() === 'p') { var inner = tmp.firstElementChild.innerHTML; // Update editor content and the underlying textarea value try { ed.setContent(inner); } catch (e) { // fallback: set textarea value directly area.value = inner; } // Ensure the textarea value matches (some setups sync on save, some don't) area.value = inner; } }); } catch (e) { // be resilient; don't prevent form submission if something goes wrong here console.error('TinyMCE submit postprocess error', e); } }, true);