Skip to content
6 changes: 1 addition & 5 deletions src/core/lzw_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ class LZWStream extends DecodeStream {
let estimatedDecodedSize = blockSize * 2;
let i, j, q;

const lzwState = this.lzwState;
if (!lzwState) {
return; // eof was found
}

const { lzwState } = this;
const earlyChange = lzwState.earlyChange;
let nextCode = lzwState.nextCode;
const dictionaryValues = lzwState.dictionaryValues;
Expand Down
4 changes: 4 additions & 0 deletions src/display/editor/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ class FloatingToolbar {
const editToolbar = (this.#toolbar = document.createElement("div"));
editToolbar.className = "editToolbar";
editToolbar.setAttribute("role", "toolbar");
// The toolbar is inserted in the text layer which is always LTR (see
// `.pdfViewer .page`), hence the direction must be set here in order to
// have `inset-inline-end` (see `show`) resolved against the UI direction.
editToolbar.dir = this.#uiManager.direction;

const signal = this.#uiManager._signal;
if (signal instanceof AbortSignal && !signal.aborted) {
Expand Down
42 changes: 42 additions & 0 deletions test/integration/highlight_editor_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,48 @@ describe("Highlight Editor", () => {
});
});

describe("Floating highlight button in a RTL locale", () => {
let pages;

beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
null,
null,
{ locale: "ar" }
);
});

afterEach(async () => {
await closePages(pages);
});

it("must check that the floating toolbar is next to the selected text", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const { x, y, width, height } = await getSpanRectFromText(
page,
1,
"Abstract"
);
await page.mouse.click(x + width / 2, y + height / 2, {
count: 2,
delay: 100,
});

const toolbarRect = await getRect(page, ".textLayer .editToolbar");

// In RTL, the left edge of the toolbar is aligned on the left edge
// of the selection (bug 2060032).
expect(toolbarRect.x)
.withContext(`In ${browserName}`)
.toBeCloseTo(x, 0);
})
);
});
});

