Browse code

new build from home

Louis Jonget authored on18/03/2023 14:55:56
Showing1 changed files
... ...
@@ -1,379 +1,379 @@
1
-'use strict';
2
-
3
-var configPageHtml = require('./tmp/config-page.html');
4
-var toSource = require('tosource');
5
-var standardComponents = require('./src/scripts/components');
6
-var deepcopy = require('deepcopy/build/deepcopy.min');
7
-var version = require('./package.json').version;
8
-var messageKeys = require('message_keys');
9
-
10
-/**
11
- * @param {Array} config - the Clay config
12
- * @param {function} [customFn] - Custom code to run from the config page. Will run
13
- *   with the ClayConfig instance as context
14
- * @param {Object} [options] - Additional options to pass to Clay
15
- * @param {boolean} [options.autoHandleEvents=true] - If false, Clay will not
16
- *   automatically handle the 'showConfiguration' and 'webviewclosed' events
17
- * @param {*} [options.userData={}] - Arbitrary data to pass to the config page. Will
18
- *   be available as `clayConfig.meta.userData`
19
- * @constructor
20
- */
21
-function Clay(config, customFn, options) {
22
-  var self = this;
23
-
24
-  if (!Array.isArray(config)) {
25
-    throw new Error('config must be an Array');
26
-  }
27
-
28
-  if (customFn && typeof customFn !== 'function') {
29
-    throw new Error('customFn must be a function or "null"');
30
-  }
31
-
32
-  options = options || {};
33
-
34
-  self.config = deepcopy(config);
35
-  self.customFn = customFn || function() {};
36
-  self.components = {};
37
-  self.meta = {
38
-    activeWatchInfo: null,
39
-    accountToken: '',
40
-    watchToken: '',
41
-    userData: {}
42
-  };
43
-  self.version = version;
44
-
45
-  /**
46
-   * Populate the meta with data from the Pebble object. Make sure to run this inside
47
-   * either the "showConfiguration" or "ready" event handler
48
-   * @return {void}
49
-   */
50
-  function _populateMeta() {
51
-    self.meta = {
52
-      activeWatchInfo: Pebble.getActiveWatchInfo && Pebble.getActiveWatchInfo(),
53
-      accountToken: Pebble.getAccountToken(),
54
-      watchToken: Pebble.getWatchToken(),
55
-      userData: deepcopy(options.userData || {})
56
-    };
57
-  }
58
-
59
-  // Let Clay handle all the magic
60
-  if (options.autoHandleEvents !== false && typeof Pebble !== 'undefined') {
61
-
62
-    Pebble.addEventListener('showConfiguration', function() {
63
-      _populateMeta();
64
-      Pebble.openURL(self.generateUrl());
65
-    });
66
-
67
-    Pebble.addEventListener('webviewclosed', function(e) {
68
-
69
-      if (!e || !e.response) { return; }
70
-
71
-      // Send settings to Pebble watchapp
72
-      Pebble.sendAppMessage(self.getSettings(e.response), function() {
73
-        console.log('Sent config data to Pebble');
74
-      }, function(error) {
75
-        console.log('Failed to send config data!');
76
-        console.log(JSON.stringify(error));
77
-      });
78
-    });
79
-  } else if (typeof Pebble !== 'undefined') {
80
-    Pebble.addEventListener('ready', function() {
81
-      _populateMeta();
82
-    });
83
-  }
84
-
85
-  /**
86
-   * If this function returns true then the callback will be executed
87
-   * @callback _scanConfig_testFn
88
-   * @param {Clay~ConfigItem} item
89
-   */
90
-
91
-  /**
92
-   * @callback _scanConfig_callback
93
-   * @param {Clay~ConfigItem} item
94
-   */
95
-
96
-  /**
97
-   * Scan over the config and run the callback if the testFn resolves to true
98
-   * @private
99
-   * @param {Clay~ConfigItem|Array} item
100
-   * @param {_scanConfig_testFn} testFn
101
-   * @param {_scanConfig_callback} callback
102
-   * @return {void}
103
-   */
104
-  function _scanConfig(item, testFn, callback) {
105
-    if (Array.isArray(item)) {
106
-      item.forEach(function(item) {
107
-        _scanConfig(item, testFn, callback);
108
-      });
109
-    } else if (item.type === 'section') {
110
-      _scanConfig(item.items, testFn, callback);
111
-    } else if (testFn(item)) {
112
-      callback(item);
113
-    }
114
-  }
115
-
116
-  // register standard components
117
-  _scanConfig(self.config, function(item) {
118
-    return standardComponents[item.type];
119
-  }, function(item) {
120
-    self.registerComponent(standardComponents[item.type]);
121
-  });
122
-
123
-  // validate config against teh use of appKeys
124
-  _scanConfig(self.config, function(item) {
125
-    return item.appKey;
126
-  }, function() {
127
-    throw new Error('appKeys are no longer supported. ' +
128
-                    'Please follow the migration guide to upgrade your project');
129
-  });
130
-}
131
-
132
-/**
133
- * Register a component to Clay.
134
- * @param {Object} component - the clay component to register
135
- * @param {string} component.name - the name of the component
136
- * @param {string} component.template - HTML template to use for the component
137
- * @param {string|Object} component.manipulator - methods to attach to the component
138
- * @param {function} component.manipulator.set - set manipulator method
139
- * @param {function} component.manipulator.get - get manipulator method
140
- * @param {Object} [component.defaults] - template defaults
141
- * @param {function} [component.initialize] - method to scaffold the component
142
- * @return {boolean} - Returns true if component was registered correctly
143
- */
144
-Clay.prototype.registerComponent = function(component) {
145
-  this.components[component.name] = component;
146
-};
147
-
148
-/**
149
- * Generate the Data URI used by the config Page with settings injected
150
- * @return {string}
151
- */
152
-Clay.prototype.generateUrl = function() {
153
-  var settings = {};
154
-  var emulator = !Pebble || Pebble.platform === 'pypkjs';
155
-  var returnTo = emulator ? '$$$RETURN_TO$$$' : 'pebblejs://close#';
156
-
157
-  try {
158
-    settings = JSON.parse(localStorage.getItem('clay-settings')) || {};
159
-  } catch (e) {
160
-    console.error(e.toString());
161
-  }
162
-
163
-  var compiledHtml = configPageHtml
164
-    .replace('$$RETURN_TO$$', returnTo)
165
-    .replace('$$CUSTOM_FN$$', toSource(this.customFn))
166
-    .replace('$$CONFIG$$', toSource(this.config))
167
-    .replace('$$SETTINGS$$', toSource(settings))
168
-    .replace('$$COMPONENTS$$', toSource(this.components))
169
-    .replace('$$META$$', toSource(this.meta));
170
-
171
-  // if we are in the emulator then we need to proxy the data via a webpage to
172
-  // obtain the return_to.
173
-  // @todo calculate this from the Pebble object or something
174
-  if (emulator) {
175
-    return Clay.encodeDataUri(
176
-      compiledHtml,
177
-      'http://clay.pebble.com.s3-website-us-west-2.amazonaws.com/#'
178
-    );
179
-  }
180
-
181
-  return Clay.encodeDataUri(compiledHtml);
182
-};
183
-
184
-/**
185
- * Parse the response from the webviewclosed event data
186
- * @param {string} response
187
- * @param {boolean} [convert=true]
188
- * @returns {Object}
189
- */
190
-Clay.prototype.getSettings = function(response, convert) {
191
-  // Decode and parse config data as JSON
192
-  var settings = {};
193
-  response = response.match(/^\{/) ? response : decodeURIComponent(response);
194
-
195
-  try {
196
-    settings = JSON.parse(response);
197
-  } catch (e) {
198
-    throw new Error('The provided response was not valid JSON');
199
-  }
200
-
201
-  // flatten the settings for localStorage
202
-  var settingsStorage = {};
203
-  Object.keys(settings).forEach(function(key) {
204
-    if (typeof settings[key] === 'object' && settings[key]) {
205
-      settingsStorage[key] = settings[key].value;
206
-    } else {
207
-      settingsStorage[key] = settings[key];
208
-    }
209
-  });
210
-
211
-  localStorage.setItem('clay-settings', JSON.stringify(settingsStorage));
212
-
213
-  return convert === false ? settings : Clay.prepareSettingsForAppMessage(settings);
214
-};
215
-
216
-/**
217
- * Updates the settings with the given value(s).
218
- *
219
- * @signature `clay.setSettings(key, value)`
220
- * @param {String} key - The property to set.
221
- * @param {*} value - the value assigned to _key_.
222
- * @return {undefined}
223
- *
224
- * @signature `clay.setSettings(settings)`
225
- * @param {Object} settings - an object containing the key/value pairs to be set.
226
- * @return {undefined}
227
- */
228
-Clay.prototype.setSettings = function(key, value) {
229
-  var settingsStorage = {};
230
-
231
-  try {
232
-    settingsStorage = JSON.parse(localStorage.getItem('clay-settings')) || {};
233
-  } catch (e) {
234
-    console.error(e.toString());
235
-  }
236
-
237
-  if (typeof key === 'object') {
238
-    var settings = key;
239
-    Object.keys(settings).forEach(function(key) {
240
-      settingsStorage[key] = settings[key];
241
-    });
242
-  } else {
243
-    settingsStorage[key] = value;
244
-  }
245
-
246
-  localStorage.setItem('clay-settings', JSON.stringify(settingsStorage));
247
-};
248
-
249
-/**
250
- * @param {string} input
251
- * @param {string} [prefix='data:text/html;charset=utf-8,']
252
- * @returns {string}
253
- */
254
-Clay.encodeDataUri = function(input, prefix) {
255
-  prefix = typeof prefix !== 'undefined' ? prefix : 'data:text/html;charset=utf-8,';
256
-  return prefix + encodeURIComponent(input);
257
-};
258
-
259
-/**
260
- * Converts the val into a type compatible with Pebble.sendAppMessage().
261
- *  - Strings will be returned without modification
262
- *  - Numbers will be returned without modification
263
- *  - Booleans will be converted to a 0 or 1
264
- *  - Arrays that contain strings will be returned without modification
265
- *    eg: ['one', 'two'] becomes ['one', 'two']
266
- *  - Arrays that contain numbers will be returned without modification
267
- *    eg: [1, 2] becomes [1, 2]
268
- *  - Arrays that contain booleans will be converted to a 0 or 1
269
- *    eg: [true, false] becomes [1, 0]
270
- *  - Arrays must be single dimensional
271
- *  - Objects that have a "value" property will apply the above rules to the type of
272
- *    value. If the value is a number or an array of numbers and the optional
273
- *    property: "precision" is provided, then the number will be multiplied by 10 to
274
- *    the power of precision (value * 10 ^ precision) and then floored.
275
- *    Eg: 1.4567 with a precision set to 3 will become 1456
276
- * @param {number|string|boolean|Array|Object} val
277
- * @param {number|string|boolean|Array} val.value
278
- * @param {number|undefined} [val.precision=0]
279
- * @returns {number|string|Array}
280
- */
281
-Clay.prepareForAppMessage = function(val) {
282
-
283
-  /**
284
-   * moves the decimal place of a number by precision then drop any remaining decimal
285
-   * places.
286
-   * @param {number} number
287
-   * @param {number} precision - number of decimal places to move
288
-   * @returns {number}
289
-   * @private
290
-   */
291
-  function _normalizeToPrecision(number, precision) {
292
-    return Math.floor(number * Math.pow(10, precision || 0));
293
-  }
294
-
295
-  var result;
296
-
297
-  if (Array.isArray(val)) {
298
-    result = [];
299
-    val.forEach(function(item, index) {
300
-      result[index] = Clay.prepareForAppMessage(item);
301
-    });
302
-  } else if (typeof val === 'object' && val) {
303
-    if (typeof val.value === 'number') {
304
-      result = _normalizeToPrecision(val.value, val.precision);
305
-    } else if (Array.isArray(val.value)) {
306
-      result = val.value.map(function(item) {
307
-        if (typeof item === 'number') {
308
-          return _normalizeToPrecision(item, val.precision);
309
-        }
310
-        return item;
311
-      });
312
-    } else {
313
-      result = Clay.prepareForAppMessage(val.value);
314
-    }
315
-  } else if (typeof val === 'boolean') {
316
-    result = val ? 1 : 0;
317
-  } else {
318
-    result = val;
319
-  }
320
-
321
-  return result;
322
-};
323
-
324
-/**
325
- * Converts a Clay settings dict into one that is compatible with
326
- * Pebble.sendAppMessage(); It also uses the provided messageKeys to correctly
327
- * assign arrays into individual keys
328
- * @see {prepareForAppMessage}
329
- * @param {Object} settings
330
- * @returns {{}}
331
- */
332
-Clay.prepareSettingsForAppMessage = function(settings) {
333
-
334
-  // flatten settings
335
-  var flatSettings = {};
336
-  Object.keys(settings).forEach(function(key) {
337
-    var val = settings[key];
338
-    var matches = key.match(/(.+?)(?:\[(\d*)\])?$/);
339
-
340
-    if (!matches[2]) {
341
-      flatSettings[key] = val;
342
-      return;
343
-    }
344
-
345
-    var position = parseInt(matches[2], 10);
346
-    key = matches[1];
347
-
348
-    if (typeof flatSettings[key] === 'undefined') {
349
-      flatSettings[key] = [];
350
-    }
351
-
352
-    flatSettings[key][position] = val;
353
-  });
354
-
355
-  var result = {};
356
-  Object.keys(flatSettings).forEach(function(key) {
357
-    var messageKey = messageKeys[key];
358
-    var settingArr = Clay.prepareForAppMessage(flatSettings[key]);
359
-    settingArr = Array.isArray(settingArr) ? settingArr : [settingArr];
360
-
361
-    settingArr.forEach(function(setting, index) {
362
-      result[messageKey + index] = setting;
363
-    });
364
-  });
365
-
366
-  // validate the settings
367
-  Object.keys(result).forEach(function(key) {
368
-    if (Array.isArray(result[key])) {
369
-      throw new Error('Clay does not support 2 dimensional arrays for item ' +
370
-                      'values. Make sure you are not attempting to use array ' +
371
-                      'syntax (eg: "myMessageKey[2]") in the messageKey for ' +
372
-                      'components that return an array, such as a checkboxgroup');
373
-    }
374
-  });
375
-
376
-  return result;
377
-};
378
-
379
-module.exports = Clay;
1
+'use strict';
2
+
3
+var configPageHtml = require('./tmp/config-page.html');
4
+var toSource = require('tosource');
5
+var standardComponents = require('./src/scripts/components');
6
+var deepcopy = require('deepcopy/build/deepcopy.min');
7
+var version = require('./package.json').version;
8
+var messageKeys = require('message_keys');
9
+
10
+/**
11
+ * @param {Array} config - the Clay config
12
+ * @param {function} [customFn] - Custom code to run from the config page. Will run
13
+ *   with the ClayConfig instance as context
14
+ * @param {Object} [options] - Additional options to pass to Clay
15
+ * @param {boolean} [options.autoHandleEvents=true] - If false, Clay will not
16
+ *   automatically handle the 'showConfiguration' and 'webviewclosed' events
17
+ * @param {*} [options.userData={}] - Arbitrary data to pass to the config page. Will
18
+ *   be available as `clayConfig.meta.userData`
19
+ * @constructor
20
+ */
21
+function Clay(config, customFn, options) {
22
+  var self = this;
23
+
24
+  if (!Array.isArray(config)) {
25
+    throw new Error('config must be an Array');
26
+  }
27
+
28
+  if (customFn && typeof customFn !== 'function') {
29
+    throw new Error('customFn must be a function or "null"');
30
+  }
31
+
32
+  options = options || {};
33
+
34
+  self.config = deepcopy(config);
35
+  self.customFn = customFn || function() {};
36
+  self.components = {};
37
+  self.meta = {
38
+    activeWatchInfo: null,
39
+    accountToken: '',
40
+    watchToken: '',
41
+    userData: {}
42
+  };
43
+  self.version = version;
44
+
45
+  /**
46
+   * Populate the meta with data from the Pebble object. Make sure to run this inside
47
+   * either the "showConfiguration" or "ready" event handler
48
+   * @return {void}
49
+   */
50
+  function _populateMeta() {
51
+    self.meta = {
52
+      activeWatchInfo: Pebble.getActiveWatchInfo && Pebble.getActiveWatchInfo(),
53
+      accountToken: Pebble.getAccountToken(),
54
+      watchToken: Pebble.getWatchToken(),
55
+      userData: deepcopy(options.userData || {})
56
+    };
57
+  }
58
+
59
+  // Let Clay handle all the magic
60
+  if (options.autoHandleEvents !== false && typeof Pebble !== 'undefined') {
61
+
62
+    Pebble.addEventListener('showConfiguration', function() {
63
+      _populateMeta();
64
+      Pebble.openURL(self.generateUrl());
65
+    });
66
+
67
+    Pebble.addEventListener('webviewclosed', function(e) {
68
+
69
+      if (!e || !e.response) { return; }
70
+
71
+      // Send settings to Pebble watchapp
72
+      Pebble.sendAppMessage(self.getSettings(e.response), function() {
73
+        console.log('Sent config data to Pebble');
74
+      }, function(error) {
75
+        console.log('Failed to send config data!');
76
+        console.log(JSON.stringify(error));
77
+      });
78
+    });
79
+  } else if (typeof Pebble !== 'undefined') {
80
+    Pebble.addEventListener('ready', function() {
81
+      _populateMeta();
82
+    });
83
+  }
84
+
85
+  /**
86
+   * If this function returns true then the callback will be executed
87
+   * @callback _scanConfig_testFn
88
+   * @param {Clay~ConfigItem} item
89
+   */
90
+
91
+  /**
92
+   * @callback _scanConfig_callback
93
+   * @param {Clay~ConfigItem} item
94
+   */
95
+
96
+  /**
97
+   * Scan over the config and run the callback if the testFn resolves to true
98
+   * @private
99
+   * @param {Clay~ConfigItem|Array} item
100
+   * @param {_scanConfig_testFn} testFn
101
+   * @param {_scanConfig_callback} callback
102
+   * @return {void}
103
+   */
104
+  function _scanConfig(item, testFn, callback) {
105
+    if (Array.isArray(item)) {
106
+      item.forEach(function(item) {
107
+        _scanConfig(item, testFn, callback);
108
+      });
109
+    } else if (item.type === 'section') {
110
+      _scanConfig(item.items, testFn, callback);
111
+    } else if (testFn(item)) {
112
+      callback(item);
113
+    }
114
+  }
115
+
116
+  // register standard components
117
+  _scanConfig(self.config, function(item) {
118
+    return standardComponents[item.type];
119
+  }, function(item) {
120
+    self.registerComponent(standardComponents[item.type]);
121
+  });
122
+
123
+  // validate config against teh use of appKeys
124
+  _scanConfig(self.config, function(item) {
125
+    return item.appKey;
126
+  }, function() {
127
+    throw new Error('appKeys are no longer supported. ' +
128
+                    'Please follow the migration guide to upgrade your project');
129
+  });
130
+}
131
+
132
+/**
133
+ * Register a component to Clay.
134
+ * @param {Object} component - the clay component to register
135
+ * @param {string} component.name - the name of the component
136
+ * @param {string} component.template - HTML template to use for the component
137
+ * @param {string|Object} component.manipulator - methods to attach to the component
138
+ * @param {function} component.manipulator.set - set manipulator method
139
+ * @param {function} component.manipulator.get - get manipulator method
140
+ * @param {Object} [component.defaults] - template defaults
141
+ * @param {function} [component.initialize] - method to scaffold the component
142
+ * @return {boolean} - Returns true if component was registered correctly
143
+ */
144
+Clay.prototype.registerComponent = function(component) {
145
+  this.components[component.name] = component;
146
+};
147
+
148
+/**
149
+ * Generate the Data URI used by the config Page with settings injected
150
+ * @return {string}
151
+ */
152
+Clay.prototype.generateUrl = function() {
153
+  var settings = {};
154
+  var emulator = !Pebble || Pebble.platform === 'pypkjs';
155
+  var returnTo = emulator ? '$$$RETURN_TO$$$' : 'pebblejs://close#';
156
+
157
+  try {
158
+    settings = JSON.parse(localStorage.getItem('clay-settings')) || {};
159
+  } catch (e) {
160
+    console.error(e.toString());
161
+  }
162
+
163
+  var compiledHtml = configPageHtml
164
+    .replace('$$RETURN_TO$$', returnTo)
165
+    .replace('$$CUSTOM_FN$$', toSource(this.customFn))
166
+    .replace('$$CONFIG$$', toSource(this.config))
167
+    .replace('$$SETTINGS$$', toSource(settings))
168
+    .replace('$$COMPONENTS$$', toSource(this.components))
169
+    .replace('$$META$$', toSource(this.meta));
170
+
171
+  // if we are in the emulator then we need to proxy the data via a webpage to
172
+  // obtain the return_to.
173
+  // @todo calculate this from the Pebble object or something
174
+  if (emulator) {
175
+    return Clay.encodeDataUri(
176
+      compiledHtml,
177
+      'http://clay.pebble.com.s3-website-us-west-2.amazonaws.com/#'
178
+    );
179
+  }
180
+
181
+  return Clay.encodeDataUri(compiledHtml);
182
+};
183
+
184
+/**
185
+ * Parse the response from the webviewclosed event data
186
+ * @param {string} response
187
+ * @param {boolean} [convert=true]
188
+ * @returns {Object}
189
+ */
190
+Clay.prototype.getSettings = function(response, convert) {
191
+  // Decode and parse config data as JSON
192
+  var settings = {};
193
+  response = response.match(/^\{/) ? response : decodeURIComponent(response);
194
+
195
+  try {
196
+    settings = JSON.parse(response);
197
+  } catch (e) {
198
+    throw new Error('The provided response was not valid JSON');
199
+  }
200
+
201
+  // flatten the settings for localStorage
202
+  var settingsStorage = {};
203
+  Object.keys(settings).forEach(function(key) {
204
+    if (typeof settings[key] === 'object' && settings[key]) {
205
+      settingsStorage[key] = settings[key].value;
206
+    } else {
207
+      settingsStorage[key] = settings[key];
208
+    }
209
+  });
210
+
211
+  localStorage.setItem('clay-settings', JSON.stringify(settingsStorage));
212
+
213
+  return convert === false ? settings : Clay.prepareSettingsForAppMessage(settings);
214
+};
215
+
216
+/**
217
+ * Updates the settings with the given value(s).
218
+ *
219
+ * @signature `clay.setSettings(key, value)`
220
+ * @param {String} key - The property to set.
221
+ * @param {*} value - the value assigned to _key_.
222
+ * @return {undefined}
223
+ *
224
+ * @signature `clay.setSettings(settings)`
225
+ * @param {Object} settings - an object containing the key/value pairs to be set.
226
+ * @return {undefined}
227
+ */
228
+Clay.prototype.setSettings = function(key, value) {
229
+  var settingsStorage = {};
230
+
231
+  try {
232
+    settingsStorage = JSON.parse(localStorage.getItem('clay-settings')) || {};
233
+  } catch (e) {
234
+    console.error(e.toString());
235
+  }
236
+
237
+  if (typeof key === 'object') {
238
+    var settings = key;
239
+    Object.keys(settings).forEach(function(key) {
240
+      settingsStorage[key] = settings[key];
241
+    });
242
+  } else {
243
+    settingsStorage[key] = value;
244
+  }
245
+
246
+  localStorage.setItem('clay-settings', JSON.stringify(settingsStorage));
247
+};
248
+
249
+/**
250
+ * @param {string} input
251
+ * @param {string} [prefix='data:text/html;charset=utf-8,']
252
+ * @returns {string}
253
+ */
254
+Clay.encodeDataUri = function(input, prefix) {
255
+  prefix = typeof prefix !== 'undefined' ? prefix : 'data:text/html;charset=utf-8,';
256
+  return prefix + encodeURIComponent(input);
257
+};
258
+
259
+/**
260
+ * Converts the val into a type compatible with Pebble.sendAppMessage().
261
+ *  - Strings will be returned without modification
262
+ *  - Numbers will be returned without modification
263
+ *  - Booleans will be converted to a 0 or 1
264
+ *  - Arrays that contain strings will be returned without modification
265
+ *    eg: ['one', 'two'] becomes ['one', 'two']
266
+ *  - Arrays that contain numbers will be returned without modification
267
+ *    eg: [1, 2] becomes [1, 2]
268
+ *  - Arrays that contain booleans will be converted to a 0 or 1
269
+ *    eg: [true, false] becomes [1, 0]
270
+ *  - Arrays must be single dimensional
271
+ *  - Objects that have a "value" property will apply the above rules to the type of
272
+ *    value. If the value is a number or an array of numbers and the optional
273
+ *    property: "precision" is provided, then the number will be multiplied by 10 to
274
+ *    the power of precision (value * 10 ^ precision) and then floored.
275
+ *    Eg: 1.4567 with a precision set to 3 will become 1456
276
+ * @param {number|string|boolean|Array|Object} val
277
+ * @param {number|string|boolean|Array} val.value
278
+ * @param {number|undefined} [val.precision=0]
279
+ * @returns {number|string|Array}
280
+ */
281
+Clay.prepareForAppMessage = function(val) {
282
+
283
+  /**
284
+   * moves the decimal place of a number by precision then drop any remaining decimal
285
+   * places.
286
+   * @param {number} number
287
+   * @param {number} precision - number of decimal places to move
288
+   * @returns {number}
289
+   * @private
290
+   */
291
+  function _normalizeToPrecision(number, precision) {
292
+    return Math.floor(number * Math.pow(10, precision || 0));
293
+  }
294
+
295
+  var result;
296
+
297
+  if (Array.isArray(val)) {
298
+    result = [];
299
+    val.forEach(function(item, index) {
300
+      result[index] = Clay.prepareForAppMessage(item);
301
+    });
302
+  } else if (typeof val === 'object' && val) {
303
+    if (typeof val.value === 'number') {
304
+      result = _normalizeToPrecision(val.value, val.precision);
305
+    } else if (Array.isArray(val.value)) {
306
+      result = val.value.map(function(item) {
307
+        if (typeof item === 'number') {
308
+          return _normalizeToPrecision(item, val.precision);
309
+        }
310
+        return item;
311
+      });
312
+    } else {
313
+      result = Clay.prepareForAppMessage(val.value);
314
+    }
315
+  } else if (typeof val === 'boolean') {
316
+    result = val ? 1 : 0;
317
+  } else {
318
+    result = val;
319
+  }
320
+
321
+  return result;
322
+};
323
+
324
+/**
325
+ * Converts a Clay settings dict into one that is compatible with
326
+ * Pebble.sendAppMessage(); It also uses the provided messageKeys to correctly
327
+ * assign arrays into individual keys
328
+ * @see {prepareForAppMessage}
329
+ * @param {Object} settings
330
+ * @returns {{}}
331
+ */
332
+Clay.prepareSettingsForAppMessage = function(settings) {
333
+
334
+  // flatten settings
335
+  var flatSettings = {};
336
+  Object.keys(settings).forEach(function(key) {
337
+    var val = settings[key];
338
+    var matches = key.match(/(.+?)(?:\[(\d*)\])?$/);
339
+
340
+    if (!matches[2]) {
341
+      flatSettings[key] = val;
342
+      return;
343
+    }
344
+
345
+    var position = parseInt(matches[2], 10);
346
+    key = matches[1];
347
+
348
+    if (typeof flatSettings[key] === 'undefined') {
349
+      flatSettings[key] = [];
350
+    }
351
+
352
+    flatSettings[key][position] = val;
353
+  });
354
+
355
+  var result = {};
356
+  Object.keys(flatSettings).forEach(function(key) {
357
+    var messageKey = messageKeys[key];
358
+    var settingArr = Clay.prepareForAppMessage(flatSettings[key]);
359
+    settingArr = Array.isArray(settingArr) ? settingArr : [settingArr];
360
+
361
+    settingArr.forEach(function(setting, index) {
362
+      result[messageKey + index] = setting;
363
+    });
364
+  });
365
+
366
+  // validate the settings
367
+  Object.keys(result).forEach(function(key) {
368
+    if (Array.isArray(result[key])) {
369
+      throw new Error('Clay does not support 2 dimensional arrays for item ' +
370
+                      'values. Make sure you are not attempting to use array ' +
371
+                      'syntax (eg: "myMessageKey[2]") in the messageKey for ' +
372
+                      'components that return an array, such as a checkboxgroup');
373
+    }
374
+  });
375
+
376
+  return result;
377
+};
378
+
379
+module.exports = Clay;
Browse code

init commit

louis.jonget authored on30/09/2022 18:58:18
Showing1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,379 @@
1
+'use strict';
2
+
3
+var configPageHtml = require('./tmp/config-page.html');
4
+var toSource = require('tosource');
5
+var standardComponents = require('./src/scripts/components');
6
+var deepcopy = require('deepcopy/build/deepcopy.min');
7
+var version = require('./package.json').version;
8
+var messageKeys = require('message_keys');
9
+
10
+/**
11
+ * @param {Array} config - the Clay config
12
+ * @param {function} [customFn] - Custom code to run from the config page. Will run
13
+ *   with the ClayConfig instance as context
14
+ * @param {Object} [options] - Additional options to pass to Clay
15
+ * @param {boolean} [options.autoHandleEvents=true] - If false, Clay will not
16
+ *   automatically handle the 'showConfiguration' and 'webviewclosed' events
17
+ * @param {*} [options.userData={}] - Arbitrary data to pass to the config page. Will
18
+ *   be available as `clayConfig.meta.userData`
19
+ * @constructor
20
+ */
21
+function Clay(config, customFn, options) {
22
+  var self = this;
23
+
24
+  if (!Array.isArray(config)) {
25
+    throw new Error('config must be an Array');
26
+  }
27
+
28
+  if (customFn && typeof customFn !== 'function') {
29
+    throw new Error('customFn must be a function or "null"');
30
+  }
31
+
32
+  options = options || {};
33
+
34
+  self.config = deepcopy(config);
35
+  self.customFn = customFn || function() {};
36
+  self.components = {};
37
+  self.meta = {
38
+    activeWatchInfo: null,
39
+    accountToken: '',
40
+    watchToken: '',
41
+    userData: {}
42
+  };
43
+  self.version = version;
44
+
45
+  /**
46
+   * Populate the meta with data from the Pebble object. Make sure to run this inside
47
+   * either the "showConfiguration" or "ready" event handler
48
+   * @return {void}
49
+   */
50
+  function _populateMeta() {
51
+    self.meta = {
52
+      activeWatchInfo: Pebble.getActiveWatchInfo && Pebble.getActiveWatchInfo(),
53
+      accountToken: Pebble.getAccountToken(),
54
+      watchToken: Pebble.getWatchToken(),
55
+      userData: deepcopy(options.userData || {})
56
+    };
57
+  }
58
+
59
+  // Let Clay handle all the magic
60
+  if (options.autoHandleEvents !== false && typeof Pebble !== 'undefined') {
61
+
62
+    Pebble.addEventListener('showConfiguration', function() {
63
+      _populateMeta();
64
+      Pebble.openURL(self.generateUrl());
65
+    });
66
+
67
+    Pebble.addEventListener('webviewclosed', function(e) {
68
+
69
+      if (!e || !e.response) { return; }
70
+
71
+      // Send settings to Pebble watchapp
72
+      Pebble.sendAppMessage(self.getSettings(e.response), function() {
73
+        console.log('Sent config data to Pebble');
74
+      }, function(error) {
75
+        console.log('Failed to send config data!');
76
+        console.log(JSON.stringify(error));
77
+      });
78
+    });
79
+  } else if (typeof Pebble !== 'undefined') {
80
+    Pebble.addEventListener('ready', function() {
81
+      _populateMeta();
82
+    });
83
+  }
84
+
85
+  /**
86
+   * If this function returns true then the callback will be executed
87
+   * @callback _scanConfig_testFn
88
+   * @param {Clay~ConfigItem} item
89
+   */
90
+
91
+  /**
92
+   * @callback _scanConfig_callback
93
+   * @param {Clay~ConfigItem} item
94
+   */
95
+
96
+  /**
97
+   * Scan over the config and run the callback if the testFn resolves to true
98
+   * @private
99
+   * @param {Clay~ConfigItem|Array} item
100
+   * @param {_scanConfig_testFn} testFn
101
+   * @param {_scanConfig_callback} callback
102
+   * @return {void}
103
+   */
104
+  function _scanConfig(item, testFn, callback) {
105
+    if (Array.isArray(item)) {
106
+      item.forEach(function(item) {
107
+        _scanConfig(item, testFn, callback);
108
+      });
109
+    } else if (item.type === 'section') {
110
+      _scanConfig(item.items, testFn, callback);
111
+    } else if (testFn(item)) {
112
+      callback(item);
113
+    }
114
+  }
115
+
116
+  // register standard components
117
+  _scanConfig(self.config, function(item) {
118
+    return standardComponents[item.type];
119
+  }, function(item) {
120
+    self.registerComponent(standardComponents[item.type]);
121
+  });
122
+
123
+  // validate config against teh use of appKeys
124
+  _scanConfig(self.config, function(item) {
125
+    return item.appKey;
126
+  }, function() {
127
+    throw new Error('appKeys are no longer supported. ' +
128
+                    'Please follow the migration guide to upgrade your project');
129
+  });
130
+}
131
+
132
+/**
133
+ * Register a component to Clay.
134
+ * @param {Object} component - the clay component to register
135
+ * @param {string} component.name - the name of the component
136
+ * @param {string} component.template - HTML template to use for the component
137
+ * @param {string|Object} component.manipulator - methods to attach to the component
138
+ * @param {function} component.manipulator.set - set manipulator method
139
+ * @param {function} component.manipulator.get - get manipulator method
140
+ * @param {Object} [component.defaults] - template defaults
141
+ * @param {function} [component.initialize] - method to scaffold the component
142
+ * @return {boolean} - Returns true if component was registered correctly
143
+ */
144
+Clay.prototype.registerComponent = function(component) {
145
+  this.components[component.name] = component;
146
+};
147
+
148
+/**
149
+ * Generate the Data URI used by the config Page with settings injected
150
+ * @return {string}
151
+ */
152
+Clay.prototype.generateUrl = function() {
153
+  var settings = {};
154
+  var emulator = !Pebble || Pebble.platform === 'pypkjs';
155
+  var returnTo = emulator ? '$$$RETURN_TO$$$' : 'pebblejs://close#';
156
+
157
+  try {
158
+    settings = JSON.parse(localStorage.getItem('clay-settings')) || {};
159
+  } catch (e) {
160
+    console.error(e.toString());
161
+  }
162
+
163
+  var compiledHtml = configPageHtml
164
+    .replace('$$RETURN_TO$$', returnTo)
165
+    .replace('$$CUSTOM_FN$$', toSource(this.customFn))
166
+    .replace('$$CONFIG$$', toSource(this.config))
167
+    .replace('$$SETTINGS$$', toSource(settings))
168
+    .replace('$$COMPONENTS$$', toSource(this.components))
169
+    .replace('$$META$$', toSource(this.meta));
170
+
171
+  // if we are in the emulator then we need to proxy the data via a webpage to
172
+  // obtain the return_to.
173
+  // @todo calculate this from the Pebble object or something
174
+  if (emulator) {
175
+    return Clay.encodeDataUri(
176
+      compiledHtml,
177
+      'http://clay.pebble.com.s3-website-us-west-2.amazonaws.com/#'
178
+    );
179
+  }
180
+
181
+  return Clay.encodeDataUri(compiledHtml);
182
+};
183
+
184
+/**
185
+ * Parse the response from the webviewclosed event data
186
+ * @param {string} response
187
+ * @param {boolean} [convert=true]
188
+ * @returns {Object}
189
+ */
190
+Clay.prototype.getSettings = function(response, convert) {
191
+  // Decode and parse config data as JSON
192
+  var settings = {};
193
+  response = response.match(/^\{/) ? response : decodeURIComponent(response);
194
+
195
+  try {
196
+    settings = JSON.parse(response);
197
+  } catch (e) {
198
+    throw new Error('The provided response was not valid JSON');
199
+  }
200
+
201
+  // flatten the settings for localStorage
202
+  var settingsStorage = {};
203
+  Object.keys(settings).forEach(function(key) {
204
+    if (typeof settings[key] === 'object' && settings[key]) {
205
+      settingsStorage[key] = settings[key].value;
206
+    } else {
207
+      settingsStorage[key] = settings[key];
208
+    }
209
+  });
210
+
211
+  localStorage.setItem('clay-settings', JSON.stringify(settingsStorage));
212
+
213
+  return convert === false ? settings : Clay.prepareSettingsForAppMessage(settings);
214
+};
215
+
216
+/**
217
+ * Updates the settings with the given value(s).
218
+ *
219
+ * @signature `clay.setSettings(key, value)`
220
+ * @param {String} key - The property to set.
221
+ * @param {*} value - the value assigned to _key_.
222
+ * @return {undefined}
223
+ *
224
+ * @signature `clay.setSettings(settings)`
225
+ * @param {Object} settings - an object containing the key/value pairs to be set.
226
+ * @return {undefined}
227
+ */
228
+Clay.prototype.setSettings = function(key, value) {
229
+  var settingsStorage = {};
230
+
231
+  try {
232
+    settingsStorage = JSON.parse(localStorage.getItem('clay-settings')) || {};
233
+  } catch (e) {
234
+    console.error(e.toString());
235
+  }
236
+
237
+  if (typeof key === 'object') {
238
+    var settings = key;
239
+    Object.keys(settings).forEach(function(key) {
240
+      settingsStorage[key] = settings[key];
241
+    });
242
+  } else {
243
+    settingsStorage[key] = value;
244
+  }
245
+
246
+  localStorage.setItem('clay-settings', JSON.stringify(settingsStorage));
247
+};
248
+
249
+/**
250
+ * @param {string} input
251
+ * @param {string} [prefix='data:text/html;charset=utf-8,']
252
+ * @returns {string}
253
+ */
254
+Clay.encodeDataUri = function(input, prefix) {
255
+  prefix = typeof prefix !== 'undefined' ? prefix : 'data:text/html;charset=utf-8,';
256
+  return prefix + encodeURIComponent(input);
257
+};
258
+
259
+/**
260
+ * Converts the val into a type compatible with Pebble.sendAppMessage().
261
+ *  - Strings will be returned without modification
262
+ *  - Numbers will be returned without modification
263
+ *  - Booleans will be converted to a 0 or 1
264
+ *  - Arrays that contain strings will be returned without modification
265
+ *    eg: ['one', 'two'] becomes ['one', 'two']
266
+ *  - Arrays that contain numbers will be returned without modification
267
+ *    eg: [1, 2] becomes [1, 2]
268
+ *  - Arrays that contain booleans will be converted to a 0 or 1
269
+ *    eg: [true, false] becomes [1, 0]
270
+ *  - Arrays must be single dimensional
271
+ *  - Objects that have a "value" property will apply the above rules to the type of
272
+ *    value. If the value is a number or an array of numbers and the optional
273
+ *    property: "precision" is provided, then the number will be multiplied by 10 to
274
+ *    the power of precision (value * 10 ^ precision) and then floored.
275
+ *    Eg: 1.4567 with a precision set to 3 will become 1456
276
+ * @param {number|string|boolean|Array|Object} val
277
+ * @param {number|string|boolean|Array} val.value
278
+ * @param {number|undefined} [val.precision=0]
279
+ * @returns {number|string|Array}
280
+ */
281
+Clay.prepareForAppMessage = function(val) {
282
+
283
+  /**
284
+   * moves the decimal place of a number by precision then drop any remaining decimal
285
+   * places.
286
+   * @param {number} number
287
+   * @param {number} precision - number of decimal places to move
288
+   * @returns {number}
289
+   * @private
290
+   */
291
+  function _normalizeToPrecision(number, precision) {
292
+    return Math.floor(number * Math.pow(10, precision || 0));
293
+  }
294
+
295
+  var result;
296
+
297
+  if (Array.isArray(val)) {
298
+    result = [];
299
+    val.forEach(function(item, index) {
300
+      result[index] = Clay.prepareForAppMessage(item);
301
+    });
302
+  } else if (typeof val === 'object' && val) {
303
+    if (typeof val.value === 'number') {
304
+      result = _normalizeToPrecision(val.value, val.precision);
305
+    } else if (Array.isArray(val.value)) {
306
+      result = val.value.map(function(item) {
307
+        if (typeof item === 'number') {
308
+          return _normalizeToPrecision(item, val.precision);
309
+        }
310
+        return item;
311
+      });
312
+    } else {
313
+      result = Clay.prepareForAppMessage(val.value);
314
+    }
315
+  } else if (typeof val === 'boolean') {
316
+    result = val ? 1 : 0;
317
+  } else {
318
+    result = val;
319
+  }
320
+
321
+  return result;
322
+};
323
+
324
+/**
325
+ * Converts a Clay settings dict into one that is compatible with
326
+ * Pebble.sendAppMessage(); It also uses the provided messageKeys to correctly
327
+ * assign arrays into individual keys
328
+ * @see {prepareForAppMessage}
329
+ * @param {Object} settings
330
+ * @returns {{}}
331
+ */
332
+Clay.prepareSettingsForAppMessage = function(settings) {
333
+
334
+  // flatten settings
335
+  var flatSettings = {};
336
+  Object.keys(settings).forEach(function(key) {
337
+    var val = settings[key];
338
+    var matches = key.match(/(.+?)(?:\[(\d*)\])?$/);
339
+
340
+    if (!matches[2]) {
341
+      flatSettings[key] = val;
342
+      return;
343
+    }
344
+
345
+    var position = parseInt(matches[2], 10);
346
+    key = matches[1];
347
+
348
+    if (typeof flatSettings[key] === 'undefined') {
349
+      flatSettings[key] = [];
350
+    }
351
+
352
+    flatSettings[key][position] = val;
353
+  });
354
+
355
+  var result = {};
356
+  Object.keys(flatSettings).forEach(function(key) {
357
+    var messageKey = messageKeys[key];
358
+    var settingArr = Clay.prepareForAppMessage(flatSettings[key]);
359
+    settingArr = Array.isArray(settingArr) ? settingArr : [settingArr];
360
+
361
+    settingArr.forEach(function(setting, index) {
362
+      result[messageKey + index] = setting;
363
+    });
364
+  });
365
+
366
+  // validate the settings
367
+  Object.keys(result).forEach(function(key) {
368
+    if (Array.isArray(result[key])) {
369
+      throw new Error('Clay does not support 2 dimensional arrays for item ' +
370
+                      'values. Make sure you are not attempting to use array ' +
371
+                      'syntax (eg: "myMessageKey[2]") in the messageKey for ' +
372
+                      'components that return an array, such as a checkboxgroup');
373
+    }
374
+  });
375
+
376
+  return result;
377
+};
378
+
379
+module.exports = Clay;