freetext.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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.parentScale);
  90. this.#fontSize = size;
  91. this.#setEditorDimensions();
  92. };
  93. const savedFontsize = this.#fontSize;
  94. this.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.addCommands({
  110. cmd: () => {
  111. this.#color = this.editorDiv.style.color = color;
  112. },
  113. undo: () => {
  114. this.#color = this.editorDiv.style.color = savedColor;
  115. },
  116. mustExec: true,
  117. type: _util.AnnotationEditorParamsType.FREETEXT_COLOR,
  118. overwriteIfSameType: true,
  119. keepUndo: true
  120. });
  121. }
  122. getInitialTranslation() {
  123. const scale = this.parentScale;
  124. return [-FreeTextEditor._internalPadding * scale, -(FreeTextEditor._internalPadding + this.#fontSize) * scale];
  125. }
  126. rebuild() {
  127. super.rebuild();
  128. if (this.div === null) {
  129. return;
  130. }
  131. if (!this.isAttachedToDOM) {
  132. this.parent.add(this);
  133. }
  134. }
  135. enableEditMode() {
  136. if (this.isInEditMode()) {
  137. return;
  138. }
  139. this.parent.setEditingState(false);
  140. this.parent.updateToolbar(_util.AnnotationEditorType.FREETEXT);
  141. super.enableEditMode();
  142. this.overlayDiv.classList.remove("enabled");
  143. this.editorDiv.contentEditable = true;
  144. this.div.draggable = false;
  145. this.div.removeAttribute("aria-activedescendant");
  146. this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown);
  147. this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus);
  148. this.editorDiv.addEventListener("blur", this.#boundEditorDivBlur);
  149. this.editorDiv.addEventListener("input", this.#boundEditorDivInput);
  150. }
  151. disableEditMode() {
  152. if (!this.isInEditMode()) {
  153. return;
  154. }
  155. this.parent.setEditingState(true);
  156. super.disableEditMode();
  157. this.overlayDiv.classList.add("enabled");
  158. this.editorDiv.contentEditable = false;
  159. this.div.setAttribute("aria-activedescendant", this.#editorDivId);
  160. this.div.draggable = true;
  161. this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown);
  162. this.editorDiv.removeEventListener("focus", this.#boundEditorDivFocus);
  163. this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur);
  164. this.editorDiv.removeEventListener("input", this.#boundEditorDivInput);
  165. this.div.focus({
  166. preventScroll: true
  167. });
  168. this.isEditing = false;
  169. this.parent.div.classList.add("freeTextEditing");
  170. }
  171. focusin(event) {
  172. super.focusin(event);
  173. if (event.target !== this.editorDiv) {
  174. this.editorDiv.focus();
  175. }
  176. }
  177. onceAdded() {
  178. if (this.width) {
  179. return;
  180. }
  181. this.enableEditMode();
  182. this.editorDiv.focus();
  183. }
  184. isEmpty() {
  185. return !this.editorDiv || this.editorDiv.innerText.trim() === "";
  186. }
  187. remove() {
  188. this.isEditing = false;
  189. this.parent.setEditingState(true);
  190. this.parent.div.classList.add("freeTextEditing");
  191. super.remove();
  192. }
  193. #extractText() {
  194. const divs = this.editorDiv.getElementsByTagName("div");
  195. if (divs.length === 0) {
  196. return this.editorDiv.innerText;
  197. }
  198. const buffer = [];
  199. for (const div of divs) {
  200. buffer.push(div.innerText.replace(/\r\n?|\n/, ""));
  201. }
  202. return buffer.join("\n");
  203. }
  204. #setEditorDimensions() {
  205. const [parentWidth, parentHeight] = this.parentDimensions;
  206. let rect;
  207. if (this.isAttachedToDOM) {
  208. rect = this.div.getBoundingClientRect();
  209. } else {
  210. const {
  211. currentLayer,
  212. div
  213. } = this;
  214. const savedDisplay = div.style.display;
  215. div.style.display = "hidden";
  216. currentLayer.div.append(this.div);
  217. rect = div.getBoundingClientRect();
  218. div.remove();
  219. div.style.display = savedDisplay;
  220. }
  221. this.width = rect.width / parentWidth;
  222. this.height = rect.height / parentHeight;
  223. }
  224. commit() {
  225. if (!this.isInEditMode()) {
  226. return;
  227. }
  228. super.commit();
  229. if (!this.#hasAlreadyBeenCommitted) {
  230. this.#hasAlreadyBeenCommitted = true;
  231. this.parent.addUndoableEditor(this);
  232. }
  233. this.disableEditMode();
  234. this.#content = this.#extractText().trimEnd();
  235. this.#setEditorDimensions();
  236. }
  237. shouldGetKeyboardEvents() {
  238. return this.isInEditMode();
  239. }
  240. dblclick(event) {
  241. this.enableEditMode();
  242. this.editorDiv.focus();
  243. }
  244. keydown(event) {
  245. if (event.target === this.div && event.key === "Enter") {
  246. this.enableEditMode();
  247. this.editorDiv.focus();
  248. }
  249. }
  250. editorDivKeydown(event) {
  251. FreeTextEditor._keyboardManager.exec(this, event);
  252. }
  253. editorDivFocus(event) {
  254. this.isEditing = true;
  255. }
  256. editorDivBlur(event) {
  257. this.isEditing = false;
  258. }
  259. editorDivInput(event) {
  260. this.parent.div.classList.toggle("freeTextEditing", this.isEmpty());
  261. }
  262. disableEditing() {
  263. this.editorDiv.setAttribute("role", "comment");
  264. this.editorDiv.removeAttribute("aria-multiline");
  265. }
  266. enableEditing() {
  267. this.editorDiv.setAttribute("role", "textbox");
  268. this.editorDiv.setAttribute("aria-multiline", true);
  269. }
  270. render() {
  271. if (this.div) {
  272. return this.div;
  273. }
  274. let baseX, baseY;
  275. if (this.width) {
  276. baseX = this.x;
  277. baseY = this.y;
  278. }
  279. super.render();
  280. this.editorDiv = document.createElement("div");
  281. this.editorDiv.className = "internal";
  282. this.editorDiv.setAttribute("id", this.#editorDivId);
  283. this.enableEditing();
  284. FreeTextEditor._l10nPromise.get("editor_free_text2_aria_label").then(msg => this.editorDiv?.setAttribute("aria-label", msg));
  285. FreeTextEditor._l10nPromise.get("free_text2_default_content").then(msg => this.editorDiv?.setAttribute("default-content", msg));
  286. this.editorDiv.contentEditable = true;
  287. const {
  288. style
  289. } = this.editorDiv;
  290. style.fontSize = `calc(${this.#fontSize}px * var(--scale-factor))`;
  291. style.color = this.#color;
  292. this.div.append(this.editorDiv);
  293. this.overlayDiv = document.createElement("div");
  294. this.overlayDiv.classList.add("overlay", "enabled");
  295. this.div.append(this.overlayDiv);
  296. (0, _tools.bindEvents)(this, this.div, ["dblclick", "keydown"]);
  297. if (this.width) {
  298. const [parentWidth, parentHeight] = this.parentDimensions;
  299. this.setAt(baseX * parentWidth, baseY * parentHeight, this.width * parentWidth, this.height * parentHeight);
  300. for (const line of this.#content.split("\n")) {
  301. const div = document.createElement("div");
  302. div.append(line ? document.createTextNode(line) : document.createElement("br"));
  303. this.editorDiv.append(div);
  304. }
  305. this.div.draggable = true;
  306. this.editorDiv.contentEditable = false;
  307. } else {
  308. this.div.draggable = false;
  309. this.editorDiv.contentEditable = true;
  310. }
  311. return this.div;
  312. }
  313. get contentDiv() {
  314. return this.editorDiv;
  315. }
  316. static deserialize(data, parent, uiManager) {
  317. const editor = super.deserialize(data, parent, uiManager);
  318. editor.#fontSize = data.fontSize;
  319. editor.#color = _util.Util.makeHexColor(...data.color);
  320. editor.#content = data.value;
  321. return editor;
  322. }
  323. serialize() {
  324. if (this.isEmpty()) {
  325. return null;
  326. }
  327. const padding = FreeTextEditor._internalPadding * this.parentScale;
  328. const rect = this.getRect(padding, padding);
  329. const color = _editor.AnnotationEditor._colorManager.convert(this.isAttachedToDOM ? getComputedStyle(this.editorDiv).color : this.#color);
  330. return {
  331. annotationType: _util.AnnotationEditorType.FREETEXT,
  332. color,
  333. fontSize: this.#fontSize,
  334. value: this.#content,
  335. pageIndex: this.pageIndex,
  336. rect,
  337. rotation: this.rotation
  338. };
  339. }
  340. }
  341. exports.FreeTextEditor = FreeTextEditor;