describe("Text layer must have the focus before highlights", () => {
let pages;

Expand Down
97 changes: 61 additions & 36 deletions test/integration/reorganize_pages_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,27 +140,33 @@ async function drawInkLine(page, pageNumber) {
}

async function waitForHavingContents(page, expected) {
await page.evaluate(() => {
// Make sure all the pages will be visible.
window.PDFViewerApplication.pdfViewer.scrollMode = 2; /* = ScrollMode.WRAPPED = */
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: 0,
scaleFactor: 0.01,
});
});
return page.waitForFunction(
ex => {
const textLayers = document.querySelectorAll(".textLayer");
const buffer = [];
for (const [i, textLayer] of textLayers.entries()) {
const text = textLayer.textContent.trim();
buffer.push(typeof ex[i] === "string" ? text : parseInt(text, 10));
await page.waitForFunction(
length => {
const { pdfViewer } = window.PDFViewerApplication;
for (let i = 0; i < length; i++) {
if (!pdfViewer.getPageView(i)?.pdfPage) {
return false;
}
}
return ex.length === buffer.length && ex.every((v, i) => v === buffer[i]);
return true;
},
{},
expected
expected.length
);
const actual = await page.evaluate(async ex => {
const { pdfViewer } = window.PDFViewerApplication;
const contents = [];
for (let i = 0, ii = ex.length; i < ii; i++) {
const { items } = await pdfViewer.getPageView(i).pdfPage.getTextContent();
const text = items
.map(item => item.str ?? "")
.join("")
.trim();
contents.push(typeof ex[i] === "string" ? text : parseInt(text, 10));
}
return contents;
}, expected);
expect(actual).toEqual(expected);
}

async function waitForPageCanvasToHaveImage(page, pageNumber) {
Expand Down Expand Up @@ -2470,13 +2476,17 @@ describe("Reorganize Pages View", () => {
await waitForThumbnailVisible(page, 1);
const rect1 = await getRect(page, getThumbnailSelector(1));
const rect2 = await getRect(page, getThumbnailSelector(2));
// Stay clear of the bottom edge, where dragging can auto-scroll and
// move the page one position too far.
const yTranslation =
rect2.y + rect2.height / 2 - (rect1.y + rect1.height / 2) + 1;

// Move page 1 after page 2: mapping becomes [2, 1, 3, …, 17].
let handlePagesEdited = await waitForPagesEdited(page);
await dragAndDrop(
page,
getThumbnailSelector(1),
[[0, rect2.y - rect1.y + rect2.height / 2]],
[[0, yTranslation]],
10
);
let pageIndices = await awaitPromise(handlePagesEdited);
Expand Down Expand Up @@ -2897,27 +2907,40 @@ describe("Reorganize Pages View", () => {
// Both the original and the cloned annotation must now be in storage.
await waitForStorageEntries(page, 2);

const editorIds = await page.evaluate(() => {
// When its layer is rendered, a clone replaces its serialized storage
// entry with a real editor having a new id. The original id doesn't
// change, hence use it to tell the two entries apart.
const originalEditorId = await page.evaluate(() => {
const entries = Array.from(
window.PDFViewerApplication.pdfDocument.annotationStorage
);
return {
original: entries.find(([, editor]) => editor.pageIndex === 0)[0],
clone: entries.find(([, editor]) => editor.pageIndex === 2)[0],
};
return entries.find(([, editor]) => editor.pageIndex === 0)[0];
});

// Move the pasted copy before the original and verify that both
// stored page indices follow the new page order.
await movePages(page, [3], 0);
const editorPageIndices = await page.evaluate(ids => {
const storage =
window.PDFViewerApplication.pdfDocument.annotationStorage;
return {
original: storage.getRawValue(ids.original).pageIndex,
clone: storage.getRawValue(ids.clone).pageIndex,
};
}, editorIds);
const editorPageIndicesHandle = await page.waitForFunction(
id => {
const storage =
window.PDFViewerApplication.pdfDocument.annotationStorage;
const original = storage.getRawValue(id);
const clone = Array.from(storage).find(
([editorId]) => editorId !== id
)?.[1];
if (!original || !clone) {
return false;
}
return {
original: original.pageIndex,
clone: clone.pageIndex,
};
},
{},
originalEditorId
);
const editorPageIndices = await editorPageIndicesHandle.jsonValue();
await editorPageIndicesHandle.dispose();
expect(editorPageIndices)
.withContext(`In ${browserName}`)
.toEqual({ original: 1, clone: 0 });
Expand Down Expand Up @@ -3117,12 +3140,12 @@ describe("Reorganize Pages View", () => {
visible: true,
});

const currentThumbnailSelector =
'.thumbnailImageContainer[aria-current="page"]';
const countCurrentThumbnails = () =>
page.evaluate(
() =>
document.querySelectorAll(
'.thumbnailImageContainer[aria-current="page"]'
).length
page.$$eval(
currentThumbnailSelector,
thumbnails => thumbnails.length
);

// Copy page 1 and paste it after page 3.
Expand Down Expand Up @@ -3151,6 +3174,7 @@ describe("Reorganize Pages View", () => {
await waitAndClick(page, "#viewsManagerStatusActionCut");
await awaitPromise(handlePagesEdited);

await page.waitForSelector(currentThumbnailSelector);
expect(await countCurrentThumbnails())
.withContext(`In ${browserName}, after cut #${i + 1}`)
.toBe(1);
Expand All @@ -3162,6 +3186,7 @@ describe("Reorganize Pages View", () => {
await waitAndClick(page, "#viewsManagerStatusUndoButton");
await awaitPromise(handlePagesEdited);

await page.waitForSelector(currentThumbnailSelector);
expect(await countCurrentThumbnails())
.withContext(`In ${browserName}, after undo #${i + 1}`)
.toBe(1);
Expand Down
27 changes: 16 additions & 11 deletions test/integration/test_utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,18 +1133,23 @@ async function highlightSpan(
}

async function showViewsManager(page) {
const hasAnimations = await page.evaluate(
() => !window.matchMedia("(prefers-reduced-motion: reduce)").matches
);
const movingPromise = hasAnimations
? page.waitForSelector("#outerContainer.viewsManagerMoving", {
visible: true,
})
: Promise.resolve();
// Opening dispatches this event synchronously when animations are disabled,
// so install the listener before clicking the toggle button. With animations,
// it's dispatched once the transition has ended and the moving class has
// been removed.
const openedHandle = await createPromise(page, resolve => {
const { eventBus, viewsManager } = window.PDFViewerApplication;
const onResize = ({ source }) => {
if (source !== viewsManager) {
return;
}
eventBus.off("resize", onResize);
resolve();
};
eventBus.on("resize", onResize);
});
await page.click("#viewsManagerToggleButton");
if (hasAnimations) {
await movingPromise;
}
await awaitPromise(openedHandle);
await page.waitForSelector("#viewsManager", { visible: true });
await page.waitForSelector(
"#outerContainer:not(.viewsManagerMoving).viewsManagerOpen",
Expand Down
2 changes: 1 addition & 1 deletion web/menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ button.hasPopupMenu {
position: absolute;
inset-inline-start: var(--menuitem-gap);
top: 50%;
transform: translateY(-50%);
transform: translateY(-50%) scaleX(var(--dir-factor));
}

&:disabled {
Expand Down
5 changes: 3 additions & 2 deletions web/views_manager.css
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,8 @@
align-self: stretch;
justify-content: space-between;
width: auto;
padding: 12px 16px 12px 8px;
padding-block: 12px;
padding-inline: 8px 16px;

&:not(:has(#viewsManagerHeaderLabel ~ button:not([hidden])))::after {
/* If one of the following buttons is visible, hide the placeholder
Expand Down Expand Up @@ -372,7 +373,7 @@
display: inline-block;
width: 12px;
height: 12px;
margin-left: 8px;
margin-inline-start: 8px;
mask-repeat: no-repeat;
mask-position: center;
mask-image: var(--views-manager-button-arrow-icon);
Expand Down
Loading