freetext.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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. #hasAlreadyBeenCommitted = false;
  37. #fontSize;
  38. static _freeTextDefaultContent = "";
  39. static _l10nPromise;
  40. static _internalPadding = 0;
  41. static _defaultColor = null;
  42. static _defaultFontSize = 10;
  43. static _keyboardManager = new _tools.KeyboardManager([[["ctrl+Enter", "mac+meta+Enter", "Escape", "mac+Escape"], FreeTextEditor.prototype.commitOrRemove]]);
  44. static _type = "freetext";
  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.enableEditing();
  141. this.overlayDiv.classList.remove("enabled");
  142. this.editorDiv.contentEditable = true;
  143. this.div.draggable = false;
  144. this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown);
  145. this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus);
  146. this.editorDiv.addEventListener("blur", this.#boundEditorDivBlur);
  147. }
  148. disableEditMode() {
  149. if (!this.isInEditMode()) {
  150. return;
  151. }
  152. this.parent.setEditingState(true);
  153. super.disableEditMode();
  154. this.disableEditing();
  155. this.overlayDiv.classList.add("enabled");
  156. this.editorDiv.contentEditable = false;
  157. this.div.draggable = true;
  158. this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown);
  159. this.editorDiv.removeEventListener("focus", this.#boundEditorDivFocus);
  160. this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur);
  161. this.div.focus();
  162. this.isEditing = false;
  163. }
  164. focusin(event) {
  165. super.focusin(event);
  166. if (event.target !== this.editorDiv) {
  167. this.editorDiv.focus();
  168. }
  169. }
  170. onceAdded() {
  171. if (this.width) {
  172. return;
  173. }
  174. this.enableEditMode();
  175. this.editorDiv.focus();
  176. }
  177. isEmpty() {
  178. return !this.editorDiv || this.editorDiv.innerText.trim() === "";
  179. }
  180. remove() {
  181. this.isEditing = false;
  182. this.parent.setEditingState(true);
  183. super.remove();
  184. }
  185. #extractText() {
  186. const divs = this.editorDiv.getElementsByTagName("div");
  187. if (divs.length === 0) {
  188. return this.editorDiv.innerText;
  189. }
  190. const buffer = [];
  191. for (let i = 0, ii = divs.length; i < ii; i++) {
  192. const div = divs[i];
  193. const first = div.firstChild;
  194. if (first?.nodeName === "#text") {
  195. buffer.push(first.data);
  196. } else {
  197. buffer.push("");
  198. }
  199. }
  200. return buffer.join("\n");
  201. }
  202. #setEditorDimensions() {
  203. const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
  204. const rect = this.div.getBoundingClientRect();
  205. this.width = rect.width / parentWidth;
  206. this.height = rect.height / parentHeight;
  207. }
  208. commit() {
  209. super.commit();
  210. if (!this.#hasAlreadyBeenCommitted) {
  211. this.#hasAlreadyBeenCommitted = true;
  212. this.parent.addUndoableEditor(this);
  213. }
  214. this.disableEditMode();
  215. this.#content = this.#extractText().trimEnd();
  216. this.#setEditorDimensions();
  217. }
  218. shouldGetKeyboardEvents() {
  219. return this.isInEditMode();
  220. }
  221. dblclick(event) {
  222. this.enableEditMode();
  223. this.editorDiv.focus();
  224. }
  225. keydown(event) {
  226. if (event.target === this.div && event.key === "Enter") {
  227. this.enableEditMode();
  228. this.editorDiv.focus();
  229. }
  230. }
  231. editorDivKeydown(event) {
  232. FreeTextEditor._keyboardManager.exec(this, event);
  233. }
  234. editorDivFocus(event) {
  235. this.isEditing = true;
  236. }
  237. editorDivBlur(event) {
  238. this.isEditing = false;
  239. }
  240. disableEditing() {
  241. this.editorDiv.setAttribute("role", "comment");
  242. this.editorDiv.removeAttribute("aria-multiline");
  243. }
  244. enableEditing() {
  245. this.editorDiv.setAttribute("role", "textbox");
  246. this.editorDiv.setAttribute("aria-multiline", true);
  247. }
  248. render() {
  249. if (this.div) {
  250. return this.div;
  251. }
  252. let baseX, baseY;
  253. if (this.width) {
  254. baseX = this.x;
  255. baseY = this.y;
  256. }
  257. super.render();
  258. this.editorDiv = document.createElement("div");
  259. this.editorDiv.className = "internal";
  260. this.editorDiv.setAttribute("id", `${this.id}-editor`);
  261. this.enableEditing();
  262. FreeTextEditor._l10nPromise.get("editor_free_text_aria_label").then(msg => this.editorDiv?.setAttribute("aria-label", msg));
  263. FreeTextEditor._l10nPromise.get("free_text_default_content").then(msg => this.editorDiv?.setAttribute("default-content", msg));
  264. this.editorDiv.contentEditable = true;
  265. const {
  266. style
  267. } = this.editorDiv;
  268. style.fontSize = `calc(${this.#fontSize}px * var(--scale-factor))`;
  269. style.color = this.#color;
  270. this.div.append(this.editorDiv);
  271. this.overlayDiv = document.createElement("div");
  272. this.overlayDiv.classList.add("overlay", "enabled");
  273. this.div.append(this.overlayDiv);
  274. (0, _tools.bindEvents)(this, this.div, ["dblclick", "keydown"]);
  275. if (this.width) {
  276. const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
  277. this.setAt(baseX * parentWidth, baseY * parentHeight, this.width * parentWidth, this.height * parentHeight);
  278. for (const line of this.#content.split("\n")) {
  279. const div = document.createElement("div");
  280. div.append(line ? document.createTextNode(line) : document.createElement("br"));
  281. this.editorDiv.append(div);
  282. }
  283. this.div.draggable = true;
  284. this.editorDiv.contentEditable = false;
  285. } else {
  286. this.div.draggable = false;
  287. this.editorDiv.contentEditable = true;
  288. }
  289. return this.div;
  290. }
  291. get contentDiv() {
  292. return this.editorDiv;
  293. }
  294. static deserialize(data, parent) {
  295. const editor = super.deserialize(data, parent);
  296. editor.#fontSize = data.fontSize;
  297. editor.#color = _util.Util.makeHexColor(...data.color);
  298. editor.#content = data.value;
  299. return editor;
  300. }
  301. serialize() {
  302. if (this.isEmpty()) {
  303. return null;
  304. }
  305. const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor;
  306. const rect = this.getRect(padding, padding);
  307. const color = _editor.AnnotationEditor._colorManager.convert(getComputedStyle(this.editorDiv).color);
  308. return {
  309. annotationType: _util.AnnotationEditorType.FREETEXT,
  310. color,
  311. fontSize: this.#fontSize,
  312. value: this.#content,
  313. pageIndex: this.parent.pageIndex,
  314. rect,
  315. rotation: this.rotation
  316. };
  317. }
  318. }
  319. exports.FreeTextEditor = FreeTextEditor;