jclic 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +5 -7
  2. package/dist-node/jclic-node.js +14157 -0
  3. package/dist-node/jclic-node.umd.cjs +530 -0
  4. package/package.json +38 -26
  5. package/.vscode/launch.json +0 -33
  6. package/.vscode/settings.json +0 -13
  7. package/CHANGELOG.md +0 -672
  8. package/TRANSLATIONS.md +0 -11
  9. package/build-locales.mjs +0 -82
  10. package/dist/jclic-node.js +0 -31680
  11. package/dist/jclic-node.js.map +0 -1
  12. package/dist/jclic.components.LICENSE +0 -2254
  13. package/dist/jclic.min.js +0 -27
  14. package/dist/jclic.min.js.map +0 -1
  15. package/eslint.config.mjs +0 -31
  16. package/jsdoc.config.js +0 -71
  17. package/locales/ar.po +0 -244
  18. package/locales/ast.po +0 -246
  19. package/locales/bs.po +0 -247
  20. package/locales/ca.po +0 -248
  21. package/locales/ca_ES@valencia.po +0 -248
  22. package/locales/cs.po +0 -244
  23. package/locales/da.po +0 -244
  24. package/locales/de.po +0 -246
  25. package/locales/el.po +0 -244
  26. package/locales/es.po +0 -248
  27. package/locales/eu.po +0 -244
  28. package/locales/fr.po +0 -244
  29. package/locales/gl.po +0 -244
  30. package/locales/he.po +0 -244
  31. package/locales/hr.po +0 -245
  32. package/locales/it.po +0 -246
  33. package/locales/ja.po +0 -242
  34. package/locales/jclic.js.pot +0 -241
  35. package/locales/nb_NO.po +0 -244
  36. package/locales/nl.po +0 -244
  37. package/locales/pl.po +0 -244
  38. package/locales/pt.po +0 -244
  39. package/locales/pt_BR.po +0 -248
  40. package/locales/ro.po +0 -248
  41. package/locales/ru.po +0 -245
  42. package/locales/ta.po +0 -244
  43. package/locales/tr.po +0 -246
  44. package/locales/uk.po +0 -247
  45. package/locales/vec.po +0 -244
  46. package/locales/zh_TW.po +0 -246
  47. package/patches/po2json+1.0.0-beta-3.patch +0 -12
  48. package/src/AWT.js +0 -2067
  49. package/src/Activity.js +0 -1311
  50. package/src/Deps.js +0 -232
  51. package/src/GlobalData.js +0 -5
  52. package/src/JClic.js +0 -196
  53. package/src/JClicPlayer.js +0 -1308
  54. package/src/PlayerHistory.js +0 -305
  55. package/src/Utils.js +0 -1355
  56. package/src/activities/associations/ComplexAssociation.js +0 -321
  57. package/src/activities/associations/SimpleAssociation.js +0 -519
  58. package/src/activities/memory/MemoryGame.js +0 -423
  59. package/src/activities/panels/Explore.js +0 -349
  60. package/src/activities/panels/Identify.js +0 -356
  61. package/src/activities/panels/InformationScreen.js +0 -262
  62. package/src/activities/panels/Menu.js +0 -209
  63. package/src/activities/panels/icons/ico00.png +0 -0
  64. package/src/activities/panels/icons/ico01.png +0 -0
  65. package/src/activities/panels/icons/ico02.png +0 -0
  66. package/src/activities/panels/icons/ico03.png +0 -0
  67. package/src/activities/panels/icons/icofolder.png +0 -0
  68. package/src/activities/puzzles/DoublePuzzle.js +0 -424
  69. package/src/activities/puzzles/ExchangePuzzle.js +0 -374
  70. package/src/activities/puzzles/HolePuzzle.js +0 -360
  71. package/src/activities/text/Complete.js +0 -127
  72. package/src/activities/text/Evaluator.js +0 -534
  73. package/src/activities/text/FillInBlanks.js +0 -426
  74. package/src/activities/text/IdentifyText.js +0 -253
  75. package/src/activities/text/OrderText.js +0 -421
  76. package/src/activities/text/TextActivityBase.js +0 -557
  77. package/src/activities/text/TextActivityDocument.js +0 -660
  78. package/src/activities/text/WrittenAnswer.js +0 -557
  79. package/src/activities/textGrid/CrossWord.js +0 -565
  80. package/src/activities/textGrid/WordSearch.js +0 -458
  81. package/src/activities/textGrid/icons/hIcon.svg +0 -3
  82. package/src/activities/textGrid/icons/vIcon.svg +0 -3
  83. package/src/automation/AutoContentProvider.js +0 -182
  84. package/src/automation/arith/Arith.js +0 -864
  85. package/src/bags/ActivitySequence.js +0 -318
  86. package/src/bags/ActivitySequenceElement.js +0 -161
  87. package/src/bags/ActivitySequenceJump.js +0 -140
  88. package/src/bags/ConditionalJumpInfo.js +0 -113
  89. package/src/bags/JumpInfo.js +0 -136
  90. package/src/bags/MediaBag.js +0 -215
  91. package/src/bags/MediaBagElement.js +0 -516
  92. package/src/boxes/AbstractBox.js +0 -699
  93. package/src/boxes/ActiveBagContent.js +0 -494
  94. package/src/boxes/ActiveBox.js +0 -810
  95. package/src/boxes/ActiveBoxBag.js +0 -357
  96. package/src/boxes/ActiveBoxContent.js +0 -484
  97. package/src/boxes/ActiveBoxGrid.js +0 -179
  98. package/src/boxes/BoxBag.js +0 -500
  99. package/src/boxes/BoxBase.js +0 -398
  100. package/src/boxes/BoxConnector.js +0 -325
  101. package/src/boxes/TextGrid.js +0 -887
  102. package/src/boxes/TextGridContent.js +0 -215
  103. package/src/init-jsdom.js +0 -65
  104. package/src/jclic-node.js +0 -219
  105. package/src/media/ActiveMediaBag.js +0 -145
  106. package/src/media/ActiveMediaPlayer.js +0 -297
  107. package/src/media/AudioBuffer.js +0 -219
  108. package/src/media/EventSounds.js +0 -169
  109. package/src/media/EventSoundsElement.js +0 -155
  110. package/src/media/MediaContent.js +0 -328
  111. package/src/media/MidiAudioPlayer.js +0 -254
  112. package/src/media/icons/audio.svg +0 -3
  113. package/src/media/icons/generic.svg +0 -3
  114. package/src/media/icons/mic.svg +0 -3
  115. package/src/media/icons/movie.svg +0 -3
  116. package/src/media/icons/music.svg +0 -3
  117. package/src/media/icons/url.svg +0 -3
  118. package/src/media/sounds/actionError.mp3 +0 -0
  119. package/src/media/sounds/actionOk.mp3 +0 -0
  120. package/src/media/sounds/click.mp3 +0 -0
  121. package/src/media/sounds/finishedError.mp3 +0 -0
  122. package/src/media/sounds/finishedOk.mp3 +0 -0
  123. package/src/media/sounds/start.mp3 +0 -0
  124. package/src/project/JClicProject.js +0 -282
  125. package/src/project/ProjectSettings.js +0 -273
  126. package/src/report/ActionReg.js +0 -123
  127. package/src/report/ActivityReg.js +0 -271
  128. package/src/report/EncryptMin.js +0 -210
  129. package/src/report/Reporter.js +0 -727
  130. package/src/report/SCORM.js +0 -272
  131. package/src/report/SequenceReg.js +0 -275
  132. package/src/report/SessionReg.js +0 -340
  133. package/src/report/SessionStorageReporter.js +0 -131
  134. package/src/report/TCPReporter.js +0 -628
  135. package/src/shapers/ClassicJigSaw.js +0 -138
  136. package/src/shapers/Holes.js +0 -77
  137. package/src/shapers/JigSaw.js +0 -161
  138. package/src/shapers/Rectangular.js +0 -78
  139. package/src/shapers/Shaper.js +0 -386
  140. package/src/shapers/TriangularJigSaw.js +0 -121
  141. package/src/skins/BlueSkin.js +0 -80
  142. package/src/skins/Counter.js +0 -152
  143. package/src/skins/CustomSkin.js +0 -412
  144. package/src/skins/DefaultSkin.js +0 -376
  145. package/src/skins/EmptySkin.js +0 -82
  146. package/src/skins/GreenSkin.js +0 -94
  147. package/src/skins/MiniSkin.js +0 -130
  148. package/src/skins/OrangeSkin.js +0 -78
  149. package/src/skins/SimpleSkin.js +0 -92
  150. package/src/skins/Skin.js +0 -1021
  151. package/src/skins/assets/actionsIcon.svg +0 -3
  152. package/src/skins/assets/appLogo.svg +0 -8
  153. package/src/skins/assets/basic.css +0 -41
  154. package/src/skins/assets/closeDialogIcon.svg +0 -3
  155. package/src/skins/assets/closeIcon.svg +0 -3
  156. package/src/skins/assets/copyIcon.svg +0 -3
  157. package/src/skins/assets/fullScreenExitIcon.svg +0 -3
  158. package/src/skins/assets/fullScreenIcon.svg +0 -3
  159. package/src/skins/assets/infoIcon.svg +0 -3
  160. package/src/skins/assets/main.css +0 -43
  161. package/src/skins/assets/mainHalf.css +0 -23
  162. package/src/skins/assets/mainTwoThirds.css +0 -23
  163. package/src/skins/assets/mini.css +0 -15
  164. package/src/skins/assets/nextIcon.svg +0 -3
  165. package/src/skins/assets/okDialogIcon.svg +0 -3
  166. package/src/skins/assets/prevIcon.svg +0 -3
  167. package/src/skins/assets/reports.css +0 -156
  168. package/src/skins/assets/reportsIcon.svg +0 -3
  169. package/src/skins/assets/scoreIcon.svg +0 -3
  170. package/src/skins/assets/simple.css +0 -16
  171. package/src/skins/assets/simpleHalf.css +0 -11
  172. package/src/skins/assets/simpleTwoThirds.css +0 -11
  173. package/src/skins/assets/timeIcon.svg +0 -4
  174. package/src/skins/assets/waitAnim.css +0 -54
  175. package/src/skins/assets/waitImgBig.svg +0 -3
  176. package/src/skins/assets/waitImgSmall.svg +0 -3
  177. package/webpack.config.mjs +0 -169
