@iebh/tera-fy 1.0.6 → 1.0.8

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.
@@ -15,13 +15,17 @@ export default class TeraFy {
15
15
  *
16
16
  * @type {Object}
17
17
  * @property {Boolean} devMode Operate in devMode - i.e. force outer refresh when encountering an existing TeraFy instance
18
+ * @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'
19
+ * @property {Number} modeTimeout How long entities have in 'detect' mode to identify themselves
18
20
  * @property {String} siteUrl The TERA URL to connect to
19
21
  * @property {String} restrictOrigin URL to restrict communications to
20
22
  */
21
23
  settings = {
22
24
  devMode: true,
25
+ mode: 'detect',
26
+ modeTimeout: 300,
23
27
  siteUrl: 'https://tera-tools.com/embed',
24
- restrictOrigin: '*', // DEBUG: Need to restrict this to TERA site
28
+ restrictOrigin: '*', // FIXME: Need to restrict this to TERA site
25
29
  };
26
30
 
27
31
 
@@ -112,7 +116,16 @@ export default class TeraFy {
112
116
  id: message.id || nanoid(),
113
117
  ...cloneDeep(message), // Need to clone to resolve promise nasties
114
118
  };
115
- this.dom.iframe.contentWindow.postMessage(payload, this.settings.restrictOrigin);
119
+
120
+ if (this.settings.mode == 'parent') {
121
+ window.parent.postMessage(payload, this.settings.restrictOrigin);
122
+ } else if (this.settings.mode == 'child') {
123
+ this.dom.iframe.contentWindow.postMessage(payload, this.settings.restrictOrigin);
124
+ } else if (this.settings.mode == 'detect') {
125
+ throw new Error('Call init() or detectMode() before trying to send data to determine the mode');
126
+ } else {
127
+ throw new Error(`Unknown TERA communication mode "${this.settings.mode}"`);
128
+ }
116
129
  } catch (e) {
117
130
  this.debug('ERROR', 'Message compose client->server:', e);
118
131
  this.debug('ERROR', 'Attempted to dispatch payload client->server', payload);
@@ -144,6 +157,8 @@ export default class TeraFy {
144
157
  * @param {MessageEvent} Raw message event to process
145
158
  */
146
159
  acceptMessage(rawMessage) {
160
+ if (rawMessage.origin == window.location.origin) return; // Message came from us
161
+
147
162
  let message = rawMessage.data;
148
163
  if (!message.TERA || !message.id) return; // Ignore non-TERA signed messages
149
164
  this.debug('Recieved', message);
@@ -200,23 +215,56 @@ export default class TeraFy {
200
215
 
201
216
  /**
202
217
  * Initalize the TERA client singleton
218
+ * This function can only be called once and will return the existing init() worker Promise if its called againt
203
219
  *
204
220
  * @returns {Promise<TeraFy>} An eventual promise which will resovle with this terafy instance
205
221
  */
206
222
  init() {
223
+ if (this.init.promise) return this.init.promise; // Aleady been called - return init promise
224
+
207
225
  window.addEventListener('message', this.acceptMessage.bind(this));
208
226
 
209
- return Promise.all([
210
- // Init core functions async
211
- this.injectComms(),
212
- this.injectStylesheet(),
213
- this.injectMethods(),
214
-
215
- // Init all plugins
216
- ...this.plugins
217
- .map(plugin => plugin.init()),
218
- ])
219
- .then(()=> this)
227
+ return this.init.promise = Promise.resolve()
228
+ .then(()=> this.detectMode())
229
+ .then(mode => this.settings.mode = mode)
230
+ .then(()=> Promise.all([
231
+ // Init core functions async
232
+ this.injectComms(),
233
+ this.injectStylesheet(),
234
+ this.injectMethods(),
235
+
236
+ // Init all plugins
237
+ ...this.plugins
238
+ .map(plugin => plugin.init()),
239
+ ]))
240
+ }
241
+
242
+
243
+
244
+ /**
245
+ * Populate `settings.mode`
246
+ * Try to communicate with a parent frame, if none assume we need to fallback to child mode
247
+ *
248
+ * @returns {Promise<String>} A promise which will resolve with the detected mode to use
249
+ */
250
+ detectMode() {
251
+ if (this.settings.mode != 'detect') { // Dev has specified a forced mode to use
252
+ return this.settings.mode;
253
+ } else if (window.self === window.parent) { // This frame is already at the top
254
+ return 'child';
255
+ } else { // No idea - try messaging
256
+ return Promise.resolve()
257
+ .then(()=> this.settings.mode = 'parent') // Switch to parent mode...
258
+ .then(()=> new Promise((resolve, reject) => { // And try messaging with a timeout
259
+ let timeoutHandle = setTimeout(()=> reject(), this.settings.modeTimeout);
260
+
261
+ this.rpc('handshake')
262
+ .then(()=> clearTimeout(timeoutHandle))
263
+ .then(()=> resolve())
264
+ }))
265
+ .then(()=> 'parent')
266
+ .catch(()=> 'child')
267
+ }
220
268
  }
221
269
 
222
270
 
@@ -226,23 +274,32 @@ export default class TeraFy {
226
274
  * @returns {Promise} A promise which will resolve when the loading has completed and we have found a parent TERA instance or initiallized a child
227
275
  */
228
276
  injectComms() { return new Promise(resolve => {
229
- this.dom.el = document.createElement('div')
230
- this.dom.el.id = 'tera-fy';
231
- this.dom.el.classList.toggle('dev-mode', this.settings.devMode);
232
- document.body.append(this.dom.el);
233
-
234
- this.dom.iframe = document.createElement('iframe')
235
-
236
- // Queue up event chain when document loads
237
- this.dom.iframe.setAttribute('sandbox', 'allow-downloads allow-scripts allow-same-origin');
238
- this.dom.iframe.addEventListener('load', ()=> {
239
- this.debug('TERA EMBED FRAME READY');
240
- resolve();
241
- });
242
-
243
- // Start document load sequence + append to DOM
244
- this.dom.iframe.src = this.settings.siteUrl;
245
- this.dom.el.append(this.dom.iframe);
277
+ switch (this.settings.mode) {
278
+ case 'child':
279
+ this.dom.el = document.createElement('div')
280
+ this.dom.el.id = 'tera-fy';
281
+ this.dom.el.classList.toggle('dev-mode', this.settings.devMode);
282
+ document.body.append(this.dom.el);
283
+
284
+ this.dom.iframe = document.createElement('iframe')
285
+
286
+ // Queue up event chain when document loads
287
+ this.dom.iframe.setAttribute('sandbox', 'allow-downloads allow-scripts allow-same-origin');
288
+ this.dom.iframe.addEventListener('load', ()=> {
289
+ this.debug('Embed frame ready');
290
+ resolve();
291
+ });
292
+
293
+ // Start document load sequence + append to DOM
294
+ this.dom.iframe.src = this.settings.siteUrl;
295
+ this.dom.el.append(this.dom.iframe);
296
+ break;
297
+ case 'parent':
298
+ resolve();
299
+ break;
300
+ default:
301
+ throw new Error(`Unsupported mode "${this.settings.mode}" when calling injectComms()`);
302
+ }
246
303
  })}
247
304
 
248
305
 
@@ -250,50 +307,58 @@ export default class TeraFy {
250
307
  * Inject a local stylesheet to handle TERA server functionality
251
308
  */
252
309
  injectStylesheet() {
253
- this.dom.stylesheet = document.createElement('style');
254
- this.dom.stylesheet.innerHTML = [
255
- ':root {',
256
- '--TERA-accent: #4d659c;',
257
- '}',
258
-
259
- '#tera-fy {',
260
- 'display: none;',
261
- 'position: fixed;',
262
- 'right: 50px;',
263
- 'bottom: 50px;',
264
- 'width: 300px;',
265
- 'height: 150px;',
266
- 'background: transparent;',
267
-
268
- '&.dev-mode {',
269
- 'display: flex;',
270
- 'border: 5px solid var(--TERA-accent);',
271
- 'background: #FFF;',
272
- '}',
273
-
274
- '& > iframe {',
275
- 'width: 100%;',
276
- 'height: 100%;',
277
- '}',
278
- '}',
279
-
280
- // Fullscreen functionality {{{
281
- 'body.tera-fy-focus {',
282
- 'overflow: hidden;',
283
-
284
- '& #tera-fy {',
285
- 'display: flex !important;',
286
- 'position: fixed !important;',
287
- 'top: 0px !important;',
288
- 'width: 100vw !important;',
289
- 'height: 100vh !important;',
290
- 'left: 0px !important;',
291
- 'z-index: 10000 !important;',
292
- '}',
293
- '}',
294
- // }}}
295
- ].join('\n');
296
- document.head.appendChild(this.dom.stylesheet);
310
+ switch (this.settings.mode) {
311
+ case 'child':
312
+ this.dom.stylesheet = document.createElement('style');
313
+ this.dom.stylesheet.innerHTML = [
314
+ ':root {',
315
+ '--TERA-accent: #4d659c;',
316
+ '}',
317
+
318
+ '#tera-fy {',
319
+ 'display: none;',
320
+ 'position: fixed;',
321
+ 'right: 50px;',
322
+ 'bottom: 50px;',
323
+ 'width: 300px;',
324
+ 'height: 150px;',
325
+ 'background: transparent;',
326
+
327
+ '&.dev-mode {',
328
+ 'display: flex;',
329
+ 'border: 5px solid var(--TERA-accent);',
330
+ 'background: #FFF;',
331
+ '}',
332
+
333
+ '& > iframe {',
334
+ 'width: 100%;',
335
+ 'height: 100%;',
336
+ '}',
337
+ '}',
338
+
339
+ // Fullscreen functionality {{{
340
+ 'body.tera-fy-focus {',
341
+ 'overflow: hidden;',
342
+
343
+ '& #tera-fy {',
344
+ 'display: flex !important;',
345
+ 'position: fixed !important;',
346
+ 'top: 0px !important;',
347
+ 'width: 100vw !important;',
348
+ 'height: 100vh !important;',
349
+ 'left: 0px !important;',
350
+ 'z-index: 10000 !important;',
351
+ '}',
352
+ '}',
353
+ // }}}
354
+ ].join('\n');
355
+ document.head.appendChild(this.dom.stylesheet);
356
+ break;
357
+ case 'parent':
358
+ break;
359
+ default:
360
+ throw new Error(`Unsupported mode "${this.settings.mode}" when injectStylesheet()`);
361
+ }
297
362
  }
298
363
 
299
364
 
@@ -304,7 +369,6 @@ export default class TeraFy {
304
369
  this.methods.forEach(method =>
305
370
  this[method] = this.rpc.bind(this, method)
306
371
  );
307
- this.debug('Remote methods installed:', this.methods);
308
372
  }
309
373
  // }}}
310
374
 
@@ -314,7 +378,7 @@ export default class TeraFy {
314
378
  * Debugging output function
315
379
  * This function will only act if `settings.devMode` is truthy
316
380
  *
317
- * @param {'INFO'|'LOG'|'WARN'|'ERROR'} [status] Optional prefixing level to mark the message as. 'WARN' and 'ERROR' will always show reguardless of devMode being enabled
381
+ * @param {'VERBOSE'|'INFO'|'LOG'|'WARN'|'ERROR'} [status] Optional prefixing level to mark the message as. 'WARN' and 'ERROR' will always show reguardless of devMode being enabled
318
382
  * @param {String} [msg...] Output to show
319
383
  */
320
384
  debug(...msg) {
@@ -387,7 +451,6 @@ export default class TeraFy {
387
451
  Object.getOwnPropertyNames(Object.getPrototypeOf(source))
388
452
  .filter(prop => !['constructor', 'prototype', 'name'].includes(prop))
389
453
  .forEach((prop) => {
390
- console.log('Merge method', prop);
391
454
  Object.defineProperty(
392
455
  target,
393
456
  prop,
@@ -126,15 +126,17 @@ export default class TeraFyServer {
126
126
  * Send raw message content to the client
127
127
  *
128
128
  * @param {Object} message Message object to send
129
+ * @param {Window} Window context to dispatch the message via if its not the same as the regular window
129
130
  */
130
- sendRaw(message) {
131
+ sendRaw(message, sendVia) {
131
132
  let payload;
132
133
  try {
133
134
  payload = {
134
135
  TERA: 1,
135
136
  ...cloneDeep(message), // Need to clone to resolve promise nasties
136
137
  };
137
- globalThis.parent.postMessage(payload, this.settings.restrictOrigin);
138
+ this.debug('INFO', 'Parent reply', message, '<=>', payload);
139
+ (sendVia || globalThis.parent).postMessage(payload, this.settings.restrictOrigin);
138
140
  } catch (e) {
139
141
  this.debug('ERROR', 'Attempted to dispatch payload server->client', payload);
140
142
  this.debug('ERROR', 'Message compose server->client:', e);
@@ -149,6 +151,8 @@ export default class TeraFyServer {
149
151
  * @param {MessageEvent} Raw message event to process
150
152
  */
151
153
  acceptMessage(rawMessage) {
154
+ if (rawMessage.origin == window.location.origin) return; // Message came from us
155
+
152
156
  let message = rawMessage.data;
153
157
  if (!message.TERA) return; // Ignore non-TERA signed messages
154
158
  this.debug('Recieved', message);
@@ -169,11 +173,11 @@ export default class TeraFyServer {
169
173
  throw new Error('Unknown message format');
170
174
  }
171
175
  })
172
- .then(res => this.sendRaw({
176
+ .then(response => this.sendRaw({
173
177
  id: message.id,
174
178
  action: 'response',
175
- response: res,
176
- }))
179
+ response,
180
+ }, rawMessage.source))
177
181
  .catch(e => {
178
182
  console.warn(`TERA-FY server threw on RPC:${message.method}:`, e);
179
183
  this.sendRaw({
@@ -369,6 +373,7 @@ export default class TeraFyServer {
369
373
  * @param {Object} [options] Additional options to mutate behaviour
370
374
  * @param {String} [options.title="Select a project to work with"] The title of the dialog to display
371
375
  * @param {Boolean} [options.allowCancel=true] Advertise cancelling the operation, the dialog can still be cancelled by closing it
376
+ * @param {Boolean} [options.setActive=false] Also set the project as active when selected
372
377
  *
373
378
  * @returns {Promise<Project>} The active project
374
379
  */
@@ -376,6 +381,7 @@ export default class TeraFyServer {
376
381
  let settings = {
377
382
  title: 'Select a project to work with',
378
383
  allowCancel: true,
384
+ setActive: false,
379
385
  ...options,
380
386
  };
381
387
 
@@ -387,6 +393,11 @@ export default class TeraFyServer {
387
393
  buttons: settings.allowCancel && ['cancel'],
388
394
  })
389
395
  ))
396
+ .then(project => settings.setActive
397
+ ? this.setActiveProject(project)
398
+ .then(()=> project)
399
+ : project
400
+ )
390
401
  }
391
402
 
392
403
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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=.",
package/plugins/vue.js CHANGED
@@ -20,7 +20,7 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
20
20
  * @param {Boolean} [options.write=true] Allow local reactivity to writes - send these to the server
21
21
  * @param {Array<String>} Paths to subscribe to e.g. ['/users/'],
22
22
  *
23
- * @returns {Promies<Reactive<Object>>} A reactive object representing the project state
23
+ * @returns {Promie<Reactive<Object>>} A reactive object representing the project state
24
24
  */
25
25
  bindProjectState(options) {
26
26
  let settings = {
@@ -36,6 +36,8 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
36
36
  paths: settings.paths,
37
37
  }))
38
38
  .then(snapshot => {
39
+ this.debug('Got project snapshot', snapshot);
40
+
39
41
  // Create initial reactive
40
42
  let stateReactive = reactive(snapshot);
41
43
 
@@ -63,6 +65,40 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
63
65
  }
64
66
 
65
67
 
68
+ /**
69
+ * List of available projects for the current session
70
+ * @type {VueReactive<Array<Object>>}
71
+ */
72
+ projects = reactive([]);
73
+
74
+
75
+ /**
76
+ * The bound, reactive state of a Vue project
77
+ * When loaded this represents the state of a project as an object
78
+ * @type {Object}
79
+ */
80
+ state = null;
81
+
82
+
83
+ /**
84
+ * Promise used when binding to state
85
+ * @type {Promise}
86
+ */
87
+ statePromisable = null;
88
+
89
+
90
+ /**
91
+ * Utility function which returns an awaitable promise when the state is loading or being refreshed
92
+ * This is used in place of `statePromisable` as it has a slightly more logical syntax as a function
93
+ *
94
+ * @example Await the state loading
95
+ * await $tera.statePromise();
96
+ */
97
+ statePromise() {
98
+ return this.statePromisable;
99
+ }
100
+
101
+
66
102
  /**
67
103
  * Provide a Vue@3 compatible plugin
68
104
  */
@@ -75,6 +111,7 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
75
111
  * Install into Vue as a generic Vue@3 plugin
76
112
  *
77
113
  * @param {Object} [options] Additional options to mutate behaviour
114
+ * @param {Boolean} [options.autoInit=true] Call Init() during the `statePromiseable` cycle if its not already been called
78
115
  * @param {String} [options.globalName='$tera'] Globa property to allocate this service as
79
116
  * @param {Objecct} [options.bindOptions] Options passed to `bindProjectState()`
80
117
  *
@@ -82,23 +119,37 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
82
119
  */
83
120
  install(app, options) {
84
121
  let settings = {
122
+ autoInit: true,
85
123
  globalName: '$tera',
86
124
  stateOptions: {
87
- autoRequire: true,
88
125
  write: true,
89
126
  },
90
127
  ...options,
91
128
  };
92
129
 
130
+ // Bind $tera.state to the active project
131
+ // Initialize state to null
132
+ $tera.state = null;
133
+
134
+ // $tera.statePromisable becomes the promise we are waiting on to resolve
135
+ $tera.statePromisable = Promise.resolve()
136
+ .then(()=> settings.autoInit && $tera.init())
137
+ .then(()=> Promise.all([
138
+ // Bind available project and wait on it
139
+ $tera.bindProjectState(settings.stateOptions)
140
+ .then(state => $tera.state = state)
141
+ .then(()=> $tera.debug('INFO', 'Loaded project state', $tera.state)),
142
+
143
+ // Fetch available projects
144
+ // TODO: It would be nice if this was responsive to remote changes
145
+ $tera.getProjects()
146
+ .then(projects => $tera.projects = reactive(projects))
147
+ .then(()=> $tera.debug('INFO', 'Loaded projects', $tera.projects)),
148
+ ]))
149
+
150
+
93
151
  // Make this module available globally
94
152
  app.config.globalProperties[settings.globalName] = $tera;
95
-
96
- // Bind $tera.state to the active project
97
- // TODO: context.bindProjectState(settings.stateOptions),
98
- $tera.state = {
99
- id: 'TERAPROJ',
100
- name: 'A fake project',
101
- };
102
153
  },
103
154
 
104
155
  };