@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.
- package/api.md +320 -200
- package/dist/plugin.vue2.es2019.js +1 -1
- package/dist/plugin.vue2.es2019.js.map +3 -3
- package/dist/terafy.es2019.js +2 -2
- package/dist/terafy.es2019.js.map +3 -3
- package/dist/terafy.js +2 -2
- package/dist/terafy.js.map +3 -3
- package/lib/terafy.client.js +76 -9
- package/lib/terafy.server.js +260 -18
- package/package.json +10 -11
- package/plugins/vue2.js +68 -0
- package/utils/pDefer.js +15 -0
package/lib/terafy.client.js
CHANGED
|
@@ -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
|
|
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' ? '
|
|
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
|
|
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(()=>
|
|
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
|
|
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
|
-
*
|
|
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
|
}
|
package/lib/terafy.server.js
CHANGED
|
@@ -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
|
|
37
|
-
static
|
|
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.
|
|
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', '
|
|
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
|
|
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 '
|
|
225
|
-
this.settings.serverMode = TeraFyServer.
|
|
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(()=>
|
|
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 =>
|
|
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
|
-
|
|
686
|
+
this._pathSet(app.service('$projects').active, path, value);
|
|
580
687
|
|
|
581
688
|
return (
|
|
582
|
-
|
|
583
|
-
:
|
|
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 (!
|
|
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:
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|