Browse Source

:arrow_up: fix https://github.com/siyuan-note/siyuan/issues/6035

Vanessa 2 years ago
parent
commit
c3e7ad0f4c

+ 135 - 0
app/src/asset/pdf/annotation_editor_layer_builder.js

@@ -0,0 +1,135 @@
+/* Copyright 2022 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.
+ */
+
+/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
+// eslint-disable-next-line max-len
+/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
+/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
+// eslint-disable-next-line max-len
+/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
+// eslint-disable-next-line max-len
+/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
+// eslint-disable-next-line max-len
+/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
+/** @typedef {import("./interfaces").IL10n} IL10n */
+
+import { AnnotationEditorLayer } from "./pdfjs";
+import { NullL10n } from "./l10n_utils.js";
+
+/**
+ * @typedef {Object} AnnotationEditorLayerBuilderOptions
+ * @property {number} mode - Editor mode
+ * @property {HTMLDivElement} pageDiv
+ * @property {PDFPageProxy} pdfPage
+ * @property {TextAccessibilityManager} accessibilityManager
+ * @property {AnnotationStorage} annotationStorage
+ * @property {IL10n} l10n - Localization service.
+ * @property {AnnotationEditorUIManager} uiManager
+ */
+
+class AnnotationEditorLayerBuilder {
+  #uiManager;
+
+  /**
+   * @param {AnnotationEditorLayerBuilderOptions} options
+   */
+  constructor(options) {
+    this.pageDiv = options.pageDiv;
+    this.pdfPage = options.pdfPage;
+    this.annotationStorage = options.annotationStorage || null;
+    this.accessibilityManager = options.accessibilityManager;
+    this.l10n = options.l10n || NullL10n;
+    this.annotationEditorLayer = null;
+    this.div = null;
+    this._cancelled = false;
+    this.#uiManager = options.uiManager;
+  }
+
+  /**
+   * @param {PageViewport} viewport
+   * @param {string} intent (default value is 'display')
+   */
+  async render(viewport, intent = "display") {
+    if (intent !== "display") {
+      return;
+    }
+
+    if (this._cancelled) {
+      return;
+    }
+
+    const clonedViewport = viewport.clone({ dontFlip: true });
+    if (this.div) {
+      this.annotationEditorLayer.update({ viewport: clonedViewport });
+      this.show();
+      return;
+    }
+
+    // Create an AnnotationEditor layer div
+    this.div = document.createElement("div");
+    this.div.className = "annotationEditorLayer";
+    this.div.tabIndex = 0;
+    this.pageDiv.append(this.div);
+
+    this.annotationEditorLayer = new AnnotationEditorLayer({
+      uiManager: this.#uiManager,
+      div: this.div,
+      annotationStorage: this.annotationStorage,
+      accessibilityManager: this.accessibilityManager,
+      pageIndex: this.pdfPage._pageIndex,
+      l10n: this.l10n,
+      viewport: clonedViewport,
+    });
+
+    const parameters = {
+      viewport: clonedViewport,
+      div: this.div,
+      annotations: null,
+      intent,
+    };
+
+    this.annotationEditorLayer.render(parameters);
+  }
+
+  cancel() {
+    this._cancelled = true;
+    this.destroy();
+  }
+
+  hide() {
+    if (!this.div) {
+      return;
+    }
+    this.div.hidden = true;
+  }
+
+  show() {
+    if (!this.div) {
+      return;
+    }
+    this.div.hidden = false;
+  }
+
+  destroy() {
+    if (!this.div) {
+      return;
+    }
+    this.pageDiv = null;
+    this.annotationEditorLayer.destroy();
+    this.div.remove();
+  }
+}
+
+export { AnnotationEditorLayerBuilder };

+ 95 - 0
app/src/asset/pdf/annotation_editor_params.js

