Initial commit
This commit is contained in:
commit
05c65aad4d
155 changed files with 93617 additions and 0 deletions
466
viewer/ui/embed-configurator.js
Normal file
466
viewer/ui/embed-configurator.js
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
import { core } from "../core.js";
|
||||
import { toastHelper } from "../viewer-utils.js";
|
||||
import { t } from "../i18n-utils.js";
|
||||
|
||||
export function attachEmbedConfigurator(Viewer) {
|
||||
Object.assign(Viewer, {
|
||||
isEmbedModeActive() {
|
||||
return this.embedConfiguratorPanel?.hidden === false;
|
||||
},
|
||||
|
||||
isEmbedMode() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const embedParam = params.get("embed");
|
||||
return window.location.pathname.endsWith("/embed.html") || embedParam === "1" || embedParam === "true";
|
||||
},
|
||||
|
||||
getEmbedPageUrl() {
|
||||
const embedUrl = new URL("embed.html", import.meta.url);
|
||||
embedUrl.search = "";
|
||||
embedUrl.hash = "";
|
||||
return embedUrl;
|
||||
},
|
||||
|
||||
async copyTextToClipboard(value) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempInput = document.createElement("textarea");
|
||||
tempInput.value = value;
|
||||
tempInput.setAttribute("readonly", "true");
|
||||
tempInput.style.position = "absolute";
|
||||
tempInput.style.left = "-9999px";
|
||||
document.body.appendChild(tempInput);
|
||||
tempInput.select();
|
||||
document.execCommand("copy");
|
||||
tempInput.remove();
|
||||
},
|
||||
|
||||
getCurrentEmbedOptions({ includeCamera = false } = {}) {
|
||||
const entityId = core.CONFIG?.entity?.id;
|
||||
const modelPath = core.fileObject?.originalPath || core.container?.getAttribute("3d") || "";
|
||||
const options = {
|
||||
model: modelPath || null,
|
||||
id: entityId || null,
|
||||
theme: this.currentTheme === "light" ? "light" : null,
|
||||
autorotate: core.controls?.autoRotate === true,
|
||||
autorotateSpeed: Number.isFinite(core.controls?.autoRotateSpeed) ? core.controls.autoRotateSpeed : null,
|
||||
disableInteraction: this.urlOptions?.disableInteraction === true,
|
||||
hideUi: this.urlOptions?.hideUi === true,
|
||||
hideMetadata: this.urlOptions?.hideMetadata === true,
|
||||
presentationMode: core.PRESENTATION_MODE === true,
|
||||
sandboxMode: core.SANDBOX_MODE === true,
|
||||
cameraPosition: null,
|
||||
cameraTarget: null,
|
||||
fov: null,
|
||||
};
|
||||
|
||||
if (includeCamera) {
|
||||
options.cameraPosition = this.formatVector3Param(core.camera?.position);
|
||||
options.cameraTarget = this.formatVector3Param(core.controls?.target);
|
||||
options.fov = Number.isFinite(core.camera?.fov) ? core.camera.fov : null;
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
applyEmbedOptionsToInputs(options = {}) {
|
||||
console.log(this.embedConfigInputs);
|
||||
if (!this.embedConfigInputs) return;
|
||||
this.embedConfigInputs.model.value = options.model ?? "";
|
||||
this.embedConfigInputs.id.value = options.id ?? "";
|
||||
this.embedConfigInputs.theme.value = options.theme === "light" ? "light" : "dark";
|
||||
this.embedConfigInputs.autorotate.checked = options.autorotate === true;
|
||||
this.embedConfigInputs.autorotateSpeed.value = Number.isFinite(options.autorotateSpeed) ? String(options.autorotateSpeed) : "";
|
||||
this.embedConfigInputs.disableInteraction.checked = options.disableInteraction === true;
|
||||
this.embedConfigInputs.hideUi.checked = options.hideUi === true;
|
||||
this.embedConfigInputs.hideMetadata.checked = options.hideMetadata === true;
|
||||
this.embedConfigInputs.presentationMode.checked = options.presentationMode === true;
|
||||
this.embedConfigInputs.camPos.value = options.cameraPosition ?? "";
|
||||
this.embedConfigInputs.camTarget.value = options.cameraTarget ?? "";
|
||||
this.embedConfigInputs.fov.value = Number.isFinite(options.fov) ? String(options.fov) : "";
|
||||
console.log(
|
||||
"fillConfiguratorWithCurrentCamera",
|
||||
this.embedConfigInputs.camPos.value
|
||||
);
|
||||
},
|
||||
|
||||
setEmbedInputError(input, hasError, message = "") {
|
||||
if (!input) return;
|
||||
input.classList.toggle("embed-input-invalid", hasError);
|
||||
if (hasError && message) {
|
||||
input.setAttribute("title", message);
|
||||
input.setAttribute("aria-invalid", "true");
|
||||
} else {
|
||||
input.removeAttribute("title");
|
||||
input.removeAttribute("aria-invalid");
|
||||
}
|
||||
},
|
||||
|
||||
validateEmbedInputFields() {
|
||||
if (!this.embedConfigInputs) return true;
|
||||
const { camPos, camTarget, fov } = this.embedConfigInputs;
|
||||
const camPosRaw = camPos.value.trim();
|
||||
const camTargetRaw = camTarget.value.trim();
|
||||
const fovRaw = fov.value.trim();
|
||||
|
||||
const camPosOk = camPosRaw === "" || this.parseVector3Param(camPosRaw) !== null;
|
||||
const camTargetOk = camTargetRaw === "" || this.parseVector3Param(camTargetRaw) !== null;
|
||||
const parsedFov = this.parseFloatParam(fovRaw);
|
||||
const fovOk = fovRaw === "" || (Number.isFinite(parsedFov) && parsedFov >= 1 && parsedFov <= 179);
|
||||
|
||||
this.setEmbedInputError(camPos, !camPosOk, "Use format: x,y,z (for example 1.2,0.8,2.5)");
|
||||
this.setEmbedInputError(camTarget, !camTargetOk, "Use format: x,y,z (for example 0,0,0)");
|
||||
this.setEmbedInputError(fov, !fovOk, "FOV must be a number from 1 to 179");
|
||||
|
||||
return camPosOk && camTargetOk && fovOk;
|
||||
},
|
||||
|
||||
buildEmbedPayload(options = {}) {
|
||||
const embedUrl = this.getEmbedPageUrl();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options.model) {
|
||||
params.set("model", options.model);
|
||||
} else if (options.id) {
|
||||
params.set("id", options.id);
|
||||
}
|
||||
|
||||
if (options.theme === "light") {
|
||||
params.set("theme", options.theme);
|
||||
}
|
||||
|
||||
if (options.autorotate === true) {
|
||||
params.set("autorotate", "1");
|
||||
if (Number.isFinite(options.autorotateSpeed)) {
|
||||
params.set("autorotateSpeed", String(options.autorotateSpeed));
|
||||
}
|
||||
}
|
||||
|
||||
if (options.disableInteraction) {
|
||||
params.set("disableInteraction", "1");
|
||||
}
|
||||
if (options.hideUi) {
|
||||
params.set("hideUi", "1");
|
||||
}
|
||||
if (options.hideMetadata) {
|
||||
params.set("hideMetadata", "1");
|
||||
}
|
||||
if (options.presentationMode) {
|
||||
params.set("presentationMode", "1");
|
||||
}
|
||||
if (options.sandboxMode) {
|
||||
params.set("sandbox", "1");
|
||||
}
|
||||
if (options.cameraPosition) {
|
||||
params.set("camPos", options.cameraPosition);
|
||||
}
|
||||
if (options.cameraTarget) {
|
||||
params.set("camTarget", options.cameraTarget);
|
||||
}
|
||||
if (Number.isFinite(options.fov)) {
|
||||
params.set("fov", String(options.fov));
|
||||
}
|
||||
|
||||
embedUrl.search = params.toString();
|
||||
|
||||
return {
|
||||
url: embedUrl.toString(),
|
||||
code: `<iframe src="${embedUrl.toString()}" title="DFG 3D Viewer" loading="lazy" allow="fullscreen; xr-spatial-tracking" referrerpolicy="strict-origin-when-cross-origin" style="width:100%; aspect-ratio: 16 / 9; border: 0;"></iframe>`,
|
||||
};
|
||||
},
|
||||
|
||||
getSharePayload() {
|
||||
return this.buildEmbedPayload(this.getCurrentEmbedOptions({ includeCamera: true }));
|
||||
},
|
||||
|
||||
collectEmbedConfiguratorOptions() {
|
||||
console.trace("collectEmbedConfiguratorOptions");
|
||||
const inputs = this.embedConfigInputs;
|
||||
if (!inputs) return this.getCurrentEmbedOptions({ includeCamera: true });
|
||||
const parsedCamPos = this.parseVector3Param(inputs.camPos.value);
|
||||
const parsedCamTarget = this.parseVector3Param(inputs.camTarget.value);
|
||||
const parsedFov = this.parseFloatParam(inputs.fov.value);
|
||||
const normalizedFov = Number.isFinite(parsedFov) ? Math.min(179, Math.max(1, parsedFov)) : null;
|
||||
return {
|
||||
model: inputs.model.value.trim() || null,
|
||||
id: inputs.id.value.trim() || null,
|
||||
theme: inputs.theme.value === "light" ? "light" : null,
|
||||
autorotate: inputs.autorotate.checked,
|
||||
autorotateSpeed: this.parseFloatParam(inputs.autorotateSpeed.value),
|
||||
disableInteraction: inputs.disableInteraction.checked,
|
||||
hideUi: inputs.hideUi.checked,
|
||||
hideMetadata: inputs.hideMetadata.checked,
|
||||
presentationMode: inputs.presentationMode.checked,
|
||||
cameraPosition: this.formatVector3Param(parsedCamPos),
|
||||
cameraTarget: this.formatVector3Param(parsedCamTarget),
|
||||
fov: normalizedFov,
|
||||
};
|
||||
},
|
||||
|
||||
hasEmbedSourceSelection(options = {}) {
|
||||
return Boolean(options.model || options.id);
|
||||
},
|
||||
|
||||
notifyMissingEmbedSource({ force = false } = {}) {
|
||||
if (!force && this.embedMissingSourceNotified) return;
|
||||
toastHelper("embedSourceMissing", "warning");
|
||||
this.embedMissingSourceNotified = true;
|
||||
},
|
||||
|
||||
updateEmbedConfiguratorPreview() {
|
||||
if (!this.embedConfigInputs) return;
|
||||
if (this.updatingEmbedFields) return;
|
||||
this.validateEmbedInputFields();
|
||||
const options = this.collectEmbedConfiguratorOptions();
|
||||
if (!this.hasEmbedSourceSelection(options)) {
|
||||
this.notifyMissingEmbedSource();
|
||||
} else {
|
||||
this.embedMissingSourceNotified = false;
|
||||
}
|
||||
const payload = this.buildEmbedPayload(options);
|
||||
this.embedConfigInputs.url.value = payload.url;
|
||||
this.embedConfigInputs.iframe.value = payload.code;
|
||||
if (this.embedConfigPreviewFrame) {
|
||||
this.embedConfigPreviewFrame.src = payload.url;
|
||||
}
|
||||
},
|
||||
|
||||
fillConfiguratorWithCurrentCamera() {
|
||||
if (!this.embedConfigInputs) return;
|
||||
|
||||
this.updatingEmbedFields = true;
|
||||
|
||||
this.embedConfigInputs.camPos.value = this.formatVector3Param(core.camera?.position) || "";
|
||||
this.embedConfigInputs.camTarget.value = this.formatVector3Param(core.controls?.target) || "";
|
||||
this.embedConfigInputs.fov.value = Number.isFinite(core.camera?.fov) ? String(core.camera.fov) : "";
|
||||
this.updatingEmbedFields = false;
|
||||
this.updateEmbedConfiguratorPreview();
|
||||
},
|
||||
|
||||
resetEmbedConfiguratorFromViewerState() {
|
||||
if (!this.embedConfigInputs) return;
|
||||
this.applyEmbedOptionsToInputs(this.getCurrentEmbedOptions({ includeCamera: true }));
|
||||
this.updateEmbedConfiguratorPreview();
|
||||
},
|
||||
|
||||
toggleEmbedConfigurator(event) {
|
||||
event?.preventDefault?.();
|
||||
this.closeActionMenu();
|
||||
if (!this.embedConfiguratorPanel) return;
|
||||
const willShow = this.embedConfiguratorPanel.hidden === true;
|
||||
this.embedConfiguratorPanel.hidden = !willShow;
|
||||
if (willShow) {
|
||||
this.updateEmbedConfiguratorPreview();
|
||||
}
|
||||
this.updateEmbedMenuEntryState();
|
||||
},
|
||||
|
||||
openEmbedConfiguratorFromMenu(event) {
|
||||
this.toggleEmbedConfigurator(event);
|
||||
},
|
||||
|
||||
createEmbedConfiguratorPanel() {
|
||||
if (!core.container || this.embedConfiguratorPanel) return;
|
||||
const defaults = this.getCurrentEmbedOptions({ includeCamera: true });
|
||||
const panelText = {
|
||||
title: t("embedPanel.title", "Embed options"),
|
||||
closeAria: t("embedPanel.closeAria", "Close embed options"),
|
||||
modelUrl: t("embedPanel.modelUrl", "Model URL"),
|
||||
modelUrlPlaceholder: t("embedPanel.modelUrlPlaceholder", "/examples/box.glb"),
|
||||
entityId: t("embedPanel.entityId", "Entity ID"),
|
||||
theme: t("embedPanel.theme", "Theme"),
|
||||
themeDark: t("embedPanel.themeDark", "Dark"),
|
||||
themeLight: t("embedPanel.themeLight", "Light"),
|
||||
autoRotateSpeed: t("embedPanel.autoRotateSpeed", "Auto-rotate speed"),
|
||||
cameraPosition: t("embedPanel.cameraPosition", "Camera position"),
|
||||
cameraTarget: t("embedPanel.cameraTarget", "Camera target"),
|
||||
cameraVectorPlaceholder: t("embedPanel.cameraVectorPlaceholder", "x,y,z"),
|
||||
fov: t("embedPanel.fov", "FOV"),
|
||||
autoRotate: t("embedPanel.autoRotate", "Auto-rotate"),
|
||||
disableInteraction: t("embedPanel.disableInteraction", "Disable interaction"),
|
||||
hideActionMenu: t("embedPanel.hideActionMenu", "Hide action menu"),
|
||||
hideMetadata: t("embedPanel.hideMetadata", "Hide metadata"),
|
||||
presentationMode: t("embedPanel.presentationMode", "Presentation mode"),
|
||||
useCurrentCamera: t("embedPanel.useCurrentCamera", "Use Current Camera"),
|
||||
resetFromViewer: t("embedPanel.resetFromViewer", "Reset From Viewer"),
|
||||
copyUrl: t("embedPanel.copyUrl", "Copy URL"),
|
||||
copyIframe: t("embedPanel.copyIframe", "Copy Iframe"),
|
||||
embedUrl: t("embedPanel.embedUrl", "Embed URL"),
|
||||
iframeCode: t("embedPanel.iframeCode", "Iframe code"),
|
||||
preview: t("embedPanel.preview", "Preview"),
|
||||
previewTitle: t("embedPanel.previewTitle", "Embed preview"),
|
||||
};
|
||||
const panel = document.createElement("div");
|
||||
panel.id = "embedConfiguratorPanel";
|
||||
panel.hidden = true;
|
||||
panel.innerHTML = `
|
||||
<div class="embed-config-header">
|
||||
<span>${panelText.title}</span>
|
||||
<button id="embedClosePanel" type="button" aria-label="${panelText.closeAria}">X</button>
|
||||
</div>
|
||||
<div class="embed-config-layout">
|
||||
<div class="embed-config-main">
|
||||
<div class="embed-config-grid">
|
||||
<label>${panelText.modelUrl}<input id="embedModelInput" type="text" placeholder="${panelText.modelUrlPlaceholder}" value="${defaults.model ?? ""}" /></label>
|
||||
<label>${panelText.entityId}<input id="embedIdInput" type="text" value="${defaults.id ?? ""}" /></label>
|
||||
<label>${panelText.theme}
|
||||
<select id="embedThemeInput">
|
||||
<option value="dark">${panelText.themeDark}</option>
|
||||
<option value="light"${defaults.theme === "light" ? " selected" : ""}>${panelText.themeLight}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>${panelText.autoRotateSpeed}<input id="embedAutorotateSpeedInput" type="number" step="0.1" value="${Number.isFinite(defaults.autorotateSpeed) ? defaults.autorotateSpeed : ""}" /></label>
|
||||
<label>${panelText.cameraPosition}<input id="embedCamPosInput" type="text" placeholder="${panelText.cameraVectorPlaceholder}" value="${defaults.cameraPosition ?? ""}" /></label>
|
||||
<label>${panelText.cameraTarget}<input id="embedCamTargetInput" type="text" placeholder="${panelText.cameraVectorPlaceholder}" value="${defaults.cameraTarget ?? ""}" /></label>
|
||||
<label>${panelText.fov}<input id="embedFovInput" type="number" step="1" min="1" max="179" value="${Number.isFinite(defaults.fov) ? defaults.fov : ""}" /></label>
|
||||
</div>
|
||||
<div class="embed-config-checks">
|
||||
<label><input id="embedAutorotateInput" type="checkbox"${defaults.autorotate ? " checked" : ""} /> ${panelText.autoRotate}</label>
|
||||
<label><input id="embedDisableInteractionInput" type="checkbox"${defaults.disableInteraction ? " checked" : ""} /> ${panelText.disableInteraction}</label>
|
||||
<label><input id="embedHideUiInput" type="checkbox"${defaults.hideUi ? " checked" : ""} /> ${panelText.hideActionMenu}</label>
|
||||
<label><input id="embedHideMetadataInput" type="checkbox"${defaults.hideMetadata ? " checked" : ""} /> ${panelText.hideMetadata}</label>
|
||||
<label><input id="embedPresentationModeInput" type="checkbox"${defaults.presentationMode ? " checked" : ""} /> ${panelText.presentationMode}</label>
|
||||
</div>
|
||||
<div class="embed-config-actions">
|
||||
<button id="embedUseCurrentCamera" type="button">${panelText.useCurrentCamera}</button>
|
||||
<button id="embedResetFromViewer" type="button">${panelText.resetFromViewer}</button>
|
||||
<button id="embedCopyUrl" type="button">${panelText.copyUrl}</button>
|
||||
<button id="embedCopyIframe" type="button">${panelText.copyIframe}</button>
|
||||
</div>
|
||||
<label class="embed-config-field">${panelText.embedUrl}<textarea id="embedUrlOutput" readonly></textarea></label>
|
||||
<label class="embed-config-field">${panelText.iframeCode}<textarea id="embedIframeOutput" readonly></textarea></label>
|
||||
</div>
|
||||
<div class="embed-config-preview-side">
|
||||
<span>${panelText.preview}</span>
|
||||
<iframe id="embedPreviewFrame" title="${panelText.previewTitle}" loading="lazy"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
core.container.appendChild(panel);
|
||||
this.embedConfiguratorPanel = panel;
|
||||
this.embedConfigInputs = {
|
||||
model: panel.querySelector("#embedModelInput"),
|
||||
id: panel.querySelector("#embedIdInput"),
|
||||
theme: panel.querySelector("#embedThemeInput"),
|
||||
autorotate: panel.querySelector("#embedAutorotateInput"),
|
||||
autorotateSpeed: panel.querySelector("#embedAutorotateSpeedInput"),
|
||||
disableInteraction: panel.querySelector("#embedDisableInteractionInput"),
|
||||
hideUi: panel.querySelector("#embedHideUiInput"),
|
||||
hideMetadata: panel.querySelector("#embedHideMetadataInput"),
|
||||
presentationMode: panel.querySelector("#embedPresentationModeInput"),
|
||||
camPos: panel.querySelector("#embedCamPosInput"),
|
||||
camTarget: panel.querySelector("#embedCamTargetInput"),
|
||||
fov: panel.querySelector("#embedFovInput"),
|
||||
url: panel.querySelector("#embedUrlOutput"),
|
||||
iframe: panel.querySelector("#embedIframeOutput"),
|
||||
};
|
||||
this.embedConfigPreviewFrame = panel.querySelector("#embedPreviewFrame");
|
||||
|
||||
const watchedInputs = [
|
||||
this.embedConfigInputs.model,
|
||||
this.embedConfigInputs.id,
|
||||
this.embedConfigInputs.theme,
|
||||
this.embedConfigInputs.autorotate,
|
||||
this.embedConfigInputs.autorotateSpeed,
|
||||
this.embedConfigInputs.disableInteraction,
|
||||
this.embedConfigInputs.hideUi,
|
||||
this.embedConfigInputs.hideMetadata,
|
||||
this.embedConfigInputs.presentationMode,
|
||||
this.embedConfigInputs.camPos,
|
||||
this.embedConfigInputs.camTarget,
|
||||
this.embedConfigInputs.fov,
|
||||
];
|
||||
watchedInputs.forEach((input) => {
|
||||
if (!input) return;
|
||||
const eventName = input.type === "checkbox" || input.tagName === "SELECT" ? "change" : "input";
|
||||
this.bindEventListener(input, eventName, () => this.updateEmbedConfiguratorPreview());
|
||||
});
|
||||
|
||||
const useCurrentCameraButton = panel.querySelector("#embedUseCurrentCamera");
|
||||
const resetFromViewerButton = panel.querySelector("#embedResetFromViewer");
|
||||
const copyUrlButton = panel.querySelector("#embedCopyUrl");
|
||||
const copyIframeButton = panel.querySelector("#embedCopyIframe");
|
||||
const closePanelButton = panel.querySelector("#embedClosePanel");
|
||||
|
||||
this.bindEventListener(useCurrentCameraButton, "click", () => this.fillConfiguratorWithCurrentCamera());
|
||||
this.bindEventListener(resetFromViewerButton, "click", () => this.resetEmbedConfiguratorFromViewerState());
|
||||
this.bindEventListener(closePanelButton, "click", () => {
|
||||
this.closeEmbedConfigurator();
|
||||
});
|
||||
this.bindEventListener(copyUrlButton, "click", async () => {
|
||||
try {
|
||||
const options = this.collectEmbedConfiguratorOptions();
|
||||
if (!this.hasEmbedSourceSelection(options)) {
|
||||
this.notifyMissingEmbedSource({ force: true });
|
||||
return;
|
||||
}
|
||||
const payload = this.buildEmbedPayload(options);
|
||||
await this.copyTextToClipboard(payload.url);
|
||||
toastHelper("embedUrlCopied", "success");
|
||||
} catch (error) {
|
||||
this.reportError(error, { context: "Copy embed URL failed" });
|
||||
toastHelper("embedUrlCopyError", "error");
|
||||
}
|
||||
});
|
||||
this.bindEventListener(copyIframeButton, "click", async () => {
|
||||
try {
|
||||
const options = this.collectEmbedConfiguratorOptions();
|
||||
if (!this.hasEmbedSourceSelection(options)) {
|
||||
this.notifyMissingEmbedSource({ force: true });
|
||||
return;
|
||||
}
|
||||
const payload = this.buildEmbedPayload(options);
|
||||
await this.copyTextToClipboard(payload.code);
|
||||
toastHelper("embedIframeCopied", "success");
|
||||
} catch (error) {
|
||||
this.reportError(error, { context: "Copy embed iframe failed" });
|
||||
toastHelper("embedIframeCopyError", "error");
|
||||
}
|
||||
});
|
||||
|
||||
this.updateEmbedConfiguratorPreview();
|
||||
},
|
||||
|
||||
async copyEmbedCode(event) {
|
||||
event?.preventDefault?.();
|
||||
this.closeActionMenu();
|
||||
|
||||
try {
|
||||
const { code } = this.getSharePayload();
|
||||
await this.copyTextToClipboard(code);
|
||||
toastHelper("embedCodeCopied", "success");
|
||||
} catch (error) {
|
||||
this.reportError(error, { context: "Copy embed code failed" });
|
||||
toastHelper("embedCodeCopyError", "error");
|
||||
}
|
||||
},
|
||||
|
||||
updateEmbedMenuEntryState() {
|
||||
if (!this.viewEntity) return;
|
||||
const isActive = this.isEmbedModeActive();
|
||||
const label = isActive ? t("menu.exitEmbed", "Exit embed") : t("menu.embed", "Embed");
|
||||
const iconClass = isActive ? "embed-exit-icon" : "embed-icon";
|
||||
this.viewEntity.innerHTML = `<span class="${iconClass}"></span><span>${label}</span>`;
|
||||
const a11yLabel = isActive
|
||||
? t("menu.exitEmbedMode", "Exit embed mode")
|
||||
: t("menu.openEmbedOptions", "Open embed options");
|
||||
this.viewEntity.setAttribute("aria-label", a11yLabel);
|
||||
this.viewEntity.setAttribute("title", a11yLabel);
|
||||
},
|
||||
|
||||
closeEmbedConfigurator() {
|
||||
if (this.embedConfiguratorPanel) {
|
||||
this.embedConfiguratorPanel.hidden = true;
|
||||
}
|
||||
this.updateEmbedMenuEntryState();
|
||||
},
|
||||
|
||||
closeEmbedMode() {
|
||||
this.closeEmbedConfigurator();
|
||||
},
|
||||
});
|
||||
}
|
||||
614
viewer/ui/loading-status.js
Normal file
614
viewer/ui/loading-status.js
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
import { core } from "../core.js";
|
||||
import { t } from "../i18n-utils.js";
|
||||
|
||||
export function attachLoadingStatus(viewer) {
|
||||
Object.assign(viewer, {
|
||||
toggleLoadingLogs() {
|
||||
this.showLoadingLogs = !this.showLoadingLogs;
|
||||
if (core.loadingLog.get()) {
|
||||
core.loadingLog.get().classList.toggle("editorToolbar-visible", this.showLoadingLogs);
|
||||
}
|
||||
this.updateEditorToolbarLabels();
|
||||
this.updateEditorToolbarState();
|
||||
},
|
||||
|
||||
getLoadingLogMessages() {
|
||||
return this.loadingLogMessageKeys.map((key) => t(key));
|
||||
},
|
||||
|
||||
getProcessingLoadingSteps() {
|
||||
return this.processingLoadingStepKeys.map((key) => t(key));
|
||||
},
|
||||
|
||||
createModelLoadingProgress(shell) {
|
||||
shell.className = "model-loader";
|
||||
shell.hidden = true;
|
||||
shell.setAttribute("role", "status");
|
||||
shell.setAttribute("aria-live", "polite");
|
||||
|
||||
const ring = document.createElement("div");
|
||||
ring.className = "model-loader__ring";
|
||||
ring.setAttribute("aria-hidden", "true");
|
||||
|
||||
const percent = document.createElement("div");
|
||||
percent.className = "model-loader__percent";
|
||||
percent.textContent = "0%";
|
||||
ring.appendChild(percent);
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.className = "model-loader__content";
|
||||
|
||||
const phase = document.createElement("div");
|
||||
phase.className = "model-loader__phase";
|
||||
phase.textContent = this.getLoadingLogMessages()[0] ?? t("loadingLog.loadingAssets", "Loading assets");
|
||||
|
||||
const phaseViewport = document.createElement("div");
|
||||
phaseViewport.className = "model-loader__phase-viewport";
|
||||
|
||||
const phaseList = document.createElement("div");
|
||||
phaseList.className = "model-loader__phase-list";
|
||||
|
||||
phaseViewport.appendChild(phaseList);
|
||||
|
||||
const messages = this.getLoadingLogMessages();
|
||||
const stageKeyToIndex = new Map(
|
||||
this.loadingLogMessageKeys.map((key, index) => [key, index])
|
||||
);
|
||||
const loadingModelKeyIndex = Math.max(
|
||||
0,
|
||||
stageKeyToIndex.get("loadingLog.loadingModel") ?? 0
|
||||
);
|
||||
let hideTimer = null;
|
||||
|
||||
messages.forEach((msg) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "model-loader__phase-item";
|
||||
item.textContent = msg;
|
||||
phaseList.appendChild(item);
|
||||
});
|
||||
|
||||
const clearHideTimer = () => {
|
||||
if (hideTimer) {
|
||||
window.clearTimeout(hideTimer);
|
||||
hideTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const updatePhase = (index) => {
|
||||
const itemHeight = phaseList.firstElementChild?.offsetHeight || 24;
|
||||
phaseList.style.transform = `translateY(-${index * itemHeight}px)`;
|
||||
phase.textContent = messages[index] || messages[loadingModelKeyIndex] || phase.textContent;
|
||||
Array.from(phaseList.children).forEach((item, itemIndex) => {
|
||||
item.toggleAttribute("data-active", itemIndex === index);
|
||||
item.toggleAttribute("data-near", itemIndex === index - 1 || itemIndex === index + 1);
|
||||
});
|
||||
};
|
||||
|
||||
const track = document.createElement("div");
|
||||
track.className = "model-loader__track";
|
||||
track.setAttribute("role", "progressbar");
|
||||
track.setAttribute("aria-valuemin", "0");
|
||||
track.setAttribute("aria-valuemax", "100");
|
||||
track.setAttribute("aria-valuenow", "0");
|
||||
content.append(phaseViewport, track);
|
||||
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "model-loader__bar";
|
||||
track.appendChild(bar);
|
||||
|
||||
shell.replaceChildren(ring, content);
|
||||
|
||||
const set = (value = 0, maxValue = 100) => {
|
||||
const safeMax = Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 100;
|
||||
const normalized = Number.isFinite(value) ? Math.min(Math.max(value / safeMax, 0), 1) : 0;
|
||||
const progress = Math.round(normalized * 100);
|
||||
|
||||
shell.style.setProperty("--model-loader-progress", String(progress));
|
||||
percent.textContent = `${progress}%`;
|
||||
bar.style.width = `${progress}%`;
|
||||
track.setAttribute("aria-valuenow", String(progress));
|
||||
updatePhase(loadingModelKeyIndex);
|
||||
};
|
||||
|
||||
set(0, 100);
|
||||
|
||||
return {
|
||||
getElement: () => shell,
|
||||
show: () => {
|
||||
clearHideTimer();
|
||||
shell.hidden = false;
|
||||
delete shell.dataset.complete;
|
||||
updatePhase(loadingModelKeyIndex);
|
||||
},
|
||||
hide: () => {
|
||||
clearHideTimer();
|
||||
shell.hidden = true;
|
||||
delete shell.dataset.complete;
|
||||
},
|
||||
setStage: (stageKey, progressValue = null) => {
|
||||
const index = stageKeyToIndex.get(stageKey);
|
||||
if (typeof index === "number") {
|
||||
updatePhase(index);
|
||||
}
|
||||
if (Number.isFinite(progressValue)) {
|
||||
const normalizedProgress = Math.max(0, Math.min(Math.round(progressValue), 100));
|
||||
shell.style.setProperty("--model-loader-progress", String(normalizedProgress));
|
||||
percent.textContent = `${normalizedProgress}%`;
|
||||
bar.style.width = `${normalizedProgress}%`;
|
||||
track.setAttribute("aria-valuenow", String(normalizedProgress));
|
||||
}
|
||||
},
|
||||
complete: (delayMs = 2400) => {
|
||||
clearHideTimer();
|
||||
updatePhase(stageKeyToIndex.get("loadingLog.modelLoaded") ?? messages.length - 1);
|
||||
shell.style.setProperty("--model-loader-progress", "100");
|
||||
percent.textContent = "100%";
|
||||
bar.style.width = "100%";
|
||||
track.setAttribute("aria-valuenow", "100");
|
||||
shell.dataset.complete = "true";
|
||||
hideTimer = window.setTimeout(() => {
|
||||
hideTimer = null;
|
||||
delete shell.dataset.complete;
|
||||
shell.hidden = true;
|
||||
}, delayMs);
|
||||
},
|
||||
set,
|
||||
reset: (maxValue = 100) => {
|
||||
clearHideTimer();
|
||||
delete shell.dataset.complete;
|
||||
set(0, maxValue);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
createLoadingLog() {
|
||||
const shell = document.createElement("div");
|
||||
shell.id = "loading-log";
|
||||
shell.setAttribute("aria-live", "polite");
|
||||
shell.hidden = true;
|
||||
|
||||
const collapsedToggle = document.createElement("button");
|
||||
collapsedToggle.type = "button";
|
||||
collapsedToggle.className = "loading-log__collapsed-toggle";
|
||||
collapsedToggle.setAttribute("aria-label", t("loadingLog.title", "Loading process log"));
|
||||
collapsedToggle.setAttribute("aria-expanded", "false");
|
||||
shell.appendChild(collapsedToggle);
|
||||
|
||||
const header = document.createElement("button");
|
||||
header.type = "button";
|
||||
header.className = "loading-log__header";
|
||||
header.setAttribute("aria-expanded", "false");
|
||||
|
||||
const headerTitle = document.createElement("span");
|
||||
headerTitle.className = "loading-log__title";
|
||||
headerTitle.textContent = t("loadingLog.title", "Loading process log");
|
||||
|
||||
const headerSummary = document.createElement("span");
|
||||
headerSummary.className = "loading-log__summary";
|
||||
|
||||
header.append(headerTitle, headerSummary);
|
||||
shell.appendChild(header);
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "loading-log__body";
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "loading-log__messages";
|
||||
|
||||
const progress = document.createElement("div");
|
||||
progress.className = "loading-log__progress";
|
||||
progress.setAttribute("role", "progressbar");
|
||||
progress.setAttribute("aria-valuemin", "0");
|
||||
progress.setAttribute("aria-valuemax", "100");
|
||||
progress.setAttribute("aria-valuenow", "0");
|
||||
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.className = "loading-log__progress-bar";
|
||||
progress.appendChild(progressBar);
|
||||
|
||||
body.append(list, progress);
|
||||
shell.appendChild(body);
|
||||
core.container.appendChild(shell);
|
||||
|
||||
shell.classList.add("editorToolbar-hidden");
|
||||
|
||||
let timer = null;
|
||||
let messageIndex = 0;
|
||||
let visibleCount = 0;
|
||||
let currentProgress = 0;
|
||||
let startedAt = 0;
|
||||
let hideTimer = null;
|
||||
let progressUpdated = false;
|
||||
const minVisibleMs = 900;
|
||||
const loadingMessages = this.getLoadingLogMessages();
|
||||
const loadingStageKeyToIndex = new Map(
|
||||
this.loadingLogMessageKeys.map((key, index) => [key, index])
|
||||
);
|
||||
const loadingModelMessageIndex = Math.max(
|
||||
0,
|
||||
loadingStageKeyToIndex.get("loadingLog.loadingModel") ?? 0
|
||||
);
|
||||
let isExpanded = false;
|
||||
|
||||
const setExpanded = (expanded) => {
|
||||
isExpanded = expanded;
|
||||
shell.dataset.expanded = expanded ? "true" : "false";
|
||||
header.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
collapsedToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
};
|
||||
|
||||
const updateSummary = () => {
|
||||
const activeMessage = loadingMessages[messageIndex] || loadingMessages[loadingModelMessageIndex] || "";
|
||||
headerSummary.textContent = `${currentProgress}% • ${activeMessage}`;
|
||||
};
|
||||
|
||||
header.addEventListener("click", () => {
|
||||
if (shell.hidden) return;
|
||||
setExpanded(!isExpanded);
|
||||
});
|
||||
|
||||
collapsedToggle.addEventListener("click", () => {
|
||||
if (shell.hidden) return;
|
||||
setExpanded(!isExpanded);
|
||||
});
|
||||
|
||||
const renderMessages = (allDone = false) => {
|
||||
const messages = loadingMessages
|
||||
.slice(Math.max(0, messageIndex - visibleCount + 1), messageIndex + 1);
|
||||
list.replaceChildren(...messages.map((message, index) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "loading-log__message";
|
||||
if (allDone) {
|
||||
row.classList.add("loading-log__message--done");
|
||||
} else if (index === messages.length - 1) {
|
||||
row.classList.add("loading-log__message--active");
|
||||
}
|
||||
row.textContent = message;
|
||||
return row;
|
||||
}));
|
||||
};
|
||||
|
||||
const setProgress = (value) => {
|
||||
const normalizedValue = Number.isFinite(value) ? Math.min(Math.max(value, 0), 100) : 0;
|
||||
currentProgress = Math.max(currentProgress, Math.round(normalizedValue));
|
||||
progressBar.style.width = `${currentProgress}%`;
|
||||
progress.setAttribute("aria-valuenow", String(currentProgress));
|
||||
updateSummary();
|
||||
};
|
||||
|
||||
const tick = () => {
|
||||
visibleCount = Math.min(4, visibleCount + 1);
|
||||
if (!progressUpdated) {
|
||||
messageIndex = loadingModelMessageIndex;
|
||||
setProgress(Math.min(currentProgress + 8, 12));
|
||||
}
|
||||
renderMessages();
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (timer) {
|
||||
window.clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const clearHideTimer = () => {
|
||||
if (hideTimer) {
|
||||
window.clearTimeout(hideTimer);
|
||||
hideTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const collapseLog = () => {
|
||||
shell.classList.remove("loading-log--done");
|
||||
shell.classList.remove("loading-log--error");
|
||||
setExpanded(false);
|
||||
hideTimer = null;
|
||||
};
|
||||
|
||||
return {
|
||||
start: () => {
|
||||
stop();
|
||||
clearHideTimer();
|
||||
progressUpdated = false;
|
||||
startedAt = performance.now();
|
||||
shell.hidden = false;
|
||||
setExpanded(false);
|
||||
shell.classList.remove("loading-log--done", "loading-log--error");
|
||||
messageIndex = loadingModelMessageIndex;
|
||||
visibleCount = 1;
|
||||
currentProgress = 0;
|
||||
renderMessages();
|
||||
setProgress(6);
|
||||
timer = window.setInterval(tick, 520);
|
||||
},
|
||||
update: (value) => {
|
||||
if (shell.hidden) return;
|
||||
progressUpdated = true;
|
||||
const normalized = Number.isFinite(value) ? value : 0;
|
||||
messageIndex = loadingModelMessageIndex;
|
||||
visibleCount = Math.min(4, Math.max(visibleCount, 2));
|
||||
renderMessages();
|
||||
setProgress(Math.min(normalized, 99));
|
||||
},
|
||||
setStage: (stageKey, progressValue = null) => {
|
||||
if (shell.hidden) return;
|
||||
const stageIndex = loadingStageKeyToIndex.get(stageKey);
|
||||
if (typeof stageIndex === "number") {
|
||||
messageIndex = stageIndex;
|
||||
visibleCount = Math.min(4, Math.max(visibleCount, 2));
|
||||
renderMessages();
|
||||
updateSummary();
|
||||
}
|
||||
if (Number.isFinite(progressValue)) {
|
||||
setProgress(progressValue);
|
||||
}
|
||||
},
|
||||
finish: () => {
|
||||
if (shell.hidden) return;
|
||||
stop();
|
||||
const messageCount = loadingMessages.length;
|
||||
messageIndex = messageCount - 1;
|
||||
visibleCount = messageCount;
|
||||
renderMessages(true);
|
||||
setProgress(100);
|
||||
updateSummary();
|
||||
shell.classList.add("loading-log--done");
|
||||
const visibleFor = performance.now() - startedAt;
|
||||
const hideDelay = Math.max(1200, minVisibleMs - visibleFor + 800);
|
||||
clearHideTimer();
|
||||
hideTimer = window.setTimeout(() => {
|
||||
collapseLog();
|
||||
}, hideDelay);
|
||||
},
|
||||
fail: () => {
|
||||
if (shell.hidden) return;
|
||||
stop();
|
||||
clearHideTimer();
|
||||
setExpanded(true);
|
||||
shell.classList.add("loading-log--error");
|
||||
hideTimer = window.setTimeout(() => {
|
||||
shell.classList.remove("loading-log--error");
|
||||
setExpanded(false);
|
||||
hideTimer = null;
|
||||
}, 2000);
|
||||
},
|
||||
get: () => shell,
|
||||
};
|
||||
},
|
||||
|
||||
showStatusNotice(message, duration = 2600, options = {}) {
|
||||
this.enqueueStatusNotice({ message, duration, tone: "info", ...options });
|
||||
},
|
||||
|
||||
localizeStatusNotice(notice) {
|
||||
if (!notice) return notice;
|
||||
const i18nKey = String(notice.i18nKey || "");
|
||||
const detailI18nKey = String(notice.detailI18nKey || "");
|
||||
return {
|
||||
...notice,
|
||||
message: i18nKey ? t(i18nKey, notice.i18nVars || {}, notice.message) : notice.message,
|
||||
detail: detailI18nKey ? t(detailI18nKey, notice.detailI18nVars || {}, notice.detail) : notice.detail,
|
||||
};
|
||||
},
|
||||
|
||||
renderStatusNoticeContent(notice) {
|
||||
if (!this.statusNotice || !notice) return;
|
||||
const message = String(notice.message ?? "");
|
||||
const detail = String(notice.detail ?? "");
|
||||
|
||||
this.statusNotice.textContent = "";
|
||||
|
||||
const messageNode = document.createElement("span");
|
||||
messageNode.className = "viewer-notice-message";
|
||||
messageNode.textContent = message;
|
||||
this.statusNotice.appendChild(messageNode);
|
||||
|
||||
if (detail) {
|
||||
const detailLines = detail.split(/\r?\n/);
|
||||
for (const line of detailLines) {
|
||||
const detailNode = document.createElement("span");
|
||||
detailNode.className = "viewer-notice-detail";
|
||||
detailNode.innerHTML = line;
|
||||
this.statusNotice.appendChild(detailNode);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getStatusNoticeText(notice) {
|
||||
return [notice?.message, notice?.detail].filter(Boolean).join(" ");
|
||||
},
|
||||
|
||||
refreshStatusNoticeLanguage() {
|
||||
if (Array.isArray(this.statusNoticeQueue)) {
|
||||
this.statusNoticeQueue = this.statusNoticeQueue.map((notice) => this.localizeStatusNotice(notice));
|
||||
}
|
||||
|
||||
if (!this.statusNoticeCurrent) return;
|
||||
this.statusNoticeCurrent = this.localizeStatusNotice(this.statusNoticeCurrent);
|
||||
this.renderStatusNoticeContent(this.statusNoticeCurrent);
|
||||
},
|
||||
|
||||
showStatusNoticeNow(notice) {
|
||||
if (!this.statusNotice || !notice) return;
|
||||
notice = this.localizeStatusNotice(notice);
|
||||
|
||||
if (this.statusNoticeHideTimer) {
|
||||
clearTimeout(this.statusNoticeHideTimer);
|
||||
this.statusNoticeHideTimer = null;
|
||||
}
|
||||
|
||||
this.statusNoticeActive = true;
|
||||
this.statusNoticeCurrent = notice;
|
||||
this.statusNotice.hidden = false;
|
||||
this.renderStatusNoticeContent(notice);
|
||||
this.statusNotice.dataset.tone = notice.tone || "info";
|
||||
if (notice.variant) {
|
||||
this.statusNotice.dataset.variant = notice.variant;
|
||||
} else {
|
||||
delete this.statusNotice.dataset.variant;
|
||||
}
|
||||
this.noticeContainer?.classList.toggle("viewer-notice-container--sandbox", notice.variant === "sandbox");
|
||||
this.statusNotice.classList.remove("is-hiding");
|
||||
this.statusNotice.classList.add("is-visible");
|
||||
|
||||
if (this.statusNoticeTimer) {
|
||||
clearTimeout(this.statusNoticeTimer);
|
||||
this.statusNoticeTimer = null;
|
||||
}
|
||||
|
||||
if (notice.persistent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.statusNoticeTimer = setTimeout(() => {
|
||||
if (this.statusNotice) {
|
||||
this.statusNotice.classList.remove("is-visible");
|
||||
this.statusNotice.classList.add("is-hiding");
|
||||
}
|
||||
|
||||
this.statusNoticeHideTimer = setTimeout(() => {
|
||||
if (this.statusNotice) {
|
||||
this.statusNotice.hidden = true;
|
||||
this.statusNotice.classList.remove("is-hiding");
|
||||
delete this.statusNotice.dataset.variant;
|
||||
}
|
||||
this.noticeContainer?.classList.remove("viewer-notice-container--sandbox");
|
||||
this.statusNoticeActive = false;
|
||||
this.statusNoticeCurrent = null;
|
||||
this.statusNoticeTimer = null;
|
||||
this.statusNoticeHideTimer = null;
|
||||
this.processStatusNoticeQueue();
|
||||
}, 220);
|
||||
}, notice.duration);
|
||||
},
|
||||
|
||||
dismissStatusNotice(key = "") {
|
||||
const normalizedKey = String(key || "");
|
||||
this.statusNoticeQueue = this.statusNoticeQueue.filter(
|
||||
(entry) => normalizedKey && (entry?.key || "") !== normalizedKey
|
||||
);
|
||||
|
||||
if (
|
||||
normalizedKey &&
|
||||
(!this.statusNoticeCurrent || (this.statusNoticeCurrent.key || "") !== normalizedKey)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.statusNoticeTimer) {
|
||||
clearTimeout(this.statusNoticeTimer);
|
||||
this.statusNoticeTimer = null;
|
||||
}
|
||||
if (this.statusNoticeHideTimer) {
|
||||
clearTimeout(this.statusNoticeHideTimer);
|
||||
this.statusNoticeHideTimer = null;
|
||||
}
|
||||
|
||||
if (this.statusNotice) {
|
||||
this.statusNotice.hidden = true;
|
||||
this.statusNotice.classList.remove("is-visible", "is-hiding");
|
||||
delete this.statusNotice.dataset.variant;
|
||||
}
|
||||
this.noticeContainer?.classList.remove("viewer-notice-container--sandbox");
|
||||
|
||||
this.statusNoticeActive = false;
|
||||
this.statusNoticeCurrent = null;
|
||||
this.processStatusNoticeQueue();
|
||||
},
|
||||
|
||||
enqueueStatusNotice({
|
||||
message,
|
||||
duration = 2600,
|
||||
tone = "info",
|
||||
key = "",
|
||||
replace = false,
|
||||
persistent = false,
|
||||
variant = "",
|
||||
i18nKey = "",
|
||||
i18nVars = {},
|
||||
detail = "",
|
||||
detailI18nKey = "",
|
||||
detailI18nVars = {},
|
||||
} = {}) {
|
||||
const text = String(message ?? "");
|
||||
if (!text) return;
|
||||
|
||||
const nextNotice = {
|
||||
message: text,
|
||||
tone,
|
||||
duration: Number.isFinite(duration) ? duration : 2600,
|
||||
key: String(key || ""),
|
||||
persistent,
|
||||
variant: String(variant || ""),
|
||||
i18nKey: String(i18nKey || ""),
|
||||
i18nVars: i18nVars && typeof i18nVars === "object" ? { ...i18nVars } : {},
|
||||
detail: String(detail || ""),
|
||||
detailI18nKey: String(detailI18nKey || ""),
|
||||
detailI18nVars: detailI18nVars && typeof detailI18nVars === "object" ? { ...detailI18nVars } : {},
|
||||
};
|
||||
|
||||
if (
|
||||
this.statusNoticeActive &&
|
||||
this.getStatusNoticeText(this.statusNoticeCurrent) === this.getStatusNoticeText(nextNotice) &&
|
||||
(this.statusNoticeCurrent?.tone || "info") === nextNotice.tone &&
|
||||
(this.statusNoticeCurrent?.key || "") === nextNotice.key
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextNotice.key) {
|
||||
this.statusNoticeQueue = this.statusNoticeQueue.filter(
|
||||
(entry) => (entry?.key || "") !== nextNotice.key
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
replace &&
|
||||
this.statusNoticeActive &&
|
||||
(!nextNotice.key || (this.statusNoticeCurrent?.key || "") === nextNotice.key)
|
||||
) {
|
||||
this.showStatusNoticeNow(nextNotice);
|
||||
return;
|
||||
}
|
||||
|
||||
const isDuplicateQueued = this.statusNoticeQueue.some(
|
||||
(entry) =>
|
||||
this.getStatusNoticeText(entry) === this.getStatusNoticeText(nextNotice) &&
|
||||
entry?.tone === nextNotice.tone &&
|
||||
(entry?.key || "") === nextNotice.key
|
||||
);
|
||||
if (isDuplicateQueued) return;
|
||||
|
||||
const priorityMap = {
|
||||
error: 0,
|
||||
warning: 1,
|
||||
info: 2,
|
||||
success: 3,
|
||||
};
|
||||
const nextPriority = priorityMap[nextNotice.tone] ?? 2;
|
||||
const insertAt = this.statusNoticeQueue.findIndex((entry) => {
|
||||
const entryPriority = priorityMap[entry?.tone] ?? 2;
|
||||
return nextPriority < entryPriority;
|
||||
});
|
||||
|
||||
if (insertAt === -1) {
|
||||
this.statusNoticeQueue.push(nextNotice);
|
||||
} else {
|
||||
this.statusNoticeQueue.splice(insertAt, 0, nextNotice);
|
||||
}
|
||||
|
||||
this.processStatusNoticeQueue();
|
||||
},
|
||||
|
||||
processStatusNoticeQueue() {
|
||||
if (this.statusNoticeActive) return;
|
||||
if (!this.statusNotice) return;
|
||||
if (!Array.isArray(this.statusNoticeQueue) || this.statusNoticeQueue.length === 0) return;
|
||||
|
||||
const nextNotice = this.statusNoticeQueue.shift();
|
||||
if (!nextNotice) return;
|
||||
this.showStatusNoticeNow(nextNotice);
|
||||
},
|
||||
});
|
||||
}
|
||||
228
viewer/ui/localization-theme.js
Normal file
228
viewer/ui/localization-theme.js
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import { core } from "../core.js";
|
||||
import { t } from "../i18n-utils.js";
|
||||
import { UltraLoader } from "../ultra-loader.js";
|
||||
|
||||
export function attachLocalizationTheme(viewer) {
|
||||
Object.assign(viewer, {
|
||||
getStoredTheme() {
|
||||
if (this.urlOptions?.theme === "light" || this.urlOptions?.theme === "dark") {
|
||||
return this.urlOptions.theme;
|
||||
}
|
||||
|
||||
const storedTheme = window.localStorage.getItem(this.THEME_STORAGE_KEY);
|
||||
return storedTheme === "0" ? "light" : "dark";
|
||||
},
|
||||
|
||||
normalizeLanguage(value) {
|
||||
if (value == null) return null;
|
||||
const normalizedValue = String(value).trim().toLowerCase();
|
||||
if (normalizedValue.startsWith("pl")) return "pl";
|
||||
if (normalizedValue.startsWith("de")) return "de";
|
||||
if (normalizedValue.startsWith("en")) return "en";
|
||||
return null;
|
||||
},
|
||||
|
||||
getStoredLanguage() {
|
||||
const fromQuery = this.normalizeLanguage(this.urlOptions?.language);
|
||||
if (fromQuery) return fromQuery;
|
||||
|
||||
const storedLanguage = this.normalizeLanguage(window.localStorage.getItem(this.LANGUAGE_STORAGE_KEY));
|
||||
if (storedLanguage) return storedLanguage;
|
||||
|
||||
const browserLanguage = this.normalizeLanguage(navigator?.language || "en");
|
||||
return browserLanguage || "en";
|
||||
},
|
||||
|
||||
updateThemeControlLabels() {
|
||||
const isDark = this.currentTheme === "dark";
|
||||
|
||||
if (this.themeMode) {
|
||||
this.themeMode.innerHTML = `
|
||||
<span class="viewer-theme-icon" aria-hidden="true">${isDark ? "☀️" : "🌙"}</span>
|
||||
<span>${isDark ? t("theme.lightMode", "Light mode") : t("theme.darkMode", "Dark mode")}</span>
|
||||
`;
|
||||
const label = isDark
|
||||
? t("theme.switchToLightMode", "Switch to light mode")
|
||||
: t("theme.switchToDarkMode", "Switch to dark mode");
|
||||
this.themeMode.setAttribute("aria-label", label);
|
||||
this.themeMode.setAttribute("title", label);
|
||||
}
|
||||
|
||||
const exampleThemeToggle = document.getElementById("example-theme-toggle");
|
||||
if (exampleThemeToggle) {
|
||||
exampleThemeToggle.textContent = isDark ? "☀️" : "🌙";
|
||||
exampleThemeToggle.setAttribute("aria-pressed", isDark ? "true" : "false");
|
||||
exampleThemeToggle.hidden = true;
|
||||
}
|
||||
},
|
||||
|
||||
applyTheme(theme, { persist = true } = {}) {
|
||||
const normalizedTheme = theme === "light" ? "light" : "dark";
|
||||
const isDark = normalizedTheme === "dark";
|
||||
|
||||
this.currentTheme = normalizedTheme;
|
||||
document.documentElement.setAttribute("data-viewer-theme", normalizedTheme);
|
||||
document.body.setAttribute("data-viewer-theme", normalizedTheme);
|
||||
document.body.classList.toggle("iiif-dark", isDark);
|
||||
this.viewerWrapper?.setAttribute("data-viewer-theme", normalizedTheme);
|
||||
this.actionMenu?.setAttribute("data-viewer-theme", normalizedTheme);
|
||||
this.metadataContainer?.setAttribute("data-viewer-theme", normalizedTheme);
|
||||
core.guiContainer?.setAttribute("data-viewer-theme", normalizedTheme);
|
||||
document.getElementById("form-IIIF")?.setAttribute("data-viewer-theme", normalizedTheme);
|
||||
UltraLoader.panel?.setAttribute("data-viewer-theme", normalizedTheme);
|
||||
|
||||
if (persist) {
|
||||
window.localStorage.setItem(this.THEME_STORAGE_KEY, isDark ? "1" : "0");
|
||||
}
|
||||
|
||||
this.updateThemeControlLabels();
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.closeActionMenu();
|
||||
this.applyTheme(this.currentTheme === "dark" ? "light" : "dark");
|
||||
},
|
||||
|
||||
updateLanguageControlLabels() {
|
||||
if (!this.languageMode) return;
|
||||
const languages = [
|
||||
{ code: "en", label: "Language: EN"},
|
||||
{ code: "pl", label: "Język: PL"},
|
||||
{ code: "de", label: "Sprache: DE"}
|
||||
];
|
||||
const currentLangLabel = languages.find((language) => language.code === core.currentLanguage)?.label || "EN";
|
||||
this.languageMode.innerHTML = `
|
||||
<span class="viewer-action-icon language-icon" aria-hidden="true"></span>
|
||||
<span>${currentLangLabel}</span>
|
||||
`;
|
||||
this.languageMode.setAttribute("aria-label", t("language.label", "Language: EN"));
|
||||
this.languageMode.setAttribute("title", t("language.label", "Language: EN"));
|
||||
|
||||
if (this.languageModeDropdown) {
|
||||
const items = this.languageModeDropdown.querySelectorAll(".language-dropdown-item");
|
||||
items.forEach((item) => {
|
||||
item.classList.toggle("active", item.dataset.lang === core.currentLanguage);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateActionMenuLabels() {
|
||||
if (!this.actionMenu) return;
|
||||
const actionMenuLabel = t("menu.mainMenu", "Main menu");
|
||||
|
||||
const toggle = this.actionMenu.querySelector(".viewer-action-menu_toggle");
|
||||
toggle?.setAttribute("title", actionMenuLabel);
|
||||
const toggleCopy = toggle?.querySelector(".viewer-editor-tool_sr");
|
||||
if (toggleCopy) toggleCopy.textContent = actionMenuLabel;
|
||||
this.actionMenu.querySelector(".viewer-action-menu_panel")?.setAttribute("aria-label", actionMenuLabel);
|
||||
},
|
||||
|
||||
updateDownloadMenuEntryLabel() {
|
||||
if (!this.downloadModel || this.downloadModel.hidden) return;
|
||||
this.downloadModel.innerHTML = `
|
||||
<span class="viewer-action-icon download-icon" aria-hidden="true"></span>
|
||||
<span>${t("menu.download", "Download")}</span>
|
||||
`;
|
||||
},
|
||||
|
||||
updateLocalizedUI() {
|
||||
const lang = ["pl", "de"].includes(core.currentLanguage) ? core.currentLanguage : "en";
|
||||
document.documentElement.setAttribute("lang", lang);
|
||||
this.updateActionMenuLabels();
|
||||
this.updateLanguageControlLabels();
|
||||
this.updateThemeControlLabels();
|
||||
this.updateEmbedMenuEntryState();
|
||||
this.updateFullscreenButtonIcon();
|
||||
this.updateDownloadMenuEntryLabel();
|
||||
this.updateEditorToolbarLabels();
|
||||
this.updateEditorToolbarState();
|
||||
this.updatePickingModeControllerLabel();
|
||||
this.updateDistanceMeasurementControllerLabel();
|
||||
this.updateSelectedFacesControllerLabel();
|
||||
this.updateLocalPreviewLabels();
|
||||
this.updateIIIFFormLabels();
|
||||
this.updateMetadataPanelLabels();
|
||||
this.updateMaterialsDialogLabels();
|
||||
this.refreshStatusNoticeLanguage();
|
||||
UltraLoader.updateHeader?.();
|
||||
if (this.pickingHint) this.pickingHint.textContent = t("hints.picking", "Shift + click to select multiple faces");
|
||||
if (this.clippingHint) this.clippingHint.textContent = t("hints.clipping", "Drag active clipping plane helper to adjust cut");
|
||||
},
|
||||
|
||||
updateLocalPreviewLabels() {
|
||||
const label = document.querySelector("#example-model-picker label[for='example-model-select']");
|
||||
if (label) {
|
||||
label.textContent = t("localPreview.loadExampleModel", "Load example model");
|
||||
}
|
||||
},
|
||||
|
||||
updateIIIFFormLabels() {
|
||||
const form = document.getElementById("form-IIIF");
|
||||
if (!form) return;
|
||||
const title = form.querySelector(".form-IIIF-header .title");
|
||||
if (title) title.textContent = t("iiif.loader", "IIIF Loader");
|
||||
const collapseBtn = document.getElementById("iiif-toggle-collapse");
|
||||
if (collapseBtn) {
|
||||
const isCollapsed = form.classList.contains("collapsed");
|
||||
collapseBtn.title = isCollapsed
|
||||
? t("iiif.expand", "Expand")
|
||||
: t("iiif.collapse", "Collapse");
|
||||
}
|
||||
const label = form.querySelector(".form-IIIF-label");
|
||||
if (label) label.textContent = t("iiif.manifest", "IIIF manifest");
|
||||
const select = document.getElementById("iiif-manifest-select");
|
||||
if (select) {
|
||||
const optionLabelByUrl = {
|
||||
"https://raw.githubusercontent.com/IIIF/3d/main/manifests/4_transform_and_position/model_transform_scale_position.json": t("iiif.optionModelPositionScale", "Model Position and Scale"),
|
||||
"https://raw.githubusercontent.com/IIIF/3d/main/manifests/1_basic_model_in_scene/model_origin.json": t("iiif.optionModelOrigin", "Model Origin"),
|
||||
"https://raw.githubusercontent.com/IIIF/3d/main/manifests/1_basic_model_in_scene/model_origin_bgcolor.json": t("iiif.optionModelOriginBg", "Model Origin with background color"),
|
||||
"https://raw.githubusercontent.com/IIIF/3d/main/manifests/4_transform_and_position/model_position.json": t("iiif.optionModelPosition", "Model Position"),
|
||||
};
|
||||
Array.from(select.options).forEach((option) => {
|
||||
const labelFromMap = optionLabelByUrl[option.value];
|
||||
if (labelFromMap) option.textContent = labelFromMap;
|
||||
});
|
||||
}
|
||||
const manifestUrl = document.getElementById("manifest-url");
|
||||
if (manifestUrl) manifestUrl.placeholder = t("iiif.manifestUrlPlaceholder", "https://example.org/iiif/manifest.json");
|
||||
const manifestText = document.getElementById("manifest-text");
|
||||
if (manifestText) manifestText.placeholder = t("iiif.manifestTextPlaceholder", "Paste IIIF manifest JSON here...");
|
||||
const loadFromUrlButton = document.getElementById("load-manifest-from-url");
|
||||
if (loadFromUrlButton) loadFromUrlButton.textContent = t("iiif.loadFromUrl", "Load from URL");
|
||||
const loadFromTextButton = document.getElementById("load-manifest-from-text");
|
||||
if (loadFromTextButton) loadFromTextButton.textContent = t("iiif.loadFromText", "Load from Text");
|
||||
},
|
||||
|
||||
updateMetadataPanelLabels() {
|
||||
const metadataContainer = document.getElementById("metadata-container");
|
||||
if (!metadataContainer) return;
|
||||
metadataContainer.querySelectorAll("[data-i18n-key]").forEach((node) => {
|
||||
const key = node.getAttribute("data-i18n-key");
|
||||
if (!key) return;
|
||||
const needsColon = node.classList.contains("metadata-label");
|
||||
const text = t(key, node.textContent?.replace(/:\s*$/, "") || "");
|
||||
node.textContent = needsColon ? `${text}:` : text;
|
||||
});
|
||||
},
|
||||
|
||||
applyLanguage({ persist = true } = {}) {
|
||||
if (persist) {
|
||||
window.localStorage.setItem(this.LANGUAGE_STORAGE_KEY, core.currentLanguage);
|
||||
}
|
||||
this.updateLocalizedUI();
|
||||
},
|
||||
|
||||
toggleLanguage() {
|
||||
if (!this.languageModeDropdown) return;
|
||||
const isVisible = !this.languageModeDropdown.hidden;
|
||||
this.languageModeDropdown.hidden = isVisible;
|
||||
},
|
||||
|
||||
selectLanguage(lang) {
|
||||
core.currentLanguage = lang;
|
||||
this.languageModeDropdown.hidden = true;
|
||||
this.closeActionMenu();
|
||||
this.applyLanguage();
|
||||
},
|
||||
});
|
||||
}
|
||||
331
viewer/ui/thumbnail-gallery.js
Normal file
331
viewer/ui/thumbnail-gallery.js
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import { core } from "../core.js";
|
||||
import { isValidUrl } from "../utils.js";
|
||||
|
||||
function getGalleryConfig() {
|
||||
return core.CONFIG?.viewer?.gallery || {};
|
||||
}
|
||||
|
||||
function getGalleryHost(Viewer, mainElement) {
|
||||
return (
|
||||
Viewer.fileElement?.[0] ||
|
||||
mainElement ||
|
||||
Viewer.container ||
|
||||
core.container ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function removeExistingGalleryDom() {
|
||||
document.getElementById("image-list")?.remove();
|
||||
document.getElementById("modalGallery")?.remove();
|
||||
}
|
||||
|
||||
function createPlaceholderSvgDataUrl(index, label = "") {
|
||||
const palette = [
|
||||
["#1f3c88", "#6da3ff"],
|
||||
["#0f766e", "#6ee7b7"],
|
||||
["#9a3412", "#fdba74"],
|
||||
["#5b21b6", "#c4b5fd"],
|
||||
];
|
||||
const [start, end] = palette[index % palette.length];
|
||||
const title = label || `Preview ${index + 1}`;
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 320">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="${start}"/>
|
||||
<stop offset="100%" stop-color="${end}"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="480" height="320" fill="url(#g)"/>
|
||||
<circle cx="92" cy="86" r="34" fill="rgba(255,255,255,0.25)"/>
|
||||
<path d="M48 248l94-98 72 66 66-86 152 118H48z" fill="rgba(255,255,255,0.22)"/>
|
||||
<text x="240" y="164" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#ffffff">${title}</text>
|
||||
</svg>
|
||||
`.trim();
|
||||
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
function getConfiguredTestImages() {
|
||||
const gallery = getGalleryConfig();
|
||||
const configuredImages = Array.isArray(gallery.testImages) ? gallery.testImages : [];
|
||||
const normalizedImages = configuredImages.map((entry, index) => {
|
||||
if (typeof entry === "string") {
|
||||
const src = normalizeGalleryUrl(entry);
|
||||
return src ? { src, alt: `Preview ${index + 1}` } : null;
|
||||
}
|
||||
if (entry && typeof entry === "object") {
|
||||
const src = normalizeGalleryUrl(entry.src || entry.url || "");
|
||||
if (!src) return null;
|
||||
return {
|
||||
src,
|
||||
alt: String(entry.alt || entry.label || `Preview ${index + 1}`),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (normalizedImages.length > 0) {
|
||||
return normalizedImages;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function createDefaultTestImages() {
|
||||
return Array.from({ length: 9 }, (_unused, index) => ({
|
||||
src: createPlaceholderSvgDataUrl(index, `Preview ${index + 1}`),
|
||||
alt: `Preview ${index + 1}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function createFakeGalleryElements(testImages) {
|
||||
return testImages.map((entry) => {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "field__item";
|
||||
wrapper.innerHTML =
|
||||
`<img loading="lazy" src="${entry.src}" width="200px" height="200px" alt="${entry.alt}" class="img-fluid image-style-wisski-preview">`;
|
||||
return wrapper;
|
||||
});
|
||||
}
|
||||
|
||||
function prepareGalleryImages(Viewer, imageElementsChildren) {
|
||||
imageElementsChildren = imageElementsChildren.filter(function (_image) {
|
||||
if (!(_image instanceof Element)) return false;
|
||||
let rawUrl = "";
|
||||
const img = _image.querySelector("img");
|
||||
const link = _image.querySelector("a");
|
||||
if (img && img.getAttribute("src")) {
|
||||
rawUrl = img.getAttribute("src");
|
||||
} else if (link && link.getAttribute("href")) {
|
||||
rawUrl = link.getAttribute("href");
|
||||
} else {
|
||||
rawUrl = (_image.textContent || _image.innerHTML || "").trim();
|
||||
}
|
||||
|
||||
const normalized = normalizeGalleryUrl(rawUrl);
|
||||
if (!isValidUrl(normalized)) {
|
||||
return false;
|
||||
}
|
||||
_image.innerHTML = normalized;
|
||||
return !!img;
|
||||
});
|
||||
imageElementsChildren.forEach(function (imgLink) {
|
||||
imgLink.innerHTML =
|
||||
'<img loading="lazy" src="' +
|
||||
imgLink.innerHTML +
|
||||
'" width="200px" height="200px" alt="" class="img-fluid image-style-wisski-preview">';
|
||||
});
|
||||
return imageElementsChildren;
|
||||
}
|
||||
|
||||
function normalizeGalleryUrl(rawUrl) {
|
||||
if (!rawUrl || typeof rawUrl !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
let url = rawUrl.trim();
|
||||
if (url === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (url.startsWith("public://")) {
|
||||
url = "/sites/default/files/" + url.substring("public://".length);
|
||||
} else if (url.startsWith("sites/default/files/")) {
|
||||
url = "/" + url;
|
||||
}
|
||||
|
||||
const base = (core.CONFIG?.mainUrl || window.location.origin || "").replace(/\/+$/, "");
|
||||
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
const host = parsed.host || "";
|
||||
const path = parsed.pathname || "";
|
||||
const normalizedHost = host.toLowerCase();
|
||||
const hasBadHost = host.includes("_") || normalizedHost === "default" || normalizedHost === "dfg_3dviewer";
|
||||
|
||||
if (path.startsWith("/sites/default/files/")) {
|
||||
if (hasBadHost) {
|
||||
return `${base}${path}`;
|
||||
}
|
||||
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
||||
return parsed.href;
|
||||
}
|
||||
return `${base}${path}`;
|
||||
}
|
||||
return parsed.href;
|
||||
} catch (_error) {
|
||||
if (url.startsWith("/sites/default/files/")) {
|
||||
return `${base}${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function handleImages(Viewer, mainElement, imageElements, imageElementsChildren) {
|
||||
if (imageElementsChildren === undefined) {
|
||||
imageElementsChildren = imageElements;
|
||||
}
|
||||
removeExistingGalleryDom();
|
||||
var imageList = document.createElement("div");
|
||||
imageList.setAttribute("id", "image-list");
|
||||
imageList.style.display = "flex";
|
||||
imageList.style.flexWrap = "wrap";
|
||||
imageList.style.gap = "16px";
|
||||
imageList.style.alignItems = "center";
|
||||
var modalGallery = document.createElement("div");
|
||||
var modalImage = document.createElement("img");
|
||||
modalImage.setAttribute("class", "modalImage");
|
||||
modalImage.style.transform = "scale(0.95)";
|
||||
Viewer.bindEventListener(modalGallery, "wheel", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.deltaY > 0 && Viewer.zoomImage > 0.15) {
|
||||
modalImage.style.transform = `scale(${(Viewer.zoomImage -= Viewer.ZOOM_SPEED_IMAGE)})`;
|
||||
} else if (e.deltaY < 0 && Viewer.zoomImage < 5) {
|
||||
modalImage.style.transform = `scale(${(Viewer.zoomImage += Viewer.ZOOM_SPEED_IMAGE)})`;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
var modalClose = document.createElement("span");
|
||||
modalGallery.setAttribute("id", "modalGallery");
|
||||
modalGallery.setAttribute("class", "modalGallery");
|
||||
modalClose.setAttribute("class", "closeGallery");
|
||||
modalClose.setAttribute("title", "Close");
|
||||
modalClose.innerHTML = "×";
|
||||
modalClose.onclick = function () {
|
||||
modalGallery.classList.remove("is-open");
|
||||
};
|
||||
|
||||
Viewer.bindEventListener(document, "click", function (event) {
|
||||
if (
|
||||
!modalGallery.contains(event.target) &&
|
||||
!imageList.contains(event.target)
|
||||
) {
|
||||
modalGallery.classList.remove("is-open");
|
||||
Viewer.zoomImage = 1.5;
|
||||
modalImage.style.transform = "scale(1.5)";
|
||||
}
|
||||
});
|
||||
|
||||
modalGallery.appendChild(modalImage);
|
||||
modalGallery.appendChild(modalClose);
|
||||
for (let i = 0; imageElementsChildren.length - i >= 0; i++) {
|
||||
if (
|
||||
imageElementsChildren[i] !== undefined &&
|
||||
imageElementsChildren[i].innerHTML !== undefined
|
||||
) {
|
||||
var imgList = imageElementsChildren[i].getElementsByTagName("a");
|
||||
for (let j = 0; j < imgList.length; j++) {
|
||||
imgList[j].setAttribute("href", "#");
|
||||
imgList[j].setAttribute("src", imgList[j].firstChild.src);
|
||||
imgList[j].setAttribute("class", "image-list-item");
|
||||
}
|
||||
imgList = imageElementsChildren[i].getElementsByTagName("img");
|
||||
if (imgList.length == 1) {
|
||||
imgList[0].style.maxWidth = "fit-content";
|
||||
imgList[0].style.maxHeight = "180px";
|
||||
}
|
||||
for (let j = 0; j < imgList.length; j++) {
|
||||
imgList[j].onclick = function () {
|
||||
modalGallery.classList.add("is-open");
|
||||
imageList.style.zIndex = 0;
|
||||
imageList.style.display = "hidden";
|
||||
modalImage.src = this.src;
|
||||
};
|
||||
}
|
||||
if (imageElementsChildren[i] instanceof HTMLElement) {
|
||||
imageElementsChildren[i].style.display = "block";
|
||||
}
|
||||
imageList.appendChild(imageElementsChildren[i]);
|
||||
}
|
||||
}
|
||||
if (
|
||||
imageList &&
|
||||
imageList.childNodes.length > 0 &&
|
||||
getGalleryHost(Viewer, mainElement)
|
||||
) {
|
||||
const galleryHost = getGalleryHost(Viewer, mainElement);
|
||||
galleryHost.insertAdjacentElement("beforebegin", modalGallery);
|
||||
galleryHost.insertAdjacentElement("beforebegin", imageList);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildThumbnailGallery(Viewer) {
|
||||
const gallery = getGalleryConfig();
|
||||
var mainElement = gallery.container
|
||||
? document.getElementById(gallery.container)
|
||||
: null;
|
||||
var imageElements;
|
||||
if (gallery.imageClass !== "") {
|
||||
imageElements = document.getElementsByClassName(
|
||||
gallery.imageClass
|
||||
);
|
||||
if (imageElements.length === 0) {
|
||||
const fallbackFields = document.querySelectorAll(
|
||||
".field--type-image"
|
||||
);
|
||||
if (fallbackFields.length > 0) {
|
||||
imageElements = fallbackFields;
|
||||
console.warn(
|
||||
"Gallery imageClass not found, falling back to .field--type-image."
|
||||
);
|
||||
}
|
||||
}
|
||||
if (imageElements.length > 0) {
|
||||
var galleryLabel = document.getElementsByClassName("field__label");
|
||||
if (galleryLabel !== undefined && galleryLabel.length > 0) {
|
||||
galleryLabel[0].innerText = "";
|
||||
}
|
||||
}
|
||||
} else if (gallery.imageId !== "") {
|
||||
imageElements = document.getElementById(gallery.imageId);
|
||||
}
|
||||
|
||||
if (imageElements != null) {
|
||||
if (imageElements.length > 0) {
|
||||
if (imageElements[0].innerHTML !== undefined) {
|
||||
let imagesList = Array.from(
|
||||
imageElements[0].getElementsByClassName("field__items")[0]
|
||||
.childNodes
|
||||
);
|
||||
imagesList = prepareGalleryImages(Viewer, imagesList);
|
||||
imageElements[0].classList.add("field--label-hidden");
|
||||
//imageElements[0].classList.add("field__items");
|
||||
handleImages(Viewer, mainElement, imagesList, imagesList);
|
||||
} else {
|
||||
handleImages(Viewer, mainElement, imageElements);
|
||||
}
|
||||
} else if (
|
||||
imageElements.childNodes !== undefined &&
|
||||
imageElements.childNodes.length > 0
|
||||
) {
|
||||
if (
|
||||
typeof imageElements.childNodes[0].innerHTML == "string" ||
|
||||
typeof imageElements.childNodes[1].innerHTML == "string"
|
||||
) {
|
||||
let imagesList = Array.from(imageElements.childNodes);
|
||||
imagesList = prepareGalleryImages(Viewer, imagesList);
|
||||
imageElements.classList.add("field--type-image");
|
||||
imageElements.classList.add("field--label-hidden");
|
||||
//imageElements.classList.add("field__items");
|
||||
handleImages(Viewer, mainElement, imagesList, imageElements);
|
||||
} else {
|
||||
handleImages(Viewer, mainElement, imageElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (core.CONFIG?.viewer?.gallery?.buildFake === true) {
|
||||
const testImages = getConfiguredTestImages();
|
||||
const fallbackImages = testImages.length > 0 ? testImages : createDefaultTestImages();
|
||||
if (gallery.build === true) {
|
||||
const fakeImages = createFakeGalleryElements(fallbackImages);
|
||||
handleImages(Viewer, mainElement, fakeImages, fakeImages);
|
||||
console.log("Built fallback thumbnail gallery for local testing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("No gallery source found");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue