Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d82e5603a7 | |||
| 7e2386bcdf | |||
| eba2a03355 | |||
| 06c01f0690 | |||
| 91e03eb9c4 | |||
| b7f3f47c61 | |||
| 0a83f0e136 | |||
| 2b048cf34f | |||
| 7e50b8cb3f | |||
| b97601a876 | |||
| 5ddeb8fe7c | |||
| 25f46162c5 | |||
| b84b0e7ce6 |
36
changelog.md
36
changelog.md
@@ -1,5 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-01 - 3.41.5 - fix(dees-service-lib-loader)
|
||||||
|
prevent horizontal scrollbar by offsetting xterm WidthCache measurement container
|
||||||
|
|
||||||
|
- Injects additional CSS into DeesServiceLibLoader to move xterm.js WidthCache measurement div off-screen horizontally (selector: body > div[style*="top: -50000px"][style*="width: 50000px"])
|
||||||
|
- Fixes root cause where xterm creates a large-width measurement container (width: 50000px) on document.body that expands scrollWidth and causes a horizontal scrollbar
|
||||||
|
- Change applied in ts_web/services/DeesServiceLibLoader.ts by concatenating the fix CSS into the injected stylesheet
|
||||||
|
|
||||||
|
## 2026-01-29 - 3.41.4 - fix()
|
||||||
|
no changes
|
||||||
|
|
||||||
|
- No files changed in this commit; no code or documentation modified
|
||||||
|
- No release required
|
||||||
|
|
||||||
|
## 2026-01-29 - 3.41.3 - fix(dees-pdf-viewer)
|
||||||
|
use in-memory PDF data for download and print; add robust print wrapper, cleanup and error handling
|
||||||
|
|
||||||
|
- Download and Print now use pdfDocument.getData() to create Blob URLs so in-memory PDFs (pdf.js) can be saved/printed.
|
||||||
|
- Print flow now opens an HTML wrapper with an iframe to allow onafterprint handling, auto-close, popup-fallback and timed cleanup of Blob URLs.
|
||||||
|
- Added try/catch logging, URL.revokeObjectURL calls and safety timeouts to avoid resource leaks.
|
||||||
|
- Removed context menu items that relied on the raw PDF URL (Open in New Tab, Copy PDF URL); Download/Print actions now await the async handlers.
|
||||||
|
|
||||||
|
## 2026-01-28 - 3.41.2 - fix(dees-pdf-viewer)
|
||||||
|
account for devicePixelRatio when setting canvas dimensions and scale 2D context to render crisp PDF pages and thumbnails on high-DPI displays
|
||||||
|
|
||||||
|
- Multiply canvas.width and canvas.height by window.devicePixelRatio (dpr) and use Math.floor to set the actual pixel buffer size
|
||||||
|
- Call ctx.scale(dpr, dpr) so drawing is rendered at device pixels while keeping CSS display size unchanged
|
||||||
|
- Apply the same high-DPI adjustments to both main page rendering and thumbnail generation
|
||||||
|
- Keep canvas.style.width and canvas.style.height set to viewport dimensions to preserve layout
|
||||||
|
|
||||||
|
## 2026-01-27 - 3.41.1 - fix(dataview-codebox)
|
||||||
|
fix dees-dataview codebox layout to ensure full-height, proper flex behavior and scrolling; bump internal dependencies
|
||||||
|
|
||||||
|
- Updated CSS in ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.ts: added height:100%, box-sizing, display:flex and flex-direction:column on container, set flex-shrink on header elements, made code grid overflow:auto with flex:1 and min-height:0 to prevent overflow issues.
|
||||||
|
- Bumped dependencies in package.json: @design.estate/dees-domtools from ^2.3.7 to ^2.3.8 and @design.estate/dees-element from ^2.1.5 to ^2.1.6.
|
||||||
|
- Non-breaking visual/layout fix — suitable for a patch release.
|
||||||
|
|
||||||
## 2026-01-27 - 3.41.0 - feat(docs)
|
## 2026-01-27 - 3.41.0 - feat(docs)
|
||||||
document new media & tile components and expand Workspace/IDE component docs; update component count to 90+
|
document new media & tile components and expand Workspace/IDE component docs; update component count to 90+
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "3.41.0",
|
"version": "3.41.5",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-domtools": "^2.3.7",
|
"@design.estate/dees-domtools": "^2.3.8",
|
||||||
"@design.estate/dees-element": "^2.1.5",
|
"@design.estate/dees-element": "^2.1.6",
|
||||||
"@design.estate/dees-wcctools": "^3.8.0",
|
"@design.estate/dees-wcctools": "^3.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -9,11 +9,11 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools':
|
'@design.estate/dees-domtools':
|
||||||
specifier: ^2.3.7
|
specifier: ^2.3.8
|
||||||
version: 2.3.7
|
version: 2.3.8
|
||||||
'@design.estate/dees-element':
|
'@design.estate/dees-element':
|
||||||
specifier: ^2.1.5
|
specifier: ^2.1.6
|
||||||
version: 2.1.5
|
version: 2.1.6
|
||||||
'@design.estate/dees-wcctools':
|
'@design.estate/dees-wcctools':
|
||||||
specifier: ^3.8.0
|
specifier: ^3.8.0
|
||||||
version: 3.8.0
|
version: 3.8.0
|
||||||
@@ -352,11 +352,11 @@ packages:
|
|||||||
'@design.estate/dees-comms@1.0.30':
|
'@design.estate/dees-comms@1.0.30':
|
||||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||||
|
|
||||||
'@design.estate/dees-domtools@2.3.7':
|
'@design.estate/dees-domtools@2.3.8':
|
||||||
resolution: {integrity: sha512-MXoDBrP7JTOpni8b12aFXHJKnKBoQppM8cYBuL9cesRmCVGdB7p39XMRQ7dRyMhmmyr66L3cOczhiCV6febCwg==}
|
resolution: {integrity: sha512-jUG9GMvPxKMwmRIZ9oLTL3c8hHvHuiwIk8cTrYnuZzGO/uJJ5/czk9o6LRXUuCOOG7TRLtqgOpK8EEQgaadfZA==}
|
||||||
|
|
||||||
'@design.estate/dees-element@2.1.5':
|
'@design.estate/dees-element@2.1.6':
|
||||||
resolution: {integrity: sha512-czUOFvBiUKi34I+/keDRDc71fuORZS0NfbSuD2jJ4D1ODiTPjaZ6A6SkdQ2QqCEzVsx73XF99Pu8pxPnaOLnHg==}
|
resolution: {integrity: sha512-7zyHkUjB8UEQgT9VbB2IJtc/yuPt9CI5JGel3b6BxA1kecY64ceIjFvof1uIkc0QP8q2fMLLY45r1c+9zDTjzg==}
|
||||||
|
|
||||||
'@design.estate/dees-wcctools@3.8.0':
|
'@design.estate/dees-wcctools@3.8.0':
|
||||||
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
|
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
|
||||||
@@ -4730,8 +4730,8 @@ snapshots:
|
|||||||
|
|
||||||
'@design.estate/dees-catalog@3.37.0(@tiptap/pm@2.27.2)':
|
'@design.estate/dees-catalog@3.37.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.7
|
'@design.estate/dees-domtools': 2.3.8
|
||||||
'@design.estate/dees-element': 2.1.5
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@design.estate/dees-wcctools': 3.8.0
|
'@design.estate/dees-wcctools': 3.8.0
|
||||||
'@fortawesome/fontawesome-svg-core': 7.1.0
|
'@fortawesome/fontawesome-svg-core': 7.1.0
|
||||||
'@fortawesome/free-brands-svg-icons': 7.1.0
|
'@fortawesome/free-brands-svg-icons': 7.1.0
|
||||||
@@ -4770,7 +4770,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
broadcast-channel: 7.3.0
|
broadcast-channel: 7.3.0
|
||||||
|
|
||||||
'@design.estate/dees-domtools@2.3.7':
|
'@design.estate/dees-domtools@2.3.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest': 3.2.5
|
'@api.global/typedrequest': 3.2.5
|
||||||
'@design.estate/dees-comms': 1.0.30
|
'@design.estate/dees-comms': 1.0.30
|
||||||
@@ -4796,9 +4796,9 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@design.estate/dees-element@2.1.5':
|
'@design.estate/dees-element@2.1.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.7
|
'@design.estate/dees-domtools': 2.3.8
|
||||||
'@push.rocks/isounique': 1.0.5
|
'@push.rocks/isounique': 1.0.5
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
lit: 3.3.2
|
lit: 3.3.2
|
||||||
@@ -4810,8 +4810,8 @@ snapshots:
|
|||||||
|
|
||||||
'@design.estate/dees-wcctools@3.8.0':
|
'@design.estate/dees-wcctools@3.8.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.7
|
'@design.estate/dees-domtools': 2.3.8
|
||||||
'@design.estate/dees-element': 2.1.5
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
lit: 3.3.2
|
lit: 3.3.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -5921,7 +5921,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartntml@2.0.8':
|
'@push.rocks/smartntml@2.0.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-element': 2.1.5
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@happy-dom/global-registrator': 15.11.7
|
'@happy-dom/global-registrator': 15.11.7
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
fake-indexeddb: 6.2.5
|
fake-indexeddb: 6.2.5
|
||||||
@@ -6178,7 +6178,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/taskbuffer@3.5.0':
|
'@push.rocks/taskbuffer@3.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-element': 2.1.5
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.10
|
||||||
|
|||||||
79
test/test.pdf-text-selection.chromium.ts
Normal file
79
test/test.pdf-text-selection.chromium.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as deesCatalog from '../ts_web/index.js';
|
||||||
|
|
||||||
|
tap.test('PDF viewer should render text layer', async () => {
|
||||||
|
const viewer = await webhelpers.fixture(
|
||||||
|
webhelpers.html`
|
||||||
|
<dees-pdf-viewer
|
||||||
|
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
|
||||||
|
initialZoom="page-fit"
|
||||||
|
style="height: 600px; width: 100%;"
|
||||||
|
></dees-pdf-viewer>
|
||||||
|
`
|
||||||
|
) as deesCatalog.DeesPdfViewer;
|
||||||
|
|
||||||
|
// Wait for PDF to load and render
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
await viewer.updateComplete;
|
||||||
|
|
||||||
|
expect(viewer.totalPages).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const textLayer = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"]');
|
||||||
|
expect(textLayer).toBeTruthy();
|
||||||
|
|
||||||
|
const textSpans = textLayer?.querySelectorAll('span');
|
||||||
|
expect(textSpans?.length).toBeGreaterThan(0);
|
||||||
|
console.log(`Text layer has ${textSpans?.length} spans`);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Text should be selectable', async () => {
|
||||||
|
const viewer = await webhelpers.fixture(
|
||||||
|
webhelpers.html`
|
||||||
|
<dees-pdf-viewer
|
||||||
|
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
|
||||||
|
initialZoom="page-fit"
|
||||||
|
style="height: 600px; width: 100%;"
|
||||||
|
></dees-pdf-viewer>
|
||||||
|
`
|
||||||
|
) as deesCatalog.DeesPdfViewer;
|
||||||
|
|
||||||
|
// Wait for PDF to load and render
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
|
const textLayer = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"]');
|
||||||
|
const firstSpan = textLayer?.querySelector('span') as HTMLElement;
|
||||||
|
|
||||||
|
if (firstSpan?.textContent) {
|
||||||
|
const range = document.createRange();
|
||||||
|
const textNode = firstSpan.firstChild;
|
||||||
|
if (textNode) {
|
||||||
|
range.setStart(textNode, 0);
|
||||||
|
range.setEnd(textNode, Math.min(5, textNode.textContent?.length || 0));
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection?.removeAllRanges();
|
||||||
|
selection?.addRange(range);
|
||||||
|
expect(selection?.toString().length).toBeGreaterThan(0);
|
||||||
|
console.log('Selected text:', selection?.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('endOfContent element exists for selection boundary', async () => {
|
||||||
|
const viewer = await webhelpers.fixture(
|
||||||
|
webhelpers.html`
|
||||||
|
<dees-pdf-viewer
|
||||||
|
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
|
||||||
|
initialZoom="page-fit"
|
||||||
|
style="height: 600px; width: 100%;"
|
||||||
|
></dees-pdf-viewer>
|
||||||
|
`
|
||||||
|
) as deesCatalog.DeesPdfViewer;
|
||||||
|
|
||||||
|
// Wait for PDF to load and render
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
|
const endOfContent = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"] .endOfContent');
|
||||||
|
expect(endOfContent).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.41.0',
|
version: '3.41.5',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export class DeesDataviewCodebox extends DeesElement {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-family: ${cssGeistFontFamily};
|
font-family: ${cssGeistFontFamily};
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.mainbox {
|
.mainbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -61,6 +63,10 @@ export class DeesDataviewCodebox extends DeesElement {
|
|||||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appbar {
|
.appbar {
|
||||||
@@ -74,6 +80,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
|||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appbar .fileName {
|
.appbar .fileName {
|
||||||
@@ -95,6 +102,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacesLabel {
|
.spacesLabel {
|
||||||
@@ -121,7 +129,9 @@ export class DeesDataviewCodebox extends DeesElement {
|
|||||||
.codegrid {
|
.codegrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 50px auto;
|
grid-template-columns: 50px auto;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lineNumbers {
|
.lineNumbers {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
accessor thumbnailData: Array<{page: number, rendered: boolean}> = [];
|
accessor thumbnailData: Array<{page: number, rendered: boolean}> = [];
|
||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
accessor pageData: Array<{page: number, rendered: boolean, rendering: boolean}> = [];
|
accessor pageData: Array<{page: number, rendered: boolean, rendering: boolean, textLayerRendered: boolean}> = [];
|
||||||
|
|
||||||
private pdfDocument: any;
|
private pdfDocument: any;
|
||||||
private renderState: RenderState = 'idle';
|
private renderState: RenderState = 'idle';
|
||||||
@@ -63,6 +63,7 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
private currentRenderPromise: Promise<void> | null = null;
|
private currentRenderPromise: Promise<void> | null = null;
|
||||||
private thumbnailRenderTasks: any[] = [];
|
private thumbnailRenderTasks: any[] = [];
|
||||||
private pageRenderTasks: Map<number, any> = new Map();
|
private pageRenderTasks: Map<number, any> = new Map();
|
||||||
|
private textLayerRenderTasks: Map<number, any> = new Map();
|
||||||
private canvas: HTMLCanvasElement | undefined;
|
private canvas: HTMLCanvasElement | undefined;
|
||||||
private ctx: CanvasRenderingContext2D | undefined;
|
private ctx: CanvasRenderingContext2D | undefined;
|
||||||
private viewerMain: HTMLElement | null = null;
|
private viewerMain: HTMLElement | null = null;
|
||||||
@@ -230,6 +231,7 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
<div class="page-wrapper" data-page="${item.page}">
|
<div class="page-wrapper" data-page="${item.page}">
|
||||||
<div class="canvas-container">
|
<div class="canvas-container">
|
||||||
<canvas class="page-canvas" data-page="${item.page}"></canvas>
|
<canvas class="page-canvas" data-page="${item.page}"></canvas>
|
||||||
|
<div class="text-layer" data-page="${item.page}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -330,7 +332,8 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
this.pageData = Array.from({length: this.totalPages}, (_, i) => ({
|
this.pageData = Array.from({length: this.totalPages}, (_, i) => ({
|
||||||
page: i + 1,
|
page: i + 1,
|
||||||
rendered: false,
|
rendered: false,
|
||||||
rendering: false
|
rendering: false,
|
||||||
|
textLayerRendered: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Set loading to false to render the pages
|
// Set loading to false to render the pages
|
||||||
@@ -444,9 +447,10 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
const page = await this.pdfDocument.getPage(pageNum);
|
const page = await this.pdfDocument.getPage(pageNum);
|
||||||
const viewport = this.computeViewport(page);
|
const viewport = this.computeViewport(page);
|
||||||
|
|
||||||
// Set canvas dimensions
|
// Set canvas dimensions with device pixel ratio for sharp rendering
|
||||||
canvas.height = viewport.height;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
canvas.width = viewport.width;
|
canvas.width = Math.floor(viewport.width * dpr);
|
||||||
|
canvas.height = Math.floor(viewport.height * dpr);
|
||||||
canvas.style.width = `${viewport.width}px`;
|
canvas.style.width = `${viewport.width}px`;
|
||||||
canvas.style.height = `${viewport.height}px`;
|
canvas.style.height = `${viewport.height}px`;
|
||||||
|
|
||||||
@@ -457,6 +461,9 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scale context for high-DPI displays
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
const renderContext = {
|
const renderContext = {
|
||||||
canvasContext: ctx,
|
canvasContext: ctx,
|
||||||
viewport: viewport,
|
viewport: viewport,
|
||||||
@@ -472,6 +479,9 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
pageInfo.rendering = false;
|
pageInfo.rendering = false;
|
||||||
this.pageRenderTasks.delete(pageNum);
|
this.pageRenderTasks.delete(pageNum);
|
||||||
|
|
||||||
|
// Render text layer for selection
|
||||||
|
await this.renderTextLayer(pageNum);
|
||||||
|
|
||||||
// Update page data to reflect rendered state
|
// Update page data to reflect rendered state
|
||||||
this.requestUpdate('pageData');
|
this.requestUpdate('pageData');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -483,6 +493,132 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async renderTextLayer(pageNum: number): Promise<void> {
|
||||||
|
const pageInfo = this.pageData.find(p => p.page === pageNum);
|
||||||
|
if (!pageInfo || pageInfo.textLayerRendered) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const textLayerDiv = this.shadowRoot?.querySelector(
|
||||||
|
`.text-layer[data-page="${pageNum}"]`
|
||||||
|
) as HTMLElement;
|
||||||
|
if (!textLayerDiv) return;
|
||||||
|
|
||||||
|
textLayerDiv.innerHTML = '';
|
||||||
|
|
||||||
|
const page = await this.pdfDocument.getPage(pageNum);
|
||||||
|
const textContent = await page.getTextContent();
|
||||||
|
const viewport = this.computeViewport(page);
|
||||||
|
|
||||||
|
// @ts-ignore - Dynamic import of pdfjs
|
||||||
|
const pdfjs = await import('https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/+esm');
|
||||||
|
|
||||||
|
textLayerDiv.style.width = `${viewport.width}px`;
|
||||||
|
textLayerDiv.style.height = `${viewport.height}px`;
|
||||||
|
|
||||||
|
// Set the scale factor CSS variable - required by PDF.js text layer
|
||||||
|
textLayerDiv.style.setProperty('--scale-factor', String(viewport.scale));
|
||||||
|
|
||||||
|
const textLayerRenderTask = pdfjs.renderTextLayer({
|
||||||
|
textContentSource: textContent,
|
||||||
|
container: textLayerDiv,
|
||||||
|
viewport: viewport,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.textLayerRenderTasks.set(pageNum, textLayerRenderTask);
|
||||||
|
await textLayerRenderTask.promise;
|
||||||
|
|
||||||
|
// Add endOfContent for selection boundary
|
||||||
|
const endOfContent = document.createElement('div');
|
||||||
|
endOfContent.className = 'endOfContent';
|
||||||
|
textLayerDiv.appendChild(endOfContent);
|
||||||
|
|
||||||
|
// Custom drag selection for Shadow DOM compatibility
|
||||||
|
// caretRangeFromPoint doesn't pierce shadow DOM, so we find spans manually
|
||||||
|
let isDragging = false;
|
||||||
|
let anchorNode: Node | null = null;
|
||||||
|
let anchorOffset = 0;
|
||||||
|
|
||||||
|
const getTextPositionFromPoint = (x: number, y: number): { node: Node; offset: number } | null => {
|
||||||
|
// Find span at coordinates by checking bounding rects
|
||||||
|
const spans = Array.from(textLayerDiv.querySelectorAll('span'));
|
||||||
|
for (const span of spans) {
|
||||||
|
const rect = span.getBoundingClientRect();
|
||||||
|
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
||||||
|
const textNode = span.firstChild;
|
||||||
|
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
||||||
|
// Calculate character offset based on x position
|
||||||
|
const text = textNode.textContent || '';
|
||||||
|
const charWidth = rect.width / text.length;
|
||||||
|
const relativeX = x - rect.left;
|
||||||
|
const offset = Math.min(Math.round(relativeX / charWidth), text.length);
|
||||||
|
return { node: textNode, offset };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
anchorNode = null;
|
||||||
|
textLayerDiv.classList.remove('selecting');
|
||||||
|
}
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDragging || !anchorNode) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const pos = getTextPositionFromPoint(e.clientX, e.clientY);
|
||||||
|
if (pos) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
try {
|
||||||
|
selection.setBaseAndExtent(anchorNode, anchorOffset, pos.node, pos.offset);
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore errors from invalid selections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
textLayerDiv.addEventListener('mousedown', (e: MouseEvent) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
|
const pos = getTextPositionFromPoint(e.clientX, e.clientY);
|
||||||
|
if (pos) {
|
||||||
|
// Prevent native selection behavior
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
isDragging = true;
|
||||||
|
anchorNode = pos.node;
|
||||||
|
anchorOffset = pos.offset;
|
||||||
|
textLayerDiv.classList.add('selecting');
|
||||||
|
|
||||||
|
// Clear existing selection
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection?.removeAllRanges();
|
||||||
|
|
||||||
|
// Add document-level listeners for drag
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pageInfo.textLayerRendered = true;
|
||||||
|
page.cleanup?.();
|
||||||
|
this.textLayerRenderTasks.delete(pageNum);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.name !== 'RenderingCancelledException') {
|
||||||
|
console.error(`Error rendering text layer for page ${pageNum}:`, error);
|
||||||
|
}
|
||||||
|
this.textLayerRenderTasks.delete(pageNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleScroll = () => {
|
private handleScroll = () => {
|
||||||
// Throttle scroll events
|
// Throttle scroll events
|
||||||
if (this.scrollThrottleTimeout) {
|
if (this.scrollThrottleTimeout) {
|
||||||
@@ -652,9 +788,10 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
const scale = maxThumbnailWidth / initialViewport.width;
|
const scale = maxThumbnailWidth / initialViewport.width;
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale });
|
||||||
|
|
||||||
// Set canvas dimensions to actual render size
|
// Set canvas dimensions with device pixel ratio for sharp thumbnails
|
||||||
canvas.width = viewport.width;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
canvas.height = viewport.height;
|
canvas.width = Math.floor(viewport.width * dpr);
|
||||||
|
canvas.height = Math.floor(viewport.height * dpr);
|
||||||
|
|
||||||
// Set the display size via style to ensure proper display
|
// Set the display size via style to ensure proper display
|
||||||
canvas.style.width = `${viewport.width}px`;
|
canvas.style.width = `${viewport.width}px`;
|
||||||
@@ -670,6 +807,9 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scale context for high-DPI displays
|
||||||
|
context.scale(dpr, dpr);
|
||||||
|
|
||||||
const renderContext = {
|
const renderContext = {
|
||||||
canvasContext: context,
|
canvasContext: context,
|
||||||
viewport: viewport,
|
viewport: viewport,
|
||||||
@@ -763,6 +903,7 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
this.pageData.forEach(page => {
|
this.pageData.forEach(page => {
|
||||||
page.rendered = false;
|
page.rendered = false;
|
||||||
page.rendering = false;
|
page.rendering = false;
|
||||||
|
page.textLayerRendered = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cancel any ongoing render tasks
|
// Cancel any ongoing render tasks
|
||||||
@@ -775,6 +916,16 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
});
|
});
|
||||||
this.pageRenderTasks.clear();
|
this.pageRenderTasks.clear();
|
||||||
|
|
||||||
|
// Cancel text layer render tasks
|
||||||
|
this.textLayerRenderTasks.forEach(task => {
|
||||||
|
try {
|
||||||
|
task.cancel?.();
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore cancellation errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.textLayerRenderTasks.clear();
|
||||||
|
|
||||||
// Request update to re-render pages
|
// Request update to re-render pages
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|
||||||
@@ -784,52 +935,138 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private downloadPdf() {
|
private async downloadPdf() {
|
||||||
|
if (!this.pdfDocument) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get raw PDF data from the loaded document
|
||||||
|
const data = await this.pdfDocument.getData();
|
||||||
|
const blob = new Blob([data.buffer], { type: 'application/pdf' });
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = this.pdfUrl;
|
link.href = blobUrl;
|
||||||
link.download = this.pdfUrl.split('/').pop() || 'document.pdf';
|
link.download = this.pdfUrl ? this.pdfUrl.split('/').pop() || 'document.pdf' : 'document.pdf';
|
||||||
link.click();
|
link.click();
|
||||||
|
|
||||||
|
// Clean up blob URL after short delay
|
||||||
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading PDF:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private printPdf() {
|
private async printPdf() {
|
||||||
window.open(this.pdfUrl, '_blank')?.print();
|
if (!this.pdfDocument) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get raw PDF data from the loaded document
|
||||||
|
const data = await this.pdfDocument.getData();
|
||||||
|
const blob = new Blob([data.buffer], { type: 'application/pdf' });
|
||||||
|
const pdfUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create an HTML wrapper page that embeds the PDF and handles print/close
|
||||||
|
// This gives us control over the afterprint event (direct PDF URLs don't support it)
|
||||||
|
const htmlContent = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Print PDF</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; }
|
||||||
|
html, body { width: 100%; height: 100%; overflow: hidden; }
|
||||||
|
iframe { width: 100%; height: 100%; border: none; }
|
||||||
|
@media print {
|
||||||
|
html, body, iframe { width: 100%; height: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<iframe src="${pdfUrl}" type="application/pdf"></iframe>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
window.focus();
|
||||||
|
window.print();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
window.onafterprint = function() {
|
||||||
|
window.close();
|
||||||
|
};
|
||||||
|
// Safety close after 2 minutes
|
||||||
|
setTimeout(function() { window.close(); }, 120000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
const htmlBlob = new Blob([htmlContent], { type: 'text/html' });
|
||||||
|
const htmlUrl = URL.createObjectURL(htmlBlob);
|
||||||
|
|
||||||
|
const printWindow = window.open(htmlUrl, '_blank', 'width=800,height=600');
|
||||||
|
if (printWindow) {
|
||||||
|
// Cleanup blob URLs when window closes
|
||||||
|
const checkClosed = setInterval(() => {
|
||||||
|
if (printWindow.closed) {
|
||||||
|
clearInterval(checkClosed);
|
||||||
|
URL.revokeObjectURL(pdfUrl);
|
||||||
|
URL.revokeObjectURL(htmlUrl);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
// Safety cleanup after 2 minutes
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkClosed);
|
||||||
|
URL.revokeObjectURL(pdfUrl);
|
||||||
|
URL.revokeObjectURL(htmlUrl);
|
||||||
|
}, 120000);
|
||||||
|
} else {
|
||||||
|
// Popup blocked - fall back to direct navigation
|
||||||
|
window.open(pdfUrl, '_blank');
|
||||||
|
setTimeout(() => URL.revokeObjectURL(pdfUrl), 60000);
|
||||||
|
URL.revokeObjectURL(htmlUrl);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error printing PDF:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide context menu items for right-click functionality
|
* Provide context menu items for right-click functionality
|
||||||
*/
|
*/
|
||||||
public getContextMenuItems() {
|
public getContextMenuItems() {
|
||||||
return [
|
const items: any[] = [];
|
||||||
{
|
|
||||||
name: 'Open PDF in New Tab',
|
// Add copy option if text is selected
|
||||||
iconName: 'lucide:ExternalLink',
|
const selection = window.getSelection();
|
||||||
action: async () => {
|
const selectedText = selection?.toString() || '';
|
||||||
window.open(this.pdfUrl, '_blank');
|
if (selectedText) {
|
||||||
}
|
items.push({
|
||||||
},
|
name: 'Copy',
|
||||||
{ divider: true },
|
|
||||||
{
|
|
||||||
name: 'Copy PDF URL',
|
|
||||||
iconName: 'lucide:Copy',
|
iconName: 'lucide:Copy',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(this.pdfUrl);
|
await navigator.clipboard.writeText(selectedText);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
|
items.push({ divider: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
{
|
{
|
||||||
name: 'Download PDF',
|
name: 'Download PDF',
|
||||||
iconName: 'lucide:Download',
|
iconName: 'lucide:Download',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
this.downloadPdf();
|
await this.downloadPdf();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Print PDF',
|
name: 'Print PDF',
|
||||||
iconName: 'lucide:Printer',
|
iconName: 'lucide:Printer',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
this.printPdf();
|
await this.printPdf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get canZoomIn(): boolean {
|
private get canZoomIn(): boolean {
|
||||||
@@ -988,6 +1225,16 @@ export class DeesPdfViewer extends DeesElement {
|
|||||||
});
|
});
|
||||||
this.pageRenderTasks.clear();
|
this.pageRenderTasks.clear();
|
||||||
|
|
||||||
|
// Cancel text layer render tasks
|
||||||
|
this.textLayerRenderTasks.forEach(task => {
|
||||||
|
try {
|
||||||
|
task.cancel?.();
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore cancellation errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.textLayerRenderTasks.clear();
|
||||||
|
|
||||||
// Cancel any thumbnail render tasks
|
// Cancel any thumbnail render tasks
|
||||||
for (const task of (this.thumbnailRenderTasks || [])) {
|
for (const task of (this.thumbnailRenderTasks || [])) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ export const viewerStyles = [
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-canvas {
|
.page-canvas {
|
||||||
@@ -284,6 +285,52 @@ export const viewerStyles = [
|
|||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Text layer for selection */
|
||||||
|
.text-layer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: 1;
|
||||||
|
text-size-adjust: none;
|
||||||
|
forced-color-adjust: none;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
z-index: 1;
|
||||||
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-layer span,
|
||||||
|
.text-layer br {
|
||||||
|
color: transparent;
|
||||||
|
position: absolute;
|
||||||
|
white-space: pre;
|
||||||
|
cursor: text;
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-layer ::selection {
|
||||||
|
background: rgba(0, 100, 200, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-layer br::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-layer .endOfContent {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
inset: 100% 0 0;
|
||||||
|
z-index: 0;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-layer.selecting .endOfContent {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-viewer.with-sidebar .viewer-main {
|
.pdf-viewer.with-sidebar .viewer-main {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,9 +193,20 @@ export class DeesServiceLibLoader {
|
|||||||
const response = await fetch(cssUrl);
|
const response = await fetch(cssUrl);
|
||||||
const cssText = await response.text();
|
const cssText = await response.text();
|
||||||
|
|
||||||
|
// Fix for xterm.js WidthCache measurement container causing horizontal scrollbar
|
||||||
|
// xterm.js creates this on document.body with width: 50000px, top: -50000px
|
||||||
|
// Moving it off-screen horizontally prevents scrollWidth expansion
|
||||||
|
const xtermMeasurementFix = `
|
||||||
|
/* Fix xterm.js WidthCache measurement container causing horizontal scrollbar */
|
||||||
|
/* xterm creates this on document.body - move it off-screen horizontally too */
|
||||||
|
body > div[style*="top: -50000px"][style*="width: 50000px"] {
|
||||||
|
left: -50000px !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.id = styleId;
|
style.id = styleId;
|
||||||
style.textContent = cssText;
|
style.textContent = cssText + xtermMeasurementFix;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user