@iebh/tera-fy 1.0.7 → 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('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
 
@@ -386,7 +451,6 @@ export default class TeraFy {
386
451
  Object.getOwnPropertyNames(Object.getPrototypeOf(source))
387
452
  .filter(prop => !['constructor', 'prototype', 'name'].includes(prop))
388
453
  .forEach((prop) => {
389
- console.log('Merge method', prop);
390
454
  Object.defineProperty(
391
455
  target,
392
456
  prop,
@@ -126,8 +126,9 @@ 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 = {
@@ -135,7 +136,7 @@ export default class TeraFyServer {
135
136
  ...cloneDeep(message), // Need to clone to resolve promise nasties
136
137
  };
137
138
  this.debug('INFO', 'Parent reply', message, '<=>', payload);
138
- globalThis.parent.postMessage(payload, this.settings.restrictOrigin);
139
+ (sendVia || globalThis.parent).postMessage(payload, this.settings.restrictOrigin);
139
140
  } catch (e) {
140
141
  this.debug('ERROR', 'Attempted to dispatch payload server->client', payload);
141
142
  this.debug('ERROR', 'Message compose server->client:', e);
@@ -150,6 +151,8 @@ export default class TeraFyServer {
150
151
  * @param {MessageEvent} Raw message event to process
151
152
  */
152
153
  acceptMessage(rawMessage) {
154
+ if (rawMessage.origin == window.location.origin) return; // Message came from us
155
+
153
156
  let message = rawMessage.data;
154
157
  if (!message.TERA) return; // Ignore non-TERA signed messages
155
158
  this.debug('Recieved', message);
@@ -174,7 +177,7 @@ export default class TeraFyServer {
174
177
  id: message.id,
175
178
  action: 'response',
176
179
  response,
177
- }))
180
+ }, rawMessage.source))
178
181
  .catch(e => {
179
182
  console.warn(`TERA-FY server threw on RPC:${message.method}:`, e);
180
183
  this.sendRaw({
@@ -370,6 +373,7 @@ export default class TeraFyServer {
370
373
  * @param {Object} [options] Additional options to mutate behaviour
371
374
  * @param {String} [options.title="Select a project to work with"] The title of the dialog to display
372
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
373
377
  *
374
378
  * @returns {Promise<Project>} The active project
375
379
  */
@@ -377,6 +381,7 @@ export default class TeraFyServer {
377
381
  let settings = {
378
382
  title: 'Select a project to work with',
379
383
  allowCancel: true,
384
+ setActive: false,
380
385
  ...options,
381
386
  };
382
387
 
@@ -388,6 +393,11 @@ export default class TeraFyServer {
388
393
  buttons: settings.allowCancel && ['cancel'],
389
394
  })
390
395
  ))
396
+ .then(project => settings.setActive
397
+ ? this.setActiveProject(project)
398
+ .then(()=> project)
399
+ : project
400
+ )
391
401
  }
392
402
 
393
403
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.0.7",
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
@@ -111,6 +111,7 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
111
111
  * Install into Vue as a generic Vue@3 plugin
112
112
  *
113
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
114
115
  * @param {String} [options.globalName='$tera'] Globa property to allocate this service as
115
116
  * @param {Objecct} [options.bindOptions] Options passed to `bindProjectState()`
116
117
  *
@@ -118,9 +119,9 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
118
119
  */
119
120
  install(app, options) {
120
121
  let settings = {
122
+ autoInit: true,
121
123
  globalName: '$tera',
122
124
  stateOptions: {
123
- autoRequire: true,
124
125
  write: true,
125
126
  },
126
127
  ...options,
@@ -131,19 +132,20 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
131
132
  $tera.state = null;
132
133
 
133
134
  // $tera.statePromisable becomes the promise we are waiting on to resolve
134
- $tera.statePromisable = Promise.all([
135
-
136
- // Bind available project and wait on it
137
- $tera.bindProjectState(settings.stateOptions)
138
- .then(state => $tera.state = state)
139
- .then(()=> $tera.debug('INFO', 'Loaded project state', $tera.state)),
140
-
141
- // Fetch available projects
142
- // TODO: It would be nice if this was responsive to remote changes
143
- $tera.getProjects()
144
- .then(projects => $tera.projects = projects)
145
- .then(()=> $tera.debug('INFO', 'Loaded projects', $tera.list)),
146
- ])
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
+ ]))
147
149
 
148
150
 
149
151
  // Make this module available globally