@iebh/tera-fy 1.0.17 → 1.0.19

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.
@@ -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,97 @@ 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
+ // Sanity Check against the login that should have been restored
451
+ if (!app.service('$auth').isLoggedIn) {
452
+ console.warn('Expecting tera-server to have restored $auth.user - but its still not logged in!');
453
+ debugger;
454
+ }
455
+
456
+ // ... then refresh the project list as we're likely going to need it
457
+ await app.service('$projects').refresh();
458
+
459
+ // Go back to start of auth checking loop and repull the user data
460
+ throw 'REDO';
461
+
462
+ break;
463
+ default:
464
+ // Pass - Implied - Cannot authenticate via other method so just fall through to scalding the user
465
+ }
466
+ })
467
+ .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
468
  title: 'TERA-tools account needed',
366
469
  isHtml: true,
470
+ buttons: false,
367
471
  }))
472
+ .then(()=> { throw 'REDO' }) // Go into loop to keep requesting user data
473
+ .catch(e => {
474
+ if (e === 'EXIT') {
475
+ return; // Exit with a valid user
476
+ } else if (e == 'REDO') {
477
+ return this.requireUser();
478
+ }
479
+ throw e;
480
+ })
368
481
  }
369
482
 
370
483
  // }}}
@@ -576,16 +689,61 @@ export default class TeraFyServer {
576
689
  if (!app.service('$projects').active) throw new Error('No active project');
577
690
  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
691
 
579
- pathSet(app.service('$projects').active, path, value)
692
+ this._pathSet(app.service('$projects').active, path, value);
580
693
 
581
694
  return (
582
- this.save && this.sync ? this.saveProjectState()
583
- : this.save ? void this.saveProjectState()
695
+ settings.save && settings.sync ? this.saveProjectState()
696
+ : settings.save ? void this.saveProjectState()
584
697
  : (()=> { throw new Error('setProjectState({sync: true, save: false}) makes no sense') })()
585
698
  );
586
699
  }
587
700
 
588
701
 
702
+ /**
703
+ * Internal recursive path setter used by setProjectState() / setProjectStateDefaults()
704
+ * The implementation defaults to _.set() unless overriden by a plugin
705
+ *
706
+ * @private
707
+ * @param {Object} subject The base subject to operate on
708
+ * @param {String|Array} path The path to set in dotted or array notation
709
+ * @param {*} value The value to set
710
+ *
711
+ * @returns {*} The set value
712
+ */
713
+ _pathSet(subject, path, value) {
714
+ return pathSet(subject, path, value);
715
+ }
716
+
717
+
718
+ /**
719
+ * Internal recursive path checker used by setProjectStateDefaults()
720
+ * The implementation defaults to _.has() unless overriden by a plugin
721
+ *
722
+ * @private
723
+ * @param {Object} subject The base subject to examine
724
+ * @param {String|Array} path The path to fetch in dotted or array notation
725
+ * @returns {Boolean} True if the given path already exists within the subject
726
+ */
727
+ _pathHas(subject, path) {
728
+ return pathExists(subject, path);
729
+ }
730
+
731
+
732
+ /**
733
+ * Internal recursive path fetcher
734
+ * The implementation defaults to _.get() unless overriden by a plugin
735
+ *
736
+ * @private
737
+ * @param {Object} subject The base subject to examine
738
+ * @param {String|Array} path The path to fetch in dotted or array notation
739
+ * @param {*} [fallback] Optional fallback to return if the end point does not exist
740
+ * @returns {*} True if the given path already exists within the subject
741
+ */
742
+ _pathGet(subject, path, fallback) {
743
+ return pathGet(subject, path, fallback);
744
+ }
745
+
746
+
589
747
  /**
590
748
  * Set a nested value within the project state - just like `setProjectState()` - but only if no value for that path exists
591
749
  *
@@ -599,7 +757,7 @@ export default class TeraFyServer {
599
757
  setProjectStateDefaults(path, value, options) {
600
758
  if (!app.service('$projects').active) throw new Error('No active project');
601
759
 
602
- if (!pathExists(app.service('$projects').active, path)) {
760
+ if (!this._pathHas(app.service('$projects').active, path)) {
603
761
  return this.setProjectState(path, value, options)
604
762
  .then(()=> true)
605
763
  } else {
@@ -944,6 +1102,7 @@ export default class TeraFyServer {
944
1102
  * @param {Object} [options] Additional options to mutate behaviour
945
1103
  * @param {String} [options.title='TERA'] The title of the alert box
946
1104
  * @param {Boolean} [options.isHtml=false] If falsy the text is rendered as plain-text otherwise it will be assumed as HTML content
1105
+ * @param {'ok'|false} [options.buttons='ok'] Button set to use or falsy to disable
947
1106
  *
948
1107
  * @returns {Promise} A promise which resolves when the alert has been dismissed
949
1108
  */
