freetext.js 10 KB

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