mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-20 15:18:08 +02:00
[Editor] Allow the user to add and save an alt-text for images (bug 1844952)
This commit is contained in:
parent
daae6589b6
commit
c12049db07
8 changed files with 316 additions and 8 deletions
190
web/alt_text_manager.js
Normal file
190
web/alt_text_manager.js
Normal file
|
@ -0,0 +1,190 @@
|
|||
/* Copyright 2023 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class AltTextManager {
|
||||
#boundUpdateUIState = this.#updateUIState.bind(this);
|
||||
|
||||
#boundSetPosition = this.#setPosition.bind(this);
|
||||
|
||||
#currentEditor = null;
|
||||
|
||||
#dialog;
|
||||
|
||||
#eventBus = null;
|
||||
|
||||
#optionDescription;
|
||||
|
||||
#optionDecorative;
|
||||
|
||||
#overlayManager;
|
||||
|
||||
#saveButton;
|
||||
|
||||
#textarea;
|
||||
|
||||
#uiManager;
|
||||
|
||||
constructor(
|
||||
{
|
||||
dialog,
|
||||
optionDescription,
|
||||
optionDecorative,
|
||||
textarea,
|
||||
cancelButton,
|
||||
saveButton,
|
||||
},
|
||||
overlayManager,
|
||||
eventBus
|
||||
) {
|
||||
this.#dialog = dialog;
|
||||
this.#optionDescription = optionDescription;
|
||||
this.#optionDecorative = optionDecorative;
|
||||
this.#textarea = textarea;
|
||||
this.#saveButton = saveButton;
|
||||
this.#overlayManager = overlayManager;
|
||||
this.#eventBus = eventBus;
|
||||
|
||||
dialog.addEventListener("close", this.#close.bind(this));
|
||||
cancelButton.addEventListener("click", this.#finish.bind(this));
|
||||
saveButton.addEventListener("click", this.#save.bind(this));
|
||||
optionDescription.addEventListener("change", this.#boundUpdateUIState);
|
||||
optionDecorative.addEventListener("change", this.#boundUpdateUIState);
|
||||
textarea.addEventListener("input", this.#boundUpdateUIState);
|
||||
|
||||
this.#overlayManager.register(dialog);
|
||||
}
|
||||
|
||||
async editAltText(uiManager, editor) {
|
||||
if (this.#currentEditor || !editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { altText, decorative } = editor.altTextData;
|
||||
if (decorative === true) {
|
||||
this.#optionDecorative.checked = true;
|
||||
this.#optionDescription.checked = false;
|
||||
} else {
|
||||
this.#optionDecorative.checked = false;
|
||||
this.#optionDescription.checked = true;
|
||||
}
|
||||
this.#textarea.value = altText?.trim() || "";
|
||||
this.#updateUIState();
|
||||
|
||||
this.#currentEditor = editor;
|
||||
this.#uiManager = uiManager;
|
||||
this.#uiManager.removeKeyboardManager();
|
||||
this.#eventBus._on("resize", this.#boundSetPosition);
|
||||
|
||||
try {
|
||||
await this.#overlayManager.open(this.#dialog);
|
||||
this.#setPosition();
|
||||
} catch (ex) {
|
||||
this.#close();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
#setPosition() {
|
||||
if (!this.#currentEditor) {
|
||||
return;
|
||||
}
|
||||
const dialog = this.#dialog;
|
||||
const { style } = dialog;
|
||||
const { innerWidth: windowW, innerHeight: windowH } = window;
|
||||
const { width: dialogW, height: dialogH } = dialog.getBoundingClientRect();
|
||||
const { x, y, width, height } = this.#currentEditor.getClientDimensions();
|
||||
const MARGIN = 10;
|
||||
const isLTR = this.#uiManager.direction === "ltr";
|
||||
|
||||
let left = null;
|
||||
let top = Math.max(0, y - MARGIN);
|
||||
top += Math.min(windowH - (top + dialogH), 0);
|
||||
|
||||
if (isLTR) {
|
||||
// Prefer to position the dialog "after" (so on the right) the editor.
|
||||
if (x + width + MARGIN + dialogW < windowW) {
|
||||
left = x + width + MARGIN;
|
||||
} else if (x > dialogW + MARGIN) {
|
||||
left = x - dialogW - MARGIN;
|
||||
}
|
||||
} else if (x > dialogW + MARGIN) {
|
||||
left = x - dialogW - MARGIN;
|
||||
} else if (x + width + MARGIN + dialogW < windowW) {
|
||||
left = x + width + MARGIN;
|
||||
}
|
||||
|
||||
if (left === null) {
|
||||
top = null;
|
||||
left = Math.max(0, x - MARGIN);
|
||||
left += Math.min(windowW - (left + dialogW), 0);
|
||||
if (y > dialogH + MARGIN) {
|
||||
top = y - dialogH - MARGIN;
|
||||
} else if (y + height + MARGIN + dialogH < windowH) {
|
||||
top = y + height + MARGIN;
|
||||
}
|
||||
}
|
||||
|
||||
if (top !== null) {
|
||||
dialog.classList.add("positioned");
|
||||
if (isLTR) {
|
||||
style.left = `${left}px`;
|
||||
} else {
|
||||
style.right = `${windowW - left - dialogW}px`;
|
||||
}
|
||||
style.top = `${top}px`;
|
||||
} else {
|
||||
dialog.classList.remove("positioned");
|
||||
style.left = "";
|
||||
style.top = "";
|
||||
}
|
||||
}
|
||||
|
||||
#finish() {
|
||||
if (this.#dialog) {
|
||||
this.#overlayManager.close(this.#dialog);
|
||||
}
|
||||
}
|
||||
|
||||
#close() {
|
||||
this.#uiManager?.addKeyboardManager();
|
||||
this.#eventBus._off("resize", this.#boundSetPosition);
|
||||
this.#currentEditor = null;
|
||||
this.#uiManager = null;
|
||||
}
|
||||
|
||||
#updateUIState() {
|
||||
const hasAltText = !!this.#textarea.value.trim();
|
||||
const decorative = this.#optionDecorative.checked;
|
||||
|
||||
this.#textarea.disabled = decorative;
|
||||
this.#saveButton.disabled = !decorative && !hasAltText;
|
||||
}
|
||||
|
||||
#save() {
|
||||
this.#currentEditor.altTextData = {
|
||||
altText: this.#textarea.value.trim(),
|
||||
decorative: this.#optionDecorative.checked,
|
||||
};
|
||||
this.#finish();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#currentEditor = null;
|
||||
this.#uiManager = null;
|
||||
this.#finish();
|
||||
}
|
||||
}
|
||||
|
||||
export { AltTextManager };
|
|
@ -528,6 +528,15 @@
|
|||
&.done::before {
|
||||
mask-image: var(--alt-text-done-image);
|
||||
}
|
||||
|
||||
& .description {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#altTextDialog {
|
||||
|
@ -622,6 +631,10 @@
|
|||
color: var(--text-primary-color);
|
||||
box-shadow: var(--dialog-shadow);
|
||||
|
||||
&.positioned {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& #altTextContainer {
|
||||
width: 300px;
|
||||
height: fit-content;
|
||||
|
@ -728,6 +741,7 @@
|
|||
}
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ import {
|
|||
import { AppOptions, OptionKind } from "./app_options.js";
|
||||
import { AutomationEventBus, EventBus } from "./event_utils.js";
|
||||
import { LinkTarget, PDFLinkService } from "./pdf_link_service.js";
|
||||
import { AltTextManager } from "./alt_text_manager.js";
|
||||
import { AnnotationEditorParams } from "web-annotation_editor_params";
|
||||
import { OverlayManager } from "./overlay_manager.js";
|
||||
import { PasswordPrompt } from "./password_prompt.js";
|
||||
|
@ -505,6 +506,13 @@ const PDFViewerApplication = {
|
|||
foreground: AppOptions.get("pageColorsForeground"),
|
||||
}
|
||||
: null;
|
||||
const altTextManager = appConfig.altTextDialog
|
||||
? new AltTextManager(
|
||||
appConfig.altTextDialog,
|
||||
this.overlayManager,
|
||||
eventBus
|
||||
)
|
||||
: null;
|
||||
|
||||
const pdfViewer = new PDFViewer({
|
||||
container,
|
||||
|
@ -513,6 +521,7 @@ const PDFViewerApplication = {
|
|||
renderingQueue: pdfRenderingQueue,
|
||||
linkService: pdfLinkService,
|
||||
downloadManager,
|
||||
altTextManager,
|
||||
findController,
|
||||
scriptingManager:
|
||||
AppOptions.get("enableScripting") && pdfScriptingManager,
|
||||
|
|
|
@ -199,6 +199,8 @@ class PDFPageViewBuffer {
|
|||
class PDFViewer {
|
||||
#buffer = null;
|
||||
|
||||
#altTextManager = null;
|
||||
|
||||
#annotationEditorMode = AnnotationEditorType.NONE;
|
||||
|
||||
#annotationEditorUIManager = null;
|
||||
|
@ -261,6 +263,7 @@ class PDFViewer {
|
|||
this.linkService = options.linkService || new SimpleLinkService();
|
||||
this.downloadManager = options.downloadManager || null;
|
||||
this.findController = options.findController || null;
|
||||
this.#altTextManager = options.altTextManager || null;
|
||||
|
||||
if (this.findController) {
|
||||
this.findController.onIsPageVisible = pageNumber =>
|
||||
|
@ -854,6 +857,7 @@ class PDFViewer {
|
|||
this.#annotationEditorUIManager = new AnnotationEditorUIManager(
|
||||
this.container,
|
||||
this.viewer,
|
||||
this.#altTextManager,
|
||||
this.eventBus,
|
||||
pdfDocument,
|
||||
this.pageColors
|
||||
|
|
|
@ -157,6 +157,14 @@ function getViewerConfiguration() {
|
|||
linearized: document.getElementById("linearizedField"),
|
||||
},
|
||||
},
|
||||
altTextDialog: {
|
||||
dialog: document.getElementById("altTextDialog"),
|
||||
optionDescription: document.getElementById("descriptionButton"),
|
||||
optionDecorative: document.getElementById("decorativeButton"),
|
||||
textarea: document.getElementById("descriptionTextarea"),
|
||||
cancelButton: document.getElementById("altTextCancel"),
|
||||
saveButton: document.getElementById("altTextSave"),
|
||||
},
|
||||
annotationEditorParams: {
|
||||
editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"),
|
||||
editorFreeTextColor: document.getElementById("editorFreeTextColor"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue