feat(browse): preview .docx/.xlsx + fix markdown-editor horizontal overflow
DOCX/XLSX preview: add renderDocx (docx-preview) and renderXlsx (SheetJS) to shared/preview-lib.js — the natural home alongside renderTiff/ renderZipListing, reusable by every tool. browse dispatches Office files to them in both the inline pane and the pop-out window via the existing preview.isOffice() check, and browse/build.sh now bundles the docx-preview + xlsx vendors. Renderers degrade to a friendly message if a tool doesn't bundle the vendor. Overflow fix: .md-shell used `grid-template-columns: 280px 1fr`. A bare `1fr` is `minmax(auto, 1fr)`, whose `auto` floor is the editor's min-content width (Toast UI's toolbar) — so the content track refused to shrink and the whole shell overflowed #previewBody as the window narrowed instead of the editor reflowing smaller. Switch both tracks to minmax(0, …) in the CSS and in the three JS spots that rewrite the columns on sidebar-drag, and give .md-shell__sidebar min-width: 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c05fc376f2
commit
3e8737b7c9
5 changed files with 154 additions and 4 deletions
|
|
@ -42,6 +42,8 @@ concat_files \
|
||||||
# without an external HTTP dependency.
|
# without an external HTTP dependency.
|
||||||
concat_files \
|
concat_files \
|
||||||
"../shared/vendor/jszip.min.js" \
|
"../shared/vendor/jszip.min.js" \
|
||||||
|
"../shared/vendor/docx-preview.min.js" \
|
||||||
|
"../shared/vendor/xlsx.full.min.js" \
|
||||||
"../shared/vendor/utif.min.js" \
|
"../shared/vendor/utif.min.js" \
|
||||||
"../shared/vendor/js-yaml.min.js" \
|
"../shared/vendor/js-yaml.min.js" \
|
||||||
"../shared/vendor/codemirror-yaml.min.js" \
|
"../shared/vendor/codemirror-yaml.min.js" \
|
||||||
|
|
|
||||||
|
|
@ -589,7 +589,14 @@ body {
|
||||||
.md-shell {
|
.md-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
grid-template-columns: 280px 1fr; /* JS overrides on resize */
|
/* minmax(0, …) on BOTH tracks is load-bearing: a bare `1fr` is
|
||||||
|
`minmax(auto, 1fr)`, whose `auto` floor is the editor's min-content
|
||||||
|
width (Toast UI's toolbar). That floor stops the content track from
|
||||||
|
shrinking, so the whole shell overflows #previewBody as the window
|
||||||
|
narrows instead of the editor getting narrower. minmax(0, 1fr) drops
|
||||||
|
the floor so the editor reflows down to nothing. JS overrides the
|
||||||
|
column widths on drag — it preserves the minmax(0, …) form. */
|
||||||
|
grid-template-columns: minmax(0, 280px) minmax(0, 1fr);
|
||||||
grid-template-areas: "sidebar content";
|
grid-template-areas: "sidebar content";
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
@ -606,6 +613,7 @@ body {
|
||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
var shell = document.createElement('div');
|
var shell = document.createElement('div');
|
||||||
shell.className = 'md-shell';
|
shell.className = 'md-shell';
|
||||||
shell.style.gridTemplateColumns = lastSidebarWidth + 'px 1fr';
|
shell.style.gridTemplateColumns = 'minmax(0, ' + lastSidebarWidth + 'px) minmax(0, 1fr)';
|
||||||
container.appendChild(shell);
|
container.appendChild(shell);
|
||||||
|
|
||||||
// ── Sidebar (col 1): front matter (top) + TOC (bottom) ──────────────
|
// ── Sidebar (col 1): front matter (top) + TOC (bottom) ──────────────
|
||||||
|
|
@ -595,7 +595,7 @@
|
||||||
var w = startW + dx;
|
var w = startW + dx;
|
||||||
w = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, w));
|
w = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, w));
|
||||||
lastSidebarWidth = w;
|
lastSidebarWidth = w;
|
||||||
shell.style.gridTemplateColumns = w + 'px 1fr';
|
shell.style.gridTemplateColumns = 'minmax(0, ' + w + 'px) minmax(0, 1fr)';
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
function onUp() {
|
function onUp() {
|
||||||
|
|
@ -620,7 +620,7 @@
|
||||||
var w = Math.max(SIDEBAR_MIN_WIDTH,
|
var w = Math.max(SIDEBAR_MIN_WIDTH,
|
||||||
Math.min(SIDEBAR_MAX_WIDTH, lastSidebarWidth + step));
|
Math.min(SIDEBAR_MAX_WIDTH, lastSidebarWidth + step));
|
||||||
lastSidebarWidth = w;
|
lastSidebarWidth = w;
|
||||||
shell.style.gridTemplateColumns = w + 'px 1fr';
|
shell.style.gridTemplateColumns = 'minmax(0, ' + w + 'px) minmax(0, 1fr)';
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,24 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Office docs (.docx via docx-preview, .xlsx/.xls via SheetJS) →
|
||||||
|
// shared/preview-lib renderers. .doc/.ppt etc. fall through to the
|
||||||
|
// download fallback below.
|
||||||
|
if (preview && preview.isOffice(ext)) {
|
||||||
|
try {
|
||||||
|
var officeBuf = await getArrayBuffer(node);
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (ext === 'docx') {
|
||||||
|
await preview.renderDocx(document, container, officeBuf, { fileName: node.name });
|
||||||
|
} else {
|
||||||
|
await preview.renderXlsx(document, container, officeBuf, { fileName: node.name });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
renderError(container, 'Failed to render ' + ext.toUpperCase() + ': ' + (e.message || e));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (preview && preview.isText(ext)) {
|
if (preview && preview.isText(ext)) {
|
||||||
try {
|
try {
|
||||||
var txtBuf = await getArrayBuffer(node);
|
var txtBuf = await getArrayBuffer(node);
|
||||||
|
|
@ -273,6 +291,13 @@
|
||||||
} else if (preview && preview.isZip(ext)) {
|
} else if (preview && preview.isZip(ext)) {
|
||||||
var zb = await getArrayBuffer(node);
|
var zb = await getArrayBuffer(node);
|
||||||
await preview.renderZipListing(win.document, c, zb, { fileName: node.name });
|
await preview.renderZipListing(win.document, c, zb, { fileName: node.name });
|
||||||
|
} else if (preview && preview.isOffice(ext)) {
|
||||||
|
var ob = await getArrayBuffer(node);
|
||||||
|
if (ext === 'docx') {
|
||||||
|
await preview.renderDocx(win.document, c, ob, { fileName: node.name });
|
||||||
|
} else {
|
||||||
|
await preview.renderXlsx(win.document, c, ob, { fileName: node.name });
|
||||||
|
}
|
||||||
} else if (preview && preview.isText(ext)) {
|
} else if (preview && preview.isText(ext)) {
|
||||||
var txb = await getArrayBuffer(node);
|
var txb = await getArrayBuffer(node);
|
||||||
var text = new TextDecoder('utf-8', { fatal: false }).decode(txb);
|
var text = new TextDecoder('utf-8', { fatal: false }).decode(txb);
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,23 @@
|
||||||
* Cross-tool helpers for previewing file types that need a decoder:
|
* Cross-tool helpers for previewing file types that need a decoder:
|
||||||
* - TIFF (UTIF.js) — multi-page, browser-PDF-viewer-style toolbar
|
* - TIFF (UTIF.js) — multi-page, browser-PDF-viewer-style toolbar
|
||||||
* - ZIP listing (JSZip) — sortable file-list view
|
* - ZIP listing (JSZip) — sortable file-list view
|
||||||
|
* - DOCX (docx-preview) — Word-styled pages in a scroll container
|
||||||
|
* - XLSX/XLS (SheetJS) — sheet-to-HTML table with a sheet tab bar
|
||||||
*
|
*
|
||||||
* Renderers operate on any document (parent window or popup window), so the
|
* Renderers operate on any document (parent window or popup window), so the
|
||||||
* same code works for tools whose preview opens in a popup (classifier,
|
* same code works for tools whose preview opens in a popup (classifier,
|
||||||
* archive, transmittal) and tools that render inline (browse).
|
* archive, transmittal) and tools that render inline (browse).
|
||||||
*
|
*
|
||||||
|
* The DOCX/XLSX renderers expect their vendor lib bundled by the calling
|
||||||
|
* tool's build.sh (docx-preview.min.js → window.docx, xlsx.full.min.js →
|
||||||
|
* window.XLSX); they degrade to a friendly message if it isn't present.
|
||||||
|
*
|
||||||
* Public API on window.zddc.preview:
|
* Public API on window.zddc.preview:
|
||||||
* loadLibrary(url) → Promise<void>
|
* loadLibrary(url) → Promise<void>
|
||||||
* renderTiff(doc, container, arrayBuffer, opts) → Promise<void>
|
* renderTiff(doc, container, arrayBuffer, opts) → Promise<void>
|
||||||
* renderZipListing(doc, container, arrayBuffer, opts) → Promise<void>
|
* renderZipListing(doc, container, arrayBuffer, opts) → Promise<void>
|
||||||
|
* renderDocx(doc, container, arrayBuffer, opts) → Promise<void>
|
||||||
|
* renderXlsx(doc, container, arrayBuffer, opts) → Promise<void>
|
||||||
* TIFF_EXTENSIONS, IMAGE_EXTENSIONS, TEXT_EXTENSIONS, OFFICE_EXTENSIONS
|
* TIFF_EXTENSIONS, IMAGE_EXTENSIONS, TEXT_EXTENSIONS, OFFICE_EXTENSIONS
|
||||||
* isTiff(ext), isImage(ext), isText(ext), isZip(ext), isOffice(ext)
|
* isTiff(ext), isImage(ext), isText(ext), isZip(ext), isOffice(ext)
|
||||||
*
|
*
|
||||||
|
|
@ -525,6 +533,111 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── DOCX (docx-preview) ────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// docx-preview renders Word-styled pages with an intrinsic page width, so
|
||||||
|
// we wrap it in a scroll container: a wide document scrolls WITHIN the
|
||||||
|
// preview pane rather than pushing the page wider. docx-preview is bundled
|
||||||
|
// by the tools that opt in (each build.sh concatenates
|
||||||
|
// shared/vendor/docx-preview.min.js → window.docx).
|
||||||
|
|
||||||
|
var DOCX_CSS =
|
||||||
|
'.zddc-docx{height:100%;min-width:0;overflow:auto;'
|
||||||
|
+ 'background:var(--bg-secondary,#eee);padding:1rem;box-sizing:border-box;}'
|
||||||
|
+ '.zddc-docx .docx-wrapper{background:transparent;padding:0;}';
|
||||||
|
|
||||||
|
function renderDocx(doc, container, arrayBuffer, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
injectStyles(doc, 'zddc-docx-styles', DOCX_CSS);
|
||||||
|
if (!window.docx || typeof window.docx.renderAsync !== 'function') {
|
||||||
|
container.innerHTML = '<div class="preview-empty">DOCX preview unavailable '
|
||||||
|
+ '(renderer not bundled in this tool).</div>';
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
container.innerHTML = '';
|
||||||
|
var scroll = doc.createElement('div');
|
||||||
|
scroll.className = 'zddc-docx';
|
||||||
|
container.appendChild(scroll);
|
||||||
|
return Promise.resolve(
|
||||||
|
window.docx.renderAsync(arrayBuffer, scroll, null, { inWrapper: true })
|
||||||
|
).catch(function (err) {
|
||||||
|
container.innerHTML = '<div class="preview-empty" style="color:var(--danger,#c00)">'
|
||||||
|
+ 'Failed to render DOCX: ' + escapeHtml(err.message || err) + '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── XLSX / XLS (SheetJS) ───────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Reads the workbook and renders the active sheet to an HTML table; a tab
|
||||||
|
// bar switches sheets when there's more than one. SheetJS is bundled by
|
||||||
|
// the tools that opt in (shared/vendor/xlsx.full.min.js → window.XLSX).
|
||||||
|
|
||||||
|
var XLSX_CSS =
|
||||||
|
'.zddc-xlsx{display:flex;flex-direction:column;height:100%;min-width:0;'
|
||||||
|
+ 'min-height:0;overflow:hidden;}'
|
||||||
|
+ '.zddc-xlsx__tabs{display:flex;flex-wrap:wrap;gap:0.25rem;padding:0.4rem;'
|
||||||
|
+ 'border-bottom:1px solid var(--border,#ccc);flex:0 0 auto;}'
|
||||||
|
+ '.zddc-xlsx__tab{padding:0.2rem 0.6rem;border:1px solid var(--border,#ccc);'
|
||||||
|
+ 'border-radius:4px;background:var(--bg,#fff);color:var(--text,#222);'
|
||||||
|
+ 'cursor:pointer;font-size:0.85rem;}'
|
||||||
|
+ '.zddc-xlsx__tab.is-active{background:var(--primary,#2563eb);color:#fff;'
|
||||||
|
+ 'border-color:var(--primary,#2563eb);}'
|
||||||
|
+ '.zddc-xlsx__body{flex:1 1 auto;min-width:0;min-height:0;overflow:auto;}'
|
||||||
|
+ '.zddc-xlsx__body table{border-collapse:collapse;font-size:0.85rem;'
|
||||||
|
+ 'color:var(--text,#222);}'
|
||||||
|
+ '.zddc-xlsx__body td,.zddc-xlsx__body th{border:1px solid var(--border,#ddd);'
|
||||||
|
+ 'padding:0.2rem 0.45rem;white-space:nowrap;}';
|
||||||
|
|
||||||
|
function renderXlsx(doc, container, arrayBuffer, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
injectStyles(doc, 'zddc-xlsx-styles', XLSX_CSS);
|
||||||
|
if (!window.XLSX || typeof window.XLSX.read !== 'function') {
|
||||||
|
container.innerHTML = '<div class="preview-empty">Spreadsheet preview unavailable '
|
||||||
|
+ '(renderer not bundled in this tool).</div>';
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var wb = window.XLSX.read(arrayBuffer, { type: 'array' });
|
||||||
|
container.innerHTML = '';
|
||||||
|
var rootEl = doc.createElement('div');
|
||||||
|
rootEl.className = 'zddc-xlsx';
|
||||||
|
var body = doc.createElement('div');
|
||||||
|
body.className = 'zddc-xlsx__body';
|
||||||
|
|
||||||
|
function showSheet(name) {
|
||||||
|
var sheet = wb.Sheets[name];
|
||||||
|
body.innerHTML = sheet
|
||||||
|
? window.XLSX.utils.sheet_to_html(sheet, { editable: false })
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wb.SheetNames.length > 1) {
|
||||||
|
var tabs = doc.createElement('div');
|
||||||
|
tabs.className = 'zddc-xlsx__tabs';
|
||||||
|
wb.SheetNames.forEach(function (name, i) {
|
||||||
|
var btn = doc.createElement('button');
|
||||||
|
btn.className = 'zddc-xlsx__tab' + (i === 0 ? ' is-active' : '');
|
||||||
|
btn.textContent = name;
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var all = tabs.querySelectorAll('.zddc-xlsx__tab');
|
||||||
|
for (var j = 0; j < all.length; j++) all[j].classList.remove('is-active');
|
||||||
|
btn.classList.add('is-active');
|
||||||
|
showSheet(name);
|
||||||
|
});
|
||||||
|
tabs.appendChild(btn);
|
||||||
|
});
|
||||||
|
rootEl.appendChild(tabs);
|
||||||
|
}
|
||||||
|
rootEl.appendChild(body);
|
||||||
|
container.appendChild(rootEl);
|
||||||
|
showSheet(wb.SheetNames[0]);
|
||||||
|
} catch (err) {
|
||||||
|
container.innerHTML = '<div class="preview-empty" style="color:var(--danger,#c00)">'
|
||||||
|
+ 'Failed to render spreadsheet: ' + escapeHtml(err.message || err) + '</div>';
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Public API ───────────────────────────────────────────────────────────
|
// ── Public API ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (!root.zddc) root.zddc = {};
|
if (!root.zddc) root.zddc = {};
|
||||||
|
|
@ -541,6 +654,8 @@
|
||||||
loadLibrary: loadLibrary,
|
loadLibrary: loadLibrary,
|
||||||
renderTiff: renderTiff,
|
renderTiff: renderTiff,
|
||||||
renderZipListing: renderZipListing,
|
renderZipListing: renderZipListing,
|
||||||
|
renderDocx: renderDocx,
|
||||||
|
renderXlsx: renderXlsx,
|
||||||
formatSize: formatSize,
|
formatSize: formatSize,
|
||||||
formatDate: formatDate
|
formatDate: formatDate
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue