freetext.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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. const first = div.firstChild;
  200. if (first?.nodeName === "#text") {
  201. buffer.push(first.data);
  202. } else {
  203. buffer.push("");
  204. }
  205. }
  206. return buffer.join("\n");
  207. }
  208. #setEditorDimensions() {
  209. const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
  210. const rect = this.div.getBoundingClientRect();
  211. this.width = rect.width / parentWidth;
  212. this.height = rect.height / parentHeight;
  213. }
  214. commit() {
  215. super.commit();
  216. if (!this.#hasAlreadyBeenCommitted) {
  217. this.#hasAlreadyBeenCommitted = true;
  218. this.parent.addUndoableEditor(this);
  219. }
  220. this.disableEditMode();
  221. this.#content = this.#extractText().trimEnd();
  222. this.#setEditorDimensions();
  223. }
  224. shouldGetKeyboardEvents() {
  225. return this.isInEditMode();
  226. }
  227. dblclick(event) {
  228. this.enableEditMode();
  229. this.editorDiv.focus();
  230. }
  231. keydown(event) {
  232. if (event.target === this.div && event.key === "Enter") {
  233. this.enableEditMode();
  234. this.editorDiv.focus();
  235. }
  236. }
  237. editorDivKeydown(event) {
  238. FreeTextEditor._keyboardManager.exec(this, event);
  239. }
  240. editorDivFocus(event) {
  241. this.isEditing = true;
  242. }
  243. editorDivBlur(event) {
  244. this.isEditing = false;
  245. }
  246. editorDivInput(event) {
  247. this.parent.div.classList.toggle("freeTextEditing", this.isEmpty());
  248. }
  249. disableEditing() {
  250. this.editorDiv.setAttribute("role", "comment");
  251. this.editorDiv.removeAttribute("aria-multiline");
  252. }
  253. enableEditing() {
  254. this.editorDiv.setAttribute("role", "textbox");
  255. this.editorDiv.setAttribute("aria-multiline", true);
  256. }
  257. render() {
  258. if (this.div) {
  259. return this.div;
  260. }
  261. let baseX, baseY;
  262. if (this.width) {
  263. baseX = this.x;
  264. baseY = this.y;
  265. }
  266. super.render();
  267. this.editorDiv = document.createElement("div");
  268. this.editorDiv.className = "internal";
  269. this.editorDiv.setAttribute("id", this.#editorDivId);
  270. this.enableEditing();
  271. FreeTextEditor._l10nPromise.get("editor_free_text2_aria_label").then(msg => this.editorDiv?.setAttribute("aria-label", msg));
  272. FreeTextEditor._l10nPromise.get("free_text2_default_content").then(msg => this.editorDiv?.setAttribute("default-content", msg));
  273. this.editorDiv.contentEditable = true;
  274. const {
  275. style
  276. } = this.editorDiv;
  277. style.fontSize = `calc(${this.#fontSize}px * var(--scale-factor))`;
  278. style.color = this.#color;
  279. this.div.append(this.editorDiv);
  280. this.overlayDiv = document.createElement("div");
  281. this.overlayDiv.classList.add("overlay", "enabled");
  282. this.div.append(this.overlayDiv);
  283. (0, _tools.bindEvents)(this, this.div, ["dblclick", "keydown"]);
  284. if (this.width) {
  285. const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
  286. this.setAt(baseX * parentWidth, baseY * parentHeight, this.width * parentWidth, this.height * parentHeight);
  287. for (const line of this.#content.split("\n")) {
  288. const div = document.createElement("div");
  289. div.append(line ? document.createTextNode(line) : document.createElement("br"));
  290. this.editorDiv.append(div);
  291. }
  292. this.div.draggable = true;
  293. this.editorDiv.contentEditable = false;
  294. } else {
  295. this.div.draggable = false;
  296. this.editorDiv.contentEditable = true;
  297. }
  298. return this.div;
  299. }
  300. get contentDiv() {
  301. return this.editorDiv;
  302. }
  303. static deserialize(data, parent) {
  304. const editor = super.deserialize(data, parent);
  305. editor.#fontSize = data.fontSize;
  306. editor.#color = _util.Util.makeHexColor(...data.color);
  307. editor.#content = data.value;
  308. return editor;
  309. }
  310. serialize() {
  311. if (this.isEmpty()) {
  312. return null;
  313. }
  314. const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor;
  315. const rect = this.getRect(padding, padding);
  316. const color = _editor.AnnotationEditor._colorManager.convert(getComputedStyle(this.editorDiv).color);
  317. return {
  318. annotationType: _util.AnnotationEditorType.FREETEXT,
  319. color,
  320. fontSize: this.#fontSize,
  321. value: this.#content,
  322. pageIndex: this.parent.pageIndex,
  323. rect,
  324. rotation: this.rotation
  325. };
  326. }
  327. }
  328. exports.FreeTextEditor = FreeTextEditor;