mirror of
https://github.com/pebble-dev/clay.git
synced 2026-08-28 04:46:57 -04:00
346 lines
11 KiB
JavaScript
Executable File
346 lines
11 KiB
JavaScript
Executable File
'use strict';
|
|
|
|
var configPageHtml = require('./tmp/config-page.html');
|
|
var toSource = require('tosource');
|
|
var standardComponents = require('./src/scripts/components');
|
|
var deepcopy = require('deepcopy/build/deepcopy.min');
|
|
var version = require('./package.json').version;
|
|
var simpleAppMessage = require('@keegan-stoneware/simple-app-message');
|
|
var utils = require('./src/scripts/lib/utils');
|
|
|
|
/**
|
|
* @param {Object} options - Clay options
|
|
* @param {Array} [options.config] - The config
|
|
* @param {function} [options.openCallback]
|
|
* @param {function} [options.closedCallback]
|
|
* @param {Object} [options.locales]
|
|
* @param {function} [options.customFunction] - Custom code to run from the config
|
|
* page. Will run with the ClayConfig instance as context
|
|
* @constructor
|
|
*/
|
|
function Clay(options) {
|
|
var self = this;
|
|
|
|
if (!options) {
|
|
utils.throw('You must define options');
|
|
}
|
|
|
|
options = deepcopy(options);
|
|
|
|
self.config = options.config;
|
|
self.customFn = options.customFn || function() {};
|
|
self.components = {};
|
|
self.meta = {
|
|
activeWatchInfo: null,
|
|
accountToken: '',
|
|
watchToken: '',
|
|
userData: {}
|
|
};
|
|
self.version = version;
|
|
|
|
/**
|
|
* Populate the meta with data from the Pebble object. Make sure to run this inside
|
|
* either the "showConfiguration" or "ready" event handler
|
|
* @return {void}
|
|
*/
|
|
function _populateMeta() {
|
|
self.meta = {
|
|
activeWatchInfo: Pebble.getActiveWatchInfo && Pebble.getActiveWatchInfo(),
|
|
accountToken: Pebble.getAccountToken(),
|
|
watchToken: Pebble.getWatchToken(),
|
|
userData: deepcopy(options.userData || {})
|
|
};
|
|
}
|
|
|
|
// Let Clay handle all the magic
|
|
Pebble.addEventListener('showConfiguration', function() {
|
|
_populateMeta();
|
|
Pebble.openURL(self.generateUrl());
|
|
});
|
|
|
|
Pebble.addEventListener('webviewclosed', function(e) {
|
|
|
|
if (!e || !e.response) { return; }
|
|
|
|
var settings = self.getSettings(e.response, false);
|
|
|
|
if (options.closedCallback) {
|
|
options.closedCallback(settings);
|
|
}
|
|
|
|
simpleAppMessage.send('CLAY', settings, function(response) {
|
|
if (response.error) {
|
|
utils.log('Failed to send config data!');
|
|
utils.log(response.error);
|
|
} else {
|
|
utils.log('Sent config data to Pebble');
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* If this function returns true then the callback will be executed
|
|
* @callback _scanConfig_testFn
|
|
* @param {Clay~ConfigItem} item
|
|
*/
|
|
|
|
/**
|
|
* @callback _scanConfig_callback
|
|
* @param {Clay~ConfigItem} item
|
|
*/
|
|
|
|
/**
|
|
* Scan over the config and run the callback if the testFn resolves to true
|
|
* @private
|
|
* @param {Clay~ConfigItem|Array} item
|
|
* @param {_scanConfig_testFn} testFn
|
|
* @param {_scanConfig_callback} callback
|
|
* @return {void}
|
|
*/
|
|
function _scanConfig(item, testFn, callback) {
|
|
if (Array.isArray(item)) {
|
|
item.forEach(function(item) {
|
|
_scanConfig(item, testFn, callback);
|
|
});
|
|
} else if (item.type === 'section') {
|
|
_scanConfig(item.items, testFn, callback);
|
|
} else if (testFn(item)) {
|
|
callback(item);
|
|
}
|
|
}
|
|
|
|
// register standard components
|
|
_scanConfig(self.config, function(item) {
|
|
return standardComponents[item.type];
|
|
}, function(item) {
|
|
self.registerComponent(standardComponents[item.type]);
|
|
});
|
|
|
|
// validate config against teh use of appKeys
|
|
_scanConfig(self.config, function(item) {
|
|
return item.appKey;
|
|
}, function() {
|
|
throw new Error('appKeys are no longer supported. ' +
|
|
'Please follow the migration guide to upgrade your project');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Register a component to Clay.
|
|
* @param {Object} component - the clay component to register
|
|
* @param {string} component.name - the name of the component
|
|
* @param {string} component.template - HTML template to use for the component
|
|
* @param {string|Object} component.manipulator - methods to attach to the component
|
|
* @param {function} component.manipulator.set - set manipulator method
|
|
* @param {function} component.manipulator.get - get manipulator method
|
|
* @param {Object} [component.defaults] - template defaults
|
|
* @param {function} [component.initialize] - method to scaffold the component
|
|
* @return {boolean} - Returns true if component was registered correctly
|
|
*/
|
|
Clay.prototype.registerComponent = function(component) {
|
|
this.components[component.name] = component;
|
|
};
|
|
|
|
/**
|
|
* Generate the Data URI used by the config Page with settings injected
|
|
* @return {string}
|
|
*/
|
|
Clay.prototype.generateUrl = function() {
|
|
var settings = {};
|
|
var emulator = !Pebble || Pebble.platform === 'pypkjs';
|
|
var returnTo = emulator ? '$$$RETURN_TO$$$' : 'pebblejs://close#';
|
|
|
|
try {
|
|
settings = JSON.parse(localStorage.getItem('clay-settings')) || {};
|
|
} catch (e) {
|
|
console.error(e.toString());
|
|
}
|
|
|
|
var compiledHtml = configPageHtml
|
|
.replace('$$RETURN_TO$$', returnTo)
|
|
.replace('$$CUSTOM_FN$$', toSource(this.customFn))
|
|
.replace('$$CONFIG$$', toSource(this.config))
|
|
.replace('$$SETTINGS$$', toSource(settings))
|
|
.replace('$$COMPONENTS$$', toSource(this.components))
|
|
.replace('$$META$$', toSource(this.meta));
|
|
|
|
// if we are in the emulator then we need to proxy the data via a webpage to
|
|
// obtain the return_to.
|
|
// @todo calculate this from the Pebble object or something
|
|
if (emulator) {
|
|
return Clay.encodeDataUri(
|
|
compiledHtml,
|
|
'http://clay.pebble.com.s3-website-us-west-2.amazonaws.com/#'
|
|
);
|
|
}
|
|
|
|
return Clay.encodeDataUri(compiledHtml);
|
|
};
|
|
|
|
/**
|
|
* Parse the response from the webviewclosed event data
|
|
* @param {string} response
|
|
* @param {boolean} [convert=true]
|
|
* @returns {Object}
|
|
*/
|
|
Clay.prototype.getSettings = function(response, convert) {
|
|
// Decode and parse config data as JSON
|
|
var settings = {};
|
|
response = response.match(/^\{/) ? response : decodeURIComponent(response);
|
|
|
|
try {
|
|
settings = JSON.parse(response);
|
|
} catch (e) {
|
|
utils.error('The provided response was not valid JSON');
|
|
}
|
|
|
|
// @todo do something with the persist flag here
|
|
// flatten the settings for localStorage
|
|
var settingsStorage = {};
|
|
Object.keys(settings).forEach(function(key) {
|
|
if (typeof settings[key] === 'object' && settings[key]) {
|
|
settingsStorage[key] = settings[key].value;
|
|
} else {
|
|
settingsStorage[key] = settings[key];
|
|
}
|
|
});
|
|
|
|
localStorage.setItem('clay-settings', JSON.stringify(settingsStorage));
|
|
|
|
return convert === false ? settingsStorage : Clay.prepareSettingsForAppMessage(settings);
|
|
};
|
|
|
|
/**
|
|
* @param {string} input
|
|
* @param {string} [prefix='data:text/html;charset=utf-8,']
|
|
* @returns {string}
|
|
*/
|
|
Clay.encodeDataUri = function(input, prefix) {
|
|
prefix = typeof prefix !== 'undefined' ? prefix : 'data:text/html;charset=utf-8,';
|
|
return prefix + encodeURIComponent(input);
|
|
};
|
|
|
|
/**
|
|
* Converts the val into a type compatible with Pebble.sendAppMessage().
|
|
* - Strings will be returned without modification
|
|
* - Numbers will be returned without modification
|
|
* - Booleans will be converted to a 0 or 1
|
|
* - Arrays that contain strings will be returned without modification
|
|
* eg: ['one', 'two'] becomes ['one', 'two']
|
|
* - Arrays that contain numbers will be returned without modification
|
|
* eg: [1, 2] becomes [1, 2]
|
|
* - Arrays that contain booleans will be converted to a 0 or 1
|
|
* eg: [true, false] becomes [1, 0]
|
|
* - Arrays must be single dimensional
|
|
* - Objects that have a "value" property will apply the above rules to the type of
|
|
* value. If the value is a number or an array of numbers and the optional
|
|
* property: "precision" is provided, then the number will be multiplied by 10 to
|
|
* the power of precision (value * 10 ^ precision) and then floored.
|
|
* Eg: 1.4567 with a precision set to 3 will become 1456
|
|
* @param {number|string|boolean|Array|Object} val
|
|
* @param {number|string|boolean|Array} val.value
|
|
* @param {number|undefined} [val.precision=0]
|
|
* @returns {number|string|Array}
|
|
*/
|
|
Clay.prepareForAppMessage = function(val) {
|
|
|
|
/**
|
|
* moves the decimal place of a number by precision then drop any remaining decimal
|
|
* places.
|
|
* @param {number} number
|
|
* @param {number} precision - number of decimal places to move
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
function _normalizeToPrecision(number, precision) {
|
|
return Math.floor(number * Math.pow(10, precision || 0));
|
|
}
|
|
|
|
var result;
|
|
|
|
if (Array.isArray(val)) {
|
|
result = [];
|
|
val.forEach(function(item, index) {
|
|
result[index] = Clay.prepareForAppMessage(item);
|
|
});
|
|
} else if (typeof val === 'object' && val) {
|
|
if (typeof val.value === 'number') {
|
|
result = _normalizeToPrecision(val.value, val.precision);
|
|
} else if (Array.isArray(val.value)) {
|
|
result = val.value.map(function(item) {
|
|
if (typeof item === 'number') {
|
|
return _normalizeToPrecision(item, val.precision);
|
|
}
|
|
return item;
|
|
});
|
|
} else {
|
|
result = Clay.prepareForAppMessage(val.value);
|
|
}
|
|
} else if (typeof val === 'boolean') {
|
|
result = val ? 1 : 0;
|
|
} else {
|
|
result = val;
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Converts a Clay settings dict into one that is compatible with
|
|
* Pebble.sendAppMessage(); It also uses the provided messageKeys to correctly
|
|
* assign arrays into individual keys
|
|
* @see {prepareForAppMessage}
|
|
* @param {Object} settings
|
|
* @returns {{}}
|
|
*/
|
|
Clay.prepareSettingsForAppMessage = function(settings) {
|
|
|
|
// flatten settings
|
|
var flatSettings = {};
|
|
Object.keys(settings).forEach(function(key) {
|
|
var val = settings[key];
|
|
var matches = key.match(/(.+?)(?:\[(\d*)\])?$/);
|
|
|
|
if (!matches[2]) {
|
|
flatSettings[key] = val;
|
|
return;
|
|
}
|
|
|
|
var position = parseInt(matches[2], 10);
|
|
key = matches[1];
|
|
|
|
if (typeof flatSettings[key] === 'undefined') {
|
|
flatSettings[key] = [];
|
|
}
|
|
|
|
flatSettings[key][position] = val;
|
|
});
|
|
|
|
var result = {};
|
|
Object.keys(flatSettings).forEach(function(key) {
|
|
// var messageKey = messageKeys[key];
|
|
// var settingArr = Clay.prepareForAppMessage(flatSettings[key]);
|
|
// settingArr = Array.isArray(settingArr) ? settingArr : [settingArr];
|
|
//
|
|
// settingArr.forEach(function(setting, index) {
|
|
// result[messageKey + index] = setting;
|
|
// });
|
|
|
|
result[key] = Clay.prepareForAppMessage(flatSettings[key]);
|
|
});
|
|
|
|
// validate the settings
|
|
// Object.keys(result).forEach(function(key) {
|
|
// if (Array.isArray(result[key])) {
|
|
// throw new Error('Clay does not support 2 dimensional arrays for item ' +
|
|
// 'values. Make sure you are not attempting to use array ' +
|
|
// 'syntax (eg: "myMessageKey[2]") in the messageKey for ' +
|
|
// 'components that return an array, such as a checkboxgroup');
|
|
// }
|
|
// });
|
|
|
|
return result;
|
|
};
|
|
|
|
module.exports = Clay;
|