pi-studio 0.9.17 → 0.9.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -4
- package/client/studio-client.js +835 -70
- package/client/studio.css +175 -1
- package/index.ts +326 -46
- package/package.json +1 -1
package/client/studio-client.js
CHANGED
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
const shortcutsBtn = document.getElementById("shortcutsBtn");
|
|
132
132
|
const shortcutsOverlayEl = document.getElementById("shortcutsOverlay");
|
|
133
133
|
const shortcutsDialogEl = document.getElementById("shortcutsDialog");
|
|
134
|
+
const shortcutsBodyEl = document.getElementById("shortcutsBody");
|
|
134
135
|
const shortcutsCloseBtn = document.getElementById("shortcutsCloseBtn");
|
|
135
136
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
136
137
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
@@ -211,6 +212,18 @@
|
|
|
211
212
|
let studioHtmlFocusFullscreenBtn = null;
|
|
212
213
|
let studioHtmlFocusLastFocusedEl = null;
|
|
213
214
|
let studioHtmlFocusRestoreState = null;
|
|
215
|
+
let studioImageFocusOverlayEl = null;
|
|
216
|
+
let studioImageFocusDialogEl = null;
|
|
217
|
+
let studioImageFocusSlotEl = null;
|
|
218
|
+
let studioImageFocusImgEl = null;
|
|
219
|
+
let studioImageFocusTitleEl = null;
|
|
220
|
+
let studioImageFocusOpenLinkEl = null;
|
|
221
|
+
let studioImageFocusFullscreenBtn = null;
|
|
222
|
+
let studioImageFocusCloseBtn = null;
|
|
223
|
+
let studioImageFocusZoomLabelEl = null;
|
|
224
|
+
let studioImageFocusLastFocusedEl = null;
|
|
225
|
+
let studioImageFocusZoomMode = "fit";
|
|
226
|
+
let studioImageFocusZoom = 1;
|
|
214
227
|
let pendingRequestId = null;
|
|
215
228
|
let pendingKind = null;
|
|
216
229
|
let stickyStudioKind = null;
|
|
@@ -1914,6 +1927,8 @@
|
|
|
1914
1927
|
matlab: { label: "MATLAB", exts: ["m"] },
|
|
1915
1928
|
latex: { label: "LaTeX", exts: ["tex", "latex"] },
|
|
1916
1929
|
diff: { label: "Diff", exts: ["diff", "patch"] },
|
|
1930
|
+
csv: { label: "CSV", exts: ["csv"] },
|
|
1931
|
+
tsv: { label: "TSV", exts: ["tsv"] },
|
|
1917
1932
|
// Languages accepted for upload/detect but without syntax highlighting
|
|
1918
1933
|
java: { label: "Java", exts: ["java"] },
|
|
1919
1934
|
go: { label: "Go", exts: ["go"] },
|
|
@@ -1941,6 +1956,9 @@
|
|
|
1941
1956
|
const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
|
|
1942
1957
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
1943
1958
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
1959
|
+
const DELIMITED_PREVIEW_MAX_DATA_ROWS = 200;
|
|
1960
|
+
const DELIMITED_PREVIEW_MAX_COLUMNS = 50;
|
|
1961
|
+
const DELIMITED_PREVIEW_MAX_CELL_CHARS = 500;
|
|
1944
1962
|
const previewPendingTimers = new WeakMap();
|
|
1945
1963
|
const htmlArtifactFramesById = new Map();
|
|
1946
1964
|
let sourcePreviewRenderTimer = null;
|
|
@@ -3666,6 +3684,12 @@
|
|
|
3666
3684
|
&& typeof studioHtmlFocusShellEl.contains === "function"
|
|
3667
3685
|
&& studioHtmlFocusShellEl.contains(event.target)
|
|
3668
3686
|
);
|
|
3687
|
+
const imageFocusOwnsEvent = Boolean(
|
|
3688
|
+
studioImageFocusDialogEl
|
|
3689
|
+
&& event.target
|
|
3690
|
+
&& typeof studioImageFocusDialogEl.contains === "function"
|
|
3691
|
+
&& studioImageFocusDialogEl.contains(event.target)
|
|
3692
|
+
);
|
|
3669
3693
|
const quizOwnsEvent = Boolean(
|
|
3670
3694
|
quizDialogEl
|
|
3671
3695
|
&& event.target
|
|
@@ -3691,6 +3715,14 @@
|
|
|
3691
3715
|
return;
|
|
3692
3716
|
}
|
|
3693
3717
|
|
|
3718
|
+
if (isStudioImageFocusOpen() && plainEscape) {
|
|
3719
|
+
event.preventDefault();
|
|
3720
|
+
closeStudioImageFocusViewer();
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
if (handleStudioImageFocusShortcut(event)) return;
|
|
3725
|
+
|
|
3694
3726
|
if (isScratchpadOpen() && plainEscape) {
|
|
3695
3727
|
event.preventDefault();
|
|
3696
3728
|
closeScratchpad();
|
|
@@ -3703,6 +3735,8 @@
|
|
|
3703
3735
|
return;
|
|
3704
3736
|
}
|
|
3705
3737
|
|
|
3738
|
+
if (handleShortcutsScrollShortcut(event)) return;
|
|
3739
|
+
|
|
3706
3740
|
if (isReviewNotesOpen() && plainEscape) {
|
|
3707
3741
|
event.preventDefault();
|
|
3708
3742
|
closeReviewNotes();
|
|
@@ -3715,7 +3749,7 @@
|
|
|
3715
3749
|
return;
|
|
3716
3750
|
}
|
|
3717
3751
|
|
|
3718
|
-
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
|
|
3752
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || imageFocusOwnsEvent || quizOwnsEvent) {
|
|
3719
3753
|
return;
|
|
3720
3754
|
}
|
|
3721
3755
|
|
|
@@ -3890,6 +3924,22 @@
|
|
|
3890
3924
|
}
|
|
3891
3925
|
}
|
|
3892
3926
|
|
|
3927
|
+
function formatStudioExportTimestamp(date) {
|
|
3928
|
+
const value = date instanceof Date ? date : new Date();
|
|
3929
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
3930
|
+
try {
|
|
3931
|
+
return String(value.getFullYear())
|
|
3932
|
+
+ pad(value.getMonth() + 1)
|
|
3933
|
+
+ pad(value.getDate())
|
|
3934
|
+
+ "-"
|
|
3935
|
+
+ pad(value.getHours())
|
|
3936
|
+
+ pad(value.getMinutes())
|
|
3937
|
+
+ pad(value.getSeconds());
|
|
3938
|
+
} catch {
|
|
3939
|
+
return String(Date.now());
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3893
3943
|
function normalizeHistoryKind(kind) {
|
|
3894
3944
|
return kind === "critique" ? "critique" : "annotation";
|
|
3895
3945
|
}
|
|
@@ -4257,9 +4307,202 @@
|
|
|
4257
4307
|
return marker + (lang ? lang : "") + newline + source + newline + marker;
|
|
4258
4308
|
}
|
|
4259
4309
|
|
|
4310
|
+
function getDelimitedTextPreviewConfig(language) {
|
|
4311
|
+
const lang = normalizeFenceLanguage(language || "");
|
|
4312
|
+
if (lang === "csv") return { kind: "csv", label: "CSV", delimiter: "," };
|
|
4313
|
+
if (lang === "tsv") return { kind: "tsv", label: "TSV", delimiter: "\t" };
|
|
4314
|
+
return null;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
function parseDelimitedTextRows(text, delimiter, maxRows) {
|
|
4318
|
+
const source = String(text || "").replace(/^\uFEFF/, "");
|
|
4319
|
+
const limit = Math.max(1, Number(maxRows) || (DELIMITED_PREVIEW_MAX_DATA_ROWS + 1));
|
|
4320
|
+
const rows = [];
|
|
4321
|
+
let row = [];
|
|
4322
|
+
let cell = "";
|
|
4323
|
+
let inQuotes = false;
|
|
4324
|
+
let truncatedRows = false;
|
|
4325
|
+
|
|
4326
|
+
const pushCell = () => {
|
|
4327
|
+
row.push(cell);
|
|
4328
|
+
cell = "";
|
|
4329
|
+
};
|
|
4330
|
+
const pushRow = (index) => {
|
|
4331
|
+
pushCell();
|
|
4332
|
+
rows.push(row);
|
|
4333
|
+
row = [];
|
|
4334
|
+
if (rows.length >= limit) {
|
|
4335
|
+
truncatedRows = index < source.length - 1;
|
|
4336
|
+
return true;
|
|
4337
|
+
}
|
|
4338
|
+
return false;
|
|
4339
|
+
};
|
|
4340
|
+
|
|
4341
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
4342
|
+
if (rows.length >= limit) {
|
|
4343
|
+
truncatedRows = true;
|
|
4344
|
+
break;
|
|
4345
|
+
}
|
|
4346
|
+
const ch = source[i];
|
|
4347
|
+
if (inQuotes) {
|
|
4348
|
+
if (ch === '"') {
|
|
4349
|
+
if (source[i + 1] === '"') {
|
|
4350
|
+
cell += '"';
|
|
4351
|
+
i += 1;
|
|
4352
|
+
} else {
|
|
4353
|
+
inQuotes = false;
|
|
4354
|
+
}
|
|
4355
|
+
} else {
|
|
4356
|
+
cell += ch;
|
|
4357
|
+
}
|
|
4358
|
+
continue;
|
|
4359
|
+
}
|
|
4360
|
+
if (ch === '"' && cell === "") {
|
|
4361
|
+
inQuotes = true;
|
|
4362
|
+
continue;
|
|
4363
|
+
}
|
|
4364
|
+
if (ch === delimiter) {
|
|
4365
|
+
pushCell();
|
|
4366
|
+
continue;
|
|
4367
|
+
}
|
|
4368
|
+
if (ch === "\n") {
|
|
4369
|
+
if (pushRow(i)) break;
|
|
4370
|
+
continue;
|
|
4371
|
+
}
|
|
4372
|
+
if (ch === "\r") {
|
|
4373
|
+
if (source[i + 1] === "\n") i += 1;
|
|
4374
|
+
if (pushRow(i)) break;
|
|
4375
|
+
continue;
|
|
4376
|
+
}
|
|
4377
|
+
cell += ch;
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
if (!truncatedRows && rows.length < limit && (cell.length > 0 || row.length > 0)) {
|
|
4381
|
+
pushCell();
|
|
4382
|
+
rows.push(row);
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
return { rows, truncatedRows };
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
function buildDelimitedTextPreviewModel(text, language) {
|
|
4389
|
+
const config = getDelimitedTextPreviewConfig(language);
|
|
4390
|
+
if (!config) return null;
|
|
4391
|
+
const parsed = parseDelimitedTextRows(text, config.delimiter, DELIMITED_PREVIEW_MAX_DATA_ROWS + 1);
|
|
4392
|
+
const rows = parsed.rows;
|
|
4393
|
+
const rawColumnCount = rows.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0);
|
|
4394
|
+
const columnCount = Math.min(rawColumnCount, DELIMITED_PREVIEW_MAX_COLUMNS);
|
|
4395
|
+
const header = rows[0] || [];
|
|
4396
|
+
const dataRows = rows.slice(1);
|
|
4397
|
+
return {
|
|
4398
|
+
...config,
|
|
4399
|
+
rows,
|
|
4400
|
+
header,
|
|
4401
|
+
dataRows,
|
|
4402
|
+
rawColumnCount,
|
|
4403
|
+
columnCount,
|
|
4404
|
+
truncatedColumns: rawColumnCount > columnCount,
|
|
4405
|
+
truncatedRows: parsed.truncatedRows,
|
|
4406
|
+
};
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
function getDelimitedHeaderLabel(header, index) {
|
|
4410
|
+
const value = String((header && header[index]) || "").trim();
|
|
4411
|
+
return value || ("Column " + (index + 1));
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
function formatDelimitedPreviewCellHtml(value) {
|
|
4415
|
+
const raw = String(value ?? "");
|
|
4416
|
+
if (raw.length <= DELIMITED_PREVIEW_MAX_CELL_CHARS) return escapeHtml(raw);
|
|
4417
|
+
return escapeHtml(raw.slice(0, DELIMITED_PREVIEW_MAX_CELL_CHARS)) + "<span class='delimited-preview-truncation'>…</span>";
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
function formatDelimitedMarkdownCell(value) {
|
|
4421
|
+
const raw = String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4422
|
+
const shortened = raw.length > DELIMITED_PREVIEW_MAX_CELL_CHARS
|
|
4423
|
+
? raw.slice(0, DELIMITED_PREVIEW_MAX_CELL_CHARS) + "…"
|
|
4424
|
+
: raw;
|
|
4425
|
+
return shortened.replace(/\n/g, "<br>").replace(/\|/g, "\\|").trim() || " ";
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
function buildDelimitedTextPreviewHtml(text, language) {
|
|
4429
|
+
const model = buildDelimitedTextPreviewModel(text, language);
|
|
4430
|
+
if (!model) return "";
|
|
4431
|
+
if (!model.rows.length || model.columnCount <= 0) {
|
|
4432
|
+
return "<div class='delimited-preview rendered-markdown'><div class='delimited-preview-header'><strong>" + escapeHtml(model.label) + " preview</strong></div><pre class='plain-markdown'>No tabular data to preview.</pre></div>";
|
|
4433
|
+
}
|
|
4434
|
+
const columnIndexes = Array.from({ length: model.columnCount }, (_, index) => index);
|
|
4435
|
+
const headerHtml = columnIndexes.map((index) => "<th scope='col'>" + escapeHtml(getDelimitedHeaderLabel(model.header, index)) + "</th>").join("");
|
|
4436
|
+
const bodyHtml = model.dataRows.length
|
|
4437
|
+
? model.dataRows.map((row, rowIndex) => {
|
|
4438
|
+
const cells = columnIndexes.map((index) => {
|
|
4439
|
+
const raw = String((row && row[index]) ?? "");
|
|
4440
|
+
const emptyClass = raw.length === 0 ? " delimited-preview-empty-cell" : "";
|
|
4441
|
+
return "<td class='" + emptyClass.trim() + "'>" + formatDelimitedPreviewCellHtml(raw) + "</td>";
|
|
4442
|
+
}).join("");
|
|
4443
|
+
return "<tr><th scope='row' class='delimited-preview-row-number'>" + String(rowIndex + 1) + "</th>" + cells + "</tr>";
|
|
4444
|
+
}).join("")
|
|
4445
|
+
: "<tr><td colspan='" + String(model.columnCount + 1) + "' class='delimited-preview-empty'>No data rows after the header.</td></tr>";
|
|
4446
|
+
const notices = [];
|
|
4447
|
+
if (model.truncatedRows) notices.push("Showing first " + String(Math.max(0, model.dataRows.length)) + " data rows.");
|
|
4448
|
+
if (model.truncatedColumns) notices.push("Showing first " + String(model.columnCount) + " of " + String(model.rawColumnCount) + " columns.");
|
|
4449
|
+
const noticeHtml = notices.length ? "<div class='preview-warning delimited-preview-notice'>" + escapeHtml(notices.join(" ")) + "</div>" : "";
|
|
4450
|
+
const summaryParts = [String(model.dataRows.length) + (model.truncatedRows ? "+" : "") + " data rows", String(model.rawColumnCount) + " columns"];
|
|
4451
|
+
return "<div class='delimited-preview rendered-markdown'>"
|
|
4452
|
+
+ "<div class='delimited-preview-header'><div><strong>" + escapeHtml(model.label) + " preview</strong><span>" + escapeHtml(summaryParts.join(" · ")) + "</span></div></div>"
|
|
4453
|
+
+ noticeHtml
|
|
4454
|
+
+ "<div class='delimited-preview-table-wrap'><table>"
|
|
4455
|
+
+ "<thead><tr><th scope='col' class='delimited-preview-row-number'>#</th>" + headerHtml + "</tr></thead>"
|
|
4456
|
+
+ "<tbody>" + bodyHtml + "</tbody>"
|
|
4457
|
+
+ "</table></div>"
|
|
4458
|
+
+ "</div>";
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
function buildDelimitedTextPreviewMarkdown(text, language) {
|
|
4462
|
+
const model = buildDelimitedTextPreviewModel(text, language);
|
|
4463
|
+
if (!model) return "";
|
|
4464
|
+
if (!model.rows.length || model.columnCount <= 0) return "_No tabular data to preview._";
|
|
4465
|
+
const columnIndexes = Array.from({ length: model.columnCount }, (_, index) => index);
|
|
4466
|
+
const lines = ["**" + model.label + " preview**", ""];
|
|
4467
|
+
const notices = [];
|
|
4468
|
+
if (model.truncatedRows) notices.push("showing first " + String(Math.max(0, model.dataRows.length)) + " data rows");
|
|
4469
|
+
if (model.truncatedColumns) notices.push("showing first " + String(model.columnCount) + " of " + String(model.rawColumnCount) + " columns");
|
|
4470
|
+
if (notices.length) lines.push("_" + notices.join("; ") + "._", "");
|
|
4471
|
+
lines.push("| " + columnIndexes.map((index) => formatDelimitedMarkdownCell(getDelimitedHeaderLabel(model.header, index))).join(" | ") + " |");
|
|
4472
|
+
lines.push("| " + columnIndexes.map(() => "---").join(" | ") + " |");
|
|
4473
|
+
if (model.dataRows.length) {
|
|
4474
|
+
model.dataRows.forEach((row) => {
|
|
4475
|
+
lines.push("| " + columnIndexes.map((index) => formatDelimitedMarkdownCell(row && row[index])).join(" | ") + " |");
|
|
4476
|
+
});
|
|
4477
|
+
} else {
|
|
4478
|
+
lines.push("| " + columnIndexes.map(() => " ").join(" | ") + " |");
|
|
4479
|
+
}
|
|
4480
|
+
return lines.join("\n");
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
function renderDelimitedTextPreview(targetEl, text, pane) {
|
|
4484
|
+
const html = buildDelimitedTextPreviewHtml(text, editorLanguage || "");
|
|
4485
|
+
if (!html || !targetEl) return false;
|
|
4486
|
+
if (pane === "source") {
|
|
4487
|
+
sourcePreviewRenderNonce += 1;
|
|
4488
|
+
} else if (pane === "response") {
|
|
4489
|
+
responsePreviewRenderNonce += 1;
|
|
4490
|
+
}
|
|
4491
|
+
clearPreviewJumpHighlight(targetEl);
|
|
4492
|
+
finishPreviewRender(targetEl);
|
|
4493
|
+
targetEl.innerHTML = html;
|
|
4494
|
+
if (pane === "response") {
|
|
4495
|
+
applyPendingResponseScrollReset();
|
|
4496
|
+
scheduleResponsePaneRepaintNudge();
|
|
4497
|
+
}
|
|
4498
|
+
return true;
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4260
4501
|
function prepareEditorTextForPdfExport(text) {
|
|
4261
4502
|
const prepared = prepareEditorTextForPreview(text);
|
|
4262
4503
|
const lang = normalizeFenceLanguage(editorLanguage || "");
|
|
4504
|
+
const delimitedPreview = buildDelimitedTextPreviewMarkdown(prepared, lang);
|
|
4505
|
+
if (delimitedPreview) return delimitedPreview;
|
|
4263
4506
|
if (lang && lang !== "markdown" && lang !== "latex") {
|
|
4264
4507
|
return wrapAsFencedCodeBlock(prepared, lang);
|
|
4265
4508
|
}
|
|
@@ -4269,6 +4512,8 @@
|
|
|
4269
4512
|
function prepareEditorTextForHtmlExport(text) {
|
|
4270
4513
|
const prepared = prepareEditorTextForPreview(text);
|
|
4271
4514
|
const lang = normalizeFenceLanguage(editorLanguage || "");
|
|
4515
|
+
const delimitedPreview = buildDelimitedTextPreviewMarkdown(prepared, lang);
|
|
4516
|
+
if (delimitedPreview) return delimitedPreview;
|
|
4272
4517
|
if (lang && lang !== "markdown" && lang !== "latex") {
|
|
4273
4518
|
return wrapAsFencedCodeBlock(prepared, lang);
|
|
4274
4519
|
}
|
|
@@ -5096,13 +5341,12 @@
|
|
|
5096
5341
|
return;
|
|
5097
5342
|
}
|
|
5098
5343
|
if (kind === "image") {
|
|
5099
|
-
|
|
5100
|
-
void openPreviewImageLink(context.href, context.title, context, pendingWindow).catch((error) => {
|
|
5344
|
+
void openPreviewImageLink(context.href, context.title, context).catch((error) => {
|
|
5101
5345
|
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
5102
5346
|
});
|
|
5103
5347
|
return;
|
|
5104
5348
|
}
|
|
5105
|
-
if (kind === "text") {
|
|
5349
|
+
if (kind === "text" || kind === "office") {
|
|
5106
5350
|
const pendingWindow = window.open("", "_blank");
|
|
5107
5351
|
void openPreviewDocumentInNewEditor(context.href, pendingWindow, context).catch((error) => {
|
|
5108
5352
|
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
@@ -5772,6 +6016,394 @@
|
|
|
5772
6016
|
}
|
|
5773
6017
|
}
|
|
5774
6018
|
|
|
6019
|
+
function isStudioImageFocusOpen() {
|
|
6020
|
+
return Boolean(studioImageFocusOverlayEl && studioImageFocusOverlayEl.hidden === false);
|
|
6021
|
+
}
|
|
6022
|
+
|
|
6023
|
+
function isStudioImageFocusSrcAllowed(src) {
|
|
6024
|
+
const value = String(src || "").trim();
|
|
6025
|
+
if (!value) return false;
|
|
6026
|
+
if (/^javascript:/i.test(value)) return false;
|
|
6027
|
+
return /^(?:data:image\/|blob:|https?:|file:|\/|\.\/|\.\.\/)/i.test(value);
|
|
6028
|
+
}
|
|
6029
|
+
|
|
6030
|
+
function clampStudioImageFocusZoom(value) {
|
|
6031
|
+
const parsed = Number(value);
|
|
6032
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 1;
|
|
6033
|
+
return Math.max(0.1, Math.min(8, parsed));
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
function getStudioImageFocusFitScale() {
|
|
6037
|
+
const img = studioImageFocusImgEl;
|
|
6038
|
+
const slot = studioImageFocusSlotEl;
|
|
6039
|
+
if (!img || !slot) return 1;
|
|
6040
|
+
const naturalWidth = Number(img.naturalWidth) || 0;
|
|
6041
|
+
const naturalHeight = Number(img.naturalHeight) || 0;
|
|
6042
|
+
if (naturalWidth <= 0 || naturalHeight <= 0) return 1;
|
|
6043
|
+
let paddingX = 0;
|
|
6044
|
+
let paddingY = 0;
|
|
6045
|
+
try {
|
|
6046
|
+
const style = window.getComputedStyle(slot);
|
|
6047
|
+
paddingX = (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0);
|
|
6048
|
+
paddingY = (Number.parseFloat(style.paddingTop) || 0) + (Number.parseFloat(style.paddingBottom) || 0);
|
|
6049
|
+
} catch {}
|
|
6050
|
+
const availableWidth = Math.max(1, (slot.clientWidth || 0) - paddingX);
|
|
6051
|
+
const availableHeight = Math.max(1, (slot.clientHeight || 0) - paddingY);
|
|
6052
|
+
return clampStudioImageFocusZoom(Math.min(1, availableWidth / naturalWidth, availableHeight / naturalHeight));
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
function getStudioImageFocusDisplayScale() {
|
|
6056
|
+
return studioImageFocusZoomMode === "fit"
|
|
6057
|
+
? getStudioImageFocusFitScale()
|
|
6058
|
+
: clampStudioImageFocusZoom(studioImageFocusZoom);
|
|
6059
|
+
}
|
|
6060
|
+
|
|
6061
|
+
function syncStudioImageFocusZoom() {
|
|
6062
|
+
if (!studioImageFocusImgEl || !studioImageFocusSlotEl) return;
|
|
6063
|
+
const fitMode = studioImageFocusZoomMode === "fit";
|
|
6064
|
+
studioImageFocusSlotEl.classList.toggle("is-fit", fitMode);
|
|
6065
|
+
studioImageFocusSlotEl.classList.toggle("is-zoomed", !fitMode);
|
|
6066
|
+
if (fitMode) {
|
|
6067
|
+
studioImageFocusImgEl.style.width = "";
|
|
6068
|
+
studioImageFocusImgEl.style.height = "";
|
|
6069
|
+
studioImageFocusImgEl.style.maxWidth = "100%";
|
|
6070
|
+
studioImageFocusImgEl.style.maxHeight = "100%";
|
|
6071
|
+
} else {
|
|
6072
|
+
const zoom = clampStudioImageFocusZoom(studioImageFocusZoom);
|
|
6073
|
+
const naturalWidth = Number(studioImageFocusImgEl.naturalWidth) || 0;
|
|
6074
|
+
studioImageFocusImgEl.style.maxWidth = "none";
|
|
6075
|
+
studioImageFocusImgEl.style.maxHeight = "none";
|
|
6076
|
+
studioImageFocusImgEl.style.height = "auto";
|
|
6077
|
+
studioImageFocusImgEl.style.width = naturalWidth > 0 ? Math.max(1, Math.round(naturalWidth * zoom)) + "px" : Math.round(zoom * 100) + "%";
|
|
6078
|
+
}
|
|
6079
|
+
if (studioImageFocusZoomLabelEl) {
|
|
6080
|
+
studioImageFocusZoomLabelEl.textContent = Math.round(getStudioImageFocusDisplayScale() * 100) + "%";
|
|
6081
|
+
}
|
|
6082
|
+
}
|
|
6083
|
+
|
|
6084
|
+
function getStudioImageFocusViewportCenter() {
|
|
6085
|
+
const slot = studioImageFocusSlotEl;
|
|
6086
|
+
if (!slot) return { x: 0.5, y: 0.5 };
|
|
6087
|
+
const scrollWidth = Math.max(slot.scrollWidth || 0, slot.clientWidth || 0, 1);
|
|
6088
|
+
const scrollHeight = Math.max(slot.scrollHeight || 0, slot.clientHeight || 0, 1);
|
|
6089
|
+
return {
|
|
6090
|
+
x: Math.max(0, Math.min(1, (slot.scrollLeft + (slot.clientWidth || 0) / 2) / scrollWidth)),
|
|
6091
|
+
y: Math.max(0, Math.min(1, (slot.scrollTop + (slot.clientHeight || 0) / 2) / scrollHeight)),
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
function restoreStudioImageFocusViewportCenter(center) {
|
|
6096
|
+
const slot = studioImageFocusSlotEl;
|
|
6097
|
+
if (!slot || !center) return;
|
|
6098
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
6099
|
+
? window.requestAnimationFrame.bind(window)
|
|
6100
|
+
: (callback) => window.setTimeout(callback, 0);
|
|
6101
|
+
schedule(() => {
|
|
6102
|
+
if (!slot.isConnected || studioImageFocusZoomMode === "fit") return;
|
|
6103
|
+
const maxLeft = Math.max(0, (slot.scrollWidth || 0) - (slot.clientWidth || 0));
|
|
6104
|
+
const maxTop = Math.max(0, (slot.scrollHeight || 0) - (slot.clientHeight || 0));
|
|
6105
|
+
slot.scrollLeft = Math.max(0, Math.min(maxLeft, (slot.scrollWidth || 0) * center.x - (slot.clientWidth || 0) / 2));
|
|
6106
|
+
slot.scrollTop = Math.max(0, Math.min(maxTop, (slot.scrollHeight || 0) * center.y - (slot.clientHeight || 0) / 2));
|
|
6107
|
+
});
|
|
6108
|
+
}
|
|
6109
|
+
|
|
6110
|
+
function getStudioImageFocusPointerCenter(event) {
|
|
6111
|
+
const slot = studioImageFocusSlotEl;
|
|
6112
|
+
if (!slot || !event || typeof slot.getBoundingClientRect !== "function") return getStudioImageFocusViewportCenter();
|
|
6113
|
+
const rect = slot.getBoundingClientRect();
|
|
6114
|
+
const scrollWidth = Math.max(slot.scrollWidth || 0, slot.clientWidth || 0, 1);
|
|
6115
|
+
const scrollHeight = Math.max(slot.scrollHeight || 0, slot.clientHeight || 0, 1);
|
|
6116
|
+
return {
|
|
6117
|
+
x: Math.max(0, Math.min(1, (slot.scrollLeft + (Number(event.clientX) || rect.left + rect.width / 2) - rect.left) / scrollWidth)),
|
|
6118
|
+
y: Math.max(0, Math.min(1, (slot.scrollTop + (Number(event.clientY) || rect.top + rect.height / 2) - rect.top) / scrollHeight)),
|
|
6119
|
+
};
|
|
6120
|
+
}
|
|
6121
|
+
|
|
6122
|
+
function setStudioImageFocusZoom(mode, zoom, options) {
|
|
6123
|
+
const center = options && options.center ? options.center : getStudioImageFocusViewportCenter();
|
|
6124
|
+
studioImageFocusZoomMode = mode === "fit" ? "fit" : "custom";
|
|
6125
|
+
studioImageFocusZoom = clampStudioImageFocusZoom(zoom);
|
|
6126
|
+
syncStudioImageFocusZoom();
|
|
6127
|
+
if (studioImageFocusZoomMode !== "fit") restoreStudioImageFocusViewportCenter(center);
|
|
6128
|
+
}
|
|
6129
|
+
|
|
6130
|
+
function zoomStudioImageFocus(factor, options) {
|
|
6131
|
+
const base = studioImageFocusZoomMode === "fit" ? getStudioImageFocusFitScale() : studioImageFocusZoom;
|
|
6132
|
+
setStudioImageFocusZoom("custom", clampStudioImageFocusZoom(base * factor), options);
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
function handleStudioImageFocusWheel(event) {
|
|
6136
|
+
if (!isStudioImageFocusOpen() || !event) return;
|
|
6137
|
+
if (!event.altKey && !event.ctrlKey && !event.metaKey) return;
|
|
6138
|
+
event.preventDefault();
|
|
6139
|
+
event.stopPropagation();
|
|
6140
|
+
const delta = Number(event.deltaY) || 0;
|
|
6141
|
+
const factor = delta < 0 ? 1.12 : 1 / 1.12;
|
|
6142
|
+
zoomStudioImageFocus(factor, { center: getStudioImageFocusPointerCenter(event) });
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6145
|
+
function handleStudioImageFocusShortcut(event) {
|
|
6146
|
+
if (!isStudioImageFocusOpen() || !event) return false;
|
|
6147
|
+
if (isTextEntryShortcutTarget(event.target)) return false;
|
|
6148
|
+
const key = typeof event.key === "string" ? event.key : "";
|
|
6149
|
+
const code = typeof event.code === "string" ? event.code : "";
|
|
6150
|
+
if (!event.altKey || event.metaKey || event.ctrlKey) return false;
|
|
6151
|
+
if (code === "Equal" || code === "NumpadAdd" || key === "=" || key === "+") {
|
|
6152
|
+
event.preventDefault();
|
|
6153
|
+
zoomStudioImageFocus(1.25);
|
|
6154
|
+
return true;
|
|
6155
|
+
}
|
|
6156
|
+
if (code === "Minus" || code === "NumpadSubtract" || key === "-" || key === "_") {
|
|
6157
|
+
event.preventDefault();
|
|
6158
|
+
zoomStudioImageFocus(1 / 1.25);
|
|
6159
|
+
return true;
|
|
6160
|
+
}
|
|
6161
|
+
if (code === "Digit0" || code === "Numpad0" || key === "0") {
|
|
6162
|
+
event.preventDefault();
|
|
6163
|
+
setStudioImageFocusZoom("fit", 1);
|
|
6164
|
+
return true;
|
|
6165
|
+
}
|
|
6166
|
+
return false;
|
|
6167
|
+
}
|
|
6168
|
+
|
|
6169
|
+
function syncStudioImageFocusFullscreenButton() {
|
|
6170
|
+
if (!studioImageFocusFullscreenBtn) return;
|
|
6171
|
+
const isFullscreen = Boolean(document.fullscreenElement && studioImageFocusDialogEl && document.fullscreenElement === studioImageFocusDialogEl);
|
|
6172
|
+
studioImageFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
|
|
6173
|
+
const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
|
|
6174
|
+
studioImageFocusFullscreenBtn.title = isFullscreen
|
|
6175
|
+
? "Exit browser fullscreen and keep the image focus viewer open."
|
|
6176
|
+
: "Ask the browser to make this image viewer fullscreen.";
|
|
6177
|
+
studioImageFocusFullscreenBtn.setAttribute("aria-label", label);
|
|
6178
|
+
studioImageFocusFullscreenBtn.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
async function toggleStudioImageFocusFullscreen() {
|
|
6182
|
+
const dialog = studioImageFocusDialogEl;
|
|
6183
|
+
if (!dialog) return;
|
|
6184
|
+
const isFullscreen = Boolean(document.fullscreenElement && document.fullscreenElement === dialog);
|
|
6185
|
+
if (isFullscreen) {
|
|
6186
|
+
try {
|
|
6187
|
+
if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
|
|
6188
|
+
} catch (error) {
|
|
6189
|
+
setStatus("Could not exit image fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
6190
|
+
} finally {
|
|
6191
|
+
syncStudioImageFocusFullscreenButton();
|
|
6192
|
+
}
|
|
6193
|
+
return;
|
|
6194
|
+
}
|
|
6195
|
+
if (typeof dialog.requestFullscreen !== "function") {
|
|
6196
|
+
setStatus("Browser fullscreen is not available for this image viewer.", "warning");
|
|
6197
|
+
return;
|
|
6198
|
+
}
|
|
6199
|
+
try {
|
|
6200
|
+
await dialog.requestFullscreen();
|
|
6201
|
+
} catch (error) {
|
|
6202
|
+
setStatus("Could not enter image fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
6203
|
+
} finally {
|
|
6204
|
+
syncStudioImageFocusFullscreenButton();
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
function appendStudioImageFocusTextButton(parent, label, title, onClick) {
|
|
6209
|
+
const button = document.createElement("button");
|
|
6210
|
+
button.type = "button";
|
|
6211
|
+
button.className = "studio-pdf-focus-btn studio-image-focus-zoom-btn";
|
|
6212
|
+
button.textContent = label;
|
|
6213
|
+
button.title = title;
|
|
6214
|
+
button.addEventListener("click", onClick);
|
|
6215
|
+
parent.appendChild(button);
|
|
6216
|
+
return button;
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
function ensureStudioImageFocusViewer() {
|
|
6220
|
+
if (studioImageFocusOverlayEl) return studioImageFocusOverlayEl;
|
|
6221
|
+
|
|
6222
|
+
const overlay = document.createElement("div");
|
|
6223
|
+
overlay.className = "studio-pdf-focus-overlay studio-image-focus-overlay";
|
|
6224
|
+
overlay.hidden = true;
|
|
6225
|
+
overlay.setAttribute("role", "dialog");
|
|
6226
|
+
overlay.setAttribute("aria-modal", "true");
|
|
6227
|
+
overlay.setAttribute("aria-labelledby", "studioImageFocusTitle");
|
|
6228
|
+
|
|
6229
|
+
const dialog = document.createElement("div");
|
|
6230
|
+
dialog.className = "studio-pdf-focus-dialog studio-image-focus-dialog";
|
|
6231
|
+
|
|
6232
|
+
const header = document.createElement("div");
|
|
6233
|
+
header.className = "studio-pdf-focus-header studio-image-focus-header";
|
|
6234
|
+
|
|
6235
|
+
const titleGroup = document.createElement("div");
|
|
6236
|
+
titleGroup.className = "studio-pdf-focus-title-group";
|
|
6237
|
+
|
|
6238
|
+
const closeBtn = document.createElement("button");
|
|
6239
|
+
closeBtn.type = "button";
|
|
6240
|
+
closeBtn.className = "studio-pdf-focus-btn studio-pdf-focus-close";
|
|
6241
|
+
closeBtn.title = "Exit image focus view.";
|
|
6242
|
+
closeBtn.setAttribute("aria-label", "Exit image focus view");
|
|
6243
|
+
closeBtn.appendChild(makeStudioUiRefreshIcon("focus-exit"));
|
|
6244
|
+
closeBtn.addEventListener("click", () => closeStudioImageFocusViewer());
|
|
6245
|
+
titleGroup.appendChild(closeBtn);
|
|
6246
|
+
|
|
6247
|
+
const titleEl = document.createElement("div");
|
|
6248
|
+
titleEl.id = "studioImageFocusTitle";
|
|
6249
|
+
titleEl.className = "studio-pdf-focus-title";
|
|
6250
|
+
titleEl.textContent = "Image preview";
|
|
6251
|
+
titleGroup.appendChild(titleEl);
|
|
6252
|
+
header.appendChild(titleGroup);
|
|
6253
|
+
|
|
6254
|
+
const actions = document.createElement("div");
|
|
6255
|
+
actions.className = "studio-pdf-focus-actions studio-image-focus-actions";
|
|
6256
|
+
|
|
6257
|
+
const openLink = document.createElement("a");
|
|
6258
|
+
openLink.className = "studio-pdf-focus-link";
|
|
6259
|
+
openLink.target = "_blank";
|
|
6260
|
+
openLink.rel = "noopener noreferrer";
|
|
6261
|
+
openLink.textContent = "Open image";
|
|
6262
|
+
actions.appendChild(openLink);
|
|
6263
|
+
|
|
6264
|
+
appendStudioImageFocusTextButton(actions, "Fit", "Fit the image to the viewer.", () => setStudioImageFocusZoom("fit", 1));
|
|
6265
|
+
appendStudioImageFocusTextButton(actions, "100%", "Show the image at its natural pixel size.", () => setStudioImageFocusZoom("custom", 1));
|
|
6266
|
+
appendStudioImageFocusTextButton(actions, "−", "Zoom out.", () => zoomStudioImageFocus(1 / 1.25));
|
|
6267
|
+
const zoomLabel = document.createElement("span");
|
|
6268
|
+
zoomLabel.className = "studio-image-focus-zoom-label";
|
|
6269
|
+
zoomLabel.textContent = "100%";
|
|
6270
|
+
actions.appendChild(zoomLabel);
|
|
6271
|
+
appendStudioImageFocusTextButton(actions, "+", "Zoom in.", () => zoomStudioImageFocus(1.25));
|
|
6272
|
+
appendStudioImageFocusTextButton(actions, "Reset", "Reset image zoom to fit.", () => setStudioImageFocusZoom("fit", 1));
|
|
6273
|
+
|
|
6274
|
+
const fullscreenBtn = document.createElement("button");
|
|
6275
|
+
fullscreenBtn.type = "button";
|
|
6276
|
+
fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
|
|
6277
|
+
fullscreenBtn.addEventListener("click", () => {
|
|
6278
|
+
void toggleStudioImageFocusFullscreen();
|
|
6279
|
+
});
|
|
6280
|
+
actions.appendChild(fullscreenBtn);
|
|
6281
|
+
|
|
6282
|
+
header.appendChild(actions);
|
|
6283
|
+
dialog.appendChild(header);
|
|
6284
|
+
|
|
6285
|
+
const slot = document.createElement("div");
|
|
6286
|
+
slot.className = "studio-image-focus-slot is-fit";
|
|
6287
|
+
const img = document.createElement("img");
|
|
6288
|
+
img.className = "studio-image-focus-img";
|
|
6289
|
+
img.alt = "Image preview";
|
|
6290
|
+
img.addEventListener("load", syncStudioImageFocusZoom);
|
|
6291
|
+
slot.addEventListener("wheel", handleStudioImageFocusWheel, { passive: false });
|
|
6292
|
+
slot.appendChild(img);
|
|
6293
|
+
dialog.appendChild(slot);
|
|
6294
|
+
|
|
6295
|
+
overlay.appendChild(dialog);
|
|
6296
|
+
overlay.addEventListener("click", (event) => {
|
|
6297
|
+
if (event.target === overlay) closeStudioImageFocusViewer();
|
|
6298
|
+
});
|
|
6299
|
+
document.addEventListener("fullscreenchange", syncStudioImageFocusFullscreenButton);
|
|
6300
|
+
|
|
6301
|
+
document.body.appendChild(overlay);
|
|
6302
|
+
studioImageFocusOverlayEl = overlay;
|
|
6303
|
+
studioImageFocusDialogEl = dialog;
|
|
6304
|
+
studioImageFocusSlotEl = slot;
|
|
6305
|
+
studioImageFocusImgEl = img;
|
|
6306
|
+
studioImageFocusTitleEl = titleEl;
|
|
6307
|
+
studioImageFocusOpenLinkEl = openLink;
|
|
6308
|
+
studioImageFocusFullscreenBtn = fullscreenBtn;
|
|
6309
|
+
studioImageFocusCloseBtn = closeBtn;
|
|
6310
|
+
studioImageFocusZoomLabelEl = zoomLabel;
|
|
6311
|
+
syncStudioImageFocusFullscreenButton();
|
|
6312
|
+
return overlay;
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6315
|
+
function openStudioImageFocusViewer(src, title) {
|
|
6316
|
+
const imageSrc = String(src || "").trim();
|
|
6317
|
+
if (!isStudioImageFocusSrcAllowed(imageSrc)) return false;
|
|
6318
|
+
ensureStudioImageFocusViewer();
|
|
6319
|
+
studioImageFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
6320
|
+
const label = String(title || "Image preview").trim() || "Image preview";
|
|
6321
|
+
if (studioImageFocusTitleEl) studioImageFocusTitleEl.textContent = label;
|
|
6322
|
+
if (studioImageFocusOpenLinkEl) studioImageFocusOpenLinkEl.href = imageSrc;
|
|
6323
|
+
if (studioImageFocusImgEl) {
|
|
6324
|
+
studioImageFocusImgEl.alt = label;
|
|
6325
|
+
studioImageFocusImgEl.src = imageSrc;
|
|
6326
|
+
}
|
|
6327
|
+
studioImageFocusZoomMode = "fit";
|
|
6328
|
+
studioImageFocusZoom = 1;
|
|
6329
|
+
syncStudioImageFocusZoom();
|
|
6330
|
+
if (document.body) document.body.classList.add("studio-image-focus-open");
|
|
6331
|
+
if (studioImageFocusOverlayEl) studioImageFocusOverlayEl.hidden = false;
|
|
6332
|
+
syncStudioImageFocusFullscreenButton();
|
|
6333
|
+
closeStudioUiRefreshMenus();
|
|
6334
|
+
closeExportPreviewMenu();
|
|
6335
|
+
closePreviewLinkMenu();
|
|
6336
|
+
window.setTimeout(() => {
|
|
6337
|
+
if (studioImageFocusCloseBtn && typeof studioImageFocusCloseBtn.focus === "function") {
|
|
6338
|
+
studioImageFocusCloseBtn.focus();
|
|
6339
|
+
}
|
|
6340
|
+
}, 0);
|
|
6341
|
+
return true;
|
|
6342
|
+
}
|
|
6343
|
+
|
|
6344
|
+
function closeStudioImageFocusViewer() {
|
|
6345
|
+
if (!isStudioImageFocusOpen()) return false;
|
|
6346
|
+
if (document.fullscreenElement && studioImageFocusDialogEl && studioImageFocusDialogEl.contains(document.fullscreenElement)) {
|
|
6347
|
+
try {
|
|
6348
|
+
const exitResult = document.exitFullscreen && document.exitFullscreen();
|
|
6349
|
+
if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
|
|
6350
|
+
} catch {}
|
|
6351
|
+
}
|
|
6352
|
+
if (studioImageFocusOverlayEl) studioImageFocusOverlayEl.hidden = true;
|
|
6353
|
+
if (studioImageFocusImgEl) studioImageFocusImgEl.removeAttribute("src");
|
|
6354
|
+
if (studioImageFocusOpenLinkEl) studioImageFocusOpenLinkEl.removeAttribute("href");
|
|
6355
|
+
if (document.body) document.body.classList.remove("studio-image-focus-open");
|
|
6356
|
+
syncStudioImageFocusFullscreenButton();
|
|
6357
|
+
const focusTarget = studioImageFocusLastFocusedEl;
|
|
6358
|
+
studioImageFocusLastFocusedEl = null;
|
|
6359
|
+
if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
|
|
6360
|
+
window.setTimeout(() => focusTarget.focus(), 0);
|
|
6361
|
+
}
|
|
6362
|
+
return true;
|
|
6363
|
+
}
|
|
6364
|
+
|
|
6365
|
+
function getPreviewImageElementTitle(imageEl) {
|
|
6366
|
+
if (!imageEl) return "Image preview";
|
|
6367
|
+
const alt = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("alt") || "").trim() : "";
|
|
6368
|
+
const title = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("title") || "").trim() : "";
|
|
6369
|
+
const src = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("src") || "").trim() : "";
|
|
6370
|
+
const srcLabel = /^data:image\//i.test(src) ? "" : (src.length > 120 ? src.slice(0, 117) + "…" : src);
|
|
6371
|
+
return alt || title || srcLabel || "Image preview";
|
|
6372
|
+
}
|
|
6373
|
+
|
|
6374
|
+
function openPreviewImageElementInFocus(imageEl) {
|
|
6375
|
+
if (!imageEl) return false;
|
|
6376
|
+
const src = String(imageEl.currentSrc || imageEl.src || imageEl.getAttribute("src") || "").trim();
|
|
6377
|
+
if (!src) return false;
|
|
6378
|
+
return openStudioImageFocusViewer(src, getPreviewImageElementTitle(imageEl));
|
|
6379
|
+
}
|
|
6380
|
+
|
|
6381
|
+
function decoratePreviewImages(targetEl) {
|
|
6382
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
6383
|
+
const images = Array.from(targetEl.querySelectorAll("img[src]"));
|
|
6384
|
+
images.forEach((imageEl) => {
|
|
6385
|
+
if (!(imageEl instanceof HTMLImageElement)) return;
|
|
6386
|
+
if (imageEl.dataset && imageEl.dataset.studioImageFocusDecorated === "1") return;
|
|
6387
|
+
if (imageEl.closest && imageEl.closest("a[href], button, .studio-html-artifact-shell, .studio-pdf-card")) return;
|
|
6388
|
+
if (!isStudioImageFocusSrcAllowed(imageEl.currentSrc || imageEl.src || imageEl.getAttribute("src") || "")) return;
|
|
6389
|
+
imageEl.classList.add("studio-image-focus-target");
|
|
6390
|
+
imageEl.tabIndex = imageEl.tabIndex >= 0 ? imageEl.tabIndex : 0;
|
|
6391
|
+
imageEl.setAttribute("role", "button");
|
|
6392
|
+
imageEl.setAttribute("aria-label", "Open image focus viewer");
|
|
6393
|
+
if (imageEl.dataset) imageEl.dataset.studioImageFocusDecorated = "1";
|
|
6394
|
+
imageEl.addEventListener("click", (event) => {
|
|
6395
|
+
event.preventDefault();
|
|
6396
|
+
event.stopPropagation();
|
|
6397
|
+
if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
|
|
6398
|
+
});
|
|
6399
|
+
imageEl.addEventListener("keydown", (event) => {
|
|
6400
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
6401
|
+
event.preventDefault();
|
|
6402
|
+
if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
|
|
6403
|
+
});
|
|
6404
|
+
});
|
|
6405
|
+
}
|
|
6406
|
+
|
|
5775
6407
|
function createStudioPdfCard(block, useEditorResourceContext) {
|
|
5776
6408
|
const options = block && block.options ? block.options : {};
|
|
5777
6409
|
const path = String(options.path || "").trim();
|
|
@@ -7003,15 +7635,16 @@
|
|
|
7003
7635
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
7004
7636
|
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
7005
7637
|
const isEditorPreview = rightView === "editor-preview";
|
|
7006
|
-
const
|
|
7638
|
+
const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
|
|
7639
|
+
const editorPdfLanguage = isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "";
|
|
7007
7640
|
const isLatex = isEditorPreview
|
|
7008
7641
|
? editorPdfLanguage === "latex"
|
|
7009
7642
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
7010
|
-
let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-
|
|
7643
|
+
let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.pdf"));
|
|
7011
7644
|
if (sourcePath) {
|
|
7012
7645
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
7013
7646
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
7014
|
-
filenameHint = stem + "
|
|
7647
|
+
filenameHint = stem + ".studio.pdf";
|
|
7015
7648
|
}
|
|
7016
7649
|
|
|
7017
7650
|
previewExportInProgress = true;
|
|
@@ -7059,6 +7692,8 @@
|
|
|
7059
7692
|
|
|
7060
7693
|
const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
|
|
7061
7694
|
const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
|
|
7695
|
+
const writeError = typeof payload.writeError === "string" ? payload.writeError.trim() : "";
|
|
7696
|
+
const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
7062
7697
|
const openedExternal = payload.openedExternal === true;
|
|
7063
7698
|
let downloadName = typeof payload.filename === "string" && payload.filename.trim()
|
|
7064
7699
|
? payload.filename.trim()
|
|
@@ -7068,10 +7703,12 @@
|
|
|
7068
7703
|
}
|
|
7069
7704
|
|
|
7070
7705
|
if (openedExternal) {
|
|
7071
|
-
if (
|
|
7706
|
+
if (writeError) {
|
|
7707
|
+
setStatus("Opened PDF in default viewer, but could not write project file: " + writeError, "warning");
|
|
7708
|
+
} else if (exportWarning) {
|
|
7072
7709
|
setStatus("Opened PDF in default viewer with warning: " + exportWarning, "warning");
|
|
7073
7710
|
} else {
|
|
7074
|
-
setStatus("Opened PDF in default viewer: " + downloadName, "success");
|
|
7711
|
+
setStatus("Opened PDF in default viewer: " + (exportPath || downloadName), "success");
|
|
7075
7712
|
}
|
|
7076
7713
|
return;
|
|
7077
7714
|
}
|
|
@@ -7090,10 +7727,12 @@
|
|
|
7090
7727
|
} else {
|
|
7091
7728
|
setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
|
|
7092
7729
|
}
|
|
7730
|
+
} else if (writeError) {
|
|
7731
|
+
setStatus("Exported PDF to browser fallback; could not write project file: " + writeError, "warning");
|
|
7093
7732
|
} else if (exportWarning) {
|
|
7094
|
-
setStatus("Exported PDF with warning: " + exportWarning, "warning");
|
|
7733
|
+
setStatus("Exported PDF with warning" + (exportPath ? " to " + exportPath : ": " + exportWarning), "warning");
|
|
7095
7734
|
} else {
|
|
7096
|
-
setStatus("Exported PDF: " + downloadName, "success");
|
|
7735
|
+
setStatus("Exported PDF: " + (exportPath || downloadName), "success");
|
|
7097
7736
|
}
|
|
7098
7737
|
return;
|
|
7099
7738
|
}
|
|
@@ -7169,16 +7808,17 @@
|
|
|
7169
7808
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
7170
7809
|
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
7171
7810
|
const isEditorPreview = rightView === "editor-preview";
|
|
7172
|
-
const
|
|
7811
|
+
const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
|
|
7812
|
+
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "");
|
|
7173
7813
|
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
7174
7814
|
? editorHtmlLanguage === "latex"
|
|
7175
7815
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown));
|
|
7176
|
-
let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-
|
|
7816
|
+
let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html"));
|
|
7177
7817
|
let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
|
|
7178
7818
|
if (sourcePath) {
|
|
7179
7819
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
7180
7820
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
7181
|
-
filenameHint = stem + "
|
|
7821
|
+
filenameHint = stem + ".studio.html";
|
|
7182
7822
|
titleHint = stem + " preview";
|
|
7183
7823
|
}
|
|
7184
7824
|
|
|
@@ -7228,6 +7868,8 @@
|
|
|
7228
7868
|
|
|
7229
7869
|
const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
|
|
7230
7870
|
const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
|
|
7871
|
+
const writeError = typeof payload.writeError === "string" ? payload.writeError.trim() : "";
|
|
7872
|
+
const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
7231
7873
|
const openedExternal = payload.openedExternal === true;
|
|
7232
7874
|
let downloadName = typeof payload.filename === "string" && payload.filename.trim()
|
|
7233
7875
|
? payload.filename.trim()
|
|
@@ -7237,10 +7879,12 @@
|
|
|
7237
7879
|
}
|
|
7238
7880
|
|
|
7239
7881
|
if (openedExternal) {
|
|
7240
|
-
if (
|
|
7882
|
+
if (writeError) {
|
|
7883
|
+
setStatus("Opened HTML in default browser, but could not write project file: " + writeError, "warning");
|
|
7884
|
+
} else if (exportWarning) {
|
|
7241
7885
|
setStatus("Opened HTML in default browser with warning: " + exportWarning, "warning");
|
|
7242
7886
|
} else {
|
|
7243
|
-
setStatus("Opened HTML in default browser: " + downloadName, "success");
|
|
7887
|
+
setStatus("Opened HTML in default browser: " + (exportPath || downloadName), "success");
|
|
7244
7888
|
}
|
|
7245
7889
|
return;
|
|
7246
7890
|
}
|
|
@@ -7259,10 +7903,12 @@
|
|
|
7259
7903
|
} else {
|
|
7260
7904
|
setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
|
|
7261
7905
|
}
|
|
7906
|
+
} else if (writeError) {
|
|
7907
|
+
setStatus("Exported HTML to browser fallback; could not write project file: " + writeError, "warning");
|
|
7262
7908
|
} else if (exportWarning) {
|
|
7263
|
-
setStatus("Exported HTML with warning: " + exportWarning, "warning");
|
|
7909
|
+
setStatus("Exported HTML with warning" + (exportPath ? " to " + exportPath : ": " + exportWarning), "warning");
|
|
7264
7910
|
} else {
|
|
7265
|
-
setStatus("Exported HTML: " + downloadName, "success");
|
|
7911
|
+
setStatus("Exported HTML: " + (exportPath || downloadName), "success");
|
|
7266
7912
|
}
|
|
7267
7913
|
return;
|
|
7268
7914
|
}
|
|
@@ -7554,6 +8200,7 @@
|
|
|
7554
8200
|
decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
|
|
7555
8201
|
}
|
|
7556
8202
|
decorateCopyablePreviewBlocks(targetEl);
|
|
8203
|
+
decoratePreviewImages(targetEl);
|
|
7557
8204
|
|
|
7558
8205
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
7559
8206
|
if (!sourceState.path && !getCurrentResourceDirValue()) {
|
|
@@ -7593,6 +8240,9 @@
|
|
|
7593
8240
|
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
7594
8241
|
return;
|
|
7595
8242
|
}
|
|
8243
|
+
if (renderDelimitedTextPreview(sourcePreviewEl, text, "source")) {
|
|
8244
|
+
return;
|
|
8245
|
+
}
|
|
7596
8246
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
7597
8247
|
renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
|
|
7598
8248
|
return;
|
|
@@ -8141,6 +8791,7 @@
|
|
|
8141
8791
|
const previousScrollTop = critiqueViewEl.scrollTop;
|
|
8142
8792
|
finishPreviewRender(critiqueViewEl);
|
|
8143
8793
|
critiqueViewEl.innerHTML = buildTracePanelHtml();
|
|
8794
|
+
decoratePreviewImages(critiqueViewEl);
|
|
8144
8795
|
critiqueViewEl.classList.remove("response-scroll-resetting");
|
|
8145
8796
|
if (shouldStick) {
|
|
8146
8797
|
critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
|
|
@@ -8197,6 +8848,7 @@
|
|
|
8197
8848
|
function getFileBrowserKindLabel(entry) {
|
|
8198
8849
|
if (!entry || entry.type === "directory") return "folder";
|
|
8199
8850
|
if (entry.kind === "text") return "document";
|
|
8851
|
+
if (entry.kind === "office") return "document";
|
|
8200
8852
|
if (entry.kind === "pdf") return "PDF";
|
|
8201
8853
|
if (entry.kind === "image") return "image";
|
|
8202
8854
|
return entry.extension ? entry.extension.replace(/^\./, "") : "file";
|
|
@@ -8213,18 +8865,21 @@
|
|
|
8213
8865
|
? entries.map((entry) => {
|
|
8214
8866
|
const type = entry.type === "directory" ? "directory" : "file";
|
|
8215
8867
|
const kind = entry.kind || (type === "directory" ? "directory" : "other");
|
|
8216
|
-
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" ? "📝" : "📦")));
|
|
8868
|
+
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
|
|
8217
8869
|
const metaParts = [];
|
|
8218
8870
|
metaParts.push(getFileBrowserKindLabel(entry));
|
|
8219
8871
|
if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
|
|
8220
8872
|
const time = formatFileBrowserTime(entry.mtimeMs);
|
|
8221
8873
|
if (time) metaParts.push(time);
|
|
8222
|
-
const
|
|
8223
|
-
? "
|
|
8874
|
+
const newTabAction = kind === "text" || kind === "office"
|
|
8875
|
+
? "open-new"
|
|
8876
|
+
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
8877
|
+
const textActions = newTabAction
|
|
8878
|
+
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
|
|
8224
8879
|
: "";
|
|
8225
8880
|
const openTitle = type === "directory"
|
|
8226
8881
|
? "Open folder"
|
|
8227
|
-
: (kind === "text" ? "Open in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file")));
|
|
8882
|
+
: (kind === "text" ? "Open in editor" : (kind === "office" ? "Convert to Markdown in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
8228
8883
|
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
8229
8884
|
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(entry.path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
8230
8885
|
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
@@ -8249,6 +8904,8 @@
|
|
|
8249
8904
|
+ "<div class='files-toolbar-actions'>"
|
|
8250
8905
|
+ "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
|
|
8251
8906
|
+ "<button type='button' data-files-action='refresh'>Refresh</button>"
|
|
8907
|
+
+ (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
|
|
8908
|
+
+ (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
|
|
8252
8909
|
+ (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
|
|
8253
8910
|
+ "</div>"
|
|
8254
8911
|
+ "</div>"
|
|
@@ -8343,7 +9000,7 @@
|
|
|
8343
9000
|
|
|
8344
9001
|
async function openFileBrowserEntry(path, kind) {
|
|
8345
9002
|
const context = getFileBrowserLocalLinkContext();
|
|
8346
|
-
if (kind === "text") {
|
|
9003
|
+
if (kind === "text" || kind === "office") {
|
|
8347
9004
|
await openPreviewDocumentHere(path, context);
|
|
8348
9005
|
return;
|
|
8349
9006
|
}
|
|
@@ -8358,6 +9015,19 @@
|
|
|
8358
9015
|
setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
|
|
8359
9016
|
}
|
|
8360
9017
|
|
|
9018
|
+
function setFileBrowserCurrentDirectoryAsWorkingDir(path) {
|
|
9019
|
+
const nextDir = normalizeStudioResourceDirValue(path || fileBrowserState.currentDir || "");
|
|
9020
|
+
if (!nextDir) {
|
|
9021
|
+
setStatus("No current folder to use as working directory.", "warning");
|
|
9022
|
+
return;
|
|
9023
|
+
}
|
|
9024
|
+
if (resourceDirInput) resourceDirInput.value = nextDir;
|
|
9025
|
+
applyResourceDir();
|
|
9026
|
+
fileBrowserState = { ...fileBrowserState, contextKey: "" };
|
|
9027
|
+
if (rightView === "files") renderFilesView();
|
|
9028
|
+
setStatus("Working dir set to current folder.", "success");
|
|
9029
|
+
}
|
|
9030
|
+
|
|
8361
9031
|
async function handleFilesPaneClick(event) {
|
|
8362
9032
|
if (rightView !== "files") return;
|
|
8363
9033
|
const target = event.target;
|
|
@@ -8388,11 +9058,19 @@
|
|
|
8388
9058
|
await openPreviewDocumentInNewEditor(path, null, getFileBrowserLocalLinkContext());
|
|
8389
9059
|
return;
|
|
8390
9060
|
}
|
|
8391
|
-
if (action === "
|
|
9061
|
+
if (action === "open-preview-new") {
|
|
9062
|
+
await openPreviewResourceInNewEditor(path, null, getFileBrowserLocalLinkContext());
|
|
9063
|
+
return;
|
|
9064
|
+
}
|
|
9065
|
+
if (action === "copy-path" || action === "copy-root" || action === "copy-current") {
|
|
8392
9066
|
const ok = await writeTextToClipboard(path);
|
|
8393
9067
|
setStatus(ok ? "Copied path." : "Clipboard write failed.", ok ? "success" : "warning");
|
|
8394
9068
|
return;
|
|
8395
9069
|
}
|
|
9070
|
+
if (action === "use-working-dir") {
|
|
9071
|
+
setFileBrowserCurrentDirectoryAsWorkingDir(path);
|
|
9072
|
+
return;
|
|
9073
|
+
}
|
|
8396
9074
|
if (action === "reveal") {
|
|
8397
9075
|
await revealPreviewLocalLink(path, getFileBrowserLocalLinkContext());
|
|
8398
9076
|
}
|
|
@@ -8429,6 +9107,9 @@
|
|
|
8429
9107
|
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
8430
9108
|
return;
|
|
8431
9109
|
}
|
|
9110
|
+
if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response")) {
|
|
9111
|
+
return;
|
|
9112
|
+
}
|
|
8432
9113
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
8433
9114
|
renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
|
|
8434
9115
|
return;
|
|
@@ -8888,7 +9569,10 @@
|
|
|
8888
9569
|
resourceDirInput.value = nextResourceDir;
|
|
8889
9570
|
updateSourceBadge();
|
|
8890
9571
|
}
|
|
8891
|
-
|
|
9572
|
+
const detectedPersistedPathLanguage = detectLanguageFromName(nextSourceState.path || nextSourceState.label || "");
|
|
9573
|
+
if (getDelimitedTextPreviewConfig(detectedPersistedPathLanguage)) {
|
|
9574
|
+
setEditorLanguage(detectedPersistedPathLanguage);
|
|
9575
|
+
} else if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
|
|
8892
9576
|
setEditorLanguage(state.editorLanguage.trim());
|
|
8893
9577
|
}
|
|
8894
9578
|
editorView = state.editorView === "preview" ? "preview" : "markdown";
|
|
@@ -9791,6 +10475,7 @@
|
|
|
9791
10475
|
".diff", ".patch",
|
|
9792
10476
|
]);
|
|
9793
10477
|
const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
10478
|
+
const PREVIEW_LOCAL_OFFICE_LINK_EXTENSIONS = new Set([".docx", ".odt"]);
|
|
9794
10479
|
const PREVIEW_LOCAL_TEXT_LINK_FILENAMES = new Set([
|
|
9795
10480
|
".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
|
|
9796
10481
|
".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
|
|
@@ -9855,6 +10540,7 @@
|
|
|
9855
10540
|
if (ext === ".pdf") return "pdf";
|
|
9856
10541
|
if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext) || PREVIEW_LOCAL_TEXT_LINK_FILENAMES.has(name)) return "text";
|
|
9857
10542
|
if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
|
|
10543
|
+
if (PREVIEW_LOCAL_OFFICE_LINK_EXTENSIONS.has(ext)) return "office";
|
|
9858
10544
|
return "other";
|
|
9859
10545
|
}
|
|
9860
10546
|
|
|
@@ -9948,11 +10634,16 @@
|
|
|
9948
10634
|
};
|
|
9949
10635
|
if (kind === "pdf") {
|
|
9950
10636
|
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
10637
|
+
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
9951
10638
|
} else if (kind === "text") {
|
|
9952
10639
|
appendPreviewLinkMenuButton(menu, "Open in new editor", "open-new");
|
|
9953
10640
|
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
10641
|
+
} else if (kind === "office") {
|
|
10642
|
+
appendPreviewLinkMenuButton(menu, "Convert in new editor", "open-new");
|
|
10643
|
+
appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
9954
10644
|
} else if (kind === "image") {
|
|
9955
10645
|
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
10646
|
+
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
9956
10647
|
}
|
|
9957
10648
|
appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
|
|
9958
10649
|
appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
|
|
@@ -9989,40 +10680,18 @@
|
|
|
9989
10680
|
}
|
|
9990
10681
|
|
|
9991
10682
|
async function openPreviewImageLink(href, title, contextOverride, pendingWindow) {
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
if (popup && popup.document && popup.document.body) {
|
|
9995
|
-
popup.document.title = "Opening image…";
|
|
9996
|
-
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening image…</p>";
|
|
9997
|
-
}
|
|
9998
|
-
} catch {}
|
|
9999
|
-
try {
|
|
10000
|
-
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
10001
|
-
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
10002
|
-
});
|
|
10003
|
-
const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
|
|
10004
|
-
if (!dataUrl) throw new Error("Studio did not return image data.");
|
|
10005
|
-
const safeTitle = escapeHtml(String(title || href || "Local image"));
|
|
10006
|
-
const safeSrc = escapeHtml(dataUrl);
|
|
10007
|
-
const html = "<!doctype html><html><head><meta charset='utf-8'><title>" + safeTitle + "</title>"
|
|
10008
|
-
+ "<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee;font:13px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}img{max-width:100vw;max-height:100vh;object-fit:contain;}header{position:fixed;left:0;right:0;top:0;padding:8px 10px;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);}</style>"
|
|
10009
|
-
+ "</head><body><header>" + safeTitle + "</header><img src='" + safeSrc + "' alt='" + safeTitle + "'></body></html>";
|
|
10010
|
-
if (popup && !popup.closed && popup.document) {
|
|
10011
|
-
popup.document.open();
|
|
10012
|
-
popup.document.write(html);
|
|
10013
|
-
popup.document.close();
|
|
10014
|
-
setStatus("Opened local image preview.", "success");
|
|
10015
|
-
return;
|
|
10016
|
-
}
|
|
10017
|
-
const opened = window.open(dataUrl, "_blank");
|
|
10018
|
-
if (!opened) throw new Error("Popup blocked while opening image preview.");
|
|
10019
|
-
setStatus("Opened local image preview.", "success");
|
|
10020
|
-
} catch (error) {
|
|
10021
|
-
if (popup && !popup.closed) {
|
|
10022
|
-
try { popup.close(); } catch {}
|
|
10023
|
-
}
|
|
10024
|
-
throw error;
|
|
10683
|
+
if (pendingWindow && !pendingWindow.closed) {
|
|
10684
|
+
try { pendingWindow.close(); } catch {}
|
|
10025
10685
|
}
|
|
10686
|
+
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
10687
|
+
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
10688
|
+
});
|
|
10689
|
+
const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
|
|
10690
|
+
if (!dataUrl) throw new Error("Studio did not return image data.");
|
|
10691
|
+
if (!openStudioImageFocusViewer(dataUrl, title || href || "Local image")) {
|
|
10692
|
+
throw new Error("Could not open image focus view.");
|
|
10693
|
+
}
|
|
10694
|
+
setStatus("Opened local image preview.", "success");
|
|
10026
10695
|
}
|
|
10027
10696
|
|
|
10028
10697
|
function editorHasPotentialUnsavedContent() {
|
|
@@ -10032,7 +10701,33 @@
|
|
|
10032
10701
|
return true;
|
|
10033
10702
|
}
|
|
10034
10703
|
|
|
10704
|
+
function getPreviewOfficeConversionLabel(href) {
|
|
10705
|
+
const cleanPath = stripPreviewLocalLinkUrlSuffix(href || "");
|
|
10706
|
+
const rawName = cleanPath.split(/[\\/]/).pop() || cleanPath || "this document";
|
|
10707
|
+
try {
|
|
10708
|
+
return decodeURIComponent(rawName) || rawName;
|
|
10709
|
+
} catch {
|
|
10710
|
+
return rawName;
|
|
10711
|
+
}
|
|
10712
|
+
}
|
|
10713
|
+
|
|
10714
|
+
function confirmPreviewOfficeConversion(href, destination) {
|
|
10715
|
+
if (getPreviewLocalLinkKind(href) !== "office") return true;
|
|
10716
|
+
const label = getPreviewOfficeConversionLabel(href);
|
|
10717
|
+
const target = destination === "here"
|
|
10718
|
+
? "replace the current editor contents with an editable Markdown copy"
|
|
10719
|
+
: "open an editable Markdown copy in a new Studio tab";
|
|
10720
|
+
const confirmed = window.confirm(
|
|
10721
|
+
"Convert " + label + " to Markdown?\n\n"
|
|
10722
|
+
+ "Studio will use Pandoc to " + target + ". Some layout or formatting may change. "
|
|
10723
|
+
+ "The original DOCX/ODT file will not be overwritten, and edits will not round-trip back to it."
|
|
10724
|
+
);
|
|
10725
|
+
if (!confirmed) setStatus("Document conversion cancelled.", "warning");
|
|
10726
|
+
return confirmed;
|
|
10727
|
+
}
|
|
10728
|
+
|
|
10035
10729
|
async function openPreviewDocumentHere(href, contextOverride) {
|
|
10730
|
+
if (!confirmPreviewOfficeConversion(href, "here")) return;
|
|
10036
10731
|
if (editorHasPotentialUnsavedContent()) {
|
|
10037
10732
|
const confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
|
|
10038
10733
|
if (!confirmed) return;
|
|
@@ -10042,18 +10737,29 @@
|
|
|
10042
10737
|
const path = typeof payload.path === "string" ? payload.path : "";
|
|
10043
10738
|
const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
|
|
10044
10739
|
const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
|
|
10740
|
+
const converted = payload && payload.converted === true;
|
|
10045
10741
|
if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
|
|
10046
10742
|
setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10743
|
+
if (converted) {
|
|
10744
|
+
setSourceState({ source: "blank", label, path: null });
|
|
10745
|
+
} else {
|
|
10746
|
+
setSourceState({ source: "file", label, path });
|
|
10747
|
+
markFileBackedBaseline(payload.text);
|
|
10748
|
+
}
|
|
10749
|
+
const detected = converted ? "markdown" : detectLanguageFromName(path || label);
|
|
10050
10750
|
if (detected) setEditorLanguage(detected);
|
|
10051
10751
|
setEditorView("markdown");
|
|
10052
10752
|
setActivePane("left");
|
|
10053
|
-
setStatus("Opened linked file in editor: " + label, "success");
|
|
10753
|
+
setStatus(converted ? ("Converted document into editor: " + label) : ("Opened linked file in editor: " + label), "success");
|
|
10054
10754
|
}
|
|
10055
10755
|
|
|
10056
10756
|
async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
|
|
10757
|
+
if (!confirmPreviewOfficeConversion(href, "new")) {
|
|
10758
|
+
if (pendingWindow && !pendingWindow.closed) {
|
|
10759
|
+
try { pendingWindow.close(); } catch {}
|
|
10760
|
+
}
|
|
10761
|
+
return;
|
|
10762
|
+
}
|
|
10057
10763
|
const popup = pendingWindow || window.open("", "_blank");
|
|
10058
10764
|
try {
|
|
10059
10765
|
if (popup && popup.document && popup.document.body) {
|
|
@@ -10071,12 +10777,44 @@
|
|
|
10071
10777
|
try {
|
|
10072
10778
|
popup.opener = null;
|
|
10073
10779
|
popup.location.href = targetUrl;
|
|
10074
|
-
setStatus("Opening linked file in a new editor.", "success");
|
|
10780
|
+
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
|
|
10075
10781
|
return;
|
|
10076
10782
|
} catch {}
|
|
10077
10783
|
}
|
|
10078
10784
|
window.open(targetUrl, "_blank", "noopener");
|
|
10079
|
-
setStatus("Opening linked file in a new editor.", "success");
|
|
10785
|
+
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
|
|
10786
|
+
} catch (error) {
|
|
10787
|
+
if (popup && !popup.closed) {
|
|
10788
|
+
try { popup.close(); } catch {}
|
|
10789
|
+
}
|
|
10790
|
+
throw error;
|
|
10791
|
+
}
|
|
10792
|
+
}
|
|
10793
|
+
|
|
10794
|
+
async function openPreviewResourceInNewEditor(href, pendingWindow, contextOverride) {
|
|
10795
|
+
const popup = pendingWindow || window.open("", "_blank");
|
|
10796
|
+
try {
|
|
10797
|
+
if (popup && popup.document && popup.document.body) {
|
|
10798
|
+
popup.document.title = "Opening preview…";
|
|
10799
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening preview…</p>";
|
|
10800
|
+
}
|
|
10801
|
+
} catch {}
|
|
10802
|
+
try {
|
|
10803
|
+
const payload = await fetchPreviewLocalLink("preview-url", href, contextOverride);
|
|
10804
|
+
const targetUrl = payload && typeof payload.relativeUrl === "string"
|
|
10805
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
10806
|
+
: (payload && typeof payload.url === "string" ? payload.url : "");
|
|
10807
|
+
if (!targetUrl) throw new Error("Studio did not return a preview URL.");
|
|
10808
|
+
if (popup && !popup.closed) {
|
|
10809
|
+
try {
|
|
10810
|
+
popup.opener = null;
|
|
10811
|
+
popup.location.href = targetUrl;
|
|
10812
|
+
setStatus("Opening preview in a new Studio tab.", "success");
|
|
10813
|
+
return;
|
|
10814
|
+
} catch {}
|
|
10815
|
+
}
|
|
10816
|
+
window.open(targetUrl, "_blank", "noopener");
|
|
10817
|
+
setStatus("Opening preview in a new Studio tab.", "success");
|
|
10080
10818
|
} catch (error) {
|
|
10081
10819
|
if (popup && !popup.closed) {
|
|
10082
10820
|
try { popup.close(); } catch {}
|
|
@@ -10115,6 +10853,10 @@
|
|
|
10115
10853
|
await openPreviewDocumentInNewEditor(href, null, context);
|
|
10116
10854
|
return;
|
|
10117
10855
|
}
|
|
10856
|
+
if (action === "open-preview-new") {
|
|
10857
|
+
await openPreviewResourceInNewEditor(href, null, context);
|
|
10858
|
+
return;
|
|
10859
|
+
}
|
|
10118
10860
|
if (action === "open-here") {
|
|
10119
10861
|
await openPreviewDocumentHere(href, context);
|
|
10120
10862
|
return;
|
|
@@ -10149,14 +10891,13 @@
|
|
|
10149
10891
|
return;
|
|
10150
10892
|
}
|
|
10151
10893
|
if (kind === "image") {
|
|
10152
|
-
|
|
10153
|
-
void openPreviewImageLink(href, title, null, pendingWindow).catch((error) => {
|
|
10894
|
+
void openPreviewImageLink(href, title).catch((error) => {
|
|
10154
10895
|
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
10155
10896
|
});
|
|
10156
10897
|
return;
|
|
10157
10898
|
}
|
|
10158
|
-
if (kind === "text") {
|
|
10159
|
-
const pendingWindow = window.open("", "_blank");
|
|
10899
|
+
if (kind === "text" || kind === "office") {
|
|
10900
|
+
const pendingWindow = kind === "office" ? null : window.open("", "_blank");
|
|
10160
10901
|
void openPreviewDocumentInNewEditor(href, pendingWindow).catch((error) => {
|
|
10161
10902
|
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
10162
10903
|
});
|
|
@@ -11405,7 +12146,7 @@
|
|
|
11405
12146
|
}
|
|
11406
12147
|
|
|
11407
12148
|
function supportsCodePreviewCommentsForCurrentEditor() {
|
|
11408
|
-
return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex";
|
|
12149
|
+
return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex" && !getDelimitedTextPreviewConfig(editorLanguage);
|
|
11409
12150
|
}
|
|
11410
12151
|
|
|
11411
12152
|
function getCodePreviewCommentKind(language) {
|
|
@@ -11606,6 +12347,26 @@
|
|
|
11606
12347
|
return Boolean(shortcutsOverlayEl && !shortcutsOverlayEl.hidden);
|
|
11607
12348
|
}
|
|
11608
12349
|
|
|
12350
|
+
function handleShortcutsScrollShortcut(event) {
|
|
12351
|
+
if (!isShortcutsOpen() || !shortcutsBodyEl || !event) return false;
|
|
12352
|
+
if (isTextEntryShortcutTarget(event.target)) return false;
|
|
12353
|
+
const key = typeof event.key === "string" ? event.key : "";
|
|
12354
|
+
let delta = 0;
|
|
12355
|
+
let targetTop = null;
|
|
12356
|
+
if (key === "ArrowDown") delta = 42;
|
|
12357
|
+
else if (key === "ArrowUp") delta = -42;
|
|
12358
|
+
else if (key === "PageDown") delta = Math.max(120, Math.round((shortcutsBodyEl.clientHeight || 0) * 0.85));
|
|
12359
|
+
else if (key === "PageUp") delta = -Math.max(120, Math.round((shortcutsBodyEl.clientHeight || 0) * 0.85));
|
|
12360
|
+
else if (key === "Home") targetTop = 0;
|
|
12361
|
+
else if (key === "End") targetTop = shortcutsBodyEl.scrollHeight;
|
|
12362
|
+
else return false;
|
|
12363
|
+
event.preventDefault();
|
|
12364
|
+
event.stopPropagation();
|
|
12365
|
+
if (targetTop !== null) shortcutsBodyEl.scrollTop = targetTop;
|
|
12366
|
+
else shortcutsBodyEl.scrollTop += delta;
|
|
12367
|
+
return true;
|
|
12368
|
+
}
|
|
12369
|
+
|
|
11609
12370
|
function isScratchpadOpen() {
|
|
11610
12371
|
return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
|
|
11611
12372
|
}
|
|
@@ -15841,7 +16602,8 @@
|
|
|
15841
16602
|
? window.requestAnimationFrame.bind(window)
|
|
15842
16603
|
: (cb) => window.setTimeout(cb, 16);
|
|
15843
16604
|
schedule(() => {
|
|
15844
|
-
if (
|
|
16605
|
+
if (shortcutsBodyEl && typeof shortcutsBodyEl.focus === "function") shortcutsBodyEl.focus({ preventScroll: true });
|
|
16606
|
+
else if (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
|
|
15845
16607
|
});
|
|
15846
16608
|
}
|
|
15847
16609
|
|
|
@@ -16039,6 +16801,9 @@
|
|
|
16039
16801
|
if (editorView === "preview") {
|
|
16040
16802
|
scheduleSourcePreviewRender(0);
|
|
16041
16803
|
}
|
|
16804
|
+
if (rightView === "editor-preview") {
|
|
16805
|
+
scheduleResponseEditorPreviewRender(0);
|
|
16806
|
+
}
|
|
16042
16807
|
updateOutlineUi();
|
|
16043
16808
|
scheduleWorkspacePersistence();
|
|
16044
16809
|
}
|