annotation_editor_layer.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. /**
  2. * @licstart The following is the entire license notice for the
  3. * JavaScript code in this page
  4. *
  5. * Copyright 2022 Mozilla Foundation
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. *
  19. * @licend The above is the entire license notice for the
  20. * JavaScript code in this page
  21. */
  22. "use strict";
  23. Object.defineProperty(exports, "__esModule", {
  24. value: true
  25. });
  26. exports.AnnotationEditorLayer = void 0;
  27. var _util = require("../../shared/util.js");
  28. var _tools = require("./tools.js");
  29. var _display_utils = require("../display_utils.js");
  30. var _freetext = require("./freetext.js");
  31. var _ink = require("./ink.js");
  32. class AnnotationEditorLayer {
  33. #allowClick = false;
  34. #boundPointerup = this.pointerup.bind(this);
  35. #boundPointerdown = this.pointerdown.bind(this);
  36. #editors = new Map();
  37. #isCleaningUp = false;
  38. #textLayerMap = new WeakMap();
  39. #textNodes = new Map();
  40. #uiManager;
  41. #waitingEditors = new Set();
  42. static _initialized = false;
  43. constructor(options) {
  44. if (!AnnotationEditorLayer._initialized) {
  45. AnnotationEditorLayer._initialized = true;
  46. _freetext.FreeTextEditor.initialize(options.l10n);
  47. _ink.InkEditor.initialize(options.l10n);
  48. options.uiManager.registerEditorTypes([_freetext.FreeTextEditor, _ink.InkEditor]);
  49. }
  50. this.#uiManager = options.uiManager;
  51. this.annotationStorage = options.annotationStorage;
  52. this.pageIndex = options.pageIndex;
  53. this.div = options.div;
  54. this.#uiManager.addLayer(this);
  55. }
  56. get textLayerElements() {
  57. const textLayer = this.div.parentNode.getElementsByClassName("textLayer").item(0);
  58. if (!textLayer) {
  59. return (0, _util.shadow)(this, "textLayerElements", null);
  60. }
  61. let textChildren = this.#textLayerMap.get(textLayer);
  62. if (textChildren) {
  63. return textChildren;
  64. }
  65. textChildren = textLayer.querySelectorAll(`span[role="presentation"]`);
  66. if (textChildren.length === 0) {
  67. return (0, _util.shadow)(this, "textLayerElements", null);
  68. }
  69. textChildren = Array.from(textChildren);
  70. textChildren.sort(AnnotationEditorLayer.#compareElementPositions);
  71. this.#textLayerMap.set(textLayer, textChildren);
  72. return textChildren;
  73. }
  74. get #hasTextLayer() {
  75. return !!this.div.parentNode.querySelector(".textLayer .endOfContent");
  76. }
  77. updateToolbar(mode) {
  78. this.#uiManager.updateToolbar(mode);
  79. }
  80. updateMode(mode = this.#uiManager.getMode()) {
  81. this.#cleanup();
  82. if (mode === _util.AnnotationEditorType.INK) {
  83. this.addInkEditorIfNeeded(false);
  84. this.disableClick();
  85. } else {
  86. this.enableClick();
  87. }
  88. this.#uiManager.unselectAll();
  89. }
  90. addInkEditorIfNeeded(isCommitting) {
  91. if (!isCommitting && this.#uiManager.getMode() !== _util.AnnotationEditorType.INK) {
  92. return;
  93. }
  94. if (!isCommitting) {
  95. for (const editor of this.#editors.values()) {
  96. if (editor.isEmpty()) {
  97. editor.setInBackground();
  98. return;
  99. }
  100. }
  101. }
  102. const editor = this.#createAndAddNewEditor({
  103. offsetX: 0,
  104. offsetY: 0
  105. });
  106. editor.setInBackground();
  107. }
  108. setEditingState(isEditing) {
  109. this.#uiManager.setEditingState(isEditing);
  110. }
  111. addCommands(params) {
  112. this.#uiManager.addCommands(params);
  113. }
  114. enable() {
  115. this.div.style.pointerEvents = "auto";
  116. for (const editor of this.#editors.values()) {
  117. editor.enableEditing();
  118. }
  119. }
  120. disable() {
  121. this.div.style.pointerEvents = "none";
  122. for (const editor of this.#editors.values()) {
  123. editor.disableEditing();
  124. }
  125. }
  126. setActiveEditor(editor) {
  127. const currentActive = this.#uiManager.getActive();
  128. if (currentActive === editor) {
  129. return;
  130. }
  131. this.#uiManager.setActiveEditor(editor);
  132. }
  133. enableClick() {
  134. this.div.addEventListener("pointerdown", this.#boundPointerdown);
  135. this.div.addEventListener("pointerup", this.#boundPointerup);
  136. }
  137. disableClick() {
  138. this.div.removeEventListener("pointerdown", this.#boundPointerdown);
  139. this.div.removeEventListener("pointerup", this.#boundPointerup);
  140. }
  141. attach(editor) {
  142. this.#editors.set(editor.id, editor);
  143. }
  144. detach(editor) {
  145. this.#editors.delete(editor.id);
  146. this.removePointerInTextLayer(editor);
  147. }
  148. remove(editor) {
  149. this.#uiManager.removeEditor(editor);
  150. this.detach(editor);
  151. this.annotationStorage.removeKey(editor.id);
  152. editor.div.style.display = "none";
  153. setTimeout(() => {
  154. editor.div.style.display = "";
  155. editor.div.remove();
  156. editor.isAttachedToDOM = false;
  157. if (document.activeElement === document.body) {
  158. this.#uiManager.focusMainContainer();
  159. }
  160. }, 0);
  161. if (!this.#isCleaningUp) {
  162. this.addInkEditorIfNeeded(false);
  163. }
  164. }
  165. #changeParent(editor) {
  166. if (editor.parent === this) {
  167. return;
  168. }
  169. this.attach(editor);
  170. editor.pageIndex = this.pageIndex;
  171. editor.parent?.detach(editor);
  172. editor.parent = this;
  173. if (editor.div && editor.isAttachedToDOM) {
  174. editor.div.remove();
  175. this.div.append(editor.div);
  176. }
  177. }
  178. static #compareElementPositions(e1, e2) {
  179. const rect1 = e1.getBoundingClientRect();
  180. const rect2 = e2.getBoundingClientRect();
  181. if (rect1.y + rect1.height <= rect2.y) {
  182. return -1;
  183. }
  184. if (rect2.y + rect2.height <= rect1.y) {
  185. return +1;
  186. }
  187. const centerX1 = rect1.x + rect1.width / 2;
  188. const centerX2 = rect2.x + rect2.width / 2;
  189. return centerX1 - centerX2;
  190. }
  191. onTextLayerRendered() {
  192. this.#textNodes.clear();
  193. for (const editor of this.#waitingEditors) {
  194. if (editor.isAttachedToDOM) {
  195. this.addPointerInTextLayer(editor);
  196. }
  197. }
  198. this.#waitingEditors.clear();
  199. }
  200. removePointerInTextLayer(editor) {
  201. if (!this.#hasTextLayer) {
  202. this.#waitingEditors.delete(editor);
  203. return;
  204. }
  205. const {
  206. id
  207. } = editor;
  208. const node = this.#textNodes.get(id);
  209. if (!node) {
  210. return;
  211. }
  212. this.#textNodes.delete(id);
  213. let owns = node.getAttribute("aria-owns");
  214. if (owns?.includes(id)) {
  215. owns = owns.split(" ").filter(x => x !== id).join(" ");
  216. if (owns) {
  217. node.setAttribute("aria-owns", owns);
  218. } else {
  219. node.removeAttribute("aria-owns");
  220. node.setAttribute("role", "presentation");
  221. }
  222. }
  223. }
  224. addPointerInTextLayer(editor) {
  225. if (!this.#hasTextLayer) {
  226. this.#waitingEditors.add(editor);
  227. return;
  228. }
  229. this.removePointerInTextLayer(editor);
  230. const children = this.textLayerElements;
  231. if (!children) {
  232. return;
  233. }
  234. const {
  235. contentDiv
  236. } = editor;
  237. const id = editor.getIdForTextLayer();
  238. const index = (0, _display_utils.binarySearchFirstItem)(children, node => AnnotationEditorLayer.#compareElementPositions(contentDiv, node) < 0);
  239. const node = children[Math.max(0, index - 1)];
  240. const owns = node.getAttribute("aria-owns");
  241. if (!owns?.includes(id)) {
  242. node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id);
  243. }
  244. node.removeAttribute("role");
  245. this.#textNodes.set(id, node);
  246. }
  247. moveDivInDOM(editor) {
  248. this.addPointerInTextLayer(editor);
  249. const {
  250. div,
  251. contentDiv
  252. } = editor;
  253. if (!this.div.hasChildNodes()) {
  254. this.div.append(div);
  255. return;
  256. }
  257. const children = Array.from(this.div.childNodes).filter(node => node !== div);
  258. if (children.length === 0) {
  259. return;
  260. }
  261. const index = (0, _display_utils.binarySearchFirstItem)(children, node => AnnotationEditorLayer.#compareElementPositions(contentDiv, node) < 0);
  262. if (index === 0) {
  263. children[0].before(div);
  264. } else {
  265. children[index - 1].after(div);
  266. }
  267. }
  268. add(editor) {
  269. this.#changeParent(editor);
  270. this.addToAnnotationStorage(editor);
  271. this.#uiManager.addEditor(editor);
  272. this.attach(editor);
  273. if (!editor.isAttachedToDOM) {
  274. const div = editor.render();
  275. this.div.append(div);
  276. editor.isAttachedToDOM = true;
  277. }
  278. this.moveDivInDOM(editor);
  279. editor.onceAdded();
  280. }
  281. addToAnnotationStorage(editor) {
  282. if (!editor.isEmpty() && !this.annotationStorage.has(editor.id)) {
  283. this.annotationStorage.setValue(editor.id, editor);
  284. }
  285. }
  286. addOrRebuild(editor) {
  287. if (editor.needsToBeRebuilt()) {
  288. editor.rebuild();
  289. } else {
  290. this.add(editor);
  291. }
  292. }
  293. addANewEditor(editor) {
  294. const cmd = () => {
  295. this.addOrRebuild(editor);
  296. };
  297. const undo = () => {
  298. editor.remove();
  299. };
  300. this.addCommands({
  301. cmd,
  302. undo,
  303. mustExec: true
  304. });
  305. }
  306. addUndoableEditor(editor) {
  307. const cmd = () => {
  308. this.addOrRebuild(editor);
  309. };
  310. const undo = () => {
  311. editor.remove();
  312. };
  313. this.addCommands({
  314. cmd,
  315. undo,
  316. mustExec: false
  317. });
  318. }
  319. getNextId() {
  320. return this.#uiManager.getId();
  321. }
  322. #createNewEditor(params) {
  323. switch (this.#uiManager.getMode()) {
  324. case _util.AnnotationEditorType.FREETEXT:
  325. return new _freetext.FreeTextEditor(params);
  326. case _util.AnnotationEditorType.INK:
  327. return new _ink.InkEditor(params);
  328. }
  329. return null;
  330. }
  331. deserialize(data) {
  332. switch (data.annotationType) {
  333. case _util.AnnotationEditorType.FREETEXT:
  334. return _freetext.FreeTextEditor.deserialize(data, this);
  335. case _util.AnnotationEditorType.INK:
  336. return _ink.InkEditor.deserialize(data, this);
  337. }
  338. return null;
  339. }
  340. #createAndAddNewEditor(event) {
  341. const id = this.getNextId();
  342. const editor = this.#createNewEditor({
  343. parent: this,
  344. id,
  345. x: event.offsetX,
  346. y: event.offsetY
  347. });
  348. if (editor) {
  349. this.add(editor);
  350. }
  351. return editor;
  352. }
  353. setSelected(editor) {
  354. this.#uiManager.setSelected(editor);
  355. }
  356. toggleSelected(editor) {
  357. this.#uiManager.toggleSelected(editor);
  358. }
  359. isSelected(editor) {
  360. return this.#uiManager.isSelected(editor);
  361. }
  362. unselect(editor) {
  363. this.#uiManager.unselect(editor);
  364. }
  365. pointerup(event) {
  366. const isMac = _tools.KeyboardManager.platform.isMac;
  367. if (event.button !== 0 || event.ctrlKey && isMac) {
  368. return;
  369. }
  370. if (event.target !== this.div) {
  371. return;
  372. }
  373. if (!this.#allowClick) {
  374. this.#allowClick = true;
  375. return;
  376. }
  377. this.#createAndAddNewEditor(event);
  378. }
  379. pointerdown(event) {
  380. const isMac = _tools.KeyboardManager.platform.isMac;
  381. if (event.button !== 0 || event.ctrlKey && isMac) {
  382. return;
  383. }
  384. if (event.target !== this.div) {
  385. return;
  386. }
  387. const editor = this.#uiManager.getActive();
  388. this.#allowClick = !editor || editor.isEmpty();
  389. }
  390. drop(event) {
  391. const id = event.dataTransfer.getData("text/plain");
  392. const editor = this.#uiManager.getEditor(id);
  393. if (!editor) {
  394. return;
  395. }
  396. event.preventDefault();
  397. event.dataTransfer.dropEffect = "move";
  398. this.#changeParent(editor);
  399. const rect = this.div.getBoundingClientRect();
  400. const endX = event.clientX - rect.x;
  401. const endY = event.clientY - rect.y;
  402. editor.translate(endX - editor.startX, endY - editor.startY);
  403. this.moveDivInDOM(editor);
  404. editor.div.focus();
  405. }
  406. dragover(event) {
  407. event.preventDefault();
  408. }
  409. destroy() {
  410. if (this.#uiManager.getActive()?.parent === this) {
  411. this.#uiManager.setActiveEditor(null);
  412. }
  413. for (const editor of this.#editors.values()) {
  414. this.removePointerInTextLayer(editor);
  415. editor.isAttachedToDOM = false;
  416. editor.div.remove();
  417. editor.parent = null;
  418. }
  419. this.#textNodes.clear();
  420. this.div = null;
  421. this.#editors.clear();
  422. this.#waitingEditors.clear();
  423. this.#uiManager.removeLayer(this);
  424. }
  425. #cleanup() {
  426. this.#isCleaningUp = true;
  427. for (const editor of this.#editors.values()) {
  428. if (editor.isEmpty()) {
  429. editor.remove();
  430. }
  431. }
  432. this.#isCleaningUp = false;
  433. }
  434. render(parameters) {
  435. this.viewport = parameters.viewport;
  436. (0, _tools.bindEvents)(this, this.div, ["dragover", "drop"]);
  437. this.setDimensions();
  438. for (const editor of this.#uiManager.getEditors(this.pageIndex)) {
  439. this.add(editor);
  440. }
  441. this.updateMode();
  442. }
  443. update(parameters) {
  444. this.viewport = parameters.viewport;
  445. this.setDimensions();
  446. this.updateMode();
  447. }
  448. get scaleFactor() {
  449. return this.viewport.scale;
  450. }
  451. get pageDimensions() {
  452. const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
  453. const width = pageURx - pageLLx;
  454. const height = pageURy - pageLLy;
  455. return [width, height];
  456. }
  457. get viewportBaseDimensions() {
  458. const {
  459. width,
  460. height,
  461. rotation
  462. } = this.viewport;
  463. return rotation % 180 === 0 ? [width, height] : [height, width];
  464. }
  465. setDimensions() {
  466. const {
  467. width,
  468. height,
  469. rotation
  470. } = this.viewport;
  471. const flipOrientation = rotation % 180 !== 0,
  472. widthStr = Math.floor(width) + "px",
  473. heightStr = Math.floor(height) + "px";
  474. this.div.style.width = flipOrientation ? heightStr : widthStr;
  475. this.div.style.height = flipOrientation ? widthStr : heightStr;
  476. this.div.setAttribute("data-main-rotation", rotation);
  477. }
  478. }
  479. exports.AnnotationEditorLayer = AnnotationEditorLayer;