field.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. /* Copyright 2020 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. import { createActionsMap, FieldType, getFieldType } from "./common.js";
  16. import { Color } from "./color.js";
  17. import { PDFObject } from "./pdf_object.js";
  18. class Field extends PDFObject {
  19. constructor(data) {
  20. super(data);
  21. this.alignment = data.alignment || "left";
  22. this.borderStyle = data.borderStyle || "";
  23. this.buttonAlignX = data.buttonAlignX || 50;
  24. this.buttonAlignY = data.buttonAlignY || 50;
  25. this.buttonFitBounds = data.buttonFitBounds;
  26. this.buttonPosition = data.buttonPosition;
  27. this.buttonScaleHow = data.buttonScaleHow;
  28. this.ButtonScaleWhen = data.buttonScaleWhen;
  29. this.calcOrderIndex = data.calcOrderIndex;
  30. this.comb = data.comb;
  31. this.commitOnSelChange = data.commitOnSelChange;
  32. this.currentValueIndices = data.currentValueIndices;
  33. this.defaultStyle = data.defaultStyle;
  34. this.defaultValue = data.defaultValue;
  35. this.doNotScroll = data.doNotScroll;
  36. this.doNotSpellCheck = data.doNotSpellCheck;
  37. this.delay = data.delay;
  38. this.display = data.display;
  39. this.doc = data.doc.wrapped;
  40. this.editable = data.editable;
  41. this.exportValues = data.exportValues;
  42. this.fileSelect = data.fileSelect;
  43. this.hidden = data.hidden;
  44. this.highlight = data.highlight;
  45. this.lineWidth = data.lineWidth;
  46. this.multiline = data.multiline;
  47. this.multipleSelection = !!data.multipleSelection;
  48. this.name = data.name;
  49. this.password = data.password;
  50. this.print = data.print;
  51. this.radiosInUnison = data.radiosInUnison;
  52. this.readonly = data.readonly;
  53. this.rect = data.rect;
  54. this.required = data.required;
  55. this.richText = data.richText;
  56. this.richValue = data.richValue;
  57. this.style = data.style;
  58. this.submitName = data.submitName;
  59. this.textFont = data.textFont;
  60. this.textSize = data.textSize;
  61. this.type = data.type;
  62. this.userName = data.userName;
  63. // Private
  64. this._actions = createActionsMap(data.actions);
  65. this._browseForFileToSubmit = data.browseForFileToSubmit || null;
  66. this._buttonCaption = null;
  67. this._buttonIcon = null;
  68. this._charLimit = data.charLimit;
  69. this._children = null;
  70. this._currentValueIndices = data.currentValueIndices || 0;
  71. this._document = data.doc;
  72. this._fieldPath = data.fieldPath;
  73. this._fillColor = data.fillColor || ["T"];
  74. this._isChoice = Array.isArray(data.items);
  75. this._items = data.items || [];
  76. this._hasValue = data.hasOwnProperty("value");
  77. this._page = data.page || 0;
  78. this._strokeColor = data.strokeColor || ["G", 0];
  79. this._textColor = data.textColor || ["G", 0];
  80. this._value = null;
  81. this._kidIds = data.kidIds || null;
  82. this._fieldType = getFieldType(this._actions);
  83. this._siblings = data.siblings || null;
  84. this._rotation = data.rotation || 0;
  85. this._globalEval = data.globalEval;
  86. this._appObjects = data.appObjects;
  87. // The value is set depending on the field type.
  88. this.value = data.value || "";
  89. }
  90. get currentValueIndices() {
  91. if (!this._isChoice) {
  92. return 0;
  93. }
  94. return this._currentValueIndices;
  95. }
  96. set currentValueIndices(indices) {
  97. if (!this._isChoice) {
  98. return;
  99. }
  100. if (!Array.isArray(indices)) {
  101. indices = [indices];
  102. }
  103. if (
  104. !indices.every(
  105. i =>
  106. typeof i === "number" &&
  107. Number.isInteger(i) &&
  108. i >= 0 &&
  109. i < this.numItems
  110. )
  111. ) {
  112. return;
  113. }
  114. indices.sort();
  115. if (this.multipleSelection) {
  116. this._currentValueIndices = indices;
  117. this._value = [];
  118. indices.forEach(i => {
  119. this._value.push(this._items[i].displayValue);
  120. });
  121. } else {
  122. if (indices.length > 0) {
  123. indices = indices.splice(1, indices.length - 1);
  124. this._currentValueIndices = indices[0];
  125. this._value = this._items[this._currentValueIndices];
  126. }
  127. }
  128. this._send({ id: this._id, indices });
  129. }
  130. get fillColor() {
  131. return this._fillColor;
  132. }
  133. set fillColor(color) {
  134. if (Color._isValidColor(color)) {
  135. this._fillColor = color;
  136. }
  137. }
  138. get bgColor() {
  139. return this.fillColor;
  140. }
  141. set bgColor(color) {
  142. this.fillColor = color;
  143. }
  144. get charLimit() {
  145. return this._charLimit;
  146. }
  147. set charLimit(limit) {
  148. if (typeof limit !== "number") {
  149. throw new Error("Invalid argument value");
  150. }
  151. this._charLimit = Math.max(0, Math.floor(limit));
  152. }
  153. get numItems() {
  154. if (!this._isChoice) {
  155. throw new Error("Not a choice widget");
  156. }
  157. return this._items.length;
  158. }
  159. set numItems(_) {
  160. throw new Error("field.numItems is read-only");
  161. }
  162. get strokeColor() {
  163. return this._strokeColor;
  164. }
  165. set strokeColor(color) {
  166. if (Color._isValidColor(color)) {
  167. this._strokeColor = color;
  168. }
  169. }
  170. get borderColor() {
  171. return this.strokeColor;
  172. }
  173. set borderColor(color) {
  174. this.strokeColor = color;
  175. }
  176. get page() {
  177. return this._page;
  178. }
  179. set page(_) {
  180. throw new Error("field.page is read-only");
  181. }
  182. get rotation() {
  183. return this._rotation;
  184. }
  185. set rotation(angle) {
  186. angle = Math.floor(angle);
  187. if (angle % 90 !== 0) {
  188. throw new Error("Invalid rotation: must be a multiple of 90");
  189. }
  190. angle %= 360;
  191. if (angle < 0) {
  192. angle += 360;
  193. }
  194. this._rotation = angle;
  195. }
  196. get textColor() {
  197. return this._textColor;
  198. }
  199. set textColor(color) {
  200. if (Color._isValidColor(color)) {
  201. this._textColor = color;
  202. }
  203. }
  204. get fgColor() {
  205. return this.textColor;
  206. }
  207. set fgColor(color) {
  208. this.textColor = color;
  209. }
  210. get value() {
  211. return this._value;
  212. }
  213. set value(value) {
  214. if (this._isChoice) {
  215. this._setChoiceValue(value);
  216. return;
  217. }
  218. if (value === "") {
  219. this._value = "";
  220. } else if (typeof value === "string") {
  221. switch (this._fieldType) {
  222. case FieldType.none:
  223. this._value = !isNaN(value) ? parseFloat(value) : value;
  224. break;
  225. case FieldType.number:
  226. case FieldType.percent:
  227. const number = parseFloat(value);
  228. this._value = !isNaN(number) ? number : 0;
  229. break;
  230. default:
  231. this._value = value;
  232. }
  233. } else {
  234. this._value = value;
  235. }
  236. }
  237. _setChoiceValue(value) {
  238. if (this.multipleSelection) {
  239. if (!Array.isArray(value)) {
  240. value = [value];
  241. }
  242. const values = new Set(value);
  243. if (Array.isArray(this._currentValueIndices)) {
  244. this._currentValueIndices.length = 0;
  245. this._value.length = 0;
  246. } else {
  247. this._currentValueIndices = [];
  248. this._value = [];
  249. }
  250. this._items.forEach((item, i) => {
  251. if (values.has(item.exportValue)) {
  252. this._currentValueIndices.push(i);
  253. this._value.push(item.exportValue);
  254. }
  255. });
  256. } else {
  257. if (Array.isArray(value)) {
  258. value = value[0];
  259. }
  260. const index = this._items.findIndex(
  261. ({ exportValue }) => value === exportValue
  262. );
  263. if (index !== -1) {
  264. this._currentValueIndices = index;
  265. this._value = this._items[index].exportValue;
  266. }
  267. }
  268. }
  269. get valueAsString() {
  270. return (this._value ?? "").toString();
  271. }
  272. set valueAsString(_) {
  273. // Do nothing.
  274. }
  275. browseForFileToSubmit() {
  276. if (this._browseForFileToSubmit) {
  277. // TODO: implement this function on Firefox side
  278. // we can use nsIFilePicker but open method is async.
  279. // Maybe it's possible to use a html input (type=file) too.
  280. this._browseForFileToSubmit();
  281. }
  282. }
  283. buttonGetCaption(nFace = 0) {
  284. if (this._buttonCaption) {
  285. return this._buttonCaption[nFace];
  286. }
  287. return "";
  288. }
  289. buttonGetIcon(nFace = 0) {
  290. if (this._buttonIcon) {
  291. return this._buttonIcon[nFace];
  292. }
  293. return null;
  294. }
  295. buttonImportIcon(cPath = null, nPave = 0) {
  296. /* Not implemented */
  297. }
  298. buttonSetCaption(cCaption, nFace = 0) {
  299. if (!this._buttonCaption) {
  300. this._buttonCaption = ["", "", ""];
  301. }
  302. this._buttonCaption[nFace] = cCaption;
  303. // TODO: send to the annotation layer
  304. // Right now the button is drawn on the canvas using its appearance so
  305. // update the caption means redraw...
  306. // We should probably have an html button for this annotation.
  307. }
  308. buttonSetIcon(oIcon, nFace = 0) {
  309. if (!this._buttonIcon) {
  310. this._buttonIcon = [null, null, null];
  311. }
  312. this._buttonIcon[nFace] = oIcon;
  313. }
  314. checkThisBox(nWidget, bCheckIt = true) {}
  315. clearItems() {
  316. if (!this._isChoice) {
  317. throw new Error("Not a choice widget");
  318. }
  319. this._items = [];
  320. this._send({ id: this._id, clear: null });
  321. }
  322. deleteItemAt(nIdx = null) {
  323. if (!this._isChoice) {
  324. throw new Error("Not a choice widget");
  325. }
  326. if (!this.numItems) {
  327. return;
  328. }
  329. if (nIdx === null) {
  330. // Current selected item.
  331. nIdx = Array.isArray(this._currentValueIndices)
  332. ? this._currentValueIndices[0]
  333. : this._currentValueIndices;
  334. nIdx = nIdx || 0;
  335. }
  336. if (nIdx < 0 || nIdx >= this.numItems) {
  337. nIdx = this.numItems - 1;
  338. }
  339. this._items.splice(nIdx, 1);
  340. if (Array.isArray(this._currentValueIndices)) {
  341. let index = this._currentValueIndices.findIndex(i => i >= nIdx);
  342. if (index !== -1) {
  343. if (this._currentValueIndices[index] === nIdx) {
  344. this._currentValueIndices.splice(index, 1);
  345. }
  346. for (const ii = this._currentValueIndices.length; index < ii; index++) {
  347. --this._currentValueIndices[index];
  348. }
  349. }
  350. } else {
  351. if (this._currentValueIndices === nIdx) {
  352. this._currentValueIndices = this.numItems > 0 ? 0 : -1;
  353. } else if (this._currentValueIndices > nIdx) {
  354. --this._currentValueIndices;
  355. }
  356. }
  357. this._send({ id: this._id, remove: nIdx });
  358. }
  359. getItemAt(nIdx = -1, bExportValue = false) {
  360. if (!this._isChoice) {
  361. throw new Error("Not a choice widget");
  362. }
  363. if (nIdx < 0 || nIdx >= this.numItems) {
  364. nIdx = this.numItems - 1;
  365. }
  366. const item = this._items[nIdx];
  367. return bExportValue ? item.exportValue : item.displayValue;
  368. }
  369. getArray() {
  370. // Gets the array of terminal child fields (that is, fields that can have
  371. // a value for this Field object, the parent field).
  372. if (this._kidIds) {
  373. const array = [];
  374. const fillArrayWithKids = kidIds => {
  375. for (const id of kidIds) {
  376. const obj = this._appObjects[id];
  377. if (!obj) {
  378. continue;
  379. }
  380. if (obj.obj._hasValue) {
  381. array.push(obj.wrapped);
  382. }
  383. if (obj.obj._kidIds) {
  384. fillArrayWithKids(obj.obj._kidIds);
  385. }
  386. }
  387. };
  388. fillArrayWithKids(this._kidIds);
  389. return array;
  390. }
  391. if (this._children === null) {
  392. this._children = this._document.obj._getTerminalChildren(this._fieldPath);
  393. }
  394. return this._children;
  395. }
  396. getLock() {
  397. return undefined;
  398. }
  399. isBoxChecked(nWidget) {
  400. return false;
  401. }
  402. isDefaultChecked(nWidget) {
  403. return false;
  404. }
  405. insertItemAt(cName, cExport = undefined, nIdx = 0) {
  406. if (!this._isChoice) {
  407. throw new Error("Not a choice widget");
  408. }
  409. if (!cName) {
  410. return;
  411. }
  412. if (nIdx < 0 || nIdx > this.numItems) {
  413. nIdx = this.numItems;
  414. }
  415. if (this._items.some(({ displayValue }) => displayValue === cName)) {
  416. return;
  417. }
  418. if (cExport === undefined) {
  419. cExport = cName;
  420. }
  421. const data = { displayValue: cName, exportValue: cExport };
  422. this._items.splice(nIdx, 0, data);
  423. if (Array.isArray(this._currentValueIndices)) {
  424. let index = this._currentValueIndices.findIndex(i => i >= nIdx);
  425. if (index !== -1) {
  426. for (const ii = this._currentValueIndices.length; index < ii; index++) {
  427. ++this._currentValueIndices[index];
  428. }
  429. }
  430. } else if (this._currentValueIndices >= nIdx) {
  431. ++this._currentValueIndices;
  432. }
  433. this._send({ id: this._id, insert: { index: nIdx, ...data } });
  434. }
  435. setAction(cTrigger, cScript) {
  436. if (typeof cTrigger !== "string" || typeof cScript !== "string") {
  437. return;
  438. }
  439. if (!(cTrigger in this._actions)) {
  440. this._actions[cTrigger] = [];
  441. }
  442. this._actions[cTrigger].push(cScript);
  443. }
  444. setFocus() {
  445. this._send({ id: this._id, focus: true });
  446. }
  447. setItems(oArray) {
  448. if (!this._isChoice) {
  449. throw new Error("Not a choice widget");
  450. }
  451. this._items.length = 0;
  452. for (const element of oArray) {
  453. let displayValue, exportValue;
  454. if (Array.isArray(element)) {
  455. displayValue = element[0]?.toString() || "";
  456. exportValue = element[1]?.toString() || "";
  457. } else {
  458. displayValue = exportValue = element?.toString() || "";
  459. }
  460. this._items.push({ displayValue, exportValue });
  461. }
  462. this._currentValueIndices = 0;
  463. this._send({ id: this._id, items: this._items });
  464. }
  465. setLock() {}
  466. signatureGetModifications() {}
  467. signatureGetSeedValue() {}
  468. signatureInfo() {}
  469. signatureSetSeedValue() {}
  470. signatureSign() {}
  471. signatureValidate() {}
  472. _isButton() {
  473. return false;
  474. }
  475. _reset() {
  476. this.value = this.defaultValue;
  477. }
  478. _runActions(event) {
  479. const eventName = event.name;
  480. if (!this._actions.has(eventName)) {
  481. return false;
  482. }
  483. const actions = this._actions.get(eventName);
  484. try {
  485. for (const action of actions) {
  486. // Action evaluation must happen in the global scope
  487. this._globalEval(action);
  488. }
  489. } catch (error) {
  490. event.rc = false;
  491. throw error;
  492. }
  493. return true;
  494. }
  495. }
  496. class RadioButtonField extends Field {
  497. constructor(otherButtons, data) {
  498. super(data);
  499. this.exportValues = [this.exportValues];
  500. this._radioIds = [this._id];
  501. this._radioActions = [this._actions];
  502. for (const radioData of otherButtons) {
  503. this.exportValues.push(radioData.exportValues);
  504. this._radioIds.push(radioData.id);
  505. this._radioActions.push(createActionsMap(radioData.actions));
  506. if (this._value === radioData.exportValues) {
  507. this._id = radioData.id;
  508. }
  509. }
  510. this._hasBeenInitialized = true;
  511. this._value = data.value || "";
  512. }
  513. get value() {
  514. return this._value;
  515. }
  516. set value(value) {
  517. if (!this._hasBeenInitialized) {
  518. return;
  519. }
  520. if (value === null || value === undefined) {
  521. this._value = "";
  522. }
  523. const i = this.exportValues.indexOf(value);
  524. if (0 <= i && i < this._radioIds.length) {
  525. this._id = this._radioIds[i];
  526. this._value = value;
  527. } else if (value === "Off" && this._radioIds.length === 2) {
  528. const nextI = (1 + this._radioIds.indexOf(this._id)) % 2;
  529. this._id = this._radioIds[nextI];
  530. this._value = this.exportValues[nextI];
  531. }
  532. }
  533. checkThisBox(nWidget, bCheckIt = true) {
  534. if (nWidget < 0 || nWidget >= this._radioIds.length || !bCheckIt) {
  535. return;
  536. }
  537. this._id = this._radioIds[nWidget];
  538. this._value = this.exportValues[nWidget];
  539. this._send({ id: this._id, value: this._value });
  540. }
  541. isBoxChecked(nWidget) {
  542. return (
  543. nWidget >= 0 &&
  544. nWidget < this._radioIds.length &&
  545. this._id === this._radioIds[nWidget]
  546. );
  547. }
  548. isDefaultChecked(nWidget) {
  549. return (
  550. nWidget >= 0 &&
  551. nWidget < this.exportValues.length &&
  552. this.defaultValue === this.exportValues[nWidget]
  553. );
  554. }
  555. _getExportValue(state) {
  556. const i = this._radioIds.indexOf(this._id);
  557. return this.exportValues[i];
  558. }
  559. _runActions(event) {
  560. const i = this._radioIds.indexOf(this._id);
  561. this._actions = this._radioActions[i];
  562. return super._runActions(event);
  563. }
  564. _isButton() {
  565. return true;
  566. }
  567. }
  568. class CheckboxField extends RadioButtonField {
  569. get value() {
  570. return this._value;
  571. }
  572. set value(value) {
  573. if (!value || value === "Off") {
  574. this._value = "Off";
  575. } else {
  576. super.value = value;
  577. }
  578. }
  579. _getExportValue(state) {
  580. return state ? super._getExportValue(state) : "Off";
  581. }
  582. isBoxChecked(nWidget) {
  583. if (this._value === "Off") {
  584. return false;
  585. }
  586. return super.isBoxChecked(nWidget);
  587. }
  588. isDefaultChecked(nWidget) {
  589. if (this.defaultValue === "Off") {
  590. return this._value === "Off";
  591. }
  592. return super.isDefaultChecked(nWidget);
  593. }
  594. checkThisBox(nWidget, bCheckIt = true) {
  595. if (nWidget < 0 || nWidget >= this._radioIds.length) {
  596. return;
  597. }
  598. this._id = this._radioIds[nWidget];
  599. this._value = bCheckIt ? this.exportValues[nWidget] : "Off";
  600. this._send({ id: this._id, value: this._value });
  601. }
  602. }
  603. export { CheckboxField, Field, RadioButtonField };