@iebh/tera-fy 1.0.17 → 1.0.18

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.
@@ -17,16 +17,19 @@ export default class TeraFy {
17
17
  *
18
18
  * @type {Object}
19
19
  * @property {Boolean} devMode Operate in devMode - i.e. force outer refresh when encountering an existing TeraFy instance
20
- * @property {'detect'|'parent'|'child'} How to communicate with TERA. 'parent' assumes that the parent of the current document is TERA, 'child' spawns an iFrame and uses TERA there, 'detect' tries parent and fallsback to 'child'
20
+ * @property {'detect'|'parent'|'child'|'popup'} mode How to communicate with TERA. 'parent' assumes that the parent of the current document is TERA, 'child' spawns an iFrame and uses TERA there, 'detect' tries parent and switches to `modeFallback` if communication fails
21
+ * @property {String} modeFallback Method to use when all method detection fails
21
22
  * @property {Number} modeTimeout How long entities have in 'detect' mode to identify themselves
22
23
  * @property {String} siteUrl The TERA URL to connect to
23
24
  * @property {String} restrictOrigin URL to restrict communications to
24
25
  * @property {Array<String>} List of sandbox allowables for the embedded if in embed mode
26
+ * @property {Number} handshakeInterval Interval in milliseconds when sanning for a handshake
25
27
  */
26
28
  settings = {
27
29
  devMode: true,
28
30
  mode: 'detect',
29
31
  modeTimeout: 300,
32
+ modeFallback: 'child', // ENUM: 'child' (use iframes), 'popup' (use popup windows)
30
33
  siteUrl: 'https://tera-tools.com/embed',
31
34
  restrictOrigin: '*', // FIXME: Need to restrict this to TERA site
32
35
  frameSandbox: [
@@ -39,8 +42,9 @@ export default class TeraFy {
39
42
  'allow-presentation',
40
43
  'allow-same-origin',
41
44
  'allow-scripts',
42
- 'allow-top-navigation'
45
+ 'allow-top-navigation',
43
46
  ],
47
+ handshakeInterval: 1000,
44
48
  };
45
49
 
46
50
 
@@ -56,12 +60,14 @@ export default class TeraFy {
56
60
  *
57
61
  * @type {Object}
58
62
  * @property {DOMElement} el The main tera-fy div wrapper
59
- * @property {DOMElement} iframe The internal iFrame element
63
+ * @property {DOMElement} iframe The internal iFrame element (if `settings.mode == 'child'`)
64
+ * @property {Window} popup The popup window context (if `settings.mode == 'popup'`)
60
65
  * @property {DOMElement} stylesheet The corresponding stylesheet
61
66
  */
62
67
  dom = {
63
68
  el: null,
64
69
  iframe: null,
70
+ popup: null,
65
71
  stylesheet: null,
66
72
  };
67
73
 
@@ -93,7 +99,7 @@ export default class TeraFy {
93
99
  'selectProjectLibrary', 'parseProjectLibrary', 'setProjectLibrary',
94
100
 
95
101
  // UI
96
- 'uiAlert',
102
+ 'uiAlert', 'uiSplat', 'uiWindow',
97
103
  ];
98
104
 
99
105
 
@@ -149,6 +155,8 @@ export default class TeraFy {
149
155
  window.parent.postMessage(payload, this.settings.restrictOrigin);
150
156
  } else if (this.settings.mode == 'child') {
151
157
  this.dom.iframe.contentWindow.postMessage(payload, this.settings.restrictOrigin);
158
+ } else if (this.settings.mode == 'popup') {
159
+ this.dom.popup.postMessage(payload, this.settings.restrictOrigin);
152
160
  } else if (this.settings.mode == 'detect') {
153
161
  throw new Error('Call init() or detectMode() before trying to send data to determine the mode');
154
162
  } else {
@@ -305,7 +313,8 @@ export default class TeraFy {
305
313
  ]))
306
314
  .then(()=> this.rpc('setServerMode', // Tell server what mode its in
307
315
  this.settings.mode == 'child' ? 'embedded'
308
- : this.settings.mode == 'parent' ? 'window'
316
+ : this.settings.mode == 'parent' ? 'frame'
317
+ : this.settings.mode == 'popup' ? 'popup'
309
318
  : (()=> { throw(`Unknown server mode "${this.settings.mode}"`) })()
310
319
  ))
311
320
  .then(()=> Promise.all( // Init all plugins (with this outer module as the context)
@@ -327,7 +336,7 @@ export default class TeraFy {
327
336
  if (this.settings.mode != 'detect') { // Dev has specified a forced mode to use
328
337
  return this.settings.mode;
329
338
  } else if (window.self === window.parent) { // This frame is already at the top
330
- return 'child';
339
+ return this.settings.modeFallback;
331
340
  } else { // No idea - try messaging
332
341
  return Promise.resolve()
333
342
  .then(()=> this.settings.mode = 'parent') // Switch to parent mode...
@@ -339,7 +348,7 @@ export default class TeraFy {
339
348
  .then(()=> resolve())
340
349
  }))
341
350
  .then(()=> 'parent')
342
- .catch(()=> 'child')
351
+ .catch(()=> this.settings.modeFallback)
343
352
  }
344
353
  }
345
354
 
@@ -373,9 +382,34 @@ export default class TeraFy {
373
382
  this.dom.el.append(this.dom.iframe);
374
383
  break;
375
384
  case 'parent':
376
- this.debug('Using TERA site stack parent');
385
+ this.debug('Using TERA window parent');
377
386
  resolve();
378
387
  break;
388
+ case 'popup':
389
+ this.debug('Injecting TERA site as a popup window');
390
+ this.dom.popup = window.open(this.settings.siteUrl, '_blank', 'popup=1, location=0, menubar=0, status=0, scrollbars=0, width=500, height=600');
391
+
392
+ // Loop until the window context returns a handshake
393
+ (()=> {
394
+ let handshakeCount = 0;
395
+ let handshakeTimer;
396
+
397
+ const tryHandshake = ()=> {
398
+ this.debug('Trying handshake', ++handshakeCount);
399
+
400
+ clearTimeout(handshakeTimer);
401
+ handshakeTimer = setTimeout(tryHandshake, this.settings.handshakeInterval);
402
+
403
+ this.rpc('handshake')
404
+ .then(()=> {
405
+ clearTimeout(handshakeTimer);
406
+ this.debug('Popup window accepted handshake');
407
+ resolve();
408
+ });
409
+ };
410
+ tryHandshake();
411
+ })();
412
+ break;
379
413
  default:
380
414
  throw new Error(`Unsupported mode "${this.settings.mode}" when calling injectComms()`);
381
415
  }
@@ -434,6 +468,7 @@ export default class TeraFy {
434
468
  document.head.appendChild(this.dom.stylesheet);
435
469
  break;
436
470
  case 'parent':
471
+ case 'popup':
437
472
  break;
438
473
  default:
439
474
  throw new Error(`Unsupported mode "${this.settings.mode}" when injectStylesheet()`);
@@ -624,7 +659,11 @@ export default class TeraFy {
624
659
  * This is an pre-requisite step for requireProject()
625
660
  *
626
661
  * @function requireUser
627
- * @returns {Promise} A promise which will resolve if the there is a user and they are logged in
662
+ *
663
+ * @param {Object} [options] Additional options to mutate behaviour
664
+ * @param {Boolean} [options.forceRetry=false] Forcabily try to refresh the user state
665
+ *
666
+ * @returns {Promise<User>} The current logged in user or null if none
628
667
  */
629
668
 
630
669
 
@@ -920,5 +959,33 @@ export default class TeraFy {
920
959
  * @returns {Promise} A promise which resolves when the alert has been dismissed
921
960
  */
922
961
 
962
+
963
+ /**
964
+ * Display HTML content full-screen within TERA
965
+ * This function is ideally called within a requestFocus() wrapper
966
+ *
967
+ * @function uiSplat
968
+ * @param {DOMElement|String|false} content Either a prepared DOM element or string to compile, set to falsy to remove existing content
969
+ *
970
+ * @param {Object} [options] Additional options to mutate behaviour
971
+ * @param {Boolean|String} [options.logo=false] Add a logo to the output, if boolean true the Tera-tools logo is used otherwise specify a path or URL
972
+ */
973
+
974
+
975
+ /**
976
+ * Open a popup window containing a new site
977
+ *
978
+ * @function uiWindow
979
+ * @param {String} url The URL to open
980
+ *
981
+ * @param {Object} [options] Additional options to mutate behaviour
982
+ * @param {Number} [options.width=500] The desired width of the window
983
+ * @param {Number} [options.height=600] The desired height of the window
984
+ * @param {Boolean} [options.center=true] Attempt to center the window on the screen
985
+ * @param {Object} [options.permissions] Additional permissions to set on opening, defaults to a suitable set of permission for popups (see code)
986
+ *
987
+ * @returns {WindowProxy} The opened window object (if `noopener` is not set in permissions)
988
+ */
989
+
923
990
  // }}}
924
991
  }
@@ -1,7 +1,8 @@
1
- import {cloneDeep, has as pathExists, set as pathSet} from 'lodash-es';
1
+ import {cloneDeep, has as pathExists, get as pathGet, set as pathSet} from 'lodash-es';
2
2
  import {diffApply, jsonPatchPathConverter as jsPatchConverter} from 'just-diff-apply';
3
- import {nanoid} from 'nanoid';
4
3
  import mixin from '#utils/mixin';
4
+ import {nanoid} from 'nanoid';
5
+ import promiseDefer from '#utils/pDefer';
5
6
  import Reflib from '@iebh/reflib';
6
7
 
7
8
  /**
@@ -21,7 +22,9 @@ export default class TeraFyServer {
21
22
  * @property {Number} subscribeTimeout Acceptable timeout period for subscribers to acklowledge a project change event, failing to respond will result in the subscriber being removed from the available subscriber list
22
23
  * @property {String} restrictOrigin URL to restrict communications to
23
24
  * @property {String} projectId The project to use as the default reference when calling various APIs
24
- * @property {Number} The current server mode matching `SERVERMODE_*`
25
+ * @property {Number} serverMode The current server mode matching `SERVERMODE_*`
26
+ * @property {String} siteUrl The main site absolute URL
27
+ * @property {String} sitePathLogin Either an absolute URL or the relative path (taken from `siteUrl`) when trying to log in the user
25
28
  */
26
29
  settings = {
27
30
  devMode: false,
@@ -29,12 +32,15 @@ export default class TeraFyServer {
29
32
  subscribeTimeout: 2000,
30
33
  projectId: null,
31
34
  serverMode: 0,
35
+ siteUrl: window.location.href,
36
+ sitePathLogin: '/login',
32
37
  };
33
38
 
34
39
  static SERVERMODE_NONE = 0;
35
40
  static SERVERMODE_EMBEDDED = 1;
36
- static SERVERMODE_WINDOW = 2;
37
- static SERVERMODE_TERA = 3; // Terafy is running as the main TERA site
41
+ static SERVERMODE_FRAME = 2;
42
+ static SERVERMODE_POPUP = 3;
43
+ static SERVERMODE_TERA = 4; // Terafy is running as the main TERA site
38
44
 
39
45
 
40
46
  // Contexts - createContext(), getClientContext(), messageEvent, senderRpc() {{{
@@ -94,7 +100,7 @@ export default class TeraFyServer {
94
100
  }
95
101
  },
96
102
  });
97
- case TeraFyServer.SERVERMODE_WINDOW:
103
+ case TeraFyServer.SERVERMODE_FRAME:
98
104
  // Server is the top-level window so we need to send messages to an embedded iFrame
99
105
  var iFrame = document.querySelector('iframe#external');
100
106
  if (!iFrame) throw new Error('Cannot locate TERA-FY top-level->iFrame#external');
@@ -114,6 +120,7 @@ export default class TeraFyServer {
114
120
  }
115
121
  },
116
122
  });
