Manuscript Workstation - Source Excerpt 14
Back to Manuscript Workstation
Summary
This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/ns12-manuscript/assets/js/manuscript-workstation.js so readers can inspect the evidence without opening the full source file.
**Source path:** Spiralist/wp-content/plugins/ns12-manuscript/assets/js/manuscript-workstation.js
const profile = this.calibrationProfiles.get(folio.slug) || (await this.sampleBorderProfile(folio.image));
const detected = await this.detectCropRect(folio.image, profile);
if (detected) {
this.setCrop(folio, detected, { dirty: true, autoDetected: true });
}
}
this.updateStatus('Auto-crop applied to the visible folios. Review the guides, then save if the boundaries look correct.');
}
async refreshVisibleFolios() {
const activeFolios = this.getVisibleFolios();
await Promise.all(
activeFolios.map((folio) =>
this.applyFolioImageSource(folio, this.state.rawMode ? 'full' : 'standard', {
cacheBust: true,
force: true,
})
)
);
this.ensureActiveImagesReady(() => {
this.refreshViewportState();
});
this.updateStatus('Visible folios refreshed.');
}
async saveDirtyFolios() {
if (!this.canEdit) {
this.updateStatus('Crop saving requires editor access on this WordPress site.');
return;
}
if (this.saveInFlight) {
return;
}
const dirty = this.folios.filter((folio) => this.dirtyFolios.has(folio.slug));
if (!dirty.length) {
this.updateDirtyState();
this.updateStatus('No crop changes are waiting to be saved.');
return;
}
this.saveInFlight = true;
this.updateDirtyState();
await runWorkstationAction(async () => {
await Promise.all(
dirty.map(async (folio) => {
const response = await fetch(`${config.saveCropBaseUrl || ''}${encodeURIComponent(folio.slug)}`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-WP-Nonce': config.restNonce || '',
},
credentials: 'same-origin',
body: JSON.stringify({
crop: folio.crop,
auto_detected: this.autoDetected.has(folio.slug),
}),
});
if (!response.ok) {
throw new Error(`Crop save failed with ${response.status}`);
}
this.dirtyFolios.delete(folio.slug);
})
);
this.updateDirtyState();
this.updateStatus(`Saved crop data for ${dirty.length} folio${dirty.length === 1 ? '' : 's'}.`);
}, {
context: 'workstation-save-crops',
onError: () => {
this.updateStatus('Crop save failed. Check permissions and try again.');
},
onFinally: () => {
this.saveInFlight = false;
this.updateDirtyState();
},
});
}
focusCurrentCrop() {
const folio = this.getCurrentFolio();
if (!folio || !folio.frame || !folio.cropLayer) {
return;
}
const crop = normalizeCropRect(folio.crop);
const cropWidth = Math.max(1, folio.cropLayer.offsetWidth * crop.width);
const cropHeight = Math.max(1, folio.cropLayer.offsetHeight * crop.height);
const viewportWidth = Math.max(1, this.viewport.clientWidth - 80);
const viewportHeight = Math.max(1, this.viewport.clientHeight - 80);
const targetScale = Math.min(
viewportWidth / cropWidth,
viewportHeight / cropHeight,
this.getScaleBounds().max
);
const cropCenterX =
folio.frame.offsetLeft +
folio.cropLayer.offsetLeft +
crop.x * folio.cropLayer.offsetWidth +
cropWidth / 2;
const cropCenterY =
folio.frame.offsetTop +
folio.cropLayer.offsetTop +
crop.y * folio.cropLayer.offsetHeight +
cropHeight / 2;
const contentCenterX = this.content.offsetWidth / 2;
const contentCenterY = this.content.offsetHeight / 2;
this.state.fitMode = 'custom';
this.state.scale = targetScale;
this.state.panX = (contentCenterX - cropCenterX) * targetScale;
this.state.panY = (contentCenterY - cropCenterY) * targetScale;
this.syncButtons();
this.renderSurface();
this.updateStatus(`Focused ${folio.title || 'the current folio'} on its crop bounds.`);
}
async ensureStudyOverlay() {
if (this.studyPayload || this.studyLoading) {
this.renderStudyOverlay();
return;
}
const overlaySource = this.folios.find(
(folio) => overlayIncludesGlobalNodes(folio.overlayData) && getOverlayNodesUrl(folio.overlayData)
);
const nodesUrl = overlaySource ? getOverlayNodesUrl(overlaySource.overlayData) : '';
if (!nodesUrl) {
this.renderStudyOverlay();
return;
}
this.studyLoading = true;
await runWorkstationAction(async () => {
const response = await fetch(nodesUrl, {
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`Study overlay failed with ${response.status}`);
}