@@ -0,0 +1,95 @@
+/* Copyright 2022 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.
+ */
+
+import { AnnotationEditorParamsType } from "./pdfjs";
+
+class AnnotationEditorParams {
+  /**
+   * @param {AnnotationEditorParamsOptions} options
+   * @param {EventBus} eventBus
+   */
+  constructor(options, eventBus) {
+    this.eventBus = eventBus;
+    this.#bindListeners(options);
+  }
+
+  #bindListeners({
+    editorFreeTextFontSize,
+    editorFreeTextColor,
+    editorInkColor,
+    editorInkThickness,
+    editorInkOpacity,
+  }) {
+    editorFreeTextFontSize.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: AnnotationEditorParamsType.FREETEXT_SIZE,
+        value: editorFreeTextFontSize.valueAsNumber,
+      });
+    });
+    editorFreeTextColor.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: AnnotationEditorParamsType.FREETEXT_COLOR,
+        value: editorFreeTextColor.value,
+      });
+    });
+    editorInkColor.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: AnnotationEditorParamsType.INK_COLOR,
+        value: editorInkColor.value,
+      });
+    });
+    editorInkThickness.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: AnnotationEditorParamsType.INK_THICKNESS,
+        value: editorInkThickness.valueAsNumber,
+      });
+    });
+    editorInkOpacity.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: AnnotationEditorParamsType.INK_OPACITY,
+        value: editorInkOpacity.valueAsNumber,
+      });
+    });
+
+    this.eventBus._on("annotationeditorparamschanged", evt => {
+      for (const [type, value] of evt.details) {
+        switch (type) {
+          case AnnotationEditorParamsType.FREETEXT_SIZE:
+            editorFreeTextFontSize.value = value;
+            break;
+          case AnnotationEditorParamsType.FREETEXT_COLOR:
+            editorFreeTextColor.value = value;
+            break;
+          case AnnotationEditorParamsType.INK_COLOR:
+            editorInkColor.value = value;
+            break;
+          case AnnotationEditorParamsType.INK_THICKNESS:
+            editorInkThickness.value = value;
+            break;
+          case AnnotationEditorParamsType.INK_OPACITY:
+            editorInkOpacity.value = value;
+            break;
+        }
+      }
+    });
+  }
+}
+
+export { AnnotationEditorParams };

+ 253 - 0
app/src/asset/pdf/text_accessibility.js

