freetext.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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.FreeTextEditor = void 0;
  27. var _util = require("../../shared/util.js");
  28. var _tools = require("./tools.js");
  29. var _editor = require("./editor.js");
  30. class FreeTextEditor extends _editor.AnnotationEditor {
  31. #boundEditorDivBlur = this.editorDivBlur.bind(this);
  32. #boundEditorDivFocus = this.editorDivFocus.bind(this);
  33. #boundEditorDivInput = this.editorDivInput.bind(this);
  34. #boundEditorDivKeydown = this.editorDivKeydown.bind(this);
  35. #color;
  36. #content = "";
  37. #editorDivId = `${this.id}-editor`;
  38. #hasAlreadyBeenCommitted = false;
  39. #fontSize;
  40. static _freeTextDefaultContent = "";
  41. static _l10nPromise;
  42. static _internalPadding = 0;
  43. static _defaultColor = null;
  44. static _defaultFontSize = 10;
  45. static _keyboardManager = new _tools.KeyboardManager([[["ctrl+Enter", "mac+meta+Enter", "Escape", "mac+Escape"], FreeTextEditor.prototype.commitOrRemove]]);
  46. static _type = "freetext";
  47. constructor(params) {
  48. super({
  49. ...params,
  50. name: "freeTextEditor"
  51. });
  52. this.#color = params.color || FreeTextEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor;
  53. this.#fontSize = params.fontSize || FreeTextEditor._defaultFontSize;
  54. }
  55. static initialize(l10n) {
  56. this._l10nPromise = new Map(["free_text2_default_content", "editor_free_text2_aria_label"].map(str => [str, l10n.get(str)]));
  57. const style = getComputedStyle(document.documentElement);
  58. this._internalPadding = parseFloat(style.getPropertyValue("--freetext-padding"));
  59. }
  60. static updateDefaultParams(type, value) {
  61. switch (type) {
  62. case _util.AnnotationEditorParamsType.FREETEXT_SIZE:
  63. FreeTextEditor._defaultFontSize = value;
  64. break;
  65. case _util.AnnotationEditorParamsType.FREETEXT_COLOR:
  66. FreeTextEditor._defaultColor = value;
  67. break;
  68. }
  69. }
  70. updateParams(type, value) {
  71. switch (type) {
  72. case _util.AnnotationEditorParamsType.FREETEXT_SIZE:
  73. this.#updateFontSize(value);
  74. break;
  75. case _util.AnnotationEditorParamsType.FREETEXT_COLOR:
  76. this.#updateColor(value);
  77. break;
  78. }
  79. }
  80. static get defaultPropertiesToUpdate() {
  81. return [[_util.AnnotationEditorParamsType.FREETEXT_SIZE, FreeTextEditor._defaultFontSize], [_util.AnnotationEditorParamsType.FREETEXT_COLOR, FreeTextEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor]];
  82. }
  83. get propertiesToUpdate() {
  84. return [[_util.AnnotationEditorParamsType.FREETEXT_SIZE, this.#fontSize], [_util.AnnotationEditorParamsType.FREETEXT_COLOR, this.#color]];
  85. }
  86. #updateFontSize(fontSize) {
  87. const setFontsize = size => {
  88. this.editorDiv.style.fontSize = `calc(${size}px * var(--scale-factor))`;
  89. this.translate(0, -(size - this.#fontSize) * this.parent.scaleFactor);
  90. this.#fontSize = size;
  91. this.#setEditorDimensions();
  92. };
  93. const savedFontsize = this.#fontSize;
  94. this.parent.addCommands({
  95. cmd: () => {
  96. setFontsize(fontSize);
  97. },
  98. undo: () => {
  99. setFontsize(savedFontsize);
  100. },
  101. mustExec: true,
  102. type: _util.AnnotationEditorParamsType.FREETEXT_SIZE,
  103. overwriteIfSameType: true,
  104. keepUndo: true
  105. });
  106. }
  107. #updateColor(color) {
  108. const savedColor = this.#color;
  109. this.parent.addCommands({
  110. cmd: () => {
  111. this.#color = color;
  112. this.editorDiv.style.color = color;
  113. },
  114. undo: () => {
  115. this.#color = savedColor;
  116. this.editorDiv.style.color = savedColor;
  117. },
  118. mustExec: true,
  119. type: _util.AnnotationEditorParamsType.FREETEXT_COLOR,
  120. overwriteIfSameType: true,
  121. keepUndo: true
  122. });
  123. }
  124. getInitialTranslation() {
  125. return [-FreeTextEditor._internalPadding * this.parent.scaleFactor, -(FreeTextEditor._internalPadding + this.#fontSize) * this.parent.scaleFactor];
  126. }
  127. rebuild() {
  128. super.rebuild();
  129. if (this.div === null) {
  130. return;
  131. }
  132. if (!this.isAttachedToDOM) {
  133. this.parent.add(this);
  134. }
  135. }
  136. enableEditMode() {
  137. if (this.isInEditMode()) {
  138. return;
  139. }
  140. this.parent.setEditingState(false);
  141. this.parent.updateToolbar(_util.AnnotationEditorType.FREETEXT);
  142. super.enableEditMode();
  143. this.overlayDiv.classList.remove("enabled");
  144. this.editorDiv.contentEditable = true;
  145. this.div.draggable = false;
  146. this.div.removeAttribute("aria-activedescendant");
  147. this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown);
  148. this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus);
  149. this.editorDiv.addEventListener("blur", this.#boundEditorDivBlur);
  150. this.editorDiv.addEventListener("input", this.#boundEditorDivInput);
  151. }
  152. disableEditMode() {
  153. if (!this.isInEditMode()) {
  154. return;
  155. }
  156. this.parent.setEditingState(true);
  157. super.disableEditMode();
  158. this.overlayDiv.classList.add("enabled");
  159. this.editorDiv.contentEditable = false;
  160. this.div.setAttribute("aria-activedescendant", this.#editorDivId);
  161. this.div.draggable = true;
  162. this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown);
  163. this.editorDiv.removeEventListener("focus", this.#boundEditorDivFocus);
  164. this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur);
  165. this.editorDiv.removeEventListener("input", this.#boundEditorDivInput);
  166. this.div.focus();
  167. this.isEditing = false;
  168. this.parent.div.classList.add("freeTextEditing");
  169. }
  170. focusin(event) {
  171. super.focusin(event);
  172. if (event.target !== this.editorDiv) {
  173. this.editorDiv.focus();
  174. }
  175. }
  176. onceAdded() {
  177. if (this.width) {
  178. return;
  179. }
  180. this.enableEditMode();
  181. this.editorDiv.focus();
  182. }
  183. isEmpty() {
  184. return !this.editorDiv || this.editorDiv.innerText.trim() === "";
  185. }
  186. remove() {
  187. this.isEditing = false;
  188. this.parent.setEditingState(true);
  189. this.parent.div.classList.add("freeTextEditing");
  190. super.remove();
  191. }
  192. #extractText() {
  193. const divs = this.editorDiv.getElementsByTagName("div");
  194. if (divs.length === 0) {
  195. return this.editorDiv.innerText;
  196. }
  197. const buffer = [];
  198. for (const div of divs) {
  199. buffer.push(div.innerText.replace(/\r\n?|\n/, ""));
  200. }
  201. return buffer.join("\n");
  202. }
  203. #setEditorDimensions() {
  204. const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
  205. const rect = this.div.getBoundingClientRect();
  206. this.width = rect.width / parentWidth;
  207. this.height = rect.height / parentHeight;
  208. }
  209. commit() {
  210. super.commit();
  211. if (!this.#hasAlreadyBeenCommitted) {
  212. this.#hasAlreadyBeenCommitted = true;
  213. this.parent.addUndoableEditor(this);
  214. }
  215. this.disableEditMode();
  216. this.#content = this.#extractText().trimEnd();
  217. this.#setEditorDimensions();
  218. }
  219. shouldGetKeyboardEvents() {
  220. return this.isInEditMode();
  221. }
  222. dblclick(event) {
  223. this.enableEditMode();
  224. this.editorDiv.focus();
  225. }
  226. keydown(event) {
  227. if (event.target === this.div && event.key === "Enter") {
  228. this.enableEditMode();
  229. this.editorDiv.focus();
  230. }
  231. }
  232. editorDivKeydown(event) {
  233. FreeTextEditor._keyboardManager.exec(this, event);
  234. }
  235. editorDivFocus(event) {
  236. this.isEditing = true;
  237. }
  238. editorDivBlur(event) {
  239. this.isEditing = false;
  240. }
  241. editorDivInput(event) {
  242. this.parent.div.classList.toggle("freeTextEditing", this.isEmpty());
  243. }
  244. disableEditing() {
  245. this.editorDiv.setAttribute("role", "comment");
  246. this.editorDiv.removeAttribute("aria-multiline");
  247. }
  248. enableEditing() {
  249. this.editorDiv.setAttribute("role", "textbox");
  250. this.editorDiv.setAttribute("aria-multiline", true);
  251. }
  252. render() {
  253. if (this.div) {
  254. return this.div;
  255. }
  256. let baseX, baseY;
  257. if (this.width) {
  258. baseX = this.x;
  259. baseY = this.y;
  260. }
  261. super.render();
  262. this.editorDiv = document.createElement("div");
  263. this.editorDiv.className = "internal";
  264. this.editorDiv.setAttribute("id", this.#editorDivId);
  265. this.enableEditing();
  266. FreeTextEditor._l10nPromise.get("editor_free_text2_aria_label").then(msg => this.editorDiv?.setAttribute("aria-label", msg));
  267. FreeTextEditor._l10nPromise.get("free_text2_default_content").then(msg => this.editorDiv?.setAttribute("default-content", msg));
  268. this.editorDiv.contentEditable = true;
  269. const {
  270. style
  271. } = this.editorDiv;
  272. style.fontSize = `calc(${this.#fontSize}px * var(--scale-factor))`;
  273. style.color = this.#color;
  274. this.div.append(this.editorDiv);
  275. this.overlayDiv = document.createElement("div");
  276. this.overlayDiv.classList.add("overlay", "enabled");
  277. this.div.append(this.overlayDiv);
  278. (0, _tools.bindEvents)(this, this.div, ["dblclick", "keydown"]);
  279. if (this.width) {
  280. const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
  281. this.setAt(baseX * parentWidth, baseY * parentHeight, this.width * parentWidth, this.height * parentHeight);
  282. for (const line of this.#content.split("\n")) {
  283. const div = document.createElement("div");
  284. div.append(line ? document.createTextNode(line) : document.createElement("br"));
  285. this.editorDiv.append(div);
  286. }
  287. this.div.draggable = true;
  288. this.editorDiv.contentEditable = false;
  289. } else {
  290. this.div.draggable = false;
  291. this.editorDiv.contentEditable = true;
  292. }
  293. return this.div;
  294. }
  295. get contentDiv() {
  296. return this.editorDiv;
  297. }
  298. static deserialize(data, parent) {
  299. const editor = super.deserialize(data, parent);
  300. editor.#fontSize = data.fontSize;
  301. editor.#color = _util.Util.makeHexColor(...data.color);
  302. editor.#content = data.value;
  303. return editor;
  304. }
  305. serialize() {
  306. if (this.isEmpty()) {
  307. return null;
  308. }
  309. const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor;
  310. const rect = this.getRect(padding, padding);
  311. const color = _editor.AnnotationEditor._colorManager.convert(getComputedStyle(this.editorDiv).color);
  312. return {
  313. annotationType: _util.AnnotationEditorType.FREETEXT,
  314. color,
  315. fontSize: this.#fontSize,
  316. value: this.#content,
  317. pageIndex: this.parent.pageIndex,
  318. rect,
  319. rotation: this.rotation
  320. };
  321. }
  322. }
  323. exports.FreeTextEditor = FreeTextEditor;