[edition] Add a FreeText editor (#14970)
- add a basic UI to edit some text in a pdf; - an editor can be moved, suppressed, cut, copied, pasted, selected; - add an undo/redo manager.
This commit is contained in:
		
							parent
							
								
									1ac33c960d
								
							
						
					
					
						commit
						be1aa11986
					
				@ -160,6 +160,10 @@
 | 
			
		||||
      ],
 | 
			
		||||
      "default": 2
 | 
			
		||||
    },
 | 
			
		||||
    "annotationEditorEnabled": {
 | 
			
		||||
      "type": "boolean",
 | 
			
		||||
      "default": false
 | 
			
		||||
    },
 | 
			
		||||
    "enablePermissions": {
 | 
			
		||||
      "type": "boolean",
 | 
			
		||||
      "default": false
 | 
			
		||||
 | 
			
		||||
@ -249,3 +249,10 @@ password_cancel=Cancel
 | 
			
		||||
printing_not_supported=Warning: Printing is not fully supported by this browser.
 | 
			
		||||
printing_not_ready=Warning: The PDF is not fully loaded for printing.
 | 
			
		||||
web_fonts_disabled=Web fonts are disabled: unable to use embedded PDF fonts.
 | 
			
		||||
 | 
			
		||||
# Editor
 | 
			
		||||
editor_none.title=Disable Annotation Editing
 | 
			
		||||
editor_none_label=Disable Editing
 | 
			
		||||
freetext_default_content=Enter some text…
 | 
			
		||||
editor_free_text.title=Add FreeText Annotation
 | 
			
		||||
editor_free_text_label=FreeText Annotation
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { AnnotationEditor } from "./editor/editor.js";
 | 
			
		||||
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
 | 
			
		||||
import { objectFromMap } from "../shared/util.js";
 | 
			
		||||
 | 
			
		||||
@ -62,6 +63,14 @@ class AnnotationStorage {
 | 
			
		||||
    return this._storage.get(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove a value from the storage.
 | 
			
		||||
   * @param {string} key
 | 
			
		||||
   */
 | 
			
		||||
  removeKey(key) {
 | 
			
		||||
    this._storage.delete(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set the value for a given key
 | 
			
		||||
   *
 | 
			
		||||
@ -123,7 +132,19 @@ class AnnotationStorage {
 | 
			
		||||
   * @ignore
 | 
			
		||||
   */
 | 
			
		||||
  get serializable() {
 | 
			
		||||
    return this._storage.size > 0 ? this._storage : null;
 | 
			
		||||
    if (this._storage.size === 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const clone = new Map();
 | 
			
		||||
    for (const [key, value] of this._storage) {
 | 
			
		||||
      if (value instanceof AnnotationEditor) {
 | 
			
		||||
        clone.set(key, value.serialize());
 | 
			
		||||
      } else {
 | 
			
		||||
        clone.set(key, value);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return clone;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										432
									
								
								src/display/editor/annotation_editor_layer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										432
									
								
								src/display/editor/annotation_editor_layer.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,432 @@
 | 
			
		||||
/* 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("./editor.js").AnnotationEditor} AnnotationEditor */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
 | 
			
		||||
/** @typedef {import("../../web/interfaces").IL10n} IL10n */
 | 
			
		||||
 | 
			
		||||
import { AnnotationEditorType, Util } from "../../shared/util.js";
 | 
			
		||||
import { bindEvents, KeyboardManager } from "./tools.js";
 | 
			
		||||
import { FreeTextEditor } from "./freetext.js";
 | 
			
		||||
import { PixelsPerInch } from "../display_utils.js";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {Object} AnnotationEditorLayerOptions
 | 
			
		||||
 * @property {Object} mode
 | 
			
		||||
 * @property {HTMLDivElement} div
 | 
			
		||||
 * @property {AnnotationEditorUIManager} uiManager
 | 
			
		||||
 * @property {boolean} enabled
 | 
			
		||||
 * @property {AnnotationStorage} annotationStorag
 | 
			
		||||
 * @property {number} pageIndex
 | 
			
		||||
 * @property {IL10n} l10n
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manage all the different editors on a page.
 | 
			
		||||
 */
 | 
			
		||||
class AnnotationEditorLayer {
 | 
			
		||||
  #boundClick;
 | 
			
		||||
 | 
			
		||||
  #editors = new Map();
 | 
			
		||||
 | 
			
		||||
  #uiManager;
 | 
			
		||||
 | 
			
		||||
  static _l10nInitialized = false;
 | 
			
		||||
 | 
			
		||||
  static _keyboardManager = new KeyboardManager([
 | 
			
		||||
    [["ctrl+a", "mac+meta+a"], AnnotationEditorLayer.prototype.selectAll],
 | 
			
		||||
    [["ctrl+c", "mac+meta+c"], AnnotationEditorLayer.prototype.copy],
 | 
			
		||||
    [["ctrl+v", "mac+meta+v"], AnnotationEditorLayer.prototype.paste],
 | 
			
		||||
    [["ctrl+x", "mac+meta+x"], AnnotationEditorLayer.prototype.cut],
 | 
			
		||||
    [["ctrl+z", "mac+meta+z"], AnnotationEditorLayer.prototype.undo],
 | 
			
		||||
    [
 | 
			
		||||
      ["ctrl+y", "ctrl+shift+Z", "mac+meta+shift+Z"],
 | 
			
		||||
      AnnotationEditorLayer.prototype.redo,
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      [
 | 
			
		||||
        "ctrl+Backspace",
 | 
			
		||||
        "mac+Backspace",
 | 
			
		||||
        "mac+ctrl+Backspace",
 | 
			
		||||
        "mac+alt+Backspace",
 | 
			
		||||
      ],
 | 
			
		||||
      AnnotationEditorLayer.prototype.suppress,
 | 
			
		||||
    ],
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {AnnotationEditorLayerOptions} options
 | 
			
		||||
   */
 | 
			
		||||
  constructor(options) {
 | 
			
		||||
    if (!AnnotationEditorLayer._l10nInitialized) {
 | 
			
		||||
      AnnotationEditorLayer._l10nInitialized = true;
 | 
			
		||||
      FreeTextEditor.setL10n(options.l10n);
 | 
			
		||||
    }
 | 
			
		||||
    this.#uiManager = options.uiManager;
 | 
			
		||||
    this.annotationStorage = options.annotationStorage;
 | 
			
		||||
    this.pageIndex = options.pageIndex;
 | 
			
		||||
    this.div = options.div;
 | 
			
		||||
    this.#boundClick = this.click.bind(this);
 | 
			
		||||
 | 
			
		||||
    for (const editor of this.#uiManager.getEditors(options.pageIndex)) {
 | 
			
		||||
      this.add(editor);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.#uiManager.addLayer(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add some commands into the CommandManager (undo/redo stuff).
 | 
			
		||||
   * @param {function} cmd
 | 
			
		||||
   * @param {function} undo
 | 
			
		||||
   */
 | 
			
		||||
  addCommands(cmd, undo) {
 | 
			
		||||
    this.#uiManager.addCommands(cmd, undo);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Undo the last command.
 | 
			
		||||
   */
 | 
			
		||||
  undo() {
 | 
			
		||||
    this.#uiManager.undo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Redo the last command.
 | 
			
		||||
   */
 | 
			
		||||
  redo() {
 | 
			
		||||
    this.#uiManager.redo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Suppress the selected editor or all editors.
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  suppress() {
 | 
			
		||||
    this.#uiManager.suppress();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Copy the selected editor.
 | 
			
		||||
   */
 | 
			
		||||
  copy() {
 | 
			
		||||
    this.#uiManager.copy();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Cut the selected editor.
 | 
			
		||||
   */
 | 
			
		||||
  cut() {
 | 
			
		||||
    this.#uiManager.cut(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Paste a previously copied editor.
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  paste() {
 | 
			
		||||
    this.#uiManager.paste(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Select all the editors.
 | 
			
		||||
   */
 | 
			
		||||
  selectAll() {
 | 
			
		||||
    this.#uiManager.selectAll();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Unselect all the editors.
 | 
			
		||||
   */
 | 
			
		||||
  unselectAll() {
 | 
			
		||||
    this.#uiManager.unselectAll();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Enable pointer events on the main div in order to enable
 | 
			
		||||
   * editor creation.
 | 
			
		||||
   */
 | 
			
		||||
  enable() {
 | 
			
		||||
    this.div.style.pointerEvents = "auto";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Disable editor creation.
 | 
			
		||||
   */
 | 
			
		||||
  disable() {
 | 
			
		||||
    this.div.style.pointerEvents = "none";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set the current editor.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   */
 | 
			
		||||
  setActiveEditor(editor) {
 | 
			
		||||
    if (editor) {
 | 
			
		||||
      this.unselectAll();
 | 
			
		||||
      this.div.removeEventListener("click", this.#boundClick);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.#uiManager.allowClick = false;
 | 
			
		||||
      this.div.addEventListener("click", this.#boundClick);
 | 
			
		||||
    }
 | 
			
		||||
    this.#uiManager.setActiveEditor(editor);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  attach(editor) {
 | 
			
		||||
    this.#editors.set(editor.id, editor);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  detach(editor) {
 | 
			
		||||
    this.#editors.delete(editor.id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove an editor.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   */
 | 
			
		||||
  remove(editor) {
 | 
			
		||||
    // Since we can undo a removal we need to keep the
 | 
			
		||||
    // parent property as it is, so don't null it!
 | 
			
		||||
 | 
			
		||||
    this.#uiManager.removeEditor(editor);
 | 
			
		||||
    this.detach(editor);
 | 
			
		||||
    this.annotationStorage.removeKey(editor.id);
 | 
			
		||||
    editor.div.remove();
 | 
			
		||||
    editor.isAttachedToDOM = false;
 | 
			
		||||
    if (this.#uiManager.isActive(editor) || this.#editors.size === 0) {
 | 
			
		||||
      this.setActiveEditor(null);
 | 
			
		||||
      this.#uiManager.allowClick = true;
 | 
			
		||||
      this.div.focus();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * An editor can have a different parent, for example after having
 | 
			
		||||
   * being dragged and droped from a page to another.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  #changeParent(editor) {
 | 
			
		||||
    if (editor.parent === this) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.attach(editor);
 | 
			
		||||
    editor.pageIndex = this.pageIndex;
 | 
			
		||||
    editor.parent.detach(editor);
 | 
			
		||||
    editor.parent = this;
 | 
			
		||||
    if (editor.div && editor.isAttachedToDOM) {
 | 
			
		||||
      editor.div.remove();
 | 
			
		||||
      this.div.appendChild(editor.div);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a new editor in the current view.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   */
 | 
			
		||||
  add(editor) {
 | 
			
		||||
    this.#changeParent(editor);
 | 
			
		||||
    this.annotationStorage.setValue(editor.id, editor);
 | 
			
		||||
    this.#uiManager.addEditor(editor);
 | 
			
		||||
    this.attach(editor);
 | 
			
		||||
 | 
			
		||||
    if (!editor.isAttachedToDOM) {
 | 
			
		||||
      const div = editor.render();
 | 
			
		||||
      this.div.appendChild(div);
 | 
			
		||||
      editor.isAttachedToDOM = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    editor.onceAdded();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add or rebuild depending if it has been removed or not.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   */
 | 
			
		||||
  addOrRebuild(editor) {
 | 
			
		||||
    if (editor.needsToBeRebuilt()) {
 | 
			
		||||
      editor.rebuild();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.add(editor);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a new editor and make this addition undoable.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   */
 | 
			
		||||
  addANewEditor(editor) {
 | 
			
		||||
    const cmd = () => {
 | 
			
		||||
      this.addOrRebuild(editor);
 | 
			
		||||
    };
 | 
			
		||||
    const undo = () => {
 | 
			
		||||
      editor.remove();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.addCommands(cmd, undo);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get an id for an editor.
 | 
			
		||||
   * @returns {string}
 | 
			
		||||
   */
 | 
			
		||||
  getNextId() {
 | 
			
		||||
    return this.#uiManager.getId();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a new editor
 | 
			
		||||
   * @param {Object} params
 | 
			
		||||
   * @returns {AnnotationEditor}
 | 
			
		||||
   */
 | 
			
		||||
  #createNewEditor(params) {
 | 
			
		||||
    switch (this.#uiManager.getMode()) {
 | 
			
		||||
      case AnnotationEditorType.FREETEXT:
 | 
			
		||||
        return new FreeTextEditor(params);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Mouseclick callback.
 | 
			
		||||
   * @param {MouseEvent} event
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  click(event) {
 | 
			
		||||
    if (!this.#uiManager.allowClick) {
 | 
			
		||||
      this.#uiManager.allowClick = true;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const id = this.getNextId();
 | 
			
		||||
    const editor = this.#createNewEditor({
 | 
			
		||||
      parent: this,
 | 
			
		||||
      id,
 | 
			
		||||
      x: event.offsetX,
 | 
			
		||||
      y: event.offsetY,
 | 
			
		||||
    });
 | 
			
		||||
    if (editor) {
 | 
			
		||||
      this.addANewEditor(editor);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Drag callback.
 | 
			
		||||
   * @param {DragEvent} event
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  drop(event) {
 | 
			
		||||
    const id = event.dataTransfer.getData("text/plain");
 | 
			
		||||
    const editor = this.#uiManager.getEditor(id);
 | 
			
		||||
    if (!editor) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    event.dataTransfer.dropEffect = "move";
 | 
			
		||||
 | 
			
		||||
    this.#changeParent(editor);
 | 
			
		||||
 | 
			
		||||
    const rect = this.div.getBoundingClientRect();
 | 
			
		||||
    editor.setAt(
 | 
			
		||||
      event.clientX - rect.x - editor.mouseX,
 | 
			
		||||
      event.clientY - rect.y - editor.mouseY
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Dragover callback.
 | 
			
		||||
   * @param {DragEvent} event
 | 
			
		||||
   */
 | 
			
		||||
  dragover(event) {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Keydown callback.
 | 
			
		||||
   * @param {KeyboardEvent} event
 | 
			
		||||
   */
 | 
			
		||||
  keydown(event) {
 | 
			
		||||
    if (!this.#uiManager.getActive()?.shouldGetKeyboardEvents()) {
 | 
			
		||||
      AnnotationEditorLayer._keyboardManager.exec(this, event);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Destroy the main editor.
 | 
			
		||||
   */
 | 
			
		||||
  destroy() {
 | 
			
		||||
    for (const editor of this.#editors.values()) {
 | 
			
		||||
      editor.isAttachedToDOM = false;
 | 
			
		||||
      editor.div.remove();
 | 
			
		||||
      editor.parent = null;
 | 
			
		||||
      this.div = null;
 | 
			
		||||
    }
 | 
			
		||||
    this.#editors.clear();
 | 
			
		||||
    this.#uiManager.removeLayer(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Render the main editor.
 | 
			
		||||
   * @param {Object} parameters
 | 
			
		||||
   */
 | 
			
		||||
  render(parameters) {
 | 
			
		||||
    this.viewport = parameters.viewport;
 | 
			
		||||
    this.inverseViewportTransform = Util.inverseTransform(
 | 
			
		||||
      this.viewport.transform
 | 
			
		||||
    );
 | 
			
		||||
    bindEvents(this, this.div, ["dragover", "drop", "keydown"]);
 | 
			
		||||
    this.div.addEventListener("click", this.#boundClick);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Update the main editor.
 | 
			
		||||
   * @param {Object} parameters
 | 
			
		||||
   */
 | 
			
		||||
  update(parameters) {
 | 
			
		||||
    const transform = Util.transform(
 | 
			
		||||
      parameters.viewport.transform,
 | 
			
		||||
      this.inverseViewportTransform
 | 
			
		||||
    );
 | 
			
		||||
    this.viewport = parameters.viewport;
 | 
			
		||||
    this.inverseViewportTransform = Util.inverseTransform(
 | 
			
		||||
      this.viewport.transform
 | 
			
		||||
    );
 | 
			
		||||
    for (const editor of this.#editors.values()) {
 | 
			
		||||
      editor.transform(transform);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the scale factor from the viewport.
 | 
			
		||||
   * @returns {number}
 | 
			
		||||
   */
 | 
			
		||||
  get scaleFactor() {
 | 
			
		||||
    return this.viewport.scale;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the zoom factor.
 | 
			
		||||
   * @returns {number}
 | 
			
		||||
   */
 | 
			
		||||
  get zoomFactor() {
 | 
			
		||||
    return this.viewport.scale / PixelsPerInch.PDF_TO_CSS_UNITS;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { AnnotationEditorLayer };
 | 
			
		||||
							
								
								
									
										305
									
								
								src/display/editor/editor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										305
									
								
								src/display/editor/editor.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,305 @@
 | 
			
		||||
/* 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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */
 | 
			
		||||
 | 
			
		||||
import { unreachable, Util } from "../../shared/util.js";
 | 
			
		||||
import { bindEvents } from "./tools.js";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {Object} AnnotationEditorParameters
 | 
			
		||||
 * @property {AnnotationEditorLayer} parent - the layer containing this editor
 | 
			
		||||
 * @property {string} id - editor id
 | 
			
		||||
 * @property {number} x - x-coordinate
 | 
			
		||||
 * @property {number} y - y-coordinate
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for editors.
 | 
			
		||||
 */
 | 
			
		||||
class AnnotationEditor {
 | 
			
		||||
  #isInEditMode = false;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {AnnotationEditorParameters} parameters
 | 
			
		||||
   */
 | 
			
		||||
  constructor(parameters) {
 | 
			
		||||
    if (this.constructor === AnnotationEditor) {
 | 
			
		||||
      unreachable("Cannot initialize AnnotationEditor.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.parent = parameters.parent;
 | 
			
		||||
    this.id = parameters.id;
 | 
			
		||||
    this.width = this.height = null;
 | 
			
		||||
    this.pageIndex = parameters.parent.pageIndex;
 | 
			
		||||
    this.name = parameters.name;
 | 
			
		||||
    this.div = null;
 | 
			
		||||
    this.x = Math.round(parameters.x);
 | 
			
		||||
    this.y = Math.round(parameters.y);
 | 
			
		||||
 | 
			
		||||
    this.isAttachedToDOM = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * onfocus callback.
 | 
			
		||||
   */
 | 
			
		||||
  focusin(/* event */) {
 | 
			
		||||
    this.parent.setActiveEditor(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * onblur callback.
 | 
			
		||||
   * @param {FocusEvent} event
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  focusout(event) {
 | 
			
		||||
    if (!this.isAttachedToDOM) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // In case of focusout, the relatedTarget is the element which
 | 
			
		||||
    // is grabbing the focus.
 | 
			
		||||
    // So if the related target is an element under the div for this
 | 
			
		||||
    // editor, then the editor isn't unactive.
 | 
			
		||||
    const target = event.relatedTarget;
 | 
			
		||||
    if (target?.closest(`#${this.id}`)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
 | 
			
		||||
    if (this.isEmpty()) {
 | 
			
		||||
      this.remove();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.commit();
 | 
			
		||||
    }
 | 
			
		||||
    this.parent.setActiveEditor(null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the pointer coordinates in order to correctly translate the
 | 
			
		||||
   * div in case of drag-and-drop.
 | 
			
		||||
   * @param {MouseEvent} event
 | 
			
		||||
   */
 | 
			
		||||
  mousedown(event) {
 | 
			
		||||
    this.mouseX = event.offsetX;
 | 
			
		||||
    this.mouseY = event.offsetY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * We use drag-and-drop in order to move an editor on a page.
 | 
			
		||||
   * @param {DragEvent} event
 | 
			
		||||
   */
 | 
			
		||||
  dragstart(event) {
 | 
			
		||||
    event.dataTransfer.setData("text/plain", this.id);
 | 
			
		||||
    event.dataTransfer.effectAllowed = "move";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set the editor position within its parent.
 | 
			
		||||
   * @param {number} x
 | 
			
		||||
   * @param {number} y
 | 
			
		||||
   */
 | 
			
		||||
  setAt(x, y) {
 | 
			
		||||
    this.x = Math.round(x);
 | 
			
		||||
    this.y = Math.round(y);
 | 
			
		||||
 | 
			
		||||
    this.div.style.left = `${this.x}px`;
 | 
			
		||||
    this.div.style.top = `${this.y}px`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Translate the editor position within its parent.
 | 
			
		||||
   * @param {number} x
 | 
			
		||||
   * @param {number} y
 | 
			
		||||
   */
 | 
			
		||||
  translate(x, y) {
 | 
			
		||||
    this.setAt(this.x + x, this.y + y);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set the dimensions of this editor.
 | 
			
		||||
   * @param {number} width
 | 
			
		||||
   * @param {number} height
 | 
			
		||||
   */
 | 
			
		||||
  setDims(width, height) {
 | 
			
		||||
    this.div.style.width = `${width}px`;
 | 
			
		||||
    this.div.style.height = `${height}px`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Render this editor in a div.
 | 
			
		||||
   * @returns {HTMLDivElement}
 | 
			
		||||
   */
 | 
			
		||||
  render() {
 | 
			
		||||
    this.div = document.createElement("div");
 | 
			
		||||
    this.div.className = this.name;
 | 
			
		||||
    this.div.setAttribute("id", this.id);
 | 
			
		||||
    this.div.draggable = true;
 | 
			
		||||
    this.div.tabIndex = 100;
 | 
			
		||||
    this.div.style.left = `${this.x}px`;
 | 
			
		||||
    this.div.style.top = `${this.y}px`;
 | 
			
		||||
 | 
			
		||||
    bindEvents(this, this.div, [
 | 
			
		||||
      "dragstart",
 | 
			
		||||
      "focusin",
 | 
			
		||||
      "focusout",
 | 
			
		||||
      "mousedown",
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return this.div;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Executed once this editor has been rendered.
 | 
			
		||||
   */
 | 
			
		||||
  onceAdded() {}
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Apply the current transform (zoom) to this editor.
 | 
			
		||||
   * @param {Array<number>} transform
 | 
			
		||||
   */
 | 
			
		||||
  transform(transform) {
 | 
			
		||||
    const { style } = this.div;
 | 
			
		||||
    const width = parseFloat(style.width);
 | 
			
		||||
    const height = parseFloat(style.height);
 | 
			
		||||
 | 
			
		||||
    const [x1, y1] = Util.applyTransform([this.x, this.y], transform);
 | 
			
		||||
 | 
			
		||||
    if (!Number.isNaN(width)) {
 | 
			
		||||
      const [x2] = Util.applyTransform([this.x + width, 0], transform);
 | 
			
		||||
      this.div.style.width = `${x2 - x1}px`;
 | 
			
		||||
    }
 | 
			
		||||
    if (!Number.isNaN(height)) {
 | 
			
		||||
      const [, y2] = Util.applyTransform([0, this.y + height], transform);
 | 
			
		||||
      this.div.style.height = `${y2 - y1}px`;
 | 
			
		||||
    }
 | 
			
		||||
    this.setAt(x1, y1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Check if the editor contains something.
 | 
			
		||||
   * @returns {boolean}
 | 
			
		||||
   */
 | 
			
		||||
  isEmpty() {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Enable edit mode.
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  enableEditMode() {
 | 
			
		||||
    this.#isInEditMode = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Disable edit mode.
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  disableEditMode() {
 | 
			
		||||
    this.#isInEditMode = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Check if the editor is edited.
 | 
			
		||||
   * @returns {boolean}
 | 
			
		||||
   */
 | 
			
		||||
  isInEditMode() {
 | 
			
		||||
    return this.#isInEditMode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * If it returns true, then this editor handle the keyboard
 | 
			
		||||
   * events itself.
 | 
			
		||||
   * @returns {boolean}
 | 
			
		||||
   */
 | 
			
		||||
  shouldGetKeyboardEvents() {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Copy the elements of an editor in order to be able to build
 | 
			
		||||
   * a new one from these data.
 | 
			
		||||
   * It's used on ctrl+c action.
 | 
			
		||||
   *
 | 
			
		||||
   * To implement in subclasses.
 | 
			
		||||
   * @returns {AnnotationEditor}
 | 
			
		||||
   */
 | 
			
		||||
  copy() {
 | 
			
		||||
    unreachable("An editor must be copyable");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Check if this editor needs to be rebuilt or not.
 | 
			
		||||
   * @returns {boolean}
 | 
			
		||||
   */
 | 
			
		||||
  needsToBeRebuilt() {
 | 
			
		||||
    return this.div && !this.isAttachedToDOM;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Rebuild the editor in case it has been removed on undo.
 | 
			
		||||
   *
 | 
			
		||||
   * To implement in subclasses.
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  rebuild() {
 | 
			
		||||
    unreachable("An editor must be rebuildable");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Serialize the editor.
 | 
			
		||||
   * The result of the serialization will be used to construct a
 | 
			
		||||
   * new annotation to add to the pdf document.
 | 
			
		||||
   *
 | 
			
		||||
   * To implement in subclasses.
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  serialize() {
 | 
			
		||||
    unreachable("An editor must be serializable");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove this editor.
 | 
			
		||||
   * It's used on ctrl+backspace action.
 | 
			
		||||
   *
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  remove() {
 | 
			
		||||
    this.parent.remove(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Select this editor.
 | 
			
		||||
   */
 | 
			
		||||
  select() {
 | 
			
		||||
    if (this.div) {
 | 
			
		||||
      this.div.classList.add("selectedEditor");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Unselect this editor.
 | 
			
		||||
   */
 | 
			
		||||
  unselect() {
 | 
			
		||||
    if (this.div) {
 | 
			
		||||
      this.div.classList.remove("selectedEditor");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { AnnotationEditor };
 | 
			
		||||
							
								
								
									
										225
									
								
								src/display/editor/freetext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								src/display/editor/freetext.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,225 @@
 | 
			
		||||
/* 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 { AnnotationEditorType, Util } from "../../shared/util.js";
 | 
			
		||||
import { AnnotationEditor } from "./editor.js";
 | 
			
		||||
import { bindEvents } from "./tools.js";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Basic text editor in order to create a FreeTex annotation.
 | 
			
		||||
 */
 | 
			
		||||
class FreeTextEditor extends AnnotationEditor {
 | 
			
		||||
  #color;
 | 
			
		||||
 | 
			
		||||
  #content = "";
 | 
			
		||||
 | 
			
		||||
  #contentHTML = "";
 | 
			
		||||
 | 
			
		||||
  #fontSize;
 | 
			
		||||
 | 
			
		||||
  static _freeTextDefaultContent = "";
 | 
			
		||||
 | 
			
		||||
  static _l10nPromise;
 | 
			
		||||
 | 
			
		||||
  constructor(params) {
 | 
			
		||||
    super({ ...params, name: "freeTextEditor" });
 | 
			
		||||
    this.#color = params.color || "CanvasText";
 | 
			
		||||
    this.#fontSize = params.fontSize || 10;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static setL10n(l10n) {
 | 
			
		||||
    this._l10nPromise = l10n.get("freetext_default_content");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  copy() {
 | 
			
		||||
    const editor = new FreeTextEditor({
 | 
			
		||||
      parent: this.parent,
 | 
			
		||||
      id: this.parent.getNextId(),
 | 
			
		||||
      x: this.x,
 | 
			
		||||
      y: this.y,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    editor.width = this.width;
 | 
			
		||||
    editor.height = this.height;
 | 
			
		||||
    editor.#color = this.#color;
 | 
			
		||||
    editor.#fontSize = this.#fontSize;
 | 
			
		||||
    editor.#content = this.#content;
 | 
			
		||||
    editor.#contentHTML = this.#contentHTML;
 | 
			
		||||
 | 
			
		||||
    return editor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  rebuild() {
 | 
			
		||||
    if (this.div === null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.isAttachedToDOM) {
 | 
			
		||||
      // At some point this editor has been removed and
 | 
			
		||||
      // we're rebuilting it, hence we must add it to its
 | 
			
		||||
      // parent.
 | 
			
		||||
      this.parent.add(this);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  enableEditMode() {
 | 
			
		||||
    super.enableEditMode();
 | 
			
		||||
    this.overlayDiv.classList.remove("enabled");
 | 
			
		||||
    this.div.draggable = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  disableEditMode() {
 | 
			
		||||
    super.disableEditMode();
 | 
			
		||||
    this.overlayDiv.classList.add("enabled");
 | 
			
		||||
    this.div.draggable = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  onceAdded() {
 | 
			
		||||
    if (this.width) {
 | 
			
		||||
      // The editor has been created in using ctrl+c.
 | 
			
		||||
      this.div.focus();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.enableEditMode();
 | 
			
		||||
    this.editorDiv.focus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  isEmpty() {
 | 
			
		||||
    return this.editorDiv.innerText.trim() === "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Extract the text from this editor.
 | 
			
		||||
   * @returns {string}
 | 
			
		||||
   */
 | 
			
		||||
  #extractText() {
 | 
			
		||||
    const divs = this.editorDiv.getElementsByTagName("div");
 | 
			
		||||
    if (divs.length === 0) {
 | 
			
		||||
      return this.editorDiv.innerText;
 | 
			
		||||
    }
 | 
			
		||||
    const buffer = [];
 | 
			
		||||
    for (let i = 0, ii = divs.length; i < ii; i++) {
 | 
			
		||||
      const div = divs[i];
 | 
			
		||||
      const first = div.firstChild;
 | 
			
		||||
      if (first?.nodeName === "#text") {
 | 
			
		||||
        buffer.push(first.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        buffer.push("");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return buffer.join("\n");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Commit the content we have in this editor.
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  commit() {
 | 
			
		||||
    this.disableEditMode();
 | 
			
		||||
    this.#contentHTML = this.editorDiv.innerHTML;
 | 
			
		||||
    this.#content = this.#extractText().trimEnd();
 | 
			
		||||
 | 
			
		||||
    const style = getComputedStyle(this.div);
 | 
			
		||||
    this.width = parseFloat(style.width);
 | 
			
		||||
    this.height = parseFloat(style.height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  shouldGetKeyboardEvents() {
 | 
			
		||||
    return this.isInEditMode();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * ondblclick callback.
 | 
			
		||||
   * @param {MouseEvent} event
 | 
			
		||||
   */
 | 
			
		||||
  dblclick(event) {
 | 
			
		||||
    this.enableEditMode();
 | 
			
		||||
    this.editorDiv.focus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  render() {
 | 
			
		||||
    if (this.div) {
 | 
			
		||||
      return this.div;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    super.render();
 | 
			
		||||
    this.editorDiv = document.createElement("div");
 | 
			
		||||
    this.editorDiv.tabIndex = 0;
 | 
			
		||||
    this.editorDiv.className = "internal";
 | 
			
		||||
 | 
			
		||||
    FreeTextEditor._l10nPromise.then(msg =>
 | 
			
		||||
      this.editorDiv.setAttribute("default-content", msg)
 | 
			
		||||
    );
 | 
			
		||||
    this.editorDiv.contentEditable = true;
 | 
			
		||||
 | 
			
		||||
    const { style } = this.editorDiv;
 | 
			
		||||
    style.fontSize = `calc(${this.#fontSize}px * var(--zoom-factor))`;
 | 
			
		||||
    style.minHeight = `calc(${1.5 * this.#fontSize}px * var(--zoom-factor))`;
 | 
			
		||||
    style.color = this.#color;
 | 
			
		||||
 | 
			
		||||
    this.div.appendChild(this.editorDiv);
 | 
			
		||||
 | 
			
		||||
    this.overlayDiv = document.createElement("div");
 | 
			
		||||
    this.overlayDiv.classList.add("overlay", "enabled");
 | 
			
		||||
    this.div.appendChild(this.overlayDiv);
 | 
			
		||||
 | 
			
		||||
    // TODO: implement paste callback.
 | 
			
		||||
    // The goal is to sanitize and have something suitable for this
 | 
			
		||||
    // editor.
 | 
			
		||||
    bindEvents(this, this.div, ["dblclick"]);
 | 
			
		||||
 | 
			
		||||
    if (this.width) {
 | 
			
		||||
      // This editor has been created in using copy (ctrl+c).
 | 
			
		||||
      this.setAt(this.x + this.width, this.y + this.height);
 | 
			
		||||
      // eslint-disable-next-line no-unsanitized/property
 | 
			
		||||
      this.editorDiv.innerHTML = this.#contentHTML;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.div;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  serialize() {
 | 
			
		||||
    const rect = this.div.getBoundingClientRect();
 | 
			
		||||
    const [x1, y1] = Util.applyTransform(
 | 
			
		||||
      [this.x, this.y + rect.height],
 | 
			
		||||
      this.parent.viewport.inverseTransform
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [x2, y2] = Util.applyTransform(
 | 
			
		||||
      [this.x + rect.width, this.y],
 | 
			
		||||
      this.parent.viewport.inverseTransform
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      annotationType: AnnotationEditorType.FREETEXT,
 | 
			
		||||
      color: [0, 0, 0],
 | 
			
		||||
      fontSize: this.#fontSize,
 | 
			
		||||
      value: this.#content,
 | 
			
		||||
      pageIndex: this.parent.pageIndex,
 | 
			
		||||
      rect: [x1, y1, x2, y2],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { FreeTextEditor };
 | 
			
		||||
							
								
								
									
										574
									
								
								src/display/editor/tools.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										574
									
								
								src/display/editor/tools.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,574 @@
 | 
			
		||||
/* 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("./editor.js").AnnotationEditor} AnnotationEditor */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  AnnotationEditorPrefix,
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  shadow,
 | 
			
		||||
} from "../../shared/util.js";
 | 
			
		||||
 | 
			
		||||
function bindEvents(obj, element, names) {
 | 
			
		||||
  for (const name of names) {
 | 
			
		||||
    element.addEventListener(name, obj[name].bind(obj));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * Class to create some unique ids for the different editors.
 | 
			
		||||
 */
 | 
			
		||||
class IdManager {
 | 
			
		||||
  #id = 0;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get a unique id.
 | 
			
		||||
   * @returns {string}
 | 
			
		||||
   */
 | 
			
		||||
  getId() {
 | 
			
		||||
    return `${AnnotationEditorPrefix}${this.#id++}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class to handle undo/redo.
 | 
			
		||||
 * Commands are just saved in a buffer.
 | 
			
		||||
 * If we hit some memory issues we could likely use a circular buffer.
 | 
			
		||||
 * It has to be used as a singleton.
 | 
			
		||||
 */
 | 
			
		||||
class CommandManager {
 | 
			
		||||
  #commands = [];
 | 
			
		||||
 | 
			
		||||
  #maxSize = 100;
 | 
			
		||||
 | 
			
		||||
  // When the position is NaN, it means the buffer is empty.
 | 
			
		||||
  #position = NaN;
 | 
			
		||||
 | 
			
		||||
  #start = 0;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a new couple of commands to be used in case of redo/undo.
 | 
			
		||||
   * @param {function} cmd
 | 
			
		||||
   * @param {function} undo
 | 
			
		||||
   */
 | 
			
		||||
  add(cmd, undo) {
 | 
			
		||||
    const save = [cmd, undo];
 | 
			
		||||
    const next = (this.#position + 1) % this.#maxSize;
 | 
			
		||||
    if (next !== this.#start) {
 | 
			
		||||
      if (this.#start < next) {
 | 
			
		||||
        this.#commands = this.#commands.slice(this.#start, next);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.#commands = this.#commands
 | 
			
		||||
          .slice(this.#start)
 | 
			
		||||
          .concat(this.#commands.slice(0, next));
 | 
			
		||||
      }
 | 
			
		||||
      this.#start = 0;
 | 
			
		||||
      this.#position = this.#commands.length - 1;
 | 
			
		||||
    }
 | 
			
		||||
    this.#setCommands(save);
 | 
			
		||||
    cmd();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Undo the last command.
 | 
			
		||||
   */
 | 
			
		||||
  undo() {
 | 
			
		||||
    if (isNaN(this.#position)) {
 | 
			
		||||
      // Nothing to undo.
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.#commands[this.#position][1]();
 | 
			
		||||
    if (this.#position === this.#start) {
 | 
			
		||||
      this.#position = NaN;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.#position = (this.#maxSize + this.#position - 1) % this.#maxSize;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Redo the last command.
 | 
			
		||||
   */
 | 
			
		||||
  redo() {
 | 
			
		||||
    if (isNaN(this.#position)) {
 | 
			
		||||
      if (this.#start < this.#commands.length) {
 | 
			
		||||
        this.#commands[this.#start][0]();
 | 
			
		||||
        this.#position = this.#start;
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const next = (this.#position + 1) % this.#maxSize;
 | 
			
		||||
    if (next !== this.#start && next < this.#commands.length) {
 | 
			
		||||
      this.#commands[next][0]();
 | 
			
		||||
      this.#position = next;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #setCommands(cmds) {
 | 
			
		||||
    if (this.#commands.length < this.#maxSize) {
 | 
			
		||||
      this.#commands.push(cmds);
 | 
			
		||||
      this.#position = isNaN(this.#position) ? 0 : this.#position + 1;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isNaN(this.#position)) {
 | 
			
		||||
      this.#position = this.#start;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.#position = (this.#position + 1) % this.#maxSize;
 | 
			
		||||
      if (this.#position === this.#start) {
 | 
			
		||||
        this.#start = (this.#start + 1) % this.#maxSize;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.#commands[this.#position] = cmds;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class to handle the different keyboards shortcuts we can have on mac or
 | 
			
		||||
 * non-mac OSes.
 | 
			
		||||
 */
 | 
			
		||||
class KeyboardManager {
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a new keyboard manager class.
 | 
			
		||||
   * @param {Array<Array>} callbacks - an array containing an array of shortcuts
 | 
			
		||||
   * and a callback to call.
 | 
			
		||||
   * A shortcut is a string like `ctrl+c` or `mac+ctrl+c` for mac OS.
 | 
			
		||||
   */
 | 
			
		||||
  constructor(callbacks) {
 | 
			
		||||
    this.buffer = [];
 | 
			
		||||
    this.callbacks = new Map();
 | 
			
		||||
    this.allKeys = new Set();
 | 
			
		||||
 | 
			
		||||
    const isMac = KeyboardManager.platform.isMac;
 | 
			
		||||
    for (const [keys, callback] of callbacks) {
 | 
			
		||||
      for (const key of keys) {
 | 
			
		||||
        const isMacKey = key.startsWith("mac+");
 | 
			
		||||
        if (isMac && isMacKey) {
 | 
			
		||||
          this.callbacks.set(key.slice(4), callback);
 | 
			
		||||
          this.allKeys.add(key.split("+").at(-1));
 | 
			
		||||
        } else if (!isMac && !isMacKey) {
 | 
			
		||||
          this.callbacks.set(key, callback);
 | 
			
		||||
          this.allKeys.add(key.split("+").at(-1));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get platform() {
 | 
			
		||||
    const platform = typeof navigator !== "undefined" ? navigator.platform : "";
 | 
			
		||||
 | 
			
		||||
    return shadow(this, "platform", {
 | 
			
		||||
      isWin: platform.includes("Win"),
 | 
			
		||||
      isMac: platform.includes("Mac"),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Serialize an event into a string in order to match a
 | 
			
		||||
   * potential key for a callback.
 | 
			
		||||
   * @param {KeyboardEvent} event
 | 
			
		||||
   * @returns {string}
 | 
			
		||||
   */
 | 
			
		||||
  #serialize(event) {
 | 
			
		||||
    if (event.altKey) {
 | 
			
		||||
      this.buffer.push("alt");
 | 
			
		||||
    }
 | 
			
		||||
    if (event.ctrlKey) {
 | 
			
		||||
      this.buffer.push("ctrl");
 | 
			
		||||
    }
 | 
			
		||||
    if (event.metaKey) {
 | 
			
		||||
      this.buffer.push("meta");
 | 
			
		||||
    }
 | 
			
		||||
    if (event.shiftKey) {
 | 
			
		||||
      this.buffer.push("shift");
 | 
			
		||||
    }
 | 
			
		||||
    this.buffer.push(event.key);
 | 
			
		||||
    const str = this.buffer.join("+");
 | 
			
		||||
    this.buffer.length = 0;
 | 
			
		||||
 | 
			
		||||
    return str;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Execute a callback, if any, for a given keyboard event.
 | 
			
		||||
   * The page is used as `this` in the callback.
 | 
			
		||||
   * @param {AnnotationEditorLayer} page.
 | 
			
		||||
   * @param {KeyboardEvent} event
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  exec(page, event) {
 | 
			
		||||
    if (!this.allKeys.has(event.key)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const callback = this.callbacks.get(this.#serialize(event));
 | 
			
		||||
    if (!callback) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    callback.bind(page)();
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Basic clipboard to copy/paste some editors.
 | 
			
		||||
 * It has to be used as a singleton.
 | 
			
		||||
 */
 | 
			
		||||
class ClipboardManager {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.element = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Copy an element.
 | 
			
		||||
   * @param {AnnotationEditor} element
 | 
			
		||||
   */
 | 
			
		||||
  copy(element) {
 | 
			
		||||
    this.element = element.copy();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a new element.
 | 
			
		||||
   * @returns {AnnotationEditor|null}
 | 
			
		||||
   */
 | 
			
		||||
  paste() {
 | 
			
		||||
    return this.element?.copy() || null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A pdf has several pages and each of them when it will rendered
 | 
			
		||||
 * will have an AnnotationEditorLayer which will contain the some
 | 
			
		||||
 * new Annotations associated to an editor in order to modify them.
 | 
			
		||||
 *
 | 
			
		||||
 * This class is used to manage all the different layers, editors and
 | 
			
		||||
 * some action like copy/paste, undo/redo, ...
 | 
			
		||||
 */
 | 
			
		||||
class AnnotationEditorUIManager {
 | 
			
		||||
  #activeEditor = null;
 | 
			
		||||
 | 
			
		||||
  #allEditors = new Map();
 | 
			
		||||
 | 
			
		||||
  #allLayers = new Set();
 | 
			
		||||
 | 
			
		||||
  #allowClick = true;
 | 
			
		||||
 | 
			
		||||
  #clipboardManager = new ClipboardManager();
 | 
			
		||||
 | 
			
		||||
  #commandManager = new CommandManager();
 | 
			
		||||
 | 
			
		||||
  #idManager = new IdManager();
 | 
			
		||||
 | 
			
		||||
  #isAllSelected = false;
 | 
			
		||||
 | 
			
		||||
  #isEnabled = false;
 | 
			
		||||
 | 
			
		||||
  #mode = AnnotationEditorType.NONE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get an id.
 | 
			
		||||
   * @returns {string}
 | 
			
		||||
   */
 | 
			
		||||
  getId() {
 | 
			
		||||
    return this.#idManager.getId();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a new layer for a page which will contains the editors.
 | 
			
		||||
   * @param {AnnotationEditorLayer} layer
 | 
			
		||||
   */
 | 
			
		||||
  addLayer(layer) {
 | 
			
		||||
    this.#allLayers.add(layer);
 | 
			
		||||
    if (this.#isEnabled) {
 | 
			
		||||
      layer.enable();
 | 
			
		||||
    } else {
 | 
			
		||||
      layer.disable();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove a layer.
 | 
			
		||||
   * @param {AnnotationEditorLayer} layer
 | 
			
		||||
   */
 | 
			
		||||
  removeLayer(layer) {
 | 
			
		||||
    this.#allLayers.delete(layer);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Change the editor mode (None, FreeText, Ink, ...)
 | 
			
		||||
   * @param {number} mode
 | 
			
		||||
   */
 | 
			
		||||
  updateMode(mode) {
 | 
			
		||||
    this.#mode = mode;
 | 
			
		||||
    if (mode === AnnotationEditorType.NONE) {
 | 
			
		||||
      this.#disableAll();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.#enableAll();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Enable all the layers.
 | 
			
		||||
   */
 | 
			
		||||
  #enableAll() {
 | 
			
		||||
    if (!this.#isEnabled) {
 | 
			
		||||
      this.#isEnabled = true;
 | 
			
		||||
      for (const layer of this.#allLayers) {
 | 
			
		||||
        layer.enable();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Disable all the layers.
 | 
			
		||||
   */
 | 
			
		||||
  #disableAll() {
 | 
			
		||||
    if (this.#isEnabled) {
 | 
			
		||||
      this.#isEnabled = false;
 | 
			
		||||
      for (const layer of this.#allLayers) {
 | 
			
		||||
        layer.disable();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get all the editors belonging to a give page.
 | 
			
		||||
   * @param {number} pageIndex
 | 
			
		||||
   * @returns {Array<AnnotationEditor>}
 | 
			
		||||
   */
 | 
			
		||||
  getEditors(pageIndex) {
 | 
			
		||||
    const editors = [];
 | 
			
		||||
    for (const editor of this.#allEditors.values()) {
 | 
			
		||||
      if (editor.pageIndex === pageIndex) {
 | 
			
		||||
        editors.push(editor);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return editors;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get an editor with the given id.
 | 
			
		||||
   * @param {string} id
 | 
			
		||||
   * @returns {AnnotationEditor}
 | 
			
		||||
   */
 | 
			
		||||
  getEditor(id) {
 | 
			
		||||
    return this.#allEditors.get(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a new editor.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   */
 | 
			
		||||
  addEditor(editor) {
 | 
			
		||||
    this.#allEditors.set(editor.id, editor);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove an editor.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   */
 | 
			
		||||
  removeEditor(editor) {
 | 
			
		||||
    this.#allEditors.delete(editor.id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set the given editor as the active one.
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   */
 | 
			
		||||
  setActiveEditor(editor) {
 | 
			
		||||
    this.#activeEditor = editor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Undo the last command.
 | 
			
		||||
   */
 | 
			
		||||
  undo() {
 | 
			
		||||
    this.#commandManager.undo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Redo the last undoed command.
 | 
			
		||||
   */
 | 
			
		||||
  redo() {
 | 
			
		||||
    this.#commandManager.redo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a command to execute (cmd) and another one to undo it.
 | 
			
		||||
   * @param {function} cmd
 | 
			
		||||
   * @param {function} undo
 | 
			
		||||
   */
 | 
			
		||||
  addCommands(cmd, undo) {
 | 
			
		||||
    this.#commandManager.add(cmd, undo);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * When set to true a click on the current layer will trigger
 | 
			
		||||
   * an editor creation.
 | 
			
		||||
   * @return {boolean}
 | 
			
		||||
   */
 | 
			
		||||
  get allowClick() {
 | 
			
		||||
    return this.#allowClick;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {boolean} allow
 | 
			
		||||
   */
 | 
			
		||||
  set allowClick(allow) {
 | 
			
		||||
    this.#allowClick = allow;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Unselect the current editor.
 | 
			
		||||
   */
 | 
			
		||||
  unselect() {
 | 
			
		||||
    if (this.#activeEditor) {
 | 
			
		||||
      this.#activeEditor.parent.setActiveEditor(null);
 | 
			
		||||
    }
 | 
			
		||||
    this.#allowClick = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Suppress some editors from the given layer.
 | 
			
		||||
   * @param {AnnotationEditorLayer} layer
 | 
			
		||||
   */
 | 
			
		||||
  suppress(layer) {
 | 
			
		||||
    let cmd, undo;
 | 
			
		||||
    if (this.#isAllSelected) {
 | 
			
		||||
      const editors = Array.from(this.#allEditors.values());
 | 
			
		||||
      cmd = () => {
 | 
			
		||||
        for (const editor of editors) {
 | 
			
		||||
          editor.remove();
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      undo = () => {
 | 
			
		||||
        for (const editor of editors) {
 | 
			
		||||
          layer.addOrRebuild(editor);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      this.addCommands(cmd, undo);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (!this.#activeEditor) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const editor = this.#activeEditor;
 | 
			
		||||
      cmd = () => {
 | 
			
		||||
        editor.remove();
 | 
			
		||||
      };
 | 
			
		||||
      undo = () => {
 | 
			
		||||
        layer.addOrRebuild(editor);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.addCommands(cmd, undo);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Copy the selected editor.
 | 
			
		||||
   */
 | 
			
		||||
  copy() {
 | 
			
		||||
    if (this.#activeEditor) {
 | 
			
		||||
      this.#clipboardManager.copy(this.#activeEditor);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Cut the selected editor.
 | 
			
		||||
   * @param {AnnotationEditorLayer}
 | 
			
		||||
   */
 | 
			
		||||
  cut(layer) {
 | 
			
		||||
    if (this.#activeEditor) {
 | 
			
		||||
      this.#clipboardManager.copy(this.#activeEditor);
 | 
			
		||||
      const editor = this.#activeEditor;
 | 
			
		||||
      const cmd = () => {
 | 
			
		||||
        editor.remove();
 | 
			
		||||
      };
 | 
			
		||||
      const undo = () => {
 | 
			
		||||
        layer.addOrRebuild(editor);
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      this.addCommands(cmd, undo);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Paste a previously copied editor.
 | 
			
		||||
   * @param {AnnotationEditorLayer}
 | 
			
		||||
   * @returns {undefined}
 | 
			
		||||
   */
 | 
			
		||||
  paste(layer) {
 | 
			
		||||
    const editor = this.#clipboardManager.paste();
 | 
			
		||||
    if (!editor) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const cmd = () => {
 | 
			
		||||
      layer.addOrRebuild(editor);
 | 
			
		||||
    };
 | 
			
		||||
    const undo = () => {
 | 
			
		||||
      editor.remove();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.addCommands(cmd, undo);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Select all the editors.
 | 
			
		||||
   */
 | 
			
		||||
  selectAll() {
 | 
			
		||||
    this.#isAllSelected = true;
 | 
			
		||||
    for (const editor of this.#allEditors.values()) {
 | 
			
		||||
      editor.select();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Unselect all the editors.
 | 
			
		||||
   */
 | 
			
		||||
  unselectAll() {
 | 
			
		||||
    this.#isAllSelected = false;
 | 
			
		||||
    for (const editor of this.#allEditors.values()) {
 | 
			
		||||
      editor.unselect();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Is the current editor the one passed as argument?
 | 
			
		||||
   * @param {AnnotationEditor} editor
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  isActive(editor) {
 | 
			
		||||
    return this.#activeEditor === editor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the current active editor.
 | 
			
		||||
   * @returns {AnnotationEditor|null}
 | 
			
		||||
   */
 | 
			
		||||
  getActive() {
 | 
			
		||||
    return this.#activeEditor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the current editor mode.
 | 
			
		||||
   * @returns {number}
 | 
			
		||||
   */
 | 
			
		||||
  getMode() {
 | 
			
		||||
    return this.#mode;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { AnnotationEditorUIManager, bindEvents, KeyboardManager };
 | 
			
		||||
@ -21,6 +21,7 @@
 | 
			
		||||
/** @typedef {import("./display/display_utils").PageViewport} PageViewport */
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  AnnotationMode,
 | 
			
		||||
  CMapCompressionType,
 | 
			
		||||
  createPromiseCapability,
 | 
			
		||||
@ -56,6 +57,8 @@ import {
 | 
			
		||||
  PixelsPerInch,
 | 
			
		||||
  RenderingCancelledException,
 | 
			
		||||
} from "./display/display_utils.js";
 | 
			
		||||
import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js";
 | 
			
		||||
import { AnnotationEditorUIManager } from "./display/editor/tools.js";
 | 
			
		||||
import { AnnotationLayer } from "./display/annotation_layer.js";
 | 
			
		||||
import { GlobalWorkerOptions } from "./display/worker_options.js";
 | 
			
		||||
import { isNodeJS } from "./shared/is_node.js";
 | 
			
		||||
@ -104,6 +107,9 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  AnnotationEditorLayer,
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  AnnotationEditorUIManager,
 | 
			
		||||
  AnnotationLayer,
 | 
			
		||||
  AnnotationMode,
 | 
			
		||||
  build,
 | 
			
		||||
 | 
			
		||||
@ -51,6 +51,13 @@ const AnnotationMode = {
 | 
			
		||||
  ENABLE_STORAGE: 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AnnotationEditorPrefix = "pdfjs_internal_editor_";
 | 
			
		||||
 | 
			
		||||
const AnnotationEditorType = {
 | 
			
		||||
  NONE: 0,
 | 
			
		||||
  FREETEXT: 1,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
 | 
			
		||||
const PermissionFlag = {
 | 
			
		||||
  PRINT: 0x04,
 | 
			
		||||
@ -1135,6 +1142,8 @@ export {
 | 
			
		||||
  AbortException,
 | 
			
		||||
  AnnotationActionEventType,
 | 
			
		||||
  AnnotationBorderStyleType,
 | 
			
		||||
  AnnotationEditorPrefix,
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  AnnotationFieldFlag,
 | 
			
		||||
  AnnotationFlag,
 | 
			
		||||
  AnnotationMarkedState,
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ async function runTests(results) {
 | 
			
		||||
      "annotation_spec.js",
 | 
			
		||||
      "accessibility_spec.js",
 | 
			
		||||
      "find_spec.js",
 | 
			
		||||
      "freetext_editor_spec.js",
 | 
			
		||||
    ],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										200
									
								
								test/integration/freetext_editor_spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								test/integration/freetext_editor_spec.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,200 @@
 | 
			
		||||
/* 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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const { closePages, loadAndWait } = require("./test_utils.js");
 | 
			
		||||
 | 
			
		||||
const editorPrefix = "#pdfjs_internal_editor_";
 | 
			
		||||
 | 
			
		||||
describe("Editor", () => {
 | 
			
		||||
  describe("FreeText", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeAll(async () => {
 | 
			
		||||
      pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterAll(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must write a string in a FreeText editor", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.click("#editorFreeText");
 | 
			
		||||
 | 
			
		||||
          const rect = await page.$eval(".annotationEditorLayer", el => {
 | 
			
		||||
            // With Chrome something is wrong when serializing a DomRect,
 | 
			
		||||
            // hence we extract the values and just return them.
 | 
			
		||||
            const { x, y } = el.getBoundingClientRect();
 | 
			
		||||
            return { x, y };
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          const data = "Hello PDF.js World !!";
 | 
			
		||||
          await page.mouse.click(rect.x + 10, rect.y + 10);
 | 
			
		||||
          await page.type(`${editorPrefix}0 .internal`, data);
 | 
			
		||||
 | 
			
		||||
          const editorRect = await page.$eval(`${editorPrefix}0`, el => {
 | 
			
		||||
            const { x, y, width, height } = el.getBoundingClientRect();
 | 
			
		||||
            return {
 | 
			
		||||
              x,
 | 
			
		||||
              y,
 | 
			
		||||
              width,
 | 
			
		||||
              height,
 | 
			
		||||
            };
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          // Commit.
 | 
			
		||||
          await page.mouse.click(
 | 
			
		||||
            editorRect.x,
 | 
			
		||||
            editorRect.y + 2 * editorRect.height
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const content = await page.$eval(`${editorPrefix}0`, el =>
 | 
			
		||||
            el.innerText.trimEnd()
 | 
			
		||||
          );
 | 
			
		||||
          expect(content).withContext(`In ${browserName}`).toEqual(data);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must copy/paste", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const editorRect = await page.$eval(`${editorPrefix}0`, el => {
 | 
			
		||||
            const { x, y, width, height } = el.getBoundingClientRect();
 | 
			
		||||
            return { x, y, width, height };
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          // Select the editor created previously.
 | 
			
		||||
          await page.mouse.click(
 | 
			
		||||
            editorRect.x + editorRect.width / 2,
 | 
			
		||||
            editorRect.y + editorRect.height / 2
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("c");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("v");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          const content = await page.$eval(`${editorPrefix}0`, el =>
 | 
			
		||||
            el.innerText.trimEnd()
 | 
			
		||||
          );
 | 
			
		||||
          let pastedContent = await page.$eval(`${editorPrefix}2`, el =>
 | 
			
		||||
            el.innerText.trimEnd()
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          expect(pastedContent)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(content);
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("c");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("v");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          pastedContent = await page.$eval(`${editorPrefix}4`, el =>
 | 
			
		||||
            el.innerText.trimEnd()
 | 
			
		||||
          );
 | 
			
		||||
          expect(pastedContent)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(content);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must clear all", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("a");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("Backspace");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          for (const n of [0, 2, 4]) {
 | 
			
		||||
            const hasEditor = await page.evaluate(sel => {
 | 
			
		||||
              return !!document.querySelector(sel);
 | 
			
		||||
            }, `${editorPrefix}${n}`);
 | 
			
		||||
 | 
			
		||||
            expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that a paste has been undone", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const rect = await page.$eval(".annotationEditorLayer", el => {
 | 
			
		||||
            const { x, y } = el.getBoundingClientRect();
 | 
			
		||||
            return { x, y };
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          const data = "Hello PDF.js World !!";
 | 
			
		||||
          await page.mouse.click(rect.x + 10, rect.y + 10);
 | 
			
		||||
          await page.type(`${editorPrefix}5 .internal`, data);
 | 
			
		||||
 | 
			
		||||
          const editorRect = await page.$eval(`${editorPrefix}5`, el => {
 | 
			
		||||
            const { x, y, width, height } = el.getBoundingClientRect();
 | 
			
		||||
            return { x, y, width, height };
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          // Commit.
 | 
			
		||||
          await page.mouse.click(
 | 
			
		||||
            editorRect.x,
 | 
			
		||||
            editorRect.y + 2 * editorRect.height
 | 
			
		||||
          );
 | 
			
		||||
          // And select it again.
 | 
			
		||||
          await page.mouse.click(
 | 
			
		||||
            editorRect.x + editorRect.width / 2,
 | 
			
		||||
            editorRect.y + editorRect.height / 2
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("c");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("v");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          let hasEditor = await page.evaluate(sel => {
 | 
			
		||||
            return !!document.querySelector(sel);
 | 
			
		||||
          }, `${editorPrefix}7`);
 | 
			
		||||
 | 
			
		||||
          expect(hasEditor).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Control");
 | 
			
		||||
          await page.keyboard.press("z");
 | 
			
		||||
          await page.keyboard.up("Control");
 | 
			
		||||
 | 
			
		||||
          hasEditor = await page.evaluate(sel => {
 | 
			
		||||
            return !!document.querySelector(sel);
 | 
			
		||||
          }, `${editorPrefix}7`);
 | 
			
		||||
 | 
			
		||||
          expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -950,6 +950,7 @@ async function startBrowser(browserName, startUrl = "") {
 | 
			
		||||
      // Avoid popup when saving is done
 | 
			
		||||
      "browser.download.always_ask_before_handling_new_types": true,
 | 
			
		||||
      "browser.download.panel.shown": true,
 | 
			
		||||
      "browser.download.alwaysOpenPanel": false,
 | 
			
		||||
      // Save file in output
 | 
			
		||||
      "browser.download.folderList": 2,
 | 
			
		||||
      "browser.download.dir": tempDir,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										85
									
								
								web/annotation_editor_layer_builder.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								web/annotation_editor_layer_builder.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
/* 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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  --focus-outline: solid 2px red;
 | 
			
		||||
  --hover-outline: dashed 2px blue;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer {
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  resize: none;
 | 
			
		||||
  width: auto;
 | 
			
		||||
  height: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor .internal {
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border: none;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  min-height: 15px;
 | 
			
		||||
  overflow: visible;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  resize: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor .overlay {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  display: none;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor .overlay.enabled {
 | 
			
		||||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor .internal:empty::before {
 | 
			
		||||
  content: attr(default-content);
 | 
			
		||||
  color: gray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor .internal:focus {
 | 
			
		||||
  outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor:focus-within {
 | 
			
		||||
  outline: var(--focus-outline);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .freeTextEditor:hover:not(:focus-within) {
 | 
			
		||||
  outline: var(--hover-outline);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer .selectedEditor {
 | 
			
		||||
  outline: var(--focus-outline);
 | 
			
		||||
  resize: none;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										128
									
								
								web/annotation_editor_layer_builder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								web/annotation_editor_layer_builder.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,128 @@
 | 
			
		||||
/* 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 */
 | 
			
		||||
/** @typedef {import("./interfaces").IL10n} IL10n */
 | 
			
		||||
 | 
			
		||||
import { AnnotationEditorLayer } from "pdfjs-lib";
 | 
			
		||||
import { NullL10n } from "./l10n_utils.js";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {Object} AnnotationEditorLayerBuilderOptions
 | 
			
		||||
 * @property {number} mode - Editor mode
 | 
			
		||||
 * @property {HTMLDivElement} pageDiv
 | 
			
		||||
 * @property {PDFPageProxy} pdfPage
 | 
			
		||||
 * @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.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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.div) {
 | 
			
		||||
      this.annotationEditorLayer.update({ viewport: viewport.clone() });
 | 
			
		||||
      this.show();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create an AnnotationEditor layer div
 | 
			
		||||
    this.div = document.createElement("div");
 | 
			
		||||
    this.div.className = "annotationEditorLayer";
 | 
			
		||||
    this.div.tabIndex = 0;
 | 
			
		||||
 | 
			
		||||
    this.annotationEditorLayer = new AnnotationEditorLayer({
 | 
			
		||||
      uiManager: this.#uiManager,
 | 
			
		||||
      div: this.div,
 | 
			
		||||
      annotationStorage: this.annotationStorage,
 | 
			
		||||
      pageIndex: this.pdfPage._pageIndex,
 | 
			
		||||
      l10n: this.l10n,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const parameters = {
 | 
			
		||||
      viewport: viewport.clone(),
 | 
			
		||||
      div: this.div,
 | 
			
		||||
      annotations: null,
 | 
			
		||||
      intent,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.annotationEditorLayer.render(parameters);
 | 
			
		||||
 | 
			
		||||
    this.pageDiv.appendChild(this.div);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  cancel() {
 | 
			
		||||
    this._cancelled = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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.div.remove();
 | 
			
		||||
    this.annotationEditorLayer.destroy();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { AnnotationEditorLayerBuilder };
 | 
			
		||||
							
								
								
									
										16
									
								
								web/app.js
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								web/app.js
									
									
									
									
									
								
							@ -525,6 +525,7 @@ const PDFViewerApplication = {
 | 
			
		||||
      l10n: this.l10n,
 | 
			
		||||
      textLayerMode: AppOptions.get("textLayerMode"),
 | 
			
		||||
      annotationMode: AppOptions.get("annotationMode"),
 | 
			
		||||
      annotationEditorEnabled: AppOptions.get("annotationEditorEnabled"),
 | 
			
		||||
      imageResourcesPath: AppOptions.get("imageResourcesPath"),
 | 
			
		||||
      enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
 | 
			
		||||
      useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
 | 
			
		||||
@ -560,6 +561,10 @@ const PDFViewerApplication = {
 | 
			
		||||
      this.findBar = new PDFFindBar(appConfig.findBar, eventBus, this.l10n);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (AppOptions.get("annotationEditorEnabled")) {
 | 
			
		||||
      document.getElementById("editorModeButtons").classList.remove("hidden");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.pdfDocumentProperties = new PDFDocumentProperties(
 | 
			
		||||
      appConfig.documentProperties,
 | 
			
		||||
      this.overlayManager,
 | 
			
		||||
@ -1878,6 +1883,10 @@ const PDFViewerApplication = {
 | 
			
		||||
    eventBus._on("namedaction", webViewerNamedAction);
 | 
			
		||||
    eventBus._on("presentationmodechanged", webViewerPresentationModeChanged);
 | 
			
		||||
    eventBus._on("presentationmode", webViewerPresentationMode);
 | 
			
		||||
    eventBus._on(
 | 
			
		||||
      "switchannotationeditormode",
 | 
			
		||||
      webViewerSwitchAnnotationEditorMode
 | 
			
		||||
    );
 | 
			
		||||
    eventBus._on("print", webViewerPrint);
 | 
			
		||||
    eventBus._on("download", webViewerDownload);
 | 
			
		||||
    eventBus._on("firstpage", webViewerFirstPage);
 | 
			
		||||
@ -2459,6 +2468,13 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
 | 
			
		||||
function webViewerPresentationMode() {
 | 
			
		||||
  PDFViewerApplication.requestPresentationMode();
 | 
			
		||||
}
 | 
			
		||||
function webViewerSwitchAnnotationEditorMode(evt) {
 | 
			
		||||
  if (evt.toggle) {
 | 
			
		||||
    PDFViewerApplication.pdfViewer.annotionEditorEnabled = true;
 | 
			
		||||
  } else {
 | 
			
		||||
    PDFViewerApplication.pdfViewer.annotationEditorMode = evt.mode;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
function webViewerPrint() {
 | 
			
		||||
  PDFViewerApplication.triggerPrinting();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -58,6 +58,11 @@ const defaultOptions = {
 | 
			
		||||
    value: 2,
 | 
			
		||||
    kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
 | 
			
		||||
  },
 | 
			
		||||
  annotationEditorEnabled: {
 | 
			
		||||
    /** @type {boolean} */
 | 
			
		||||
    value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING"),
 | 
			
		||||
    kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
 | 
			
		||||
  },
 | 
			
		||||
  cursorToolOnLoad: {
 | 
			
		||||
    /** @type {number} */
 | 
			
		||||
    value: 0,
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,8 @@
 | 
			
		||||
/** @typedef {import("./interfaces").IL10n} IL10n */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
 | 
			
		||||
@ -30,6 +32,8 @@
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  AnnotationEditorUIManager,
 | 
			
		||||
  AnnotationMode,
 | 
			
		||||
  createPromiseCapability,
 | 
			
		||||
  PermissionFlag,
 | 
			
		||||
@ -61,6 +65,7 @@ import {
 | 
			
		||||
  VERTICAL_PADDING,
 | 
			
		||||
  watchScroll,
 | 
			
		||||
} from "./ui_utils.js";
 | 
			
		||||
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
 | 
			
		||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
 | 
			
		||||
import { NullL10n } from "./l10n_utils.js";
 | 
			
		||||
import { PDFPageView } from "./pdf_page_view.js";
 | 
			
		||||
@ -104,6 +109,8 @@ const PagesCountLimit = {
 | 
			
		||||
 *   being rendered. The constants from {@link AnnotationMode} should be used;
 | 
			
		||||
 *   see also {@link RenderParameters} and {@link GetOperatorListParameters}.
 | 
			
		||||
 *   The default value is `AnnotationMode.ENABLE_FORMS`.
 | 
			
		||||
 * @property {boolean} [annotationEditorEnabled] - Enables the creation and
 | 
			
		||||
 *   editing of new Annotations.
 | 
			
		||||
 * @property {string} [imageResourcesPath] - Path for image resources, mainly
 | 
			
		||||
 *   mainly for annotation icons. Include trailing slash.
 | 
			
		||||
 * @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of
 | 
			
		||||
@ -194,6 +201,7 @@ class PDFPageViewBuffer {
 | 
			
		||||
 * Simple viewer control to display PDF content/pages.
 | 
			
		||||
 *
 | 
			
		||||
 * @implements {IPDFAnnotationLayerFactory}
 | 
			
		||||
 * @implements {IPDFAnnotationEditorLayerFactory}
 | 
			
		||||
 * @implements {IPDFStructTreeLayerFactory}
 | 
			
		||||
 * @implements {IPDFTextLayerFactory}
 | 
			
		||||
 * @implements {IPDFXfaLayerFactory}
 | 
			
		||||
@ -201,6 +209,10 @@ class PDFPageViewBuffer {
 | 
			
		||||
class BaseViewer {
 | 
			
		||||
  #buffer = null;
 | 
			
		||||
 | 
			
		||||
  #annotationEditorMode = AnnotationEditorType.NONE;
 | 
			
		||||
 | 
			
		||||
  #annotationEditorUIManager = null;
 | 
			
		||||
 | 
			
		||||
  #annotationMode = AnnotationMode.ENABLE_FORMS;
 | 
			
		||||
 | 
			
		||||
  #previousAnnotationMode = null;
 | 
			
		||||
@ -268,6 +280,10 @@ class BaseViewer {
 | 
			
		||||
    this.#enablePermissions = options.enablePermissions || false;
 | 
			
		||||
    this.pageColors = options.pageColors || null;
 | 
			
		||||
 | 
			
		||||
    if (options.annotationEditorEnabled === true) {
 | 
			
		||||
      this.#annotationEditorUIManager = new AnnotationEditorUIManager();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
 | 
			
		||||
      if (
 | 
			
		||||
        this.pageColors &&
 | 
			
		||||
@ -699,6 +715,9 @@ class BaseViewer {
 | 
			
		||||
        const annotationLayerFactory =
 | 
			
		||||
          this.#annotationMode !== AnnotationMode.DISABLE ? this : null;
 | 
			
		||||
        const xfaLayerFactory = isPureXfa ? this : null;
 | 
			
		||||
        const annotationEditorLayerFactory = this.#annotationEditorUIManager
 | 
			
		||||
          ? this
 | 
			
		||||
          : null;
 | 
			
		||||
 | 
			
		||||
        for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
 | 
			
		||||
          const pageView = new PDFPageView({
 | 
			
		||||
@ -714,6 +733,7 @@ class BaseViewer {
 | 
			
		||||
            annotationLayerFactory,
 | 
			
		||||
            annotationMode: this.#annotationMode,
 | 
			
		||||
            xfaLayerFactory,
 | 
			
		||||
            annotationEditorLayerFactory,
 | 
			
		||||
            textHighlighterFactory: this,
 | 
			
		||||
            structTreeLayerFactory: this,
 | 
			
		||||
            imageResourcesPath: this.imageResourcesPath,
 | 
			
		||||
@ -1656,6 +1676,30 @@ class BaseViewer {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {HTMLDivElement} pageDiv
 | 
			
		||||
   * @param {PDFPageProxy} pdfPage
 | 
			
		||||
   * @param {IL10n} l10n
 | 
			
		||||
   * @param {AnnotationStorage} [annotationStorage] - Storage for annotation
 | 
			
		||||
   *   data in forms.
 | 
			
		||||
   * @returns {AnnotationEditorLayerBuilder}
 | 
			
		||||
   */
 | 
			
		||||
  createAnnotationEditorLayerBuilder(
 | 
			
		||||
    pageDiv,
 | 
			
		||||
    pdfPage,
 | 
			
		||||
    l10n,
 | 
			
		||||
    annotationStorage = null
 | 
			
		||||
  ) {
 | 
			
		||||
    return new AnnotationEditorLayerBuilder({
 | 
			
		||||
      uiManager: this.#annotationEditorUIManager,
 | 
			
		||||
      pageDiv,
 | 
			
		||||
      pdfPage,
 | 
			
		||||
      annotationStorage:
 | 
			
		||||
        annotationStorage || this.pdfDocument?.annotationStorage,
 | 
			
		||||
      l10n,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {HTMLDivElement} pageDiv
 | 
			
		||||
   * @param {PDFPageProxy} pdfPage
 | 
			
		||||
@ -2072,6 +2116,36 @@ class BaseViewer {
 | 
			
		||||
      docStyle.setProperty("--viewer-container-height", `${height}px`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get annotationEditorMode() {
 | 
			
		||||
    return this.#annotationEditorMode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {number} mode - Annotation Editor mode (None, FreeText, Ink, ...)
 | 
			
		||||
   */
 | 
			
		||||
  set annotationEditorMode(mode) {
 | 
			
		||||
    if (!this.#annotationEditorUIManager) {
 | 
			
		||||
      throw new Error(`The AnnotationEditor is not enabled.`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.#annotationEditorMode === mode) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!Object.values(AnnotationEditorType).includes(mode)) {
 | 
			
		||||
      throw new Error(`Invalid AnnotationEditor mode: ${mode}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If the mode is the same as before, it means that this mode is disabled
 | 
			
		||||
    // and consequently the mode is NONE.
 | 
			
		||||
    this.#annotationEditorMode = mode;
 | 
			
		||||
    this.eventBus.dispatch("annotationeditormodechanged", {
 | 
			
		||||
      source: this,
 | 
			
		||||
      mode,
 | 
			
		||||
    });
 | 
			
		||||
    this.#annotationEditorUIManager.updateMode(mode);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { BaseViewer, PagesCountLimit, PDFPageViewBuffer };
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,8 @@
 | 
			
		||||
/** @typedef {import("./interfaces").IL10n} IL10n */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
 | 
			
		||||
@ -29,6 +31,7 @@
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
 | 
			
		||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
 | 
			
		||||
 | 
			
		||||
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
 | 
			
		||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
 | 
			
		||||
import { NullL10n } from "./l10n_utils.js";
 | 
			
		||||
import { SimpleLinkService } from "./pdf_link_service.js";
 | 
			
		||||
@ -87,6 +90,32 @@ class DefaultAnnotationLayerFactory {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @implements IPDFAnnotationEditorLayerFactory
 | 
			
		||||
 */
 | 
			
		||||
class DefaultAnnotationEditorLayerFactory {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {HTMLDivElement} pageDiv
 | 
			
		||||
   * @param {PDFPageProxy} pdfPage
 | 
			
		||||
   * @param {IL10n} l10n
 | 
			
		||||
   * @param {AnnotationStorage} [annotationStorage]
 | 
			
		||||
   * @returns {AnnotationEditorLayerBuilder}
 | 
			
		||||
   */
 | 
			
		||||
  createAnnotationEditorLayerBuilder(
 | 
			
		||||
    pageDiv,
 | 
			
		||||
    pdfPage,
 | 
			
		||||
    l10n,
 | 
			
		||||
    annotationStorage = null
 | 
			
		||||
  ) {
 | 
			
		||||
    return new AnnotationEditorLayerBuilder({
 | 
			
		||||
      pageDiv,
 | 
			
		||||
      pdfPage,
 | 
			
		||||
      l10n,
 | 
			
		||||
      annotationStorage,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @implements IPDFStructTreeLayerFactory
 | 
			
		||||
 */
 | 
			
		||||
@ -161,6 +190,7 @@ class DefaultXfaLayerFactory {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  DefaultAnnotationEditorLayerFactory,
 | 
			
		||||
  DefaultAnnotationLayerFactory,
 | 
			
		||||
  DefaultStructTreeLayerFactory,
 | 
			
		||||
  DefaultTextLayerFactory,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								web/images/toolbarButton-editorFreeText.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								web/images/toolbarButton-editorFreeText.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
<?xml version="1.0" encoding="iso-8859-1"?>
 | 
			
		||||
<!-- copied from https://www.svgrepo.com/svg/255881/text -->
 | 
			
		||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
	 viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
 | 
			
		||||
<g>
 | 
			
		||||
	<g transform="scale(0.03125)">
 | 
			
		||||
		<path d="M405.787,43.574H8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83c0,4.512,3.657,8.17,8.17,8.17h32.681
 | 
			
		||||
			c4.513,0,8.17-3.658,8.17-8.17v-24.511h95.319v119.83c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-128
 | 
			
		||||
			c0-4.512-3.657-8.17-8.17-8.17H40.851c-4.513,0-8.17,3.658-8.17,8.17v24.511H16.34V59.915h381.277v103.489h-16.34v-24.511
 | 
			
		||||
			c0-4.512-3.657-8.17-8.17-8.17h-111.66c-4.513,0-8.17,3.658-8.17,8.17v288.681c0,4.512,3.657,8.17,8.17,8.17h57.191v16.34H95.319
 | 
			
		||||
			v-16.34h57.191c4.513,0,8.17-3.658,8.17-8.17v-128c0-4.512-3.657-8.17-8.17-8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83H87.149
 | 
			
		||||
			c-4.513,0-8.17,3.658-8.17,8.17v32.681c0,4.512,3.657,8.17,8.17,8.17h239.66c4.513,0,8.17-3.658,8.17-8.17v-32.681
 | 
			
		||||
			c0-4.512-3.657-8.17-8.17-8.17h-57.192v-272.34h95.319v24.511c0,4.512,3.657,8.17,8.17,8.17h32.681c4.513,0,8.17-3.658,8.17-8.17
 | 
			
		||||
			V51.745C413.957,47.233,410.3,43.574,405.787,43.574z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<g transform="scale(0.03125)">
 | 
			
		||||
		<path d="M503.83,452.085h-24.511V59.915h24.511c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-65.362
 | 
			
		||||
			c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h24.511v392.17h-24.511c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17
 | 
			
		||||
			h65.362c4.513,0,8.17-3.658,8.17-8.17S508.343,452.085,503.83,452.085z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.6 KiB  | 
							
								
								
									
										4
									
								
								web/images/toolbarButton-editorNone.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								web/images/toolbarButton-editorNone.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
 | 
			
		||||
   - License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
			
		||||
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 478 B  | 
@ -19,6 +19,8 @@
 | 
			
		||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./annotation_layer_builder").AnnotationLayerBuilder} AnnotationLayerBuilder */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./annotation_editor_layer_builder").AnnotationEditorLayerBuilder} AnnotationEditorLayerBuilder */
 | 
			
		||||
/** @typedef {import("./event_utils").EventBus} EventBus */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./struct_tree_builder").StructTreeLayerBuilder} StructTreeLayerBuilder */
 | 
			
		||||
@ -208,6 +210,26 @@ class IPDFAnnotationLayerFactory {
 | 
			
		||||
  ) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @interface
 | 
			
		||||
 */
 | 
			
		||||
class IPDFAnnotationEditorLayerFactory {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {HTMLDivElement} pageDiv
 | 
			
		||||
   * @param {PDFPageProxy} pdfPage
 | 
			
		||||
   * @param {IL10n} l10n
 | 
			
		||||
   * @param {AnnotationStorage} [annotationStorage] - Storage for annotation
 | 
			
		||||
   *   data in forms.
 | 
			
		||||
   * @returns {AnnotationEditorLayerBuilder}
 | 
			
		||||
   */
 | 
			
		||||
  createAnnotationEditorLayerBuilder(
 | 
			
		||||
    pageDiv,
 | 
			
		||||
    pdfPage,
 | 
			
		||||
    l10n = undefined,
 | 
			
		||||
    annotationStorage = null
 | 
			
		||||
  ) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @interface
 | 
			
		||||
 */
 | 
			
		||||
@ -307,6 +329,7 @@ class IL10n {
 | 
			
		||||
export {
 | 
			
		||||
  IDownloadManager,
 | 
			
		||||
  IL10n,
 | 
			
		||||
  IPDFAnnotationEditorLayerFactory,
 | 
			
		||||
  IPDFAnnotationLayerFactory,
 | 
			
		||||
  IPDFLinkService,
 | 
			
		||||
  IPDFStructTreeLayerFactory,
 | 
			
		||||
 | 
			
		||||
@ -81,6 +81,7 @@ const DEFAULT_L10N_STRINGS = {
 | 
			
		||||
  printing_not_ready: "Warning: The PDF is not fully loaded for printing.",
 | 
			
		||||
  web_fonts_disabled:
 | 
			
		||||
    "Web fonts are disabled: unable to use embedded PDF fonts.",
 | 
			
		||||
  freetext_default_content: "Enter some text…",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getL10nFallback(key, args) {
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,8 @@
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
 | 
			
		||||
@ -72,6 +74,7 @@ import { NullL10n } from "./l10n_utils.js";
 | 
			
		||||
 *   see also {@link RenderParameters} and {@link GetOperatorListParameters}.
 | 
			
		||||
 *   The default value is `AnnotationMode.ENABLE_FORMS`.
 | 
			
		||||
 * @property {IPDFAnnotationLayerFactory} annotationLayerFactory
 | 
			
		||||
 * @property {IPDFAnnotationEditorLayerFactory} annotationEditorLayerFactory
 | 
			
		||||
 * @property {IPDFXfaLayerFactory} xfaLayerFactory
 | 
			
		||||
 * @property {IPDFStructTreeLayerFactory} structTreeLayerFactory
 | 
			
		||||
 * @property {Object} [textHighlighterFactory]
 | 
			
		||||
@ -128,6 +131,7 @@ class PDFPageView {
 | 
			
		||||
    this.renderingQueue = options.renderingQueue;
 | 
			
		||||
    this.textLayerFactory = options.textLayerFactory;
 | 
			
		||||
    this.annotationLayerFactory = options.annotationLayerFactory;
 | 
			
		||||
    this.annotationEditorLayerFactory = options.annotationEditorLayerFactory;
 | 
			
		||||
    this.xfaLayerFactory = options.xfaLayerFactory;
 | 
			
		||||
    this.textHighlighter =
 | 
			
		||||
      options.textHighlighterFactory?.createTextHighlighter(
 | 
			
		||||
@ -148,6 +152,7 @@ class PDFPageView {
 | 
			
		||||
    this._annotationCanvasMap = null;
 | 
			
		||||
 | 
			
		||||
    this.annotationLayer = null;
 | 
			
		||||
    this.annotationEditorLayer = null;
 | 
			
		||||
    this.textLayer = null;
 | 
			
		||||
    this.zoomLayer = null;
 | 
			
		||||
    this.xfaLayer = null;
 | 
			
		||||
@ -204,6 +209,24 @@ class PDFPageView {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  async _renderAnnotationEditorLayer() {
 | 
			
		||||
    let error = null;
 | 
			
		||||
    try {
 | 
			
		||||
      await this.annotationEditorLayer.render(this.viewport, "display");
 | 
			
		||||
    } catch (ex) {
 | 
			
		||||
      error = ex;
 | 
			
		||||
    } finally {
 | 
			
		||||
      this.eventBus.dispatch("annotationeditorlayerrendered", {
 | 
			
		||||
        source: this,
 | 
			
		||||
        pageNumber: this.id,
 | 
			
		||||
        error,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
@ -259,9 +282,14 @@ class PDFPageView {
 | 
			
		||||
  reset({
 | 
			
		||||
    keepZoomLayer = false,
 | 
			
		||||
    keepAnnotationLayer = false,
 | 
			
		||||
    keepAnnotationEditorLayer = false,
 | 
			
		||||
    keepXfaLayer = false,
 | 
			
		||||
  } = {}) {
 | 
			
		||||
    this.cancelRendering({ keepAnnotationLayer, keepXfaLayer });
 | 
			
		||||
    this.cancelRendering({
 | 
			
		||||
      keepAnnotationLayer,
 | 
			
		||||
      keepAnnotationEditorLayer,
 | 
			
		||||
      keepXfaLayer,
 | 
			
		||||
    });
 | 
			
		||||
    this.renderingState = RenderingStates.INITIAL;
 | 
			
		||||
 | 
			
		||||
    const div = this.div;
 | 
			
		||||
@ -272,12 +300,15 @@ class PDFPageView {
 | 
			
		||||
      zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
 | 
			
		||||
      annotationLayerNode =
 | 
			
		||||
        (keepAnnotationLayer && this.annotationLayer?.div) || null,
 | 
			
		||||
      annotationEditorLayerNode =
 | 
			
		||||
        (keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null,
 | 
			
		||||
      xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null;
 | 
			
		||||
    for (let i = childNodes.length - 1; i >= 0; i--) {
 | 
			
		||||
      const node = childNodes[i];
 | 
			
		||||
      switch (node) {
 | 
			
		||||
        case zoomLayerNode:
 | 
			
		||||
        case annotationLayerNode:
 | 
			
		||||
        case annotationEditorLayerNode:
 | 
			
		||||
        case xfaLayerNode:
 | 
			
		||||
          continue;
 | 
			
		||||
      }
 | 
			
		||||
@ -290,6 +321,12 @@ class PDFPageView {
 | 
			
		||||
      // so they are not displayed on the already resized page.
 | 
			
		||||
      this.annotationLayer.hide();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (annotationEditorLayerNode) {
 | 
			
		||||
      this.annotationEditorLayer.hide();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.annotationEditorLayer?.destroy();
 | 
			
		||||
    }
 | 
			
		||||
    if (xfaLayerNode) {
 | 
			
		||||
      // Hide the XFA layer until all elements are resized
 | 
			
		||||
      // so they are not displayed on the already resized page.
 | 
			
		||||
@ -347,6 +384,7 @@ class PDFPageView {
 | 
			
		||||
      this.cssTransform({
 | 
			
		||||
        target: this.svg,
 | 
			
		||||
        redrawAnnotationLayer: true,
 | 
			
		||||
        redrawAnnotationEditorLayer: true,
 | 
			
		||||
        redrawXfaLayer: true,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@ -380,6 +418,7 @@ class PDFPageView {
 | 
			
		||||
        this.cssTransform({
 | 
			
		||||
          target: this.canvas,
 | 
			
		||||
          redrawAnnotationLayer: true,
 | 
			
		||||
          redrawAnnotationEditorLayer: true,
 | 
			
		||||
          redrawXfaLayer: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -403,6 +442,7 @@ class PDFPageView {
 | 
			
		||||
    this.reset({
 | 
			
		||||
      keepZoomLayer: true,
 | 
			
		||||
      keepAnnotationLayer: true,
 | 
			
		||||
      keepAnnotationEditorLayer: true,
 | 
			
		||||
      keepXfaLayer: true,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
@ -411,7 +451,11 @@ class PDFPageView {
 | 
			
		||||
   * PLEASE NOTE: Most likely you want to use the `this.reset()` method,
 | 
			
		||||
   *              rather than calling this one directly.
 | 
			
		||||
   */
 | 
			
		||||
  cancelRendering({ keepAnnotationLayer = false, keepXfaLayer = false } = {}) {
 | 
			
		||||
  cancelRendering({
 | 
			
		||||
    keepAnnotationLayer = false,
 | 
			
		||||
    keepAnnotationEditorLayer = false,
 | 
			
		||||
    keepXfaLayer = false,
 | 
			
		||||
  } = {}) {
 | 
			
		||||
    if (this.paintTask) {
 | 
			
		||||
      this.paintTask.cancel();
 | 
			
		||||
      this.paintTask = null;
 | 
			
		||||
@ -430,6 +474,13 @@ class PDFPageView {
 | 
			
		||||
      this.annotationLayer = null;
 | 
			
		||||
      this._annotationCanvasMap = null;
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
      this.annotationEditorLayer &&
 | 
			
		||||
      (!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
 | 
			
		||||
    ) {
 | 
			
		||||
      this.annotationEditorLayer.cancel();
 | 
			
		||||
      this.annotationEditorLayer = null;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
 | 
			
		||||
      this.xfaLayer.cancel();
 | 
			
		||||
      this.xfaLayer = null;
 | 
			
		||||
@ -444,6 +495,7 @@ class PDFPageView {
 | 
			
		||||
  cssTransform({
 | 
			
		||||
    target,
 | 
			
		||||
    redrawAnnotationLayer = false,
 | 
			
		||||
    redrawAnnotationEditorLayer = false,
 | 
			
		||||
    redrawXfaLayer = false,
 | 
			
		||||
  }) {
 | 
			
		||||
    // Scale target (canvas or svg), its wrapper and page container.
 | 
			
		||||
@ -517,6 +569,9 @@ class PDFPageView {
 | 
			
		||||
    if (redrawAnnotationLayer && this.annotationLayer) {
 | 
			
		||||
      this._renderAnnotationLayer();
 | 
			
		||||
    }
 | 
			
		||||
    if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
 | 
			
		||||
      this._renderAnnotationEditorLayer();
 | 
			
		||||
    }
 | 
			
		||||
    if (redrawXfaLayer && this.xfaLayer) {
 | 
			
		||||
      this._renderXfaLayer();
 | 
			
		||||
    }
 | 
			
		||||
@ -567,9 +622,12 @@ class PDFPageView {
 | 
			
		||||
    canvasWrapper.style.height = div.style.height;
 | 
			
		||||
    canvasWrapper.classList.add("canvasWrapper");
 | 
			
		||||
 | 
			
		||||
    if (this.annotationLayer?.div) {
 | 
			
		||||
    const lastDivBeforeTextDiv =
 | 
			
		||||
      this.annotationLayer?.div || this.annotationEditorLayer?.div;
 | 
			
		||||
 | 
			
		||||
    if (lastDivBeforeTextDiv) {
 | 
			
		||||
      // The annotation layer needs to stay on top.
 | 
			
		||||
      div.insertBefore(canvasWrapper, this.annotationLayer.div);
 | 
			
		||||
      div.insertBefore(canvasWrapper, lastDivBeforeTextDiv);
 | 
			
		||||
    } else {
 | 
			
		||||
      div.appendChild(canvasWrapper);
 | 
			
		||||
    }
 | 
			
		||||
@ -580,9 +638,9 @@ class PDFPageView {
 | 
			
		||||
      textLayerDiv.className = "textLayer";
 | 
			
		||||
      textLayerDiv.style.width = canvasWrapper.style.width;
 | 
			
		||||
      textLayerDiv.style.height = canvasWrapper.style.height;
 | 
			
		||||
      if (this.annotationLayer?.div) {
 | 
			
		||||
      if (lastDivBeforeTextDiv) {
 | 
			
		||||
        // The annotation layer needs to stay on top.
 | 
			
		||||
        div.insertBefore(textLayerDiv, this.annotationLayer.div);
 | 
			
		||||
        div.insertBefore(textLayerDiv, lastDivBeforeTextDiv);
 | 
			
		||||
      } else {
 | 
			
		||||
        div.appendChild(textLayerDiv);
 | 
			
		||||
      }
 | 
			
		||||
@ -693,7 +751,18 @@ class PDFPageView {
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (this.annotationLayer) {
 | 
			
		||||
            this._renderAnnotationLayer();
 | 
			
		||||
            this._renderAnnotationLayer().then(() => {
 | 
			
		||||
              if (this.annotationEditorLayerFactory) {
 | 
			
		||||
                this.annotationEditorLayer ||=
 | 
			
		||||
                  this.annotationEditorLayerFactory.createAnnotationEditorLayerBuilder(
 | 
			
		||||
                    div,
 | 
			
		||||
                    pdfPage,
 | 
			
		||||
                    this.l10n,
 | 
			
		||||
                    /* annotationStorage = */ null
 | 
			
		||||
                  );
 | 
			
		||||
                this._renderAnnotationEditorLayer();
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@
 | 
			
		||||
@import url(text_layer_builder.css);
 | 
			
		||||
@import url(annotation_layer_builder.css);
 | 
			
		||||
@import url(xfa_layer_builder.css);
 | 
			
		||||
@import url(annotation_editor_layer_builder.css);
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  --viewer-container-height: 0;
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ import {
 | 
			
		||||
  MIN_SCALE,
 | 
			
		||||
  noContextMenuHandler,
 | 
			
		||||
} from "./ui_utils.js";
 | 
			
		||||
import { AnnotationEditorType } from "pdfjs-lib";
 | 
			
		||||
 | 
			
		||||
const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
 | 
			
		||||
 | 
			
		||||
@ -43,6 +44,9 @@ const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
 | 
			
		||||
 * @property {HTMLButtonElement} openFile - Button to open a new document.
 | 
			
		||||
 * @property {HTMLButtonElement} presentationModeButton - Button to switch to
 | 
			
		||||
 *   presentation mode.
 | 
			
		||||
 * @property {HTMLButtonElement} editorNoneButton - Button to disable editing.
 | 
			
		||||
 * @property {HTMLButtonElement} editorFreeTextButton - Button to switch to Free
 | 
			
		||||
 *   Text edition.
 | 
			
		||||
 * @property {HTMLButtonElement} download - Button to download the document.
 | 
			
		||||
 * @property {HTMLAnchorElement} viewBookmark - Button to obtain a bookmark link
 | 
			
		||||
 *   to the current location in the document.
 | 
			
		||||
@ -70,6 +74,16 @@ class Toolbar {
 | 
			
		||||
      },
 | 
			
		||||
      { element: options.download, eventName: "download" },
 | 
			
		||||
      { element: options.viewBookmark, eventName: null },
 | 
			
		||||
      {
 | 
			
		||||
        element: options.editorNoneButton,
 | 
			
		||||
        eventName: "switchannotationeditormode",
 | 
			
		||||
        eventDetails: { mode: AnnotationEditorType.NONE },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        element: options.editorFreeTextButton,
 | 
			
		||||
        eventName: "switchannotationeditormode",
 | 
			
		||||
        eventDetails: { mode: AnnotationEditorType.FREETEXT },
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
 | 
			
		||||
      this.buttons.push({ element: options.openFile, eventName: "openfile" });
 | 
			
		||||
@ -89,7 +103,7 @@ class Toolbar {
 | 
			
		||||
    this.reset();
 | 
			
		||||
 | 
			
		||||
    // Bind the event listeners for click and various other actions.
 | 
			
		||||
    this._bindListeners();
 | 
			
		||||
    this._bindListeners(options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setPageNumber(pageNumber, pageLabel) {
 | 
			
		||||
@ -121,15 +135,21 @@ class Toolbar {
 | 
			
		||||
    this.updateLoadingIndicatorState();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _bindListeners() {
 | 
			
		||||
  _bindListeners(options) {
 | 
			
		||||
    const { pageNumber, scaleSelect } = this.items;
 | 
			
		||||
    const self = this;
 | 
			
		||||
 | 
			
		||||
    // The buttons within the toolbar.
 | 
			
		||||
    for (const { element, eventName } of this.buttons) {
 | 
			
		||||
    for (const { element, eventName, eventDetails } of this.buttons) {
 | 
			
		||||
      element.addEventListener("click", evt => {
 | 
			
		||||
        if (eventName !== null) {
 | 
			
		||||
          this.eventBus.dispatch(eventName, { source: this });
 | 
			
		||||
          const details = { source: this };
 | 
			
		||||
          if (eventDetails) {
 | 
			
		||||
            for (const property in eventDetails) {
 | 
			
		||||
              details[property] = eventDetails[property];
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          this.eventBus.dispatch(eventName, details);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
@ -174,6 +194,23 @@ class Toolbar {
 | 
			
		||||
      this.#adjustScaleWidth();
 | 
			
		||||
      this._updateUIState(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.#bindEditorToolsListener(options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #bindEditorToolsListener({ editorNoneButton, editorFreeTextButton }) {
 | 
			
		||||
    this.eventBus._on("annotationeditormodechanged", evt => {
 | 
			
		||||
      const editorButtons = [
 | 
			
		||||
        [AnnotationEditorType.NONE, editorNoneButton],
 | 
			
		||||
        [AnnotationEditorType.FREETEXT, editorFreeTextButton],
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      for (const [mode, button] of editorButtons) {
 | 
			
		||||
        const checked = mode === evt.mode;
 | 
			
		||||
        button.classList.toggle("toggled", checked);
 | 
			
		||||
        button.setAttribute("aria-checked", checked);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _updateUIState(resetNumPages = false) {
 | 
			
		||||
 | 
			
		||||
@ -71,6 +71,8 @@
 | 
			
		||||
  --loading-icon: url(images/loading.svg);
 | 
			
		||||
  --treeitem-expanded-icon: url(images/treeitem-expanded.svg);
 | 
			
		||||
  --treeitem-collapsed-icon: url(images/treeitem-collapsed.svg);
 | 
			
		||||
  --toolbarButton-editorFreeText-icon: url(images/toolbarButton-editorFreeText.svg);
 | 
			
		||||
  --toolbarButton-editorNone-icon: url(images/toolbarButton-editorNone.svg);
 | 
			
		||||
  --toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
 | 
			
		||||
  --toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg);
 | 
			
		||||
  --toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg);
 | 
			
		||||
@ -824,6 +826,14 @@ select {
 | 
			
		||||
  mask-image: var(--toolbarButton-presentationMode-icon);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#editorNone::before {
 | 
			
		||||
  mask-image: var(--toolbarButton-editorNone-icon);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#editorFreeText::before {
 | 
			
		||||
  mask-image: var(--toolbarButton-editorFreeText-icon);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#print::before,
 | 
			
		||||
#secondaryPrint::before {
 | 
			
		||||
  mask-image: var(--toolbarButton-print-icon);
 | 
			
		||||
 | 
			
		||||
@ -263,30 +263,39 @@ See https://github.com/adobe-type-tools/cmap-resources
 | 
			
		||||
                <span id="numPages" class="toolbarLabel"></span>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div id="toolbarViewerRight">
 | 
			
		||||
                <button id="presentationMode" class="toolbarButton hiddenLargeView" title="Switch to Presentation Mode" tabindex="31" data-l10n-id="presentation_mode">
 | 
			
		||||
                <div id="editorModeButtons" class="splitToolbarButton hidden" role="radiogroup">
 | 
			
		||||
                  <button id="editorNone" class="toolbarButton" title="Disable Annotation Editing" role="radio" aria-checked="false" tabindex="31" data-l10n-id="editor_none">
 | 
			
		||||
                    <span data-l10n-id="editor_none_label">Disable Editing</span>
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button id="editorFreeText" class="toolbarButton" title="Add FreeText Annotation" role="radio" aria-checked="false" tabindex="32" data-l10n-id="editor_free_text">
 | 
			
		||||
                    <span data-l10n-id="editor_free_text_label">FreeText Annotation</span>
 | 
			
		||||
                  </button>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <button id="presentationMode" class="toolbarButton hiddenLargeView" title="Switch to Presentation Mode" tabindex="43" data-l10n-id="presentation_mode">
 | 
			
		||||
                  <span data-l10n-id="presentation_mode_label">Presentation Mode</span>
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
<!--#if GENERIC-->
 | 
			
		||||
                <button id="openFile" class="toolbarButton hiddenLargeView" title="Open File" tabindex="32" data-l10n-id="open_file">
 | 
			
		||||
                <button id="openFile" class="toolbarButton hiddenLargeView" title="Open File" tabindex="44" data-l10n-id="open_file">
 | 
			
		||||
                  <span data-l10n-id="open_file_label">Open</span>
 | 
			
		||||
                </button>
 | 
			
		||||
<!--#endif-->
 | 
			
		||||
 | 
			
		||||
                <button id="print" class="toolbarButton hiddenMediumView" title="Print" tabindex="33" data-l10n-id="print">
 | 
			
		||||
                <button id="print" class="toolbarButton hiddenMediumView" title="Print" tabindex="45" data-l10n-id="print">
 | 
			
		||||
                  <span data-l10n-id="print_label">Print</span>
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
                <button id="download" class="toolbarButton hiddenMediumView" title="Download" tabindex="34" data-l10n-id="download">
 | 
			
		||||
                <button id="download" class="toolbarButton hiddenMediumView" title="Download" tabindex="46" data-l10n-id="download">
 | 
			
		||||
                  <span data-l10n-id="download_label">Download</span>
 | 
			
		||||
                </button>
 | 
			
		||||
                <a href="#" id="viewBookmark" class="toolbarButton hiddenSmallView" title="Current view (copy or open in new window)" tabindex="35" data-l10n-id="bookmark">
 | 
			
		||||
                <a href="#" id="viewBookmark" class="toolbarButton hiddenSmallView" title="Current view (copy or open in new window)" tabindex="47" data-l10n-id="bookmark">
 | 
			
		||||
                  <span data-l10n-id="bookmark_label">Current View</span>
 | 
			
		||||
                </a>
 | 
			
		||||
 | 
			
		||||
                <div class="verticalToolbarSeparator hiddenSmallView"></div>
 | 
			
		||||
 | 
			
		||||
                <button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="36" data-l10n-id="tools" aria-expanded="false" aria-controls="secondaryToolbar">
 | 
			
		||||
                <button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="48" data-l10n-id="tools" aria-expanded="false" aria-controls="secondaryToolbar">
 | 
			
		||||
                  <span data-l10n-id="tools_label">Tools</span>
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
@ -93,6 +93,8 @@ function getViewerConfiguration() {
 | 
			
		||||
          ? document.getElementById("openFile")
 | 
			
		||||
          : null,
 | 
			
		||||
      print: document.getElementById("print"),
 | 
			
		||||
      editorFreeTextButton: document.getElementById("editorFreeText"),
 | 
			
		||||
      editorNoneButton: document.getElementById("editorNone"),
 | 
			
		||||
      presentationModeButton: document.getElementById("presentationMode"),
 | 
			
		||||
      download: document.getElementById("download"),
 | 
			
		||||
      viewBookmark: document.getElementById("viewBookmark"),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user