package/src/Utils.js DELETED
@@ -1,1355 +0,0 @@
1
- /**
2
- * File : Utils.js
3
- * Created : 01/04/2015
4
- * By : Francesc Busquets <francesc@gmail.com>
5
- *
6
- * JClic.js
7
- * An HTML5 player of JClic activities
8
- * https://projectestac.github.io/jclic.js
9
- *
10
- * @source https://github.com/projectestac/jclic.js
11
- *
12
- * @license EUPL-1.2
13
- * @licstart
14
- * (c) 2000-2020 Educational Telematic Network of Catalonia (XTEC)
15
- *
16
- * Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
17
- * the European Commission- subsequent versions of the EUPL (the "Licence");
18
- * You may not use this work except in compliance with the Licence.
19
- *
20
- * You may obtain a copy of the Licence at:
21
- * https://joinup.ec.europa.eu/software/page/eupl
22
- *
23
- * Unless required by applicable law or agreed to in writing, software
24
- * distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
25
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
26
- * Licence for the specific language governing permissions and limitations
27
- * under the Licence.
28
- * @licend
29
- * @module
30
- */
31
-
32
- /* global Promise, window, document, console, HTMLElement */
33
-
34
- import $ from 'jquery';
35
- import JSZip from 'jszip';
36
- import JSZipUtils from 'jszip-utils';
37
- import GlobalData from './GlobalData.js';
38
-
39
- /**
40
- * Exports third-party NPM packages used by JClic, so they become available to other scripts through
41
- * the global variable `JClicObject` (defined in {@link module:JClic.JClic})
42
- * @type: {object}
43
- */
44
- export const pkg = {
45
- $,
46
- JSZip,
47
- JSZipUtils,
48
- };
49
-
50
- /**
51
- * List of valid verbosity levels
52
- * @const {string[]}
53
- */
54
- export const LOG_LEVELS = ['none', 'error', 'warn', 'info', 'debug', 'trace', 'all'];
55
-
56
- /**
57
- * Labels printed on logs for each message type
58
- * @const {string[]}
59
- */
60
- export const LOG_PRINT_LABELS = [' ', 'ERROR', 'WARN ', 'INFO ', 'DEBUG', 'TRACE', 'ALL '];
61
-
62
- /**
63
- * Options of the logging system
64
- * @type {object} */
65
- export const LOG_OPTIONS = {
66
- level: 2, // warn
67
- prefix: 'JClic',
68
- timestamp: true,
69
- popupOnErrors: false,
70
- chainTo: null,
71
- pipeTo: null,
72
- };
73
-
74
- /**
75
- * Current dictionary of string translations
76
- */
77
- let _messages = {};
78
-
79
- /**
80
- * Initializes the global settings
81
- * @param {object} options - An object with global settings
82
- * @param {boolean} [setLog=true] - When `true`, the log level will be set
83
- * @param {boolean} [setLang=true] - When `true`, the current language will be set
84
- * @returns {object} The normalized `options` object
85
- */
86
- export function init(options, setLog = true, setLang = true) {
87
- options = normalizeObject(options);
88
- if (setLog) {
89
- if (typeof options.logLevel !== 'undefined')
90
- setLogLevel(options.logLevel);
91
- if (typeof options.chainLogTo === 'function')
92
- LOG_OPTIONS.chainTo = options.chainLogTo;
93
- if (typeof options.pipeLogTo === 'function')
94
- LOG_OPTIONS.pipeTo = options.pipeLogTo;
95
- }
96
-
97
- if (setLang) {
98
- const lngRequested = options.lang;
99
- const lng = checkPreferredLanguage(GlobalData.languages, 'en', lngRequested);
100
- log('debug', `Language ${lngRequested ? `requested: "${lngRequested}" ` : ''} used: "${lng}"`);
101
- _messages = lng === 'en' ? {} : GlobalData.messages[lng];
102
- }
103
-
104
- return options;
105
- };
106
-
107
- /**
108
- * Function that will return the translation of the provided key
109
- * into the current language.
110
- * @param {string} key - ID of the expression to be translated
111
- * @returns {string} - The translated text
112
- */
113
- export function getMsg(key) {
114
- return _messages[key] || key;
115
- }
116
-
117
- /**
118
- * Converts expressions of type 'pt-br', 'FR', 'ca_es@valencia'... to the format expected by the i18n system:
119
- * lc[_CC][@variant] where 'lc' is a two or three lowercase letter language code, CC is an optional two uppercase
120
- * letter country code, followed by an optional 'variant' consisting in letters and/or digits.
121
- * @param {string} locale - The locale expression to be normalized
122
- * @returns string - The normalized locale
123
- */
124
- export function normalizeLocale(locale = '') {
125
- const [, language = null, country = null, variant = null] = /^([a-zA-Z]{2,3})[_-]?([a-zA-Z]{2})?@?([a-zA-Z0-9]*)?$/.exec(locale.trim()) || [];
126
- return language
127
- ? `${language.toLowerCase()}${country ? `_${country.toUpperCase()}` : ''}${variant ? `@${variant.toLowerCase()}` : ''}`
128
- : '';
129
- };
130
-
131
- /**
132
- * Checks if the language preferred by the user (based on browser and/or specific settings)
133
- * is in a list of available languages.
134
- * @param {string[]} availableLangs - Array of available languages. It should contain at least one item.
135
- * @param {string} [defaultLang=en] - Language to be used by default when not found the selected one
136
- * @param {string} [requestedLang=''] - Request this specific language
137
- * @returns {string} - The most suitable language for this request
138
- */
139
- export function checkPreferredLanguage(availableLangs, defaultLang = 'en', requestedLang = '') {
140
- let result = -1;
141
-
142
- // Create an array to store possible values
143
- let tries = [];
144
-
145
- // If "setLang" is specified, check it
146
- if (requestedLang) {
147
- // Normalize requested locale
148
- const lang = normalizeLocale(requestedLang);
149
- if (lang)
150
- tries.push(lang);
151
- }
152
-
153
- // Add user's preferred languages, if any
154
- if (window.navigator.languages)
155
- tries = tries.concat(window.navigator.languages);
156
-
157
- // Add the navigator main language, if defined
158
- if (window.navigator.language)
159
- tries.push(window.navigator.language);
160
-
161
- // Add English as final option
162
- tries.push(defaultLang);
163
-
164
- for (let i = 0; i < tries.length; i++) {
165
- let match = -1;
166
- for (let n in availableLangs) {
167
- if (tries[i].indexOf(availableLangs[n]) === 0) {
168
- match = n;
169
- if (tries[i] === availableLangs[n]) {
170
- result = n;
171
- break;
172
- }
173
- }
174
- }
175
- if (result >= 0 || (result = match) >= 0)
176
- break;
177
- }
178
- return availableLangs[result >= 0 ? result : 0];
179
- };
180
-
181
- /**
182
- * Establishes the current verbosity level of the logging system
183
- * @param {string} level - One of the valid strings in {@link module:Utils.LOG_LEVELS}
184
- */
185
- export function setLogLevel(level) {
186
- const log = LOG_LEVELS.indexOf(level);
187
- if (log >= 0)
188
- LOG_OPTIONS.level = log;
189
- };
190
-
191
- /**
192
- * Reports a new message to the logging system
193
- * @param {string} type - The type of message. Mus be `error`, `warn`, `info`, `debug` or `trace`.
194
- * @param {string} msg - The main message to be logged. Additional parameters can be added, like
195
- * in `console.log` (see: {@link https://developer.mozilla.org/en-US/docs/Web/API/Console/log})
196
- */
197
- export function log(type, msg) {
198
- const level = LOG_LEVELS.indexOf(type);
199
- const args = Array.prototype.slice.call(arguments);
200
-
201
- // Check if message should currently be logged
202
- if (level < 0 || level <= LOG_OPTIONS.level) {
203
- if (LOG_OPTIONS.pipeTo)
204
- LOG_OPTIONS.pipeTo.apply(null, args);
205
- else {
206
- const mainMsg = `${LOG_OPTIONS.prefix || ''} ${LOG_PRINT_LABELS[level]} ${LOG_OPTIONS.timestamp ? getDateTime() : ''} ${msg}`;
207
- console[level === 1 ? 'error' : level === 2 ? 'warn' : 'log'].apply(console, [mainMsg].concat(args.slice(2)));
208
- // Call chained logger, if anny
209
- if (LOG_OPTIONS.chainTo)
210
- LOG_OPTIONS.chainTo.apply(null, args);
211
- }
212
- }
213
- };
214
-
215
- /**
216
- * Gets a boolean value from a textual expression
217
- * @param {string} val - The value to be parsed (`true` for true, null or otherwise for `false`)
218
- * @param {boolean} [defaultValue=false] - The default value to return when `val` is false
219
- * @returns {number}
220
- */
221
- export function getBoolean(val, defaultValue = false) {
222
- return val === 'true' ? true : val === 'false' ? false : defaultValue;
223
- };
224
-
225
- /**
226
- * Gets a value from an given expression that can be `null`, `undefined` or empty string ('')
227
- * @param {any} val - The expression to parse
228
- * @param {any} [defaultValue=null] - The value to return when `val` is `null`, `''` or `undefined`
229
- * @returns {any}
230
- */
231
- export function getVal(val, defaultValue = null) {
232
- return (val === '' || val === null || typeof val === 'undefined') ? defaultValue : val;
233
- };
234
-
235
- /**
236
- * Gets a number from a string or another number
237
- * @param {any} val - The expression to parse
238
- * @param {number} [defaultValue=0] - The default value
239
- * @returns {number}
240
- */
241
- export function getNumber(val, defaultValue) {
242
- return Number(getVal(val, defaultValue));
243
- };
244
-
245
- /**
246
- * Gets the plain percent expression (without decimals) of the given value
247
- * @param {number} val - The value to be expressed as a percentile
248
- * @returns {string}
249
- */
250
- export function getPercent(val) {
251
- return `${Math.round(val * 100)}%`;
252
- }
253
-
254
- /**
255
- * Returns the two-digits text expression representing the given number (lesser than 100) zero-padded at left
256
- * Useful for representing hours, minutes and seconds
257
- * @param {number} val - The number to be processed
258
- * @returns {string}
259
- */
260
- export function zp(val) {
261
- return `0${val}`.slice(-2);
262
- };
263
-
264
- /**
265
- * Returns a given time in [00h 00'00"] format
266
- * @param {number} millis - Amount of milliseconds to be processed
267
- * @returns {string}
268
- */
269
- export function getHMStime(millis) {
270
- const d = new Date(millis);
271
- const h = d.getUTCHours(), m = d.getUTCMinutes(), s = d.getUTCSeconds();
272
- return `${h ? h + 'h ' : ''}${h || m ? zp(m) + '\'' : ''}${zp(s)}"`;
273
- };
274
-
275
- /**
276
- * Returns a formatted string with the provided date and time
277
- * @param {external:Date} date - The date to be formatted. When `null` or `undefined`, the current date will be used.
278
- * @returns {string}
279
- */
280
- export function getDateTime(date = new Date()) {
281
- return `${date.getFullYear()}/${zp(date.getMonth() + 1)}/${zp(date.getDate())} ${zp(date.getHours())}:${zp(date.getMinutes())}:${zp(date.getSeconds())}`;
282
- };
283
-
284
- /**
285
- * Parse 'date' fields generated by "JClic Author" in format d/m/y, with
286
- * variable number of digits.
287
- * @param {string} text - The old 'date' field
288
- * @returns {external:Date} - Always return a Date object (now, if text was invalid)
289
- */
290
- export function parseOldDate(text) {
291
- let result = null;
292
- if (text) {
293
- const elements = text.trim().split('/');
294
- if (elements.length === 3) {
295
- let m = parseInt(elements[0]) || 0;
296
- let d = parseInt(elements[1]) || 0;
297
- let y = parseInt(elements[2]) || 0;
298
- if (m > 12 && d <= 12) {
299
- const t = m;
300
- m = d;
301
- d = t;
302
- }
303
- if (y < 1980)
304
- y += (y < 90 ? 2000 : 1900);
305
- if (d && m && y) {
306
- result = new Date(Date.parse(`${m}/${d}/${y}`));
307
- }
308
- }
309
- }
310
- return result || new Date();
311
- };
312
-
313
- /**
314
- * Extracts just the ISO-639 language code from complex
315
- * expressions like "English (en)", buid by JClic Author.
316
- * @param {string} text - The expression to parse
317
- * @returns {string} - The ISO-639 language code, or '--' if none found
318
- */
319
- export function cleanOldLanguageTag(text) {
320
- if (!text)
321
- text = '--';
322
- // Allow only ISO-639-1 and ISO-639-2 language codes
323
- else if (!text.match(/^[a-z][a-z][a-z]?$/)) {
324
- const matches = text.match(/\(([a-z][a-z][a-z]?)\)/);
325
- if (matches && matches.length === 2)
326
- text = matches[1];
327
- else
328
- text = '--';
329
- }
330
- return text;
331
- };
332
-
333
- /** @const {number} */
334
- export const FALSE = 0;
335
-
336
- /** @const {number} */
337
- export const TRUE = 1;
338
-
339
- /** @const {number} */
340
- export const DEFAULT = 2;
341
-
342
- /**
343
- * Gets a numeric value (0, 1 or 2) from a set of possible values: `false`, `true` and `default`.
344
- * @param {string} val - The text to be parsed
345
- * @param {any} def - An optional default value
346
- * @returns {number}
347
- */
348
- export function getTriState(val, def = DEFAULT) {
349
- return val === 'true' ? TRUE : val === 'false' ? FALSE : def;
350
- };
351
-
352
- /**
353
- * Returns a string with the given `tag` repeated n times
354
- * @param {string} tag - The tag to be repeated
355
- * @param {number} repeats - The number of times to repeat the tag
356
- * @returns {string}
357
- */
358
- export function fillString(tag, repeats = 0) {
359
- return Array(repeats).fill(tag).join('');
360
- };
361
-
362
- /**
363
- * Checks if the provided value is 'null' or 'undefined'.
364
- * @param {any} val - The value to be parsed
365
- * @returns {boolean}
366
- */
367
- export function isNullOrUndef(val) {
368
- return typeof val === 'undefined' || val === null;
369
- };
370
-
371
- /**
372
- * Checks if two expressions are equivalent.
373
- * Returns `true` when both parameters are `null` or `undefined`, and also when both have
374
- * equivalent values.
375
- * @param {any} a
376
- * @param {any} b
377
- * @returns {boolean}
378
- */
379
- export function isEquivalent(a, b) {
380
- return (typeof a === 'undefined' || a === null) && (typeof b === 'undefined' || b === null) || a === b;
381
- };
382
-
383
- /**
384
- * Reads paragraphs, identified by `<p></p>` elements, inside XML data
385
- * @param {object} xml - The DOM-XML element to be parsed
386
- * @returns {string}
387
- */
388
- export function getXmlText(xml) {
389
- let text = '';
390
- $(xml).children('p').each((_n, child) => { text += `<p>${child.textContent}</p>`; });
391
- return text;
392
- };
393
-
394
- /**
395
- * Parse the provided XML element node, returning a complex object
396
- * @param {object} xml - The root XML element to parse
397
- * @param {boolean} [withText=false] - When `true`, any text found inside the XML element is also included in the resulting object.
398
- * @returns {object}
399
- */
400
- export function parseXmlNode(xml, withText = false) {
401
- // Initialize the resulting object
402
- const result = {};
403
- // Direct copy of root element attributes as object properties
404
- if (xml.attributes)
405
- attrForEach(xml.attributes, (name, value) => result[name] = /^-?\d*$/.test(value) ? Number(value) : value);
406
-
407
- const keys = [];
408
- const children = Array.from(xml.children || xml.childNodes || []);
409
-
410
- // If all children is of type 'p', just compile it in a single string
411
- const paragraphs = children.filter(child => child.nodeName === 'p');
412
- if (paragraphs.length > 0 && paragraphs.length === children.filter(ch => ch.nodeName !== '#text').length) {
413
- const text = paragraphs.map(ch => ch.textContent).join('\n');
414
- if (xml.attributes) {
415
- result.text = text;
416
- return result;
417
- }
418
- return text;
419
- }
420
-
421
- // Process children elements
422
- children.forEach(child => {
423
- // Avoid extra text content collected by [xmldom](https://www.npmjs.com/package/xmldom)
424
- if (child.nodeName === '#text' && !withText)
425
- return;
426
-
427
- // Recursive processing of children
428
- const ch = parseXmlNode(child, withText);
429
- // Store the result into a temporary object named as the child node name,
430
- if (!result[child.nodeName]) {
431
- // Create object and save key for later processing
432
- result[child.nodeName] = {};
433
- keys.push(child.nodeName);
434
- }
435
- // Use 'id' (or an incremental number if 'id' is not set) as a key
436
- if (ch.id)
437
- result[child.nodeName][ch.id] = ch;
438
- else {
439
- const n = Object.keys(result[child.nodeName]).length;
440
- result[child.nodeName][n] = ch;
441
- }
442
- });
443
- // Check temporary objects, converting it to an array, a single object or a complex object
444
- keys.forEach(k => {
445
- // Retrieve temporary object from `keys`
446
- const kx = Object.keys(result[k]);
447
- // If all keys are numbers, convert object into an array (or leave it as a single object)
448
- if (!kx.find(kk => isNaN(kk))) {
449
- if (kx.length === 1)
450
- // Array with a single element. Leave it as a simple object:
451
- result[k] = result[k][0];
452
- else {
453
- // Object with numeric keys. Convert it to array:
454
- const arr = [];
455
- kx.forEach(kk => arr.push(result[k][kk]));
456
- result[k] = arr;
457
- }
458
- }
459
- });
460
- // Save text content, if any:
461
- if (children.length === 0 && xml.textContent)
462
- result.textContent = xml.textContent;
463
- return result;
464
- };
465
-
466
- /**
467
- * Parse the given XML node, known as containing only text elements,
468
- * and return its content as a string (when possible)
469
- * @param {object} xml - The XML element to parse
470
- * @returns {string|object}
471
- */
472
- export function getXmlNodeText(node) {
473
- const result = parseXmlNode(node);
474
- return typeof result === 'string' ?
475
- result :
476
- result.hasOwnProperty('text') ?
477
- result.text :
478
- result.hasOwnProperty('textContent') ?
479
- result.textContent :
480
- result;
481
- };
482
-
483
- /**
484
- * Recursively explore the given object, converting to a string
485
- * all attributes with a single attribute named 'text'.
486
- * Example:
487
- * {a:1, b:{text:"hello"}, c:{d:2, text:"world"}} => {a:1, b:"hello", c:{d:2, text:"world"}}
488
- * @param {object} obj - The object to explore
489
- * @returns {object} - The same object, with text attributes reduced to strings
490
- */
491
- export function reduceTextsToStrings(obj) {
492
- if (obj) {
493
- const keys = Object.keys(obj);
494
- keys.forEach(k => {
495
- const attr = obj[k];
496
- if (typeof attr === 'object') {
497
- const ko = Object.keys(attr);
498
- if (ko.length === 1 && ko[0] === 'text')
499
- obj[k] = attr.text;
500
- else
501
- obj[k] = reduceTextsToStrings(attr);
502
- }
503
- });
504
- }
505
- return obj;
506
- };
507
-
508
- /**
509
- * Creates a string suitable to be used in the 'style' attribute of HTML tags, filled with the
510
- * CSS attributes contained in the provided object.
511
- * @param {object} cssObj
512
- * @returns {string}
513
- */
514
- export function cssToString(cssObj) {
515
- return Object.keys(cssObj).reduce((s, key) => `${s}${key}:${cssObj[key]};`, '');
516
- };
517
-
518
- /**
519
- * Converts java-like color codes (like '0xRRGGBB') to valid CSS values like '#RRGGBB' or 'rgba(r,g,b,a)'
520
- * @param {string} [color] - A color, as codified in Java
521
- * @param {string} [defaultColor] - The default color to be used
522
- * @returns {string}
523
- */
524
- export function checkColor(color, defaultColor = settings.BoxBase.BACK_COLOR) {
525
- if (typeof color === 'undefined' || color === null)
526
- color = defaultColor;
527
- color = color.replace('0x', '#');
528
- // Check for Alpha value
529
- if (color.charAt(0) === '#' && color.length > 7) {
530
- const alpha = fx(parseInt(color.substring(1, 3), 16) / 255.0, 2);
531
- color = `rgba(${parseInt(color.substring(3, 5), 16)},${parseInt(color.substring(5, 7), 16)},${parseInt(color.substring(7, 9), 16)},${alpha})`;
532
- }
533
- return color;
534
- };
535
-
536
- /**
537
- * Checks if the provided color has an alpha value less than 1
538
- * @param {string} color - The color to be analyzed
539
- * @returns {boolean}
540
- */
541
- export function colorHasTransparency(color) {
542
- if (startsWith(color, 'rgba(')) {
543
- var alpha = parseInt(color.substring(color.lastIndexOf(',')));
544
- return typeof alpha === 'number' && alpha < 1.0;
545
- }
546
- return false;
547
- };
548
-
549
- /**
550
- * Clones the provided object
551
- * See: https://stackoverflow.com/questions/41474986/how-to-clone-a-javascript-es6-class-instance
552
- * @param {object} obj
553
- * @returns {object}
554
- */
555
- //cloneObject: obj => Object.assign(Object.create(Object.getPrototypeOf(obj)), obj),
556
- export function cloneObject(obj) {
557
- return $.extend(true, Object.create(Object.getPrototypeOf(obj)), obj);
558
- };
559
-
560
- /**
561
- * Converts string values to number or boolean when needed
562
- * @param {object} obj - The object to be processed
563
- * @returns {object} - A new object with normalized content
564
- */
565
- export function normalizeObject(obj) {
566
- const result = {};
567
- if (obj)
568
- $.each(obj, (key, value) => {
569
- let s;
570
- if (typeof value === 'string' && (s = value.trim().toLowerCase()) !== '')
571
- value = s === 'true' ? true : s === 'false' ? false : isNaN(s) ? value : Number(s);
572
- result[key] = value;
573
- });
574
- return result;
575
- };
576
-
577
- /**
578
- * Returns an partial clone of an object, containing only the own attributes specified in an array of possible keys.
579
- * When the value of an attribute is of type 'Object' and this object has a method named `getAttributes`, the result of calling
580
- * this method is returned instead of the crude object.
581
- * @param {object} obj - The object to be processed
582
- * @param {string[]} [keys] - An optional array of keys to be included in the resulting object.
583
- * When null or not set, all keys of `obj` are included. Keys can include a default value separed by '|'.
584
- * Attributes with default value will be excluded from the resulting object.
585
- * @returns {object}
586
- */
587
- export function getAttr(obj, keys = null) {
588
- let result = {};
589
- keys = keys || Object.keys(obj);
590
- keys.forEach(key => {
591
- const [k, d] = key.split('|');
592
- if (obj.hasOwnProperty(k) && typeof obj[k] !== 'undefined' && obj[k] !== null && obj[k].toString() !== d) {
593
- const v = getValue(obj[k]);
594
- if (!isEmpty(v))
595
- result[k] = v;
596
- }
597
- });
598
-
599
- // Convert to string objects with only a "text" attribute
600
- keys = Object.keys(result);
601
- if (keys.length === 1 && keys[0] === 'text')
602
- result = result.text;
603
-
604
- return result;
605
- };
606
-
607
- /**
608
- * Gets the minimal representation of the given value (object, array, string, number...)
609
- * @param {any} value - The value to be processed
610
- * @returns {any}
611
- */
612
- export function getValue(value) {
613
- return value.getAttributes ?
614
- value.getAttributes() :
615
- value instanceof Array ?
616
- value.map(e => getValue(e)) :
617
- value instanceof Date ?
618
- value.toISOString() :
619
- value instanceof Object ?
620
- getAttr(value) :
621
- value;
622
- };
623
-
624
- /**
625
- * Checks if the given value is an empty object, null or a zero-length string
626
- * @param {any} v - The value to be checked
627
- * @returns {boolean} - `true` if `v` is `{}`, `null` or `""`
628
- */
629
- export function isEmpty(v) {
630
- let result = (typeof v === 'undefined' || v === null);
631
- if (!result) {
632
- switch (typeof v) {
633
- case 'object':
634
- result = Object.keys(v).length === 0;
635
- break;
636
-
637
- case 'string':
638
- result = v.length === 0;
639
- break;
640
- }
641
- }
642
- return result;
643
- };
644
-
645
- /**
646
- * Fills an object with specific attributes from another data object
647
- * @param {object} obj - The target object
648
- * @param {object} data - The data object
649
- * @param {string[]} attr - The list of attributes to be copied from `data` to `obj`
650
- * Elements of this list can be:
651
- * a) Just a string. In this case, the native object will be used as a value
652
- * b) An object with the following members:
653
- * - `key`{string} - The attribute name
654
- * - `fn` {function} - The function to be invoked to build the object
655
- * - `params` {string[]} - Optional params to be passed to the `setAttributes` method of the created object
656
- * - `group` {string} - Used when `data` is an object or an array (possible values are `object` and `array`), and multiple results
657
- * should be aggregated in a resulting object or array with the same keys (or ordering) as data.
658
- * - `init` {string} - Optional parameter indicating if `fn` should be passed with an additional param. This param can be:
659
- * - `key` - The member's key
660
- *
661
- * @returns {object} - Always returns `obj`
662
- */
663
- export function setAttr(obj, data, attr) {
664
- attr.forEach(a => {
665
- if (a.key) {
666
- const { key, fn, group, init, params } = a;
667
- // A new object should be built
668
- if (!isEmpty(data[key])) {
669
- const dataset = data[key];
670
- if (group === 'object')
671
- obj[key] = Object.keys(dataset).reduce((o, k) => {
672
- o[k] = buildObj(fn, dataset[k], init === 'key' ? k : init, params);
673
- return o;
674
- }, {});
675
- else if (group === 'array')
676
- obj[key] = dataset.map((element, n) => buildObj(fn, element, init === 'key' ? n : init, params));
677
- else
678
- obj[key] = buildObj(fn, dataset, init, params);
679
- }
680
- } else if (!isEmpty(data[a]))
681
- obj[a] = data[a];
682
- });
683
- return obj;
684
- };
685
-
686
- /**
687
- * Builds a new object based on the provided constructor, data and initialization value
688
- * Objects used with this function should implement `setAttributes`, or an static method named `factory`
689
- * @param {function} objType - A class or function to be invoked to build the object.
690
- * @param {object} [data] - An optional object filled with the attributes to be assigned to the newly created object.
691
- * @param {any} [init] - An optional value to be passed to the function when invoked with `new`
692
- * @param {object[]} [params=[]] - Optional array of params to be passed when calling `setAttributes` on the final object
693
- * @returns {object} - The resulting object
694
- */
695
- export function buildObj(objType, data, init, params = []) {
696
- return objType.factory ? objType.factory(data, init, params) : new objType(init).setAttributes(data, ...params);
697
- };
698
-
699
- /**
700
- * Check if the given char is a separator
701
- * @param {string} ch - A string with a single character
702
- * @returns {boolean}
703
- */
704
- export function isSeparator(ch) {
705
- return settings.SEPARATORS.includes(ch);
706
- };
707
-
708
- /**
709
- * Check if the given char is a word delimiter
710
- * @param {string} ch - A string with a single character
711
- * @returns {boolean}
712
- */
713
- export function isWordDelimiter(ch) {
714
- return settings.WORD_DELIMITERS.includes(ch);
715
- }
716
-
717
- /**
718
- * Converts a string in an array of objects with 'text' and 'sep' attributes, where 'text' are single words and 'sep'
719
- * are the word separators following each word in the sentence.
720
- * @example
721
- * stringToWords("Hello, World! That's all") returns:
722
- * [
723
- * {text: "Hello", sep: ", "},
724
- * {text: "World", sep: "! "},
725
- * {text: "That", sep: "'"},
726
- * {text: "s", sep: " "},
727
- * {text: "all", sep: ""},
728
- * ]
729
- * @param {*} str - The text to be tokenized
730
- * @returns {object[]}
731
- */
732
- export function stringToWords(str) {
733
- const result = [];
734
- let token = { text: '', sep: '' };
735
- let inWord = true;
736
- for (let i = 0; i < str.length; i++) {
737
- const ch = str.charAt(i);
738
- const delim = isWordDelimiter(ch);
739
- if (inWord) {
740
- if (!delim)
741
- token.text += ch;
742
- else {
743
- inWord = false;
744
- token.sep = ch;
745
- }
746
- } else {
747
- if (delim)
748
- token.sep += ch;
749
- else {
750
- result.push(token);
751
- token = { text: ch, sep: '' };
752
- inWord = true;
753
- }
754
- }
755
- }
756
- result.push(token);
757
- return result;
758
- }
759
-
760
- /**
761
- * Rounds `v` to the nearest multiple of `n`
762
- * @param {number} v
763
- * @param {number} n - Cannot be zero!
764
- * @returns {number}
765
- */
766
- export function roundTo(v, n) {
767
- return Math.round(v / n) * n;
768
- };
769
-
770
- /**
771
- * Set the maximum number of decimals for a number
772
- * @param {any} v - The value to be converted to a fixed number of decimals. Can be anything.
773
- * @param {number} n=4 - the maximum number of decimals
774
- * @returns {any} - When `v` is a number, a number with fixed decimals is returned. Otherwise, returns `v`
775
- */
776
- export function fx(v, n = 4) {
777
- return v.toFixed ? Number(v.toFixed(n)) : v;
778
- };
779
-
780
- /**
781
- * Compares the provided answer against multiple valid options. These valid options are
782
- * concatenated in a string, separated by pipe chars (`|`). The comparing can be case sensitive.
783
- * @param {string} answer - The text to check against to
784
- * @param {string} check - String containing one or multiple options, separated by `|`
785
- * @param {boolean} [checkCase=false] - When true, the comparing will be case-sensitive
786
- * @param {boolean} [numeric=false] - When true, we are comparing numeric expressions
787
- * @returns {boolean}
788
- */
789
- export function compareMultipleOptions(answer, check, checkCase = false, numeric = false) {
790
- if (answer === null || answer.length === 0 || check === null || check.length === 0)
791
- return false;
792
- if (!checkCase && !numeric)
793
- answer = answer.toUpperCase();
794
- answer = answer.trim();
795
-
796
- // Check for numeric digits in answer!
797
- numeric = numeric && /\d/.test(answer);
798
-
799
- for (let token of check.split('|')) {
800
- if (numeric) {
801
- if (Number.parseFloat(answer.replace(/,/, '.')) === Number.parseFloat(token.replace(/,/, '.')))
802
- return true;
803
- }
804
- else if (answer === (checkCase ? token : token.toUpperCase()).trim())
805
- return true;
806
- }
807
- return false;
808
- };
809
-
810
- /**
811
- * Checks if the given string ends with the specified expression
812
- * @param {string} text - The string where to find the expression
813
- * @param {string} expr - The expression to search for.
814
- * @param {boolean} [trim] - When `true`, the `text` string will be trimmed before check
815
- * @returns {boolean}
816
- */
817
- export function endsWith(text = '', expr, trim) {
818
- return typeof text === 'string' && (trim ? text.trim() : text).endsWith(expr);
819
- };
820
-
821
- /**
822
- * Checks if the given string starts with the specified expression
823
- * @param {string} text - The string where to find the expression
824
- * @param {string} expr - The expression to search for.
825
- * @param {boolean} [trim] - When `true`, the `text` string will be trimmed before check
826
- * @returns {boolean}
827
- */
828
- export function startsWith(text = '', expr, trim) {
829
- return typeof text === 'string' && (trim ? text.trim() : text).indexOf(expr) === 0;
830
- };
831
-
832
- /**
833
- * Replaces all occurrences of the backslash character (`\`) by a regular slash (`/`)
834
- * This is useful to normalize bad path names present in some old JClic projects
835
- * @param {string} str - The string to be normalized
836
- * @returns {string}
837
- */
838
- export function nSlash(str) {
839
- return str ? str.replace(/\\/g, '/') : str;
840
- };
841
-
842
- /**
843
- * Checks if the given expression is an absolute URL
844
- * @param {string} exp - The expression to be checked
845
- * @returns {boolean}
846
- */
847
- export function isURL(exp) {
848
- return /^(filesystem:)?(https?|file|data|ftps?):/i.test(exp);
849
- };
850
-
851
- /**
852
- * Gets the base path of the given file path (absolute or full URL). This base path always ends
853
- * with `/`, meaning it can be concatenated with relative paths without adding a separator.
854
- * @param {string} path - The full path to be parsed
855
- * @returns {string}
856
- */
857
- export function getBasePath(path) {
858
- const p = path.lastIndexOf('/');
859
- return p >= 0 ? path.substring(0, p + 1) : '';
860
- };
861
-
862
- /**
863
- * Gets the full path of `file` relative to `basePath`
864
- * @param {string} file - The file name
865
- * @param {string} [path] - The base path
866
- * @returns {string}
867
- */
868
- export function getRelativePath(file, path) {
869
- return (!path || path === '' || file.indexOf(path) !== 0) ? file : file.substring(path.length);
870
- };
871
-
872
- /**
873
- * Gets the complete path of a relative or absolute URL, using the provided `basePath`
874
- * @param {string} basePath - The base URL
875
- * @param {string} path - The filename
876
- * @returns {string}
877
- */
878
- export function getPath(basePath, path) {
879
- return isURL(path) ? path : basePath + path;
880
- };
881
-
882
- /**
883
- * Gets a promise with the complete path of a relative or absolute URL, using the provided `basePath`
884
- * @param {string} basePath - The base URL
885
- * @param {string} path - The filename
886
- * @param {external:JSZip} [zip] - An optional {@link external:JSZip} object where to look
887
- * for the file
888
- * @returns {external:Promise}
889
- */
890
- export function getPathPromise(basePath, path, zip) {
891
- if (zip) {
892
- const fName = getRelativePath(basePath + path, zip.zipBasePath);
893
- if (zip.files[fName]) {
894
- return new Promise((resolve, reject) => {
895
- zip.file(fName).async('base64').then(data => {
896
- const ext = path.toLowerCase().split('.').pop();
897
- const mime = settings.MIME_TYPES[ext] || 'application/octet-stream';
898
- resolve(`data:${mime};base64,${data}`);
899
- }).catch(reject);
900
- });
901
- }
902
- }
903
- return Promise.resolve(getPath(basePath, path));
904
- };
905
-
906
- /**
907
- * Utility object that provides several methods to build simple and complex DOM objects
908
- * @type {object}
909
- */
910
- export const $HTML = {
911
- doubleCell: (a, b) => $('<tr/>').append($('<td/>').html(a)).append($('<td/>').html(b)),
912
- p: txt => $('<p/>').html(txt),
913
- td: (txt, className) => $('<td/>', className ? { class: className } : null).html(txt),
914
- th: (txt, className) => $('<th/>', className ? { class: className } : null).html(txt),
915
- };
916
-
917
- /**
918
- * Replaces `width`, `height` and `fill` attributes of a simple SVG image
919
- * with the provided values
920
- * @param {string} svg - The SVG image as XML string
921
- * @param {string} [width] - Optional setting for "width" property
922
- * @param {string} [height] - Optional setting for "height" property
923
- * @param {string} [fill] - Optional setting for "fill" property
924
- * @returns {string} - The resulting svg code
925
- */
926
- export function getSvg(svg, width, height, fill) {
927
- if (width)
928
- svg = svg.replace(/width=\"\d*\"/, `width="${width}"`);
929
- if (height)
930
- svg = svg.replace(/height=\"\d*\"/, `height="${height}"`);
931
- if (fill)
932
- svg = svg.replace(/fill=\"[#A-Za-z0-9]*\"/, `fill="${fill}"`);
933
- return svg;
934
- };
935
-
936
- /**
937
- * Encodes a svg expression into a {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs|data URI}
938
- * suitable for the `src` property of `img` elements, optionally changing its original size and fill values.
939
- * @param {string} svg - The SVG image as XML string
940
- * @param {string} [width] - Optional setting for "width" property
941
- * @param {string} [height] - Optional setting for "height" property
942
- * @param {string} [fill] - Optional setting for "fill" property
943
- * @returns {string} - The resulting Data URI
944
- */
945
- export function svgToURI(svg, width, height, fill) {
946
- return 'data:image/svg+xml;base64,' + window.btoa(getSvg(svg, width, height, fill));
947
- };
948
-
949
- /**
950
- * Converts the given expression into a valid value for CSS size values
951
- * @param {string|number} exp - The expression to be evaluated. Can be a numeric value, `null` or `undefined`.
952
- * Positive values are in "px" units, negative ones are "%"
953
- * @param {object} css - An optional Object where the resulting expression (if any) will be saved
954
- * @param {string} key - The key under which the result will be stored in `css`
955
- * @param {string} def - Default value to be used when `exp` is `null` or `undefined`
956
- * @returns {string} - A valid CSS value, or `null` if it can't be found. Default units are `px`
957
- */
958
- export function toCssSize(exp, css, key, def) {
959
- const result = typeof exp === 'undefined' || exp === null ? null : isNaN(exp) ? exp : exp < 0 ? `${Math.abs(exp)}%` : `${exp}px`;
960
- if (css && key && (result || def))
961
- css[key] = result !== null ? result : def;
962
- return result;
963
- };
964
-
965
- /**
966
- * Gets a clip of the give image data, in a URL base64 encoded format
967
- * @param {object} img - The binary data of the realized image, usually obtained from a {@link module:bads/MediaBagElement.MediaBagElement}
968
- * @param {module:AWT.Rectangle} rect - A rectangle containing the requested clip
969
- * @returns {string} - The URL with the image clip, as a PNG file encoded in base64
970
- */
971
- export function getImgClipUrl(img, rect) {
972
- const canvas = document.createElement('canvas');
973
- canvas.width = rect.dim.width;
974
- canvas.height = rect.dim.height;
975
- const ctx = canvas.getContext('2d');
976
- let result = '';
977
- try {
978
- ctx.drawImage(img, rect.pos.x, rect.pos.y, rect.dim.width, rect.dim.height, 0, 0, rect.dim.width, rect.dim.height);
979
- result = canvas.toDataURL();
980
- } catch (err) {
981
- // catch 'tainted canvases may not be exported' and other errors
982
- log('error', err);
983
- }
984
- return result;
985
- };
986
-
987
- /**
988
- * Finds the nearest `head` or root node of a given HTMLElement, useful to place `<style/>` elements when
989
- * the main component of JClic is behind a shadow-root.
990
- * This method will be replaced by a call to [Node.getRootNode()](https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode)
991
- * when fully supported by all major browsers.
992
- * @param {external:HTMLElement} [el] - The element from which to start the search
993
- * @returns {external:HTMLElement}
994
- */
995
- export function getRootHead(el) {
996
- if (el) {
997
- // Skip HTMLElements
998
- while (el.parentElement)
999
- el = el.parentElement;
1000
- // Get the parent node of the last HTMLElement
1001
- if (el instanceof HTMLElement)
1002
- el = el.parentNode || el;
1003
- // If the root node has a `head`, take it
1004
- el = el['head'] || el;
1005
- }
1006
- return el || document.head;
1007
- };
1008
-
1009
- /**
1010
- * Appends a stylesheet element to the `head` or root node nearest to the given `HTMLElement`.
1011
- * @param {string} css - The content of the stylesheet
1012
- * @param {module:JClicPlayer.JClicPlayer} [ps] - An optional `PlayStation` (currently a {@link module:JClicPlayer.JClicPlayer JClicPlayer}) used as a base to find the root node
1013
- * @returns {external:HTMLStyleElement} - The appended style element
1014
- */
1015
- export function appendStyleAtHead(css, ps) {
1016
- const root = getRootHead(ps && ps.$topDiv ? ps.$topDiv[0] : null);
1017
- const style = document.createElement('style');
1018
- style.type = 'text/css';
1019
- style.appendChild(document.createTextNode(css));
1020
- return root.appendChild(style);
1021
- };
1022
-
1023
- /**
1024
- * Traverses all the attributes defined in an Element, calling a function with its name and value as a parameters
1025
- * @param {external:NamedNodeMap} attributes - The [Element.attributes](https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes)
1026
- * object to be traversed
1027
- * @param {function} callback - The function to be called for each [Attr](https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap)
1028
- * object. It should take two parametres: `name` and `value`
1029
- */
1030
- export function attrForEach(attributes, callback) {
1031
- for (let i = 0; i < attributes.length; i++)
1032
- callback(attributes[i].name, attributes[i].value);
1033
- };
1034
-
1035
- /**
1036
- * Recursive traversal of all nodes of the given object looking for children having the `childName` attribute
1037
- * WARNING: Don't call this method on objects with circular dependencies!
1038
- * @param {object} obj - The object to be analized
1039
- * @param {string} childName - Name of the attribute to search for
1040
- * @returns {object[]} - Array of children having the searched attribute
1041
- */
1042
- export function findParentsWithChild(obj, childName, _result = []) {
1043
- if (obj[childName])
1044
- _result.push(obj);
1045
- else
1046
- Object.values(obj).forEach(val => {
1047
- if (typeof val === 'object')
1048
- findParentsWithChild(val, childName, _result);
1049
- });
1050
- return _result;
1051
- };
1052
-
1053
- //
1054
- // Functions useful to deal with caret position in `contentEditable` DOM elements
1055
- //
1056
- /**
1057
- * Gets the caret position within the given element. Thanks to
1058
- * {@link http://stackoverflow.com/users/96100/tim-down|Tim Down} answers in:
1059
- * {@link http://stackoverflow.com/questions/4811822/get-a-ranges-start-and-end-offsets-relative-to-its-parent-container}
1060
- * and {@link http://stackoverflow.com/questions/6240139/highlight-text-range-using-javascript/6242538}
1061
- * @param {object} element - A DOM element
1062
- * @returns {number}
1063
- */
1064
- export function getCaretCharacterOffsetWithin(element) {
1065
- let caretOffset = 0;
1066
- const doc = element.ownerDocument || element.document;
1067
- const win = doc.defaultView || doc.parentWindow;
1068
- let sel;
1069
- if (typeof win.getSelection !== "undefined") {
1070
- sel = win.getSelection();
1071
- if (sel.rangeCount > 0) {
1072
- const range = win.getSelection().getRangeAt(0);
1073
- const preCaretRange = range.cloneRange();
1074
- preCaretRange.selectNodeContents(element);
1075
- preCaretRange.setEnd(range.endContainer, range.endOffset);
1076
- caretOffset = preCaretRange.toString().length;
1077
- }
1078
- } else if ((sel = doc.selection) && sel.type !== "Control") {
1079
- const textRange = sel.createRange();
1080
- const preCaretTextRange = doc.body.createTextRange();
1081
- preCaretTextRange.moveToElementText(element);
1082
- preCaretTextRange.setEndPoint("EndToEnd", textRange);
1083
- caretOffset = preCaretTextRange.text.length;
1084
- }
1085
- return caretOffset;
1086
- };
1087
-
1088
- /**
1089
- * Utility function called by {@link module:Utils.getCaretCharacterOffsetWithin}
1090
- * @param {object} node - A text node
1091
- * @returns {object[]}
1092
- */
1093
- export function getTextNodesIn(node) {
1094
- const textNodes = [];
1095
- if (node.nodeType === 3) {
1096
- textNodes.push(node);
1097
- } else {
1098
- const children = node.childNodes;
1099
- for (let i = 0, len = children.length; i < len; ++i) {
1100
- textNodes.push.apply(textNodes, getTextNodesIn(children[i]));
1101
- }
1102
- }
1103
- return textNodes;
1104
- };
1105
-
1106
- /**
1107
- * Sets the selection range (or the cursor position, when `start` and `end` are the same) to a
1108
- * specific position inside a DOM element.
1109
- * @param {object} el - The DOM element where to set the cursor
1110
- * @param {number} start - The start position of the selection (or cursor position)
1111
- * @param {number} end - The end position of the selection. When null or identical to `start`,
1112
- * indicates a cursor position.
1113
- */
1114
- export function setSelectionRange(el, start, end) {
1115
- if (isNullOrUndef(end))
1116
- end = start;
1117
- if (document.createRange && window.getSelection) {
1118
- const range = document.createRange();
1119
- range.selectNodeContents(el);
1120
- const textNodes = getTextNodesIn(el);
1121
- let foundStart = false;
1122
- let charCount = 0, endCharCount, textNode;
1123
-
1124
- for (let i = 0; i < textNodes.length; i++) {
1125
- textNode = textNodes[i];
1126
- endCharCount = charCount + textNode.length;
1127
- if (!foundStart && start >= charCount &&
1128
- (start < endCharCount ||
1129
- start === endCharCount && i + 1 <= textNodes.length)) {
1130
- range.setStart(textNode, start - charCount);
1131
- foundStart = true;
1132
- }
1133
- if (foundStart && end <= endCharCount) {
1134
- range.setEnd(textNode, end - charCount);
1135
- break;
1136
- }
1137
- charCount = endCharCount;
1138
- }
1139
- const sel = window.getSelection();
1140
- sel.removeAllRanges();
1141
- sel.addRange(range);
1142
- } else if (document.selection && document.body.createTextRange) {
1143
- const textRange = document.body.createTextRange();
1144
- textRange.moveToElementText(el);
1145
- textRange.collapse(true);
1146
- textRange.moveEnd('character', end);
1147
- textRange.moveStart('character', start);
1148
- textRange.select();
1149
- }
1150
- };
1151
-
1152
- /**
1153
- * Performs multiple replacements on the provided string
1154
- * See: https://stackoverflow.com/questions/2501435/replacing-multiple-patterns-in-a-block-of-data
1155
- * @param {Object[]} replacements - Array of pairs formed by an "expression" (regexp or string) and a "value" (string) to replace the fragments found
1156
- * @param {String} str - The string to be checked for replacements
1157
- * @returns {String} - The original string with the fragments found already replaced
1158
- */
1159
- export function mReplace(replacements, str) {
1160
- return replacements.reduce((result, [exp, replacement]) => result.replace(exp, replacement), str);
1161
- };
1162
-
1163
- /**
1164
- * Global constants
1165
- * @const
1166
- */
1167
- export const settings = {
1168
- // JClic.js Version
1169
- VERSION: GlobalData.version,
1170
- // Check if we are running on NodeJS with JSDOM
1171
- NODEJS: typeof window === 'undefined' || window?.navigator?.userAgent?.includes('jsdom'),
1172
- // layout constants
1173
- AB: 0, BA: 1, AUB: 2, BUA: 3,
1174
- LAYOUT_NAMES: ['AB', 'BA', 'AUB', 'BUA'],
1175
- DEFAULT_WIDTH: 400,
1176
- DEFAULT_HEIGHT: 300,
1177
- MINIMUM_WIDTH: 40,
1178
- MINIMUM_HEIGHT: 40,
1179
- DEFAULT_NAME: '---',
1180
- DEFAULT_MARGIN: 8,
1181
- DEFAULT_SHUFFLES: 31,
1182
- DEFAULT_GRID_ELEMENT_SIZE: 20,
1183
- MIN_CELL_SIZE: 10,
1184
- //DEFAULT_BG_COLOR: '#D3D3D3', // LightGray
1185
- DEFAULT_BG_COLOR: '#C0C0C0', // LightGray
1186
- ACTIONS: {
1187
- ACTION_MATCH: 'MATCH', ACTION_PLACE: 'PLACE',
1188
- ACTION_WRITE: 'WRITE', ACTION_SELECT: 'SELECT', ACTION_HELP: 'HELP'
1189
- },
1190
- PREVIOUS: 0, MAIN: 1, END: 2, END_ERROR: 3, NUM_MSG: 4,
1191
- MSG_TYPE: ['previous', 'initial', 'final', 'finalError'],
1192
- RANDOM_CHARS: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
1193
- NUM_COUNTERS: 3,
1194
- MAX_RECORD_LENGTH: 180,
1195
- // BoxBase defaults
1196
- BoxBase: {
1197
- REDUCE_FONT_STEP: 1.0,
1198
- MIN_FONT_SIZE: 8,
1199
- STROKE: 1,
1200
- AC_MARGIN: 6,
1201
- //BACK_COLOR: 'lightgray',
1202
- BACK_COLOR: '#C0C0C0',
1203
- TEXT_COLOR: 'black',
1204
- SHADOW_COLOR: 'gray',
1205
- INACTIVE_COLOR: 'gray',
1206
- ALTERNATIVE_COLOR: 'gray',
1207
- BORDER_COLOR: 'black',
1208
- BORDER_STROKE_WIDTH: 0.75,
1209
- MARKER_STROKE_WIDTH: 2.75
1210
- },
1211
- FILE_TYPES: {
1212
- image: 'gif,jpg,png,jpeg,bmp,ico,svg',
1213
- audio: 'wav,mp3,ogg,oga,au,aiff,flac',
1214
- video: 'avi,mov,mpeg,mp4,ogv,m4v,webm',
1215
- font: 'ttf,otf,eot,woff,woff2',
1216
- midi: 'mid,midi',
1217
- anim: 'swf',
1218
- // Used in custom skins
1219
- xml: 'xml'
1220
- },
1221
- MIME_TYPES: {
1222
- xml: 'text/xml',
1223
- gif: 'image/gif',
1224
- jpg: 'image/jpeg',
1225
- jpeg: 'image/jpeg',
1226
- png: 'image/png',
1227
- bmp: 'image/bmp',
1228
- svg: 'image/svg+xml',
1229
- ico: 'image/x-icon',
1230
- wav: 'audio/wav',
1231
- mp3: 'audio/mpeg',
1232
- mp4: 'video/mp4',
1233
- m4v: 'video/mp4',
1234
- ogg: 'audio/ogg',
1235
- oga: 'audio/ogg',
1236
- ogv: 'video/ogg',
1237
- webm: 'video/webm',
1238
- au: 'audio/basic',
1239
- aiff: 'audio/x-aiff',
1240
- flac: 'audio/flac',
1241
- avi: 'video/avi',
1242
- mov: 'video/quicktime',
1243
- mpeg: 'video/mpeg',
1244
- ttf: 'application/font-sfnt',
1245
- otf: 'application/font-sfnt',
1246
- eot: ' application/vnd.ms-fontobject',
1247
- woff: 'application/font-woff',
1248
- woff2: 'application/font-woff2',
1249
- swf: 'application/x-shockwave-flash',
1250
- mid: 'audio/midi',
1251
- midi: 'audio/midi'
1252
- },
1253
- // Global settings susceptible to be modified
1254
- COMPRESS_IMAGES: true,
1255
- // Keyboard key codes
1256
- VK: {
1257
- LEFT: 37,
1258
- UP: 38,
1259
- RIGHT: 39,
1260
- DOWN: 40
1261
- },
1262
- // Flag to indicate that we are running on a touch device
1263
- TOUCH_DEVICE: false,
1264
- // Amount of time (in milliseconds) to wait before a media resource is loaded
1265
- LOAD_TIMEOUT: 10000,
1266
- // Number of points to be calculated as polygon vertexs when simplifying bezier curves
1267
- BEZIER_POINTS: 4,
1268
- // Check if canvas accessibility features are enabled
1269
- // See: http://codepen.io/francesc/pen/amwvRp
1270
- // UPDATED May 2020: Detection removed since Canvas HitRegions have been deprecated
1271
- // See: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Hit_regions_and_accessibility
1272
- //
1273
- // CANVAS_HITREGIONS: typeof CanvasRenderingContext2D !== 'undefined' && typeof CanvasRenderingContext2D.prototype.addHitRegion === 'function',
1274
- // CANVAS_HITREGIONS_FOCUS: typeof CanvasRenderingContext2D !== 'undefined' && typeof CanvasRenderingContext2D.prototype.drawFocusIfNeeded === 'function',
1275
- //
1276
- CANVAS_DRAW_FOCUS: typeof window !== 'undefined' && typeof window?.CanvasRenderingContext2D?.prototype?.drawFocusIfNeeded === 'function',
1277
- // See: https://emptycharacter.com/
1278
- // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
1279
- WHITESPACES: ' \f\n\r\t\v\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202f\u205f\u3000\ufeff',
1280
- };
1281
- settings.SEPARATORS = `${settings.WHITESPACES}.,;-|`;
1282
- settings.WORD_DELIMITERS = `${settings.SEPARATORS}…_<>"“”«»'\xB4\x60\u2018\u2019\u2022~+\u2013\u2014\u2015=%¿?¡!:/\\()[]{}$£€`;
1283
-
1284
- /**
1285
- * Miscellaneous utility functions and constants
1286
- */
1287
- export const Utils = {
1288
- pkg,
1289
- settings,
1290
- getMsg,
1291
- LOG_LEVELS,
1292
- LOG_PRINT_LABELS,
1293
- LOG_OPTIONS,
1294
- init,
1295
- setLogLevel,
1296
- log,
1297
- getBoolean,
1298
- getVal,
1299
- getNumber,
1300
- getPercent,
1301
- zp,
1302
- getHMStime,
1303
- getDateTime,
1304
- parseOldDate,
1305
- cleanOldLanguageTag,
1306
- FALSE,
1307
- TRUE,
1308
- DEFAULT,
1309
- getTriState,
1310
- fillString,
1311
- isNullOrUndef,
1312
- isEquivalent,
1313
- getXmlText,
1314
- parseXmlNode,
1315
- getXmlNodeText,
1316
- reduceTextsToStrings,
1317
- cssToString,
1318
- checkColor,
1319
- colorHasTransparency,
1320
- cloneObject,
1321
- normalizeObject,
1322
- getAttr,
1323
- getValue,
1324
- isEmpty,
1325
- setAttr,
1326
- buildObj,
1327
- isSeparator,
1328
- isWordDelimiter,
1329
- stringToWords,
1330
- roundTo,
1331
- fx,
1332
- compareMultipleOptions,
1333
- endsWith,
1334
- startsWith,
1335
- nSlash,
1336
- isURL,
1337
- getBasePath,
1338
- getRelativePath,
1339
- getPath,
1340
- getPathPromise,
1341
- $HTML,
1342
- getSvg,
1343
- svgToURI,
1344
- toCssSize,
1345
- getImgClipUrl,
1346
- getRootHead,
1347
- appendStyleAtHead,
1348
- attrForEach,
1349
- findParentsWithChild,
1350
- getCaretCharacterOffsetWithin,
1351
- getTextNodesIn,
1352
- setSelectionRange
1353
- };
1354
-
1355
- export default Utils;