@@ -951,6 +1110,7 @@ export default class TeraFyServer {
951
1110
  let settings = {
952
1111
  title: 'TERA',
953
1112
  isHtml: false,
1113
+ buttons: 'ok',
954
1114
  ...options,
955
1115
  };
956
1116
 
@@ -958,12 +1118,100 @@ export default class TeraFyServer {
958
1118
  app.service('$prompt').dialog({
959
1119
  title: settings.title,
960
1120
  body: text,
961
- buttons: ['ok'],
1121
+ buttons:
1122
+ settings.buttons == 'ok' ? ['ok']
1123
+ : false,
962
1124
  isHtml: settings.isHtml,
963
1125
  dialogClose: 'resolve',
964
1126
  })
965
1127
  );
966
1128
  }
1129
+
1130
+
1131
+ /**
1132
+ * Open a popup window containing a new site
1133
+ *
1134
+ * @param {String} url The URL to open
1135
+ *
1136
+ * @param {Object} [options] Additional options to mutate behaviour
1137
+ * @param {Number} [options.width=500] The desired width of the window
1138
+ * @param {Number} [options.height=600] The desired height of the window
1139
+ * @param {Boolean} [options.center=true] Attempt to center the window on the screen
1140
+ * @param {Object} [options.permissions] Additional permissions to set on opening, defaults to a suitable set of permission for popups (see code)
1141
+ *
1142
+ * @returns {WindowProxy} The opened window object (if `noopener` is not set in permissions)
1143
+ */
1144
+ uiWindow(url, options) {
1145
+ let settings = {
1146
+ width: 500,
1147
+ height: 600,
1148
+ center: true,
1149
+ permissions: {
1150
+ popup: true,
1151
+ location: false,
1152
+ menubar: false,
1153
+ status: false,
1154
+ scrolbars: false,
1155
+ },
1156
+ ...options,
1157
+ };
1158
+
1159
+ return window.open(url, '_blank', Object.entries({
1160
+ ...settings.permissions,
1161
+ width: settings.width,
1162
+ height: settings.height,
1163
+ ...(settings.center && {
1164
+ left: screen.width/2 - settings.width/2,
1165
+ top: screen.height/2 - settings.height/2,
1166
+ }),
1167
+ })
1168
+ .map(([key, val]) => key + '=' + (
1169
+ typeof val == 'boolean' ? val ? '1' : '0' // Map booleans to 1/0
1170
+ : val
1171
+ ))
1172
+ .join(', ')
1173
+ );
1174
+ }
1175
+
1176
+
1177
+ /**
1178
+ * Display HTML content full-screen within TERA
1179
+ * This function is ideally called within a requestFocus() wrapper
1180
+ *
1181
+ * @param {DOMElement|String|false} content Either a prepared DOM element or string to compile, set to falsy to remove existing content
1182
+ *
1183
+ * @param {Object} [options] Additional options to mutate behaviour
1184
+ * @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
1185
+ */
1186
+ uiSplat(content, options) {
1187
+ let settings = {
1188
+ logo: false,
1189
+ ...options,
1190
+ };
1191
+
1192
+ if (!content) { // Remove content
1193
+ globalThis.document.body.querySelector('.tera-fy-uiSplat').remove();
1194
+ return;
1195
+ }
1196
+
1197
+ let compiledContent = typeof content == 'string'
1198
+ ? (()=> {
1199
+ let el = document.createElement('div')
1200
+ el.innerHTML = content;
1201
+ return el;
1202
+ })()
1203
+ : content;
1204
+
1205
+ compiledContent.classList.add('tera-fy-uiSplat');
1206
+
1207
+ if (settings.logo) {
1208
+ let logoEl = document.createElement('div');
1209
+ logoEl.innerHTML = `<img src="${typeof settings.logo == 'string' ? settings.logo : '/assets/logo/logo.svg'}" class="img-logo"/>`;
1210
+ compiledContent.prepend(logoEl);
1211
+ }
1212
+
1213
+ globalThis.document.body.append(compiledContent);
1214
+ }
967
1215
  // }}}
