Files
penracourses/anatomy/static/js/anatomy.js
T

901 lines
30 KiB
JavaScript

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");
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($("<span class='google-link' title='search for answer with google'><a href='https://www.google.com/search?q=" + $(element).text() + "' target='_blank'>G</a></span>"));
//});
// 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)
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");
const clearButton = wrapper.querySelector(".clear-input-button");
// 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) {
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=800');
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 <p> elements from TinyMCE content before any form submit.
// This prevents a single wrapping <p> 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 <p>, 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);