123
+ case TeraFyServer.SERVERMODE_POPUP:
117
124
  }
118
125
  }
119
126
 
@@ -202,10 +209,10 @@ export default class TeraFyServer {
202
209
  TERA: 1,
203
210
  ...cloneDeep(message), // Need to clone to resolve promise nasties
204
211
  };
205
- this.debug('INFO', 'Parent send', message, '<=>', payload);
212
+ this.debug('INFO', 'Dispatch response', message, '<=>', payload);
206
213
  (sendVia || globalThis.parent).postMessage(payload, this.settings.restrictOrigin);
207
214
  } catch (e) {
208
- this.debug('ERROR', 'Attempted to dispatch payload server->client', payload);
215
+ this.debug('ERROR', 'Attempted to dispatch response server->client', payload);
209
216
  this.debug('ERROR', 'Message compose server->client:', e);
210
217
  }
211
218
  }
@@ -221,8 +228,11 @@ export default class TeraFyServer {
221
228
  case 'embedded':
222
229
  this.settings.serverMode = TeraFyServer.SERVERMODE_EMBEDDED;
223
230
  break;
224
- case 'window':
225
- this.settings.serverMode = TeraFyServer.SERVERMODE_WINDOW;
231
+ case 'frame':
232
+ this.settings.serverMode = TeraFyServer.SERVERMODE_FRAME;
233
+ break;
234
+ case 'popup':
235
+ this.settings.serverMode = TeraFyServer.SERVERMODE_POPUP;
226
236
  break;
227
237
  default:
228
238
  throw new Error(`Unsupported server mode "${mode}"`);
@@ -330,9 +340,17 @@ export default class TeraFyServer {
330
340
  /**
331
341
  * Fetch the current session user
332
342
  *
343
+ * @param {Object} [options] Additional options to mutate behaviour
344
+ * @param {Boolean} [options.forceRetry=false] Forcabily try to refresh the user state
345
+ *
333
346
  * @returns {Promise<User>} The current logged in user or null if none
334
347
  */
335
- getUser() {
348
+ getUser(options) {
349
+ let settings = {
350
+ forceRetry: false,
351
+ ...options,
352
+ };
353
+
336
354
  let $auth = app.service('$auth');
337
355
  let $subscriptions = app.service('$subscriptions');
338
356
 
@@ -340,7 +358,11 @@ export default class TeraFyServer {
340
358
  $auth.promise(),
341
359
  $subscriptions.promise(),
342
360
  ])
343
- .then(()=> $auth.user.id ? {
361
+ .then(()=> {
362
+ if (!$auth.isLoggedIn && settings.forceRetry)
363
+ return $auth.restoreLogin();
364
+ })
365
+ .then(()=> $auth.user?.id ? {
344
366
  id: $auth.user.id,
345
367
  email: $auth.user.email,
346
368
  name: [
@@ -349,6 +371,10 @@ export default class TeraFyServer {
349
371
  ].filter(Boolean).join(' '),
350
372
  isSubscribed: $subscriptions.isSubscribed,
351
373
  } : null)
374
+ .catch(e => {
375
+ console.warn('Catch', e);
376
+ debugger;
377
+ })
352
378
  }
353
379
 
354
380
 
@@ -361,10 +387,91 @@ export default class TeraFyServer {
361
387
  */
362
388
  requireUser() {
363
389
  return this.getUser()
364
- .then(user => user || this.uiAlert('You must be logged in to <a href="https://tera-tools.com" target="_blank">TERA-tools.com</a> to use this tool', {
390
+ .then(user => {
391
+ this.debug('Current user is', user ? user : 'Not valid');
392
+ if (user) throw 'EXIT'; // Valid user? Escape promise chain
393
+ })
394
+ .then(async ()=> { // No user present - try to validate with other methods
395
+ switch (this.settings.serverMode) {
396
+ case TeraFyServer.SERVERMODE_EMBEDDED:
397
+ /* - Doesn't work because Kinde sets the CSP header `frame-ancestors 'self'` which prevents usage within an iFrame
398
+ const $auth = app.service('$auth');
399
+ return this.requestFocus(()=> $auth.login()
400
+ .then(()=> {
401
+ console.log('New user state', $auth.isLoggedIn);
402
+ })
403
+ );
404
+ */
405
+
406
+ var focusContent = document.createElement('div');
407
+ focusContent.innerHTML = '<div>Authenticate with <a href="https://tera-tools.com" target="_blank">TERA-tools.com</a></div>'
408
+ + '<div class="mt-2"><a class="btn btn-light">Open Popup...</a></div>';
409
+
410
+ // Attach click listner to internal button to re-popup the auth window (in case popups are blocked)
411
+ focusContent.querySelector('a.btn').addEventListener('click', ()=>
412
+ this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl))
413
+ );
414
+
415
+ // Create a deferred promise which will (eventually) resolve when the downstream window signals its ready
416
+ let waitOnWindowAuth = promiseDefer();
417
+
418
+ // Create a listener for the message from the downstream window to resolve the promise
419
+ let listenMessages = ({data}) => {
420
+ if (data.TERA && data.action == 'noop' && data.isLoggedIn) { // Signal sent from landing page - we're logged in, yey!
421
+ this.debug('Received ok message from popup window');
422
+ waitOnWindowAuth.resolve();
423
+ }
424
+ };
425
+ window.addEventListener('message', listenMessages);
426
+
427
+ // Go fullscreen, try to open the auth window + prompt the user to retry (if popups are blocked) and wait for resolution
428
+ await this.requestFocus(()=> {
429
+ // Try opening the popup automatically - this will likely fail if the user has popup blocking enabled
430
+ this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl));
431
+
432
+ // Display a message to the user, offering the ability to re-open the popup if it was originally denied
433
+ this.uiSplat(focusContent, {logo: true});
434
+
435
+ this.debug('Begin auth-check deferred wait...');
436
+ return waitOnWindowAuth.promise;
437
+ });
438
+
439
+ this.debug('Cleaning up popup auth');
440
+
441
+ // Remove message subscription
442
+ window.removeEventListener('message', listenMessages);
443
+
444
+ // Disable overlay content
445
+ this.uiSplat(false);
446
+
447
+ // Tell $auth to forcibly refresh its user data
448
+ await app.service('$auth').restoreLogin();
449
+
450
+ // ... then refresh the project list as we're likely going to need it
451
+ await app.service('$projects').refresh();
452
+
453
+ // Go back to start of auth checking loop and repull the user data
454
+ throw 'REDO';
455
+
456
+ break;
457
+ default:
458
+ // Pass - Implied - Cannot authenticate via other method so just fall through to scalding the user
459
+ }
460
+ })
461
+ .then(()=> this.uiAlert('You must be logged in to <a href="https://tera-tools.com" target="_blank">TERA-tools.com</a> to use this tool', {
365
462
  title: 'TERA-tools account needed',
366
463
  isHtml: true,
464
+ buttons: false,
367
465
  }))
466
+ .then(()=> { throw 'REDO' }) // Go into loop to keep requesting user data
467
+ .catch(e => {
468
+ if (e === 'EXIT') {
469
+ return; // Exit with a valid user
470
+ } else if (e == 'REDO') {
471
+ return this.requireUser();
472
+ }
473
+ throw e;
474
+ })
368
475
  }
369
476
 
370
477
  // }}}
@@ -576,16 +683,61 @@ export default class TeraFyServer {
576
683
  if (!app.service('$projects').active) throw new Error('No active project');
577
684
  if (typeof path != 'string' && !Array.isArray(path)) throw new Error('setProjectStateDefaults(path, value) - path must be a dotted string or array of path segments');
578
685
 
579
- pathSet(app.service('$projects').active, path, value)
686
+ this._pathSet(app.service('$projects').active, path, value);
580
687
 
581
688
  return (
582
- this.save && this.sync ? this.saveProjectState()
583
- : this.save ? void this.saveProjectState()
689
+ settings.save && settings.sync ? this.saveProjectState()
690
+ : settings.save ? void this.saveProjectState()
584
691
  : (()=> { throw new Error('setProjectState({sync: true, save: false}) makes no sense') })()
585
692
  );
586
693
  }
587
694
 
588
695
 
696
+ /**
697
+ * Internal recursive path setter used by setProjectState() / setProjectStateDefaults()
698
+ * The implementation defaults to _.set() unless overriden by a plugin
699
+ *
700
+ * @private
701
+ * @param {Object} subject The base subject to operate on
702
+ * @param {String|Array} path The path to set in dotted or array notation
703
+ * @param {*} value The value to set
704
+ *
705
+ * @returns {*} The set value
706
+ */
707
+ _pathSet(subject, path, value) {
708
+ return pathSet(subject, path, value);
709
+ }
710
+
711
+
712
+ /**
713
+ * Internal recursive path checker used by setProjectStateDefaults()
714
+ * The implementation defaults to _.has() unless overriden by a plugin
715
+ *
716
+ * @private
717
+ * @param {Object} subject The base subject to examine
718
+ * @param {String|Array} path The path to fetch in dotted or array notation
719
+ * @returns {Boolean} True if the given path already exists within the subject
720
+ */
721
+ _pathHas(subject, path) {
722
+ return pathExists(subject, path);
723
+ }
724
+
725
+
726
+ /**
727
+ * Internal recursive path fetcher
728
+ * The implementation defaults to _.get() unless overriden by a plugin
729
+ *
730
+ * @private
731
+ * @param {Object} subject The base subject to examine
732
+ * @param {String|Array} path The path to fetch in dotted or array notation
733
+ * @param {*} [fallback] Optional fallback to return if the end point does not exist
734
+ * @returns {*} True if the given path already exists within the subject
735
+ */
736
+ _pathGet(subject, path, fallback) {
737
+ return pathGet(subject, path, fallback);
738
+ }
739
+
740
+
589
741
  /**
590
742
  * Set a nested value within the project state - just like `setProjectState()` - but only if no value for that path exists
591
743
  *
@@ -599,7 +751,7 @@ export default class TeraFyServer {
599
751
  setProjectStateDefaults(path, value, options) {
600
752
  if (!app.service('$projects').active) throw new Error('No active project');
601
753
 
602
- if (!pathExists(app.service('$projects').active, path)) {
754
+ if (!this._pathHas(app.service('$projects').active, path)) {
603
755
  return this.setProjectState(path, value, options)
604
756
  .then(()=> true)
605
757
  } else {
@@ -944,6 +1096,7 @@ export default class TeraFyServer {
944
1096
  * @param {Object} [options] Additional options to mutate behaviour
945
1097
  * @param {String} [options.title='TERA'] The title of the alert box
946
1098
  * @param {Boolean} [options.isHtml=false] If falsy the text is rendered as plain-text otherwise it will be assumed as HTML content
1099
+ * @param {'ok'|false} [options.buttons='ok'] Button set to use or falsy to disable
947
1100
  *
948
1101
  * @returns {Promise} A promise which resolves when the alert has been dismissed
949
1102
  */
@@ -951,6 +1104,7 @@ export default class TeraFyServer {
951
1104
  let settings = {
952
1105
  title: 'TERA',
953
1106
  isHtml: false,
1107
+ buttons: 'ok',
954
1108
  ...options,
955
1109
  };
956
1110
 
@@ -958,12 +1112,100 @@ export default class TeraFyServer {
958
1112
  app.service('$prompt').dialog({
959
1113
  title: settings.title,
960
1114
  body: text,
961
- buttons: ['ok'],
1115
+ buttons:
1116
+ settings.buttons == 'ok' ? ['ok']
1117
+ : false,
962
1118
  isHtml: settings.isHtml,
963
1119
  dialogClose: 'resolve',
964
1120
  })
965
1121
  );
966
1122
  }
1123
+
1124
+
1125
+ /**
1126
+ * Open a popup window containing a new site
1127
+ *
1128
+ * @param {String} url The URL to open
1129
+ *
1130
+ * @param {Object} [options] Additional options to mutate behaviour
1131
+ * @param {Number} [options.width=500] The desired width of the window
1132
+ * @param {Number} [options.height=600] The desired height of the window
1133
+ * @param {Boolean} [options.center=true] Attempt to center the window on the screen
1134
+ * @param {Object} [options.permissions] Additional permissions to set on opening, defaults to a suitable set of permission for popups (see code)
1135
+ *
1136
+ * @returns {WindowProxy} The opened window object (if `noopener` is not set in permissions)
1137
+ */
1138
+ uiWindow(url, options) {
1139
+ let settings = {
1140
+ width: 500,
1141
+ height: 600,
1142
+ center: true,
1143
+ permissions: {
1144
+ popup: true,
1145
+ location: false,
1146
+ menubar: false,
1147
+ status: false,
1148
+ scrolbars: false,
1149
+ },
1150
+ ...options,
1151
+ };
1152
+
1153
+ return window.open(url, '_blank', Object.entries({
1154
+ ...settings.permissions,
1155
+ width: settings.width,
1156
+ height: settings.height,
1157
+ ...(settings.center && {
1158
+ left: screen.width/2 - settings.width/2,
1159
+ top: screen.height/2 - settings.height/2,
1160
+ }),
1161
+ })
1162
+ .map(([key, val]) => key + '=' + (
1163
+ typeof val == 'boolean' ? val ? '1' : '0' // Map booleans to 1/0
1164
+ : val
1165
+ ))
1166
+ .join(', ')
1167
+ );
1168
+ }
1169
+
1170
+
1171
+ /**
1172
+ * Display HTML content full-screen within TERA
1173
+ * This function is ideally called within a requestFocus() wrapper
1174
+ *
1175
+ * @param {DOMElement|String|false} content Either a prepared DOM element or string to compile, set to falsy to remove existing content
1176
+ *
1177
+ * @param {Object} [options] Additional options to mutate behaviour
1178
+ * @param {Boolean|String} [options.logo=false] Add a logo to the output, if boolean true the Tera-tools logo is used otherwise specify a path or URL
1179
+ */
1180
+ uiSplat(content, options) {
1181
+ let settings = {
1182
+ logo: false,
1183
+ ...options,
1184
+ };
1185
+
1186
+ if (!content) { // Remove content
1187
+ globalThis.document.body.querySelector('.tera-fy-uiSplat').remove();
1188
+ return;
1189
+ }
1190
+
1191
+ let compiledContent = typeof content == 'string'
1192
+ ? (()=> {
1193
+ let el = document.createElement('div')
1194
+ el.innerHTML = content;
1195
+ return el;
1196
+ })()
1197
+ : content;
1198
+
1199
+ compiledContent.classList.add('tera-fy-uiSplat');
1200
+
1201
+ if (settings.logo) {
1202
+ let logoEl = document.createElement('div');
1203
+ logoEl.innerHTML = `<img src="${typeof settings.logo == 'string' ? settings.logo : '/assets/logo/logo.svg'}" class="img-logo"/>`;
1204
+ compiledContent.prepend(logoEl);
1205
+ }
1206
+
1207
+ globalThis.document.body.append(compiledContent);
1208
+ }
967
1209
  // }}}
968
1210
 
969
1211
  // Utility - debug() {{{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "TERA website worker",
5
5
  "scripts": {
6
6
  "dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --sourcemap --serve --servedir=.",
@@ -10,6 +10,7 @@
10
10
  "build:plugins-vue2:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle plugins/vue2.js --outfile=dist/plugin.vue2.es2019.js --minify --sourcemap",
11
11
  "build:docs:api": "documentation build lib/terafy.client.js --format html --config documentation.yml --output docs/",
12
12
  "build:docs:markdown": "documentation build lib/terafy.client.js --format md --markdown-toc --output api.md",
13
+ "watch": "nodemon --watch lib/terafy.client.js --exec npm run build",
13
14
  "lint": "eslint ."
14
15
  },
15
16
  "type": "module",
@@ -62,21 +63,23 @@
62
63
  "engines": {
63
64
  "node": ">=18"
64
65
  },
65
- "peerDependencies": {
66
+ "dependencies": {
67
+ "just-diff": "^6.0.2",
66
68
  "lodash-es": "^4.17.21",
69
+ "mitt": "^3.0.1",
67
70
  "nanoid": "^5.0.2"
68
71
  },
69
- "optionalDependencies": {
70
- "just-diff": "^6.0.2",
71
- "just-diff-apply": "^5.5.0",
72
- "vue": "^3.3.7"
73
- },
74
72
  "devDependencies": {
75
73
  "@momsfriendlydevco/eslint-config": "^1.0.7",
76
74
  "concurrently": "^8.2.2",
77
75
  "documentation": "^14.0.2",
78
76
  "esbuild": "^0.19.5"
79
77
  },
78
+ "optionalDependencies": {
79
+ "@iebh/reflib": "^2.2.2",
80
+ "just-diff-apply": "^5.5.0",
81
+ "vue": "^3.3.7"
82
+ },
80
83
  "eslintConfig": {
81
84
  "extends": "@momsfriendlydevco",
82
85
  "env": {
@@ -87,9 +90,5 @@
87
90
  "ecmaVersion": 13,
88
91
  "sourceType": "module"
89
92
  }
90
- },
91
- "dependencies": {
92
- "@momsfriendlydevco/supabase-reactive": "^1.0.7",
93
- "mitt": "^3.0.1"
94
93
  }
95
94
  }