diff --git a/README.md b/README.md index 13a39f8..9ff7c1e 100755 --- a/README.md +++ b/README.md @@ -441,6 +441,49 @@ A list of options where a user may choose more than one option to submit. --- +### Range Slider + +**Manipulator:** [`slider`](#slider) + +The range slider is used to allow users to select numbers between two values. + +**NOTE** If you set the `step` property to anything less than `1`, +it will multiply the final value sent to the watch by 10 to the power of the number of decimal places in the value of `step`. +Eg: If you set the `step` to `0.25` and the slider has value of `34.5`, the watch will receive `3450`. +The reason why this is necessary, is because you can not send floats to the watch via `Pebble.sendAppMessage()`. +This allows your users to still be able to input values that have decimals, +you must just remember to divide the received value on the watch accordingly. + +##### Properties + +| Property | Type | Description | +|----------|------|-------------| +| type | string | Set to `slider`. | +| defaultValue | number | The value of the slider. | +| label | string | The label that should appear next to this item. | +| min | number | The minimum allowed value of the slider. Defaults to `100` | +| max | number | The maximum allowed value of the slider. Defaults to `0` | +| step | number | The multiple of the values allowed to be set on the slider. The slider will snap to these values. This value also determines the precision used when the value is sent to the watch. Defaults to 1 | +| attributes | object | An object containing HTML attributes to set on the input field. | +| description | string | Optional sub-text to include below the component | + +##### Example + +```javascript +{ + "type": "slider", + "appKey": "slider", + "defaultValue": 15, + "label": "Slider", + "description": "This is the description for the slider", + "min": 10, + "max": 20, + "step": 0.25 +}, +``` + +--- + ### Submit **Manipulator:** [`button`](#button) @@ -468,7 +511,6 @@ The submit button for the page. You **MUST** include this component somewhere in ### Coming Soon -- Range Slider - Tabs - Footer - Dynamic + draggable list @@ -562,6 +604,17 @@ Eg: If you run the `.show()` manipulator on an item that is already visible, the | `.hide()` | `ClayItem` | `hide` | Hides the item | | `.show()` | `ClayItem` | `show` | Shows the item | +#### slider + +| Method | Returns | Event Fired | Description | +|--------|---------|-------------| ------------| +| `.set( [number] value)` | `ClayItem` | `change` | Sets the value of this item. | +| `.get()` | `number` | | Gets the value of this item. | +| `.disable()` | `ClayItem` | `disabled` | Prevents this item from being edited by the user. | +| `.enable()` | `ClayItem` | `enabled` | Allows this item to be edited by the user. | +| `.hide()` | `ClayItem` | `hide` | Hides the item | +| `.show()` | `ClayItem` | `show` | Shows the item | + # Extending Clay Clay is built to allow developers to add their own basic interactivity to the config page. This can be done in a number of ways: @@ -711,7 +764,7 @@ This is the main way of talking to your generated config page. An instance of th | `.getItemByAppKey( [string] appKey )` | `ConfigItem\|undefined` - a single `ConfigItem` that has the provided `appKey`, otherwise `undefined`. | | `.getItemById( [string] id )` | `ConfigItem\|undefined` - a single `ConfigItem` that has the provided `id`, otherwise `undefined`. | | `.getItemsByType( [string] type )` | `Array.` - an array of config items that match the provided `type`. | -| `.getSettings()` | `Object` - an object representing all items with an `appKey` where the key is the `appKey` and the value is the result of running `.get()` on the Clay item. | +| `.serialize()` | `Object` - an object representing all items with an `appKey` where the key is the `appKey` and the value is an object with the `value` property set to the result of running `.get()` on the Clay item. If the Clay item has a `precision` property set, it is included in the object | | `.build()`
Builds the config page. Will dispatch the `BEFORE_BUILD` event prior to building the page, then the `AFTER_BUILD` event once it is complete. | `ClayConfig` | | `.on( [string] events, [function] handler )`
Register an event to the provided handler. The handler will be called with this instance of `ClayConfig` as the context. If you wish to register multiple events to the same handler, then separate the events with a space | `ClayConfig` | | `.off( [function] handler )`
Remove the given event handler. **NOTE:** This will remove the handler from all registered events. | `ClayConfig` | diff --git a/dev/config.js b/dev/config.js index 6ef7b4d..62416a2 100644 --- a/dev/config.js +++ b/dev/config.js @@ -38,8 +38,8 @@ module.exports = [ "label": "Slider", description: "This is the description for the slider", "min": 10, - "max": 18, - "step": 0.5 + "max": 20, + "step": 0.25 }, { "type": "toggle", diff --git a/index.js b/index.js index a77ec45..66f2e04 100755 --- a/index.js +++ b/index.js @@ -3,7 +3,6 @@ var configPageHtml = require('./tmp/config-page.html'); var toSource = require('tosource'); var standardComponents = require('./src/scripts/components'); -var utils = require('./src/scripts/lib/utils'); /** * @param {Array} config - the Clay config @@ -163,9 +162,19 @@ Clay.prototype.getSettings = function(response, convert) { throw new Error('The provided response was not valid JSON'); } - localStorage.setItem('clay-settings', JSON.stringify(settings)); + // 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]; + } + }); - return convert === false ? settings : utils.prepareSettingsForAppMessage(settings); + localStorage.setItem('clay-settings', JSON.stringify(settingsStorage)); + + return convert === false ? settings : Clay.prepareSettingsForAppMessage(settings); }; /** @@ -224,4 +233,88 @@ Clay.encodeDataUri = function(input, prefix) { return prefix + encodeURIComponent(out.join('')); }; +/** + * 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 split with a zero. + * eg: ['one', 'two'] becomes ['one', 0, 'two', 0] + * - 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 multipled 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} [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) { + var itemConverted = Clay.prepareForAppMessage(item); + result.push(itemConverted); + if (typeof itemConverted === 'string') { + result.push(0); + } + }); + } 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(); + * @see {prepareForAppMessage} + * @param {Object} settings + * @returns {{}} + */ +Clay.prepareSettingsForAppMessage = function(settings) { + var result = {}; + Object.keys(settings).forEach(function(key) { + result[key] = Clay.prepareForAppMessage(settings[key]); + }); + return result; +}; + module.exports = Clay; diff --git a/src/images/example.png b/src/images/example.png index 6d2ebc8..f7246b9 100644 Binary files a/src/images/example.png and b/src/images/example.png differ diff --git a/src/scripts/components/slider.js b/src/scripts/components/slider.js index b63d29d..b183ae7 100644 --- a/src/scripts/components/slider.js +++ b/src/scripts/components/slider.js @@ -29,6 +29,10 @@ module.exports = { $valuePad.set('innerHTML', value); } + var step = self.$manipulatorTarget.get('step'); + step = step.toString(10).split('.')[1]; + self.precision = step ? step.length : 0; + self.on('change', setValueDisplay); setValueDisplay(); diff --git a/src/scripts/lib/clay-config.js b/src/scripts/lib/clay-config.js index b8622b8..e809876 100644 --- a/src/scripts/lib/clay-config.js +++ b/src/scripts/lib/clay-config.js @@ -158,7 +158,13 @@ function ClayConfig(settings, config, $rootContainer, meta) { self.serialize = function() { _checkBuilt('serialize'); _.eachObj(_itemsByAppKey, function(appKey, item) { - _settings[appKey] = item.get(); + _settings[appKey] = { + value: item.get() + }; + + if (item.precision) { + _settings[appKey].precision = item.precision; + } }); return _settings; }; diff --git a/src/scripts/lib/utils.js b/src/scripts/lib/utils.js index 156a3c4..02079f3 100644 --- a/src/scripts/lib/utils.js +++ b/src/scripts/lib/utils.js @@ -17,54 +17,3 @@ module.exports.updateProperties = function(obj, descriptor) { Object.defineProperty(obj, prop, descriptor); }); }; - -/** - * 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 split with a zero. - * eg: ['one', 'two'] becomes ['one', 0, 'two', 0] - * - 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 - * @param {number|string|boolean|Array} val - * @returns {number|string|Array} - */ -module.exports.prepareForAppMessage = function(val) { - var result; - - if (typeof val === 'boolean') { - result = val ? 1 : 0; - } else if (Array.isArray(val)) { - result = []; - val.forEach(function(item) { - var itemConverted = module.exports.prepareForAppMessage(item); - result.push(itemConverted); - if (typeof itemConverted === 'string') { - result.push(0); - } - }); - } else { - result = val; - } - - return result; -}; - -/** - * Converts a Clay settings dict into one that is compatible with - * Pebble.sendAppMessage(); - * @see {prepareForAppMessage} - * @param {Object} settings - * @returns {{}} - */ -module.exports.prepareSettingsForAppMessage = function(settings) { - var result = {}; - Object.keys(settings).forEach(function(key) { - result[key] = module.exports.prepareForAppMessage(settings[key]); - }); - return result; -}; diff --git a/test/spec/components/slider.js b/test/spec/components/slider.js index 1f78737..637f8dc 100644 --- a/test/spec/components/slider.js +++ b/test/spec/components/slider.js @@ -3,7 +3,7 @@ var assert = require('chai').assert; var fixture = require('../../fixture'); -describe('component - select', function() { +describe('component - slider', function() { it('sets the value display to the correct value on set', function() { var clayConfig = fixture.clayConfig(['slider']); var sliderItem = clayConfig.getItemsByType('slider')[0]; @@ -55,4 +55,14 @@ describe('component - select', function() { $valueDisplay.set('value', 75).trigger('change'); assert.strictEqual($slider.get('value'), '75'); }); + + it('sets the precision correctly', function() { + var clayConfig = fixture.clayConfig([{ + type: 'slider', + step: 0.25 + }]); + var sliderItem = clayConfig.getItemsByType('slider')[0]; + + assert.strictEqual(sliderItem.precision, 2); + }); }); diff --git a/test/spec/index.js b/test/spec/index.js index 099e378..f868064 100644 --- a/test/spec/index.js +++ b/test/spec/index.js @@ -235,13 +235,21 @@ describe('Clay', function() { }); }); - describe('.getSettings()', function() { + describe('.getSettings', function() { it('stores the response to localStorage and returns the decoded data', function() { var clay = fixture.clay([]); - var result = clay.getSettings('%7B%22appKey%22%3A%22value%22%7D'); - assert.equal(localStorage.getItem('clay-settings'), '{"appKey":"value"}'); - assert.deepEqual(result, {appKey: 'value'}); + var settings = encodeURIComponent(JSON.stringify({ + key1: 'value1', + key2: {value: 'value2'} + })); + var expected = { + key1: 'value1', + key2: 'value2' + }; + var result = clay.getSettings(settings); + assert.equal(localStorage.getItem('clay-settings'), JSON.stringify(expected)); + assert.deepEqual(result, expected); }); it('does not store the response if it is invalid JSON and logs an error', @@ -264,7 +272,15 @@ describe('Clay', function() { test4: ['cb-1', 'cb-3'], test5: 12345, test6: [1, 2, 3, 4], - test7: [true, false, true] + test7: [true, false, true], + test8: { + precision: 2, + value: 12.34 + }, + test9: { + precision: 1, + value: [1, 2, 3, 4] + } })); var expected = { test1: 0, @@ -273,7 +289,9 @@ describe('Clay', function() { test4: ['cb-1', 0, 'cb-3', 0], test5: 12345, test6: [1, 2, 3, 4], - test7: [1, 0, 1] + test7: [1, 0, 1], + test8: 1234, + test9: [10, 20, 30, 40] }; assert.deepEqual(clay.getSettings(response), expected); @@ -289,7 +307,11 @@ describe('Clay', function() { test4: ['cb-1', 'cb-3'], test5: 12345, test6: [1, 2, 3, 4], - test7: [true, false, true] + test7: [true, false, true], + test8: { + precision: 2, + value: 12.34 + } }; var response = encodeURIComponent(JSON.stringify(settings)); @@ -389,4 +411,98 @@ describe('Clay', function() { }); }); + + describe('.prepareForAppMessage', function() { + it('converts an array correctly when array contains numbers', function() { + assert.deepEqual(Clay.prepareForAppMessage([1, 2, 3]), [1, 2, 3]); + assert.deepEqual(Clay.prepareForAppMessage( + {value: [1.5, 2.34, 3], precision: 2}), + [150, 234, 300] + ); + }); + + it('converts an array correctly when array contains booleans', function() { + assert.deepEqual(Clay.prepareForAppMessage([true, false, true]), [1, 0, 1]); + assert.deepEqual( + Clay.prepareForAppMessage([{value: true}, false, true]), + [1, 0, 1]); + }); + + it('converts booleans to ints', function() { + assert.strictEqual(Clay.prepareForAppMessage(false), 0); + assert.strictEqual(Clay.prepareForAppMessage(true), 1); + assert.strictEqual(Clay.prepareForAppMessage({value: false}), 0); + assert.strictEqual(Clay.prepareForAppMessage({value: true}), 1); + }); + + it('leaves strings alone', function() { + assert.strictEqual(Clay.prepareForAppMessage('test'), 'test'); + assert.strictEqual(Clay.prepareForAppMessage({value: 'test'}), 'test'); + }); + + it('Multiples the number by the precision', function() { + assert.strictEqual(Clay.prepareForAppMessage(123), 123); + assert.strictEqual(Clay.prepareForAppMessage({ + value: 1.23, + precision: 3 + }), 1230); + assert.strictEqual(Clay.prepareForAppMessage({ + value: 123, + precision: 2 + }), 12300); + assert.strictEqual(Clay.prepareForAppMessage({ + value: 1.23456, + precision: 4 + }), 12345); + }); + + it('ignores precision for anything but numbers', function() { + assert.strictEqual(Clay.prepareForAppMessage({ + value: 'not a number', + precision: 3 + }), 'not a number'); + + assert.deepEqual(Clay.prepareForAppMessage({ + value: ['not', 'a', 'number'], + precision: 3 + }), ['not', 'a', 'number']); + }); + + it('treats an undefined precision as 0', function() { + assert.strictEqual(Clay.prepareForAppMessage({ + value: 12.45, + precision: undefined + }), 12); + }); + }); + + describe('.prepareSettingsForAppMessage', function() { + it('converts the settings correctly', function() { + var settings = { + test1: false, + test2: 'val-2', + test3: true, + test4: ['cb-1', 'cb-3'], + test5: 12345, + test6: [1, 2, 3, 4], + test7: [true, false, true], + test8: { + precision: 2, + value: 12.34 + } + }; + var expected = { + test1: 0, + test2: 'val-2', + test3: 1, + test4: ['cb-1', 0, 'cb-3', 0], + test5: 12345, + test6: [1, 2, 3, 4], + test7: [1, 0, 1], + test8: 1234 + }; + + assert.deepEqual(Clay.prepareSettingsForAppMessage(settings), expected); + }); + }); }); diff --git a/test/spec/lib/clay-config.js b/test/spec/lib/clay-config.js index 4594f74..dba8b34 100644 --- a/test/spec/lib/clay-config.js +++ b/test/spec/lib/clay-config.js @@ -112,7 +112,8 @@ describe('ClayConfig', function() { {label: 'label-1', value: 'cb-1'}, {label: 'label-2', value: 'cb-2'}, {label: 'label-2', value: 'cb-3'} - ]} + ]}, + {type: 'slider', appKey: 'test5', step: 0.05, defaultValue: 12.5} ]; var settings = { test2: 'val-2' @@ -121,10 +122,11 @@ describe('ClayConfig', function() { var clayConfig = fixtures.clayConfig(config, true, true, settings); assert.deepEqual(clayConfig.serialize(), { - test1: 'default val', - test2: 'val-2', - test3: false, - test4: [] + test1: {value: 'default val'}, + test2: {value: 'val-2'}, + test3: {value: false}, + test4: {value: []}, + test5: {value: 12.5, precision: 2} }); clayConfig.getItemByAppKey('test1').set('val-1'); @@ -132,17 +134,29 @@ describe('ClayConfig', function() { clayConfig.getItemByAppKey('test4').set(['cb-1', 'cb-3']); assert.deepEqual(clayConfig.serialize(), { - test1: 'val-1', - test2: 'val-2', - test3: true, - test4: ['cb-1', 'cb-3'] + test1: {value: 'val-1'}, + test2: {value: 'val-2'}, + test3: {value: true}, + test4: {value: ['cb-1', 'cb-3']}, + test5: {value: 12.5, precision: 2} }); // make sure the result of serialize() can actually be fed back in to // a new instance of ClayConfig + var clay = fixtures.clay(config); + var response = encodeURIComponent(JSON.stringify(clayConfig.serialize())); + clay.getSettings(response); + var loadedSettings = JSON.parse(localStorage.getItem('clay-settings')); + + assert.deepEqual(loadedSettings, { + test1: 'val-1', + test2: 'val-2', + test3: true, + test4: ['cb-1', 'cb-3'], + test5: 12.5 + }); assert.doesNotThrow(function() { - settings = clayConfig.serialize(); - fixtures.clayConfig(config, true, true, settings); + fixtures.clayConfig(config, true, true, loadedSettings); }); }); }); diff --git a/test/spec/lib/utils.js b/test/spec/lib/utils.js index 22127e9..4acbcce 100644 --- a/test/spec/lib/utils.js +++ b/test/spec/lib/utils.js @@ -26,60 +26,5 @@ describe('Utils', function() { ); }); }); - - describe('.prepareForAppMessage', function() { - it('converts an array correctly when array contains strings', function() { - assert.deepEqual( - utils.prepareForAppMessage(['one', 'two']), - ['one', 0, 'two', 0] - ); - }); - - it('converts an array correctly when array contains numbers', function() { - assert.deepEqual(utils.prepareForAppMessage([1, 2, 3]), [1, 2, 3]); - }); - - it('converts an array correctly when array contains booleans', function() { - assert.deepEqual(utils.prepareForAppMessage([true, false, true]), [1, 0, 1]); - }); - - it('converts booleans to ints', function() { - assert.strictEqual(utils.prepareForAppMessage(false), 0); - assert.strictEqual(utils.prepareForAppMessage(true), 1); - }); - - it('leaves strings alone', function() { - assert.strictEqual(utils.prepareForAppMessage('test'), 'test'); - }); - - it('leaves numbers alone', function() { - assert.strictEqual(utils.prepareForAppMessage(123), 123); - }); - }); - - describe('.prepareSettingsForAppMessage', function() { - it('converts the settings correctly', function() { - var settings = { - test1: false, - test2: 'val-2', - test3: true, - test4: ['cb-1', 'cb-3'], - test5: 12345, - test6: [1, 2, 3, 4], - test7: [true, false, true] - }; - var expected = { - test1: 0, - test2: 'val-2', - test3: 1, - test4: ['cb-1', 0, 'cb-3', 0], - test5: 12345, - test6: [1, 2, 3, 4], - test7: [1, 0, 1] - }; - - assert.deepEqual(utils.prepareSettingsForAppMessage(settings), expected); - }); - }); });