2023-09-19 07:03:49 +09:00
|
|
|
/* 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.
|
|
|
|
*/
|
|
|
|
|
2023-09-22 01:40:35 +09:00
|
|
|
import { DOMSVGFactory, shadow } from "pdfjs-lib";
|
2023-09-20 19:45:24 +09:00
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
class AltTextManager {
|
|
|
|
#boundUpdateUIState = this.#updateUIState.bind(this);
|
|
|
|
|
|
|
|
#boundSetPosition = this.#setPosition.bind(this);
|
|
|
|
|
2023-09-20 19:45:24 +09:00
|
|
|
#boundPointerDown = this.#pointerDown.bind(this);
|
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
#currentEditor = null;
|
|
|
|
|
2023-09-20 19:45:24 +09:00
|
|
|
#cancelButton;
|
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
#dialog;
|
|
|
|
|
2023-09-20 19:45:24 +09:00
|
|
|
#eventBus;
|
|
|
|
|
|
|
|
#hasUsedPointer = false;
|
2023-09-19 07:03:49 +09:00
|
|
|
|
|
|
|
#optionDescription;
|
|
|
|
|
|
|
|
#optionDecorative;
|
|
|
|
|
|
|
|
#overlayManager;
|
|
|
|
|
|
|
|
#saveButton;
|
|
|
|
|
|
|
|
#textarea;
|
|
|
|
|
|
|
|
#uiManager;
|
|
|
|
|
2023-09-20 19:45:24 +09:00
|
|
|
#previousAltText = null;
|
|
|
|
|
2023-09-22 01:40:35 +09:00
|
|
|
#svgElement = null;
|
|
|
|
|
|
|
|
#rectElement = null;
|
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
constructor(
|
|
|
|
{
|
|
|
|
dialog,
|
|
|
|
optionDescription,
|
|
|
|
optionDecorative,
|
|
|
|
textarea,
|
|
|
|
cancelButton,
|
|
|
|
saveButton,
|
|
|
|
},
|
|
|
|
overlayManager,
|
|
|
|
eventBus
|
|
|
|
) {
|
|
|
|
this.#dialog = dialog;
|
|
|
|
this.#optionDescription = optionDescription;
|
|
|
|
this.#optionDecorative = optionDecorative;
|
|
|
|
this.#textarea = textarea;
|
2023-09-20 19:45:24 +09:00
|
|
|
this.#cancelButton = cancelButton;
|
2023-09-19 07:03:49 +09:00
|
|
|
this.#saveButton = saveButton;
|
|
|
|
this.#overlayManager = overlayManager;
|
|
|
|
this.#eventBus = eventBus;
|
|
|
|
|
|
|
|
dialog.addEventListener("close", this.#close.bind(this));
|
2023-09-20 19:45:24 +09:00
|
|
|
cancelButton.addEventListener("click", this.#cancel.bind(this));
|
2023-09-19 07:03:49 +09:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-09-20 19:45:24 +09:00
|
|
|
get _elements() {
|
|
|
|
return shadow(this, "_elements", [
|
|
|
|
this.#optionDescription,
|
|
|
|
this.#optionDecorative,
|
|
|
|
this.#textarea,
|
|
|
|
this.#saveButton,
|
|
|
|
this.#cancelButton,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2023-09-22 01:40:35 +09:00
|
|
|
#createSVGElement() {
|
|
|
|
if (this.#svgElement) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We create a mask to add to the dialog backdrop: the idea is to have a
|
|
|
|
// darken background everywhere except on the editor to clearly see the
|
|
|
|
// picture to describe.
|
|
|
|
|
|
|
|
const svgFactory = new DOMSVGFactory();
|
|
|
|
const svg = (this.#svgElement = svgFactory.createElement("svg"));
|
|
|
|
svg.setAttribute("width", "0");
|
|
|
|
svg.setAttribute("height", "0");
|
|
|
|
const defs = svgFactory.createElement("defs");
|
|
|
|
svg.append(defs);
|
|
|
|
const mask = svgFactory.createElement("mask");
|
|
|
|
defs.append(mask);
|
|
|
|
mask.setAttribute("id", "alttext-manager-mask");
|
|
|
|
mask.setAttribute("maskContentUnits", "objectBoundingBox");
|
|
|
|
let rect = svgFactory.createElement("rect");
|
|
|
|
mask.append(rect);
|
|
|
|
rect.setAttribute("fill", "white");
|
|
|
|
rect.setAttribute("width", "1");
|
|
|
|
rect.setAttribute("height", "1");
|
|
|
|
rect.setAttribute("x", "0");
|
|
|
|
rect.setAttribute("y", "0");
|
|
|
|
|
|
|
|
rect = this.#rectElement = svgFactory.createElement("rect");
|
|
|
|
mask.append(rect);
|
|
|
|
rect.setAttribute("fill", "black");
|
|
|
|
this.#dialog.append(svg);
|
|
|
|
}
|
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
async editAltText(uiManager, editor) {
|
|
|
|
if (this.#currentEditor || !editor) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-22 01:40:35 +09:00
|
|
|
this.#createSVGElement();
|
|
|
|
|
2023-09-20 19:45:24 +09:00
|
|
|
this.#hasUsedPointer = false;
|
|
|
|
for (const element of this._elements) {
|
|
|
|
element.addEventListener("pointerdown", this.#boundPointerDown);
|
|
|
|
}
|
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
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;
|
|
|
|
}
|
2023-09-20 19:45:24 +09:00
|
|
|
this.#previousAltText = this.#textarea.value = altText?.trim() || "";
|
2023-09-19 07:03:49 +09:00
|
|
|
this.#updateUIState();
|
|
|
|
|
|
|
|
this.#currentEditor = editor;
|
|
|
|
this.#uiManager = uiManager;
|
2023-09-20 05:41:47 +09:00
|
|
|
this.#uiManager.removeEditListeners();
|
2023-09-19 07:03:49 +09:00
|
|
|
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";
|
|
|
|
|
2023-09-22 01:40:35 +09:00
|
|
|
this.#rectElement.setAttribute("width", `${width / windowW}`);
|
|
|
|
this.#rectElement.setAttribute("height", `${height / windowH}`);
|
|
|
|
this.#rectElement.setAttribute("x", `${x / windowW}`);
|
|
|
|
this.#rectElement.setAttribute("y", `${y / windowH}`);
|
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
let left = null;
|
2023-09-21 01:12:12 +09:00
|
|
|
let top = y;
|
2023-09-19 07:03:49 +09:00
|
|
|
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;
|
2023-09-21 01:12:12 +09:00
|
|
|
left = x;
|
2023-09-19 07:03:49 +09:00
|
|
|
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() {
|
2023-09-20 14:45:33 +09:00
|
|
|
if (this.#overlayManager.active === this.#dialog) {
|
2023-09-19 07:03:49 +09:00
|
|
|
this.#overlayManager.close(this.#dialog);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-20 19:45:24 +09:00
|
|
|
#cancel() {
|
|
|
|
this.#eventBus.dispatch("reporttelemetry", {
|
|
|
|
source: this,
|
|
|
|
details: {
|
|
|
|
type: "editing",
|
|
|
|
subtype: this.#currentEditor.editorType,
|
|
|
|
data: {
|
|
|
|
action: "alt_text_cancel",
|
|
|
|
alt_text_keyboard: !this.#hasUsedPointer,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
this.#finish();
|
|
|
|
}
|
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
#close() {
|
2023-09-20 19:45:24 +09:00
|
|
|
this.#removePointerDownListeners();
|
2023-09-20 05:41:47 +09:00
|
|
|
this.#uiManager?.addEditListeners();
|
2023-09-19 07:03:49 +09:00
|
|
|
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() {
|
2023-09-20 19:45:24 +09:00
|
|
|
const altText = this.#textarea.value.trim();
|
|
|
|
const decorative = this.#optionDecorative.checked;
|
2023-09-19 07:03:49 +09:00
|
|
|
this.#currentEditor.altTextData = {
|
2023-09-20 19:45:24 +09:00
|
|
|
altText,
|
|
|
|
decorative,
|
2023-09-19 07:03:49 +09:00
|
|
|
};
|
2023-09-20 19:45:24 +09:00
|
|
|
this.#eventBus.dispatch("reporttelemetry", {
|
|
|
|
source: this,
|
|
|
|
details: {
|
|
|
|
type: "editing",
|
|
|
|
subtype: this.#currentEditor.editorType,
|
|
|
|
data: {
|
|
|
|
action: "alt_text_save",
|
|
|
|
alt_text_description: !!altText,
|
|
|
|
alt_text_edit:
|
2023-09-21 15:29:51 +09:00
|
|
|
!!this.#previousAltText && this.#previousAltText !== altText,
|
2023-09-20 19:45:24 +09:00
|
|
|
alt_text_decorative: decorative,
|
|
|
|
alt_text_keyboard: !this.#hasUsedPointer,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
2023-09-19 07:03:49 +09:00
|
|
|
this.#finish();
|
|
|
|
}
|
|
|
|
|
2023-09-20 19:45:24 +09:00
|
|
|
#pointerDown() {
|
|
|
|
this.#hasUsedPointer = true;
|
|
|
|
this.#removePointerDownListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
#removePointerDownListeners() {
|
|
|
|
for (const element of this._elements) {
|
|
|
|
element.removeEventListener("pointerdown", this.#boundPointerDown);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-19 07:03:49 +09:00
|
|
|
destroy() {
|
|
|
|
this.#currentEditor = null;
|
|
|
|
this.#uiManager = null;
|
|
|
|
this.#finish();
|
2023-09-22 01:40:35 +09:00
|
|
|
this.#svgElement?.remove();
|
|
|
|
this.#svgElement = this.#rectElement = null;
|
2023-09-19 07:03:49 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export { AltTextManager };
|