968
1216
 
969
1217
  // Utility - debug() {{{
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "TERA website worker",
5
5
  "scripts": {
6
- "dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --sourcemap --serve --servedir=.",
6
+ "dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --serve --servedir=.",
7
7
  "build": "concurrently 'npm:build:*'",
8
- "build:client": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --sourcemap",
9
- "build:client:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle lib/terafy.client.js --outfile=dist/terafy.es2019.js --minify --sourcemap",
10
- "build:plugins-vue2:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle plugins/vue2.js --outfile=dist/plugin.vue2.es2019.js --minify --sourcemap",
8
+ "build:client": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify",
9
+ "build:client:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle lib/terafy.client.js --outfile=dist/terafy.es2019.js --minify",
10
+ "build:plugins-vue2:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle plugins/vue2.js --outfile=dist/plugin.vue2.es2019.js --minify",
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",
@@ -25,7 +26,8 @@
25
26
  "default": "./dist/terafy.js"
26
27
  },
27
28
  "./server": "./lib/terafy.server.js",
28
- "./plugins/*": "./plugins/*.js"
29
+ "./plugins/*": "./plugins/*.js",
30
+ "./widgets/*": "./widgets/*.js"
29
31
  },
30
32
  "repository": {
31
33
  "type": "git",
@@ -62,21 +64,23 @@
62
64
  "engines": {
63
65
  "node": ">=18"
64
66
  },
65
- "peerDependencies": {
67
+ "dependencies": {
68
+ "just-diff": "^6.0.2",
66
69
  "lodash-es": "^4.17.21",
70
+ "mitt": "^3.0.1",
67
71
  "nanoid": "^5.0.2"
68
72
  },
69
- "optionalDependencies": {
70
- "just-diff": "^6.0.2",
71
- "just-diff-apply": "^5.5.0",
72
- "vue": "^3.3.7"
73
- },
74
73
  "devDependencies": {
75
74
  "@momsfriendlydevco/eslint-config": "^1.0.7",
76
75
  "concurrently": "^8.2.2",
77
76
  "documentation": "^14.0.2",
78
77
  "esbuild": "^0.19.5"
79
78
  },
79
+ "optionalDependencies": {
80
+ "@iebh/reflib": "^2.2.2",
81
+ "just-diff-apply": "^5.5.0",
82
+ "vue": "^3.3.7"
83
+ },
80
84
  "eslintConfig": {
81
85
  "extends": "@momsfriendlydevco",
82
86
  "env": {
@@ -87,9 +91,5 @@
87
91
  "ecmaVersion": 13,
88
92
  "sourceType": "module"
89
93
  }
90
- },
91
- "dependencies": {
92
- "@momsfriendlydevco/supabase-reactive": "^1.0.7",
93
- "mitt": "^3.0.1"
94
94
  }
95
95
  }
package/plugins/vue2.js CHANGED
@@ -151,6 +151,7 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
151
151
  * @param {Object} options.app Root level Vue app to bind against
152
152
  * @param {Vue} options.Vue Vue@2 instance to bind against
153
153
  * @param {String} [options.globalName='$tera'] Global property to allocate this service as within Vue2
154
+ * @param {Boolean} [options.requireProject=true] Automatically call requireProject() prior to any operation
154
155
  * @param {Boolean} [options.subscribeState=true] Setup `vm.$tera.state` as a live binding on init
155
156
  * @param {Boolean} [options.subscribeList=true] Setup `vm.$tera.projects` as a list of accesible projects on init
156
157
  * @param {Objecct} [options.stateOptions] Options passed to `bindProjectState()` when setting up the main state
@@ -162,6 +163,7 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
162
163
  app: null,
163
164
  Vue: null,
164
165
  globalName: '$tera',
166
+ requireProject: true,
165
167
  subscribeState: true,
166
168
  subscribeProjects: true,
167
169
  stateOptions: {
@@ -190,6 +192,7 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
190
192
 
191
193
  // this.statePromisable becomes the promise we are waiting on to resolve
192
194
  return Promise.resolve()
195
+ .then(()=> settings.requireProject && this.requireProject())
193
196
  .then(()=> Promise.all([
194
197
  // Bind available project and wait on it
195
198
  settings.subscribeState && this.bindProjectState({
@@ -205,4 +208,69 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
205
208
  .then(()=> this.debug('INFO', 'Loaded projects', this.projects)),
206
209
  ]))
207
210
  }
211
+
212
+
213
+ /**
214
+ * Override the regular _pathSet() handler so that it stays as a Vue observable
215
+ * This function will correctly populate any missing entities, calling vm.$set on each traversal of the path This is mainly to fix the issue where Vue can't see changes to objects if a key is added or removed
216
+ * Passing undefined as a value removes the key (unless removeUndefined is set to false)
217
+ *
218
+ * @override
219
+ * @param {Object} [target] The target to set the path of, if omitted the `vm` object is used as the base for traversal
220
+ * @param {string|array} path The path to set within the target / vm
221
+ * @param {*} value The value to set
222
+ * @param {Object} [options] Additional options
223
+ * @param {boolean} [options.arrayNumeric=true] Process numeric path segments as arrays
224
+ * @param {boolean} [options.removeUndefined=true] If undefined is specified as a value the key is removed instead of being set
225
+ * @param {boolean} [options.debug=false] Also print out debugging information when setting the value
226
+ * @returns {Object} The set value, like $set()
227
+ *
228
+ * @example Set a deeply nested path within a target object
229
+ * vm.$setPath(this, 'foo.bar.baz', 123); // this.$data.foo.bar.baz = 123
230
+ *
231
+ * @example Set a deeply nested path, with arrays, assuming VM as the root node
232
+ * vm.$setPath('foo.1.bar', 123); // vm.$data.foo = [{bar: 123}]
233
+ */
234
+ _pathSet(target, path, value, options) {
235
+ // Sanity checks {{{
236
+ if (typeof target != 'object') throw new Error('Cannot use _pathSet on non-object target');
237
+ // }}}
238
+
239
+ let settings = {
240
+ arrayNumeric: true,
241
+ debug: false,
242
+ removeUndefined: true,
243
+ ...options,
244
+ };
245
+
246
+ if (settings.debug) console.log('[_pathSet]', path, '=', value, {target, options});
247
+
248
+ let node = target;
249
+ if (!path) throw new Error('Cannot _pathSet with undefined path');
250
+ (typeof path == 'string' ? path.split('.') : path).some((chunk, chunkIndex, chunks) => {
251
+ if (chunkIndex == chunks.length - 1) { // Leaf node
252
+ if (settings.removeUndefined && value === undefined) {
253
+ this.Vue.$delete(node, chunk);
254
+ } else {
255
+ this.Vue.set(node, chunk, value);
256
+ }
257
+ } else if (node[chunk] === undefined) { // This chunk (and all following chunks) does't exist - populate from here
258
+ chunks.slice(chunkIndex, chunks.length - 1).forEach(chunk => {
259
+ if (settings.arrayNumeric && isFinite(chunk)) {
260
+ this.Vue.set(node, chunk, []);
261
+ } else {
262
+ this.Vue.set(node, chunk, {});
263
+ }
264
+ node = node[chunk];
265
+ });
266
+ this.Vue.set(node, chunks[chunks.length - 1], value);
267
+ return true;
268
+ } else {
269
+ node = node[chunk];
270
+ return false;
271
+ }
272
+ });
273
+
274
+ return value;
275
+ }
208
276
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Returns a defer object which represents a promise object which can resolve in the future - but without enclosing a function
3
+ * The defer object has the keys {promise, resolve(), reject()}
4
+ * @returns {Defer} A defered promise
5
+ */
6
+ export default function() {
7
+ var deferred = {};
8
+
9
+ deferred.promise = new Promise((resolve, reject) => {
10
+ deferred.resolve = resolve;
11
+ deferred.reject = reject;
12
+ });
13
+
14
+ return deferred;
15
+ }
@@ -0,0 +1,8 @@
1
+ <script>
2
+ export default {
3
+ }
4
+ </script>
5
+
6
+ <template>
7
+ <div>PLACEHOLDER: Library select</div>
8
+ </template>