@@ -0,0 +1,253 @@
+/* Copyright 2022 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.
+ */
+
+import { binarySearchFirstItem } from "./ui_utils.js";
+
+/**
+ * This class aims to provide some methods:
+ *  - to reorder elements in the DOM with respect to the visual order;
+ *  - to create a link, using aria-owns, between spans in the textLayer and
+ *    annotations in the annotationLayer. The goal is to help to know
+ *    where the annotations are in the text flow.
+ */
+class TextAccessibilityManager {
+  #enabled = false;
+
+  #textChildren = null;
+
+  #textNodes = new Map();
+
+  #waitingElements = new Map();
+
+  setTextMapping(textDivs) {
+    this.#textChildren = textDivs;
+  }
+
+  /**
+   * Compare the positions of two elements, it must correspond to
+   * the visual ordering.
+   *
+   * @param {HTMLElement} e1
+   * @param {HTMLElement} e2
+   * @returns {number}
+   */
+  static #compareElementPositions(e1, e2) {
+    const rect1 = e1.getBoundingClientRect();
+    const rect2 = e2.getBoundingClientRect();
+
+    if (rect1.width === 0 && rect1.height === 0) {
+      return +1;
+    }
+
+    if (rect2.width === 0 && rect2.height === 0) {
+      return -1;
+    }
+
+    const top1 = rect1.y;
+    const bot1 = rect1.y + rect1.height;
+    const mid1 = rect1.y + rect1.height / 2;
+
+    const top2 = rect2.y;
+    const bot2 = rect2.y + rect2.height;
+    const mid2 = rect2.y + rect2.height / 2;
+
+    if (mid1 <= top2 && mid2 >= bot1) {
+      return -1;
+    }
+
+    if (mid2 <= top1 && mid1 >= bot2) {
+      return +1;
+    }
+
+    const centerX1 = rect1.x + rect1.width / 2;
+    const centerX2 = rect2.x + rect2.width / 2;
+
+    return centerX1 - centerX2;
+  }
+
+  /**
+   * Function called when the text layer has finished rendering.
+   */
+  enable() {
+    if (this.#enabled) {
+      throw new Error("TextAccessibilityManager is already enabled.");
+    }
+    if (!this.#textChildren) {
+      throw new Error("Text divs and strings have not been set.");
+    }
+
+    this.#enabled = true;
+    this.#textChildren = this.#textChildren.slice();
+    this.#textChildren.sort(TextAccessibilityManager.#compareElementPositions);
+
+    if (this.#textNodes.size > 0) {
+      // Some links have been made before this manager has been disabled, hence
+      // we restore them.
+      const textChildren = this.#textChildren;
+      for (const [id, nodeIndex] of this.#textNodes) {
+        const element = document.getElementById(id);
+        if (!element) {
+          // If the page was *fully* reset the element no longer exists, and it
+          // will be re-inserted later (i.e. when the annotationLayer renders).
+          this.#textNodes.delete(id);
+          continue;
+        }
+        this.#addIdToAriaOwns(id, textChildren[nodeIndex]);
+      }
+    }
+
+    for (const [element, isRemovable] of this.#waitingElements) {
+      this.addPointerInTextLayer(element, isRemovable);
+    }
+    this.#waitingElements.clear();
+  }
+
+  disable() {
+    if (!this.#enabled) {
+      return;
+    }
+
+    // Don't clear this.#textNodes which is used to rebuild the aria-owns
+    // in case it's re-enabled at some point.
+
+    this.#waitingElements.clear();
+    this.#textChildren = null;
+    this.#enabled = false;
+  }
+
+  /**
+   * Remove an aria-owns id from a node in the text layer.
+   * @param {HTMLElement} element
+   */
+  removePointerInTextLayer(element) {
+    if (!this.#enabled) {
+      this.#waitingElements.delete(element);
+      return;
+    }
+
+    const children = this.#textChildren;
+    if (!children || children.length === 0) {
+      return;
+    }
+
+    const { id } = element;
+    const nodeIndex = this.#textNodes.get(id);
+    if (nodeIndex === undefined) {
+      return;
+    }
+
+    const node = children[nodeIndex];
+
+    this.#textNodes.delete(id);
+    let owns = node.getAttribute("aria-owns");
+    if (owns?.includes(id)) {
+      owns = owns
+        .split(" ")
+        .filter(x => x !== id)
+        .join(" ");
+      if (owns) {
+        node.setAttribute("aria-owns", owns);
+      } else {
+        node.removeAttribute("aria-owns");
+        node.setAttribute("role", "presentation");
+      }
+    }
+  }
+
+  #addIdToAriaOwns(id, node) {
+    const owns = node.getAttribute("aria-owns");
+    if (!owns?.includes(id)) {
+      node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id);
+    }
+    node.removeAttribute("role");
+  }
+
+  /**
+   * Find the text node which is the nearest and add an aria-owns attribute
+   * in order to correctly position this editor in the text flow.
+   * @param {HTMLElement} element
+   * @param {boolean} isRemovable
+   */
+  addPointerInTextLayer(element, isRemovable) {
+    const { id } = element;
+    if (!id) {
+      return;
+    }
+
+    if (!this.#enabled) {
+      // The text layer needs to be there, so we postpone the association.
+      this.#waitingElements.set(element, isRemovable);
+      return;
+    }
+
+    if (isRemovable) {
+      this.removePointerInTextLayer(element);
+    }
+
+    const children = this.#textChildren;
+    if (!children || children.length === 0) {
+      return;
+    }
+
+    const index = binarySearchFirstItem(
+      children,
+      node =>
+        TextAccessibilityManager.#compareElementPositions(element, node) < 0
+    );
+
+    const nodeIndex = Math.max(0, index - 1);
+    this.#addIdToAriaOwns(id, children[nodeIndex]);
+    this.#textNodes.set(id, nodeIndex);
+  }
+
+  /**
+   * Move a div in the DOM in order to respect the visual order.
+   * @param {HTMLDivElement} element
+   */
+  moveElementInDOM(container, element, contentElement, isRemovable) {
+    this.addPointerInTextLayer(contentElement, isRemovable);
+
+    if (!container.hasChildNodes()) {
+      container.append(element);
+      return;
+    }
+
+    const children = Array.from(container.childNodes).filter(
+      node => node !== element
+    );
+
+    if (children.length === 0) {
+      return;
+    }
+
+    const elementToCompare = contentElement || element;
+    const index = binarySearchFirstItem(
+      children,
+      node =>
+        TextAccessibilityManager.#compareElementPositions(
+          elementToCompare,
+          node
+        ) < 0
+    );
+
+    if (index === 0) {
+      children[0].before(element);
+    } else {
+      children[index - 1].after(element);
+    }
+  }
+}
+
+export { TextAccessibilityManager };