mirror of
https://github.com/pebble-dev/clay.git
synced 2026-08-29 13:26:59 -04:00
Merge pull request #7 from pebble/PBL-33804/duplicate-components
Fix duplicate components being injected into data URI
This commit is contained in:
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -4,13 +4,149 @@ var configPageHtml = require('./tmp/config-page.html');
|
||||
var toSource = require('tosource');
|
||||
var standardComponents = require('./src/scripts/components');
|
||||
|
||||
/**
|
||||
* @param {Array} config - the Clay config
|
||||
* @param {function} [customFn] - Custom code to run from the config page. Will run
|
||||
* with the ClayConfig instance as context
|
||||
* @param {Object} [options] - Additional options to pass to Clay
|
||||
* @param {boolean} [options.autoHandleEvents] - If false, Clay will not
|
||||
* automatically handle the 'showConfiguration' and 'webviewclosed' events
|
||||
* @constructor
|
||||
*/
|
||||
function Clay(config, customFn, options) {
|
||||
var self = this;
|
||||
|
||||
if (!Array.isArray(config)) {
|
||||
throw new Error('config must be an Array');
|
||||
}
|
||||
|
||||
if (customFn && typeof customFn !== 'function') {
|
||||
throw new Error('customFn must be a function or "null"');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
self.config = config;
|
||||
self.customFn = customFn || function() {};
|
||||
self.components = {};
|
||||
|
||||
// Let Clay handle all the magic
|
||||
if (options.autoHandleEvents !== false && typeof Pebble !== 'undefined') {
|
||||
|
||||
Pebble.addEventListener('showConfiguration', function() {
|
||||
Pebble.openURL(self.generateUrl());
|
||||
});
|
||||
|
||||
Pebble.addEventListener('webviewclosed', function(e) {
|
||||
|
||||
if (!e || !e.response) { return; }
|
||||
|
||||
// Send settings to Pebble watchapp
|
||||
Pebble.sendAppMessage(self.getSettings(e.response), function() {
|
||||
console.log('Sent config data to Pebble');
|
||||
}, function(error) {
|
||||
console.log('Failed to send config data!');
|
||||
console.log(JSON.stringify(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Clay~ConfigItem|Array} item
|
||||
* @return {void}
|
||||
*/
|
||||
function _registerStandardComponents(item) {
|
||||
if (Array.isArray(item)) {
|
||||
item.forEach(function(item) {
|
||||
_registerStandardComponents(item);
|
||||
});
|
||||
} else if (item.type === 'section') {
|
||||
_registerStandardComponents(item.items);
|
||||
} else if (standardComponents[item.type]) {
|
||||
self.registerComponent(standardComponents[item.type]);
|
||||
}
|
||||
}
|
||||
|
||||
_registerStandardComponents(self.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
|
||||
// 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
|
||||
* @returns {Object}
|
||||
*/
|
||||
Clay.prototype.getSettings = function(response) {
|
||||
// Decode and parse config data as JSON
|
||||
var settings = {};
|
||||
|
||||
try {
|
||||
settings = JSON.parse(decodeURIComponent(response));
|
||||
} catch (e) {
|
||||
throw new Error('The provided response was not valid JSON');
|
||||
}
|
||||
|
||||
localStorage.setItem('clay-settings', JSON.stringify(settings));
|
||||
return settings;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {string} [prefix]
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeDataUri(input, prefix) {
|
||||
Clay.encodeDataUri = function(input, prefix) {
|
||||
prefix = typeof prefix !== 'undefined' ? prefix : 'data:text/html;base64,';
|
||||
|
||||
if (window.btoa) {
|
||||
@@ -53,146 +189,12 @@ function encodeDataUri(input, prefix) {
|
||||
}
|
||||
|
||||
out.push(B64_ALPHABET.charAt(e1),
|
||||
B64_ALPHABET.charAt(e2),
|
||||
B64_ALPHABET.charAt(e3),
|
||||
B64_ALPHABET.charAt(e4));
|
||||
B64_ALPHABET.charAt(e2),
|
||||
B64_ALPHABET.charAt(e3),
|
||||
B64_ALPHABET.charAt(e4));
|
||||
}
|
||||
|
||||
return prefix + encodeURIComponent(out.join(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} config - the Clay config
|
||||
* @param {function} [customFn] - Custom code to run from the config page. Will run
|
||||
* with the ClayConfig instance as context
|
||||
* @param {Object} [options] - Additional options to pass to Clay
|
||||
* @param {boolean} [options.autoHandleEvents] - If false, Clay will not
|
||||
* automatically handle the 'showConfiguration' and 'webviewclosed' events
|
||||
* @constructor
|
||||
*/
|
||||
function Clay(config, customFn, options) {
|
||||
var self = this;
|
||||
|
||||
if (!Array.isArray(config)) {
|
||||
throw new Error('config must be an Array');
|
||||
}
|
||||
|
||||
if (customFn && typeof customFn !== 'function') {
|
||||
throw new Error('customFn must be an function or "null"');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
self.config = config;
|
||||
self.customFn = customFn || function() {};
|
||||
self.components = [];
|
||||
|
||||
// Let Clay handle all the magic
|
||||
if (options.autoHandleEvents !== false && Pebble) {
|
||||
|
||||
Pebble.addEventListener('showConfiguration', function(e) {
|
||||
Pebble.openURL(self.generateUrl());
|
||||
});
|
||||
|
||||
Pebble.addEventListener('webviewclosed', function(e) {
|
||||
|
||||
if (e && !e.response) { return; }
|
||||
|
||||
// Send settings to Pebble watchapp
|
||||
Pebble.sendAppMessage(self.getSettings(e.response), function(e) {
|
||||
console.log('Sent config data to Pebble');
|
||||
}, function() {
|
||||
console.log('Failed to send config data!');
|
||||
console.log(JSON.stringify(e));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Clay~ConfigItem|Array} item
|
||||
* @return {void}
|
||||
*/
|
||||
function _registerStandardComponents(item) {
|
||||
if (Array.isArray(item)) {
|
||||
item.forEach(function(item) {
|
||||
_registerStandardComponents(item);
|
||||
});
|
||||
} else if (item.type === 'section') {
|
||||
_registerStandardComponents(item.items);
|
||||
} else if (standardComponents[item.type]) {
|
||||
self.registerComponent(standardComponents[item.type]);
|
||||
}
|
||||
}
|
||||
|
||||
_registerStandardComponents(self.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.push(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);
|
||||
settings = {};
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// 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 encodeDataUri(
|
||||
compiledHtml,
|
||||
'http://clay.pebble.com.s3-website-us-west-2.amazonaws.com/#'
|
||||
);
|
||||
}
|
||||
|
||||
return encodeDataUri(compiledHtml);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse the response from the webviewclosed event data
|
||||
* @param {string} response
|
||||
* @returns {Object}
|
||||
*/
|
||||
Clay.prototype.getSettings = function(response) {
|
||||
// Decode and parse config data as JSON
|
||||
var settings = JSON.parse(decodeURIComponent(response));
|
||||
|
||||
if (!settings) return {};
|
||||
|
||||
localStorage.setItem('clay-settings', JSON.stringify(settings));
|
||||
return settings;
|
||||
};
|
||||
|
||||
module.exports = Clay;
|
||||
|
||||
+3
-3
@@ -7,9 +7,9 @@
|
||||
"example": "example"
|
||||
},
|
||||
"scripts": {
|
||||
"test-travis": "./node_modules/.bin/karma start ./test/karma.conf.js --single-run --browsers chromeTravisCI && ./node_modules/.bin/eslint ./",
|
||||
"test-debug": "(export DEBUG=true && ./node_modules/.bin/karma start ./test/karma.conf.js --no-single-run)",
|
||||
"test": "./node_modules/.bin/karma start ./test/karma.conf.js --single-run",
|
||||
"test-travis": "./node_modules/.bin/gulp && ./node_modules/.bin/karma start ./test/karma.conf.js --single-run --browsers chromeTravisCI && ./node_modules/.bin/eslint ./",
|
||||
"test-debug": "(export DEBUG=true && ./node_modules/.bin/gulp && ./node_modules/.bin/karma start ./test/karma.conf.js --no-single-run)",
|
||||
"test": "./node_modules/.bin/gulp && ./node_modules/.bin/karma start ./test/karma.conf.js --single-run",
|
||||
"lint": "./node_modules/.bin/eslint ./",
|
||||
"build": "gulp",
|
||||
"dev": "gulp dev"
|
||||
|
||||
@@ -5,6 +5,7 @@ var $ = require('../src/scripts/vendor/minified').$;
|
||||
var HTML = require('../src/scripts/vendor/minified').HTML;
|
||||
var ClayItem = require('../src/scripts/lib/clay-item');
|
||||
var ClayConfig = require('../src/scripts/lib/clay-config');
|
||||
var Clay = require('../index');
|
||||
var components = require('../src/scripts/components');
|
||||
var componentRegistry = require('../src/scripts/lib/component-registry');
|
||||
var idCounter = 0;
|
||||
@@ -74,3 +75,19 @@ module.exports.clayConfig = function(types, build, autoRegister, settings) {
|
||||
return build === false ? clayConfig : clayConfig.build();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array} config - the Clay config
|
||||
* @param {function} [customFn] - Custom code to run from the config page. Will run
|
||||
* with the ClayConfig instance as context
|
||||
* @param {Object} [options] - Additional options to pass to Clay
|
||||
* @param {boolean} [options.autoHandleEvents] - If false, Clay will not
|
||||
* automatically handle the 'showConfiguration' and 'webviewclosed' events
|
||||
* @param {boolean} [destroyLocalStorage=true]
|
||||
* @return {Clay}
|
||||
*/
|
||||
module.exports.clay = function(config, customFn, options, destroyLocalStorage) {
|
||||
if (destroyLocalStorage !== false) {
|
||||
localStorage.removeItem('clay-settings');
|
||||
}
|
||||
return new Clay(config, customFn, options);
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@ module.exports = function(config) {
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
'index.js',
|
||||
'src/scripts/**/*.js',
|
||||
'test/spec/**/*.js'
|
||||
],
|
||||
@@ -62,6 +63,7 @@ module.exports = function(config) {
|
||||
// preprocess matching files before serving them to the browser
|
||||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
'index.js': ['browserify'],
|
||||
'src/scripts/**/*.js': ['browserify'],
|
||||
'test/spec/**/*.js': ['browserify']
|
||||
},
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
'use strict';
|
||||
|
||||
var fixture = require('../fixture');
|
||||
var Clay = require('../../index');
|
||||
var assert = require('chai').assert;
|
||||
var standardComponents = require('../../src/scripts/components');
|
||||
var sinon = require('sinon');
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
function stubPebble() {
|
||||
global.Pebble = {
|
||||
addEventListener: sinon.stub(),
|
||||
openURL: sinon.stub(),
|
||||
sendAppMessage: sinon.stub()
|
||||
};
|
||||
}
|
||||
|
||||
describe('Clay', function() {
|
||||
describe('Clay constructor', function() {
|
||||
it('throws if the config is not an array', function() {
|
||||
assert.throws(function() {
|
||||
fixture.clay({});
|
||||
}, /must be an Array/i);
|
||||
});
|
||||
|
||||
it('throws if customFn is not a function', function() {
|
||||
assert.throws(function() {
|
||||
fixture.clay([], {});
|
||||
}, /must be a function/i);
|
||||
});
|
||||
|
||||
it('does not throw if customFn is undefined or a function', function() {
|
||||
assert.doesNotThrow(function() {
|
||||
fixture.clay([], function() {});
|
||||
});
|
||||
|
||||
assert.doesNotThrow(function() {
|
||||
fixture.clay([]).customFn();
|
||||
});
|
||||
});
|
||||
|
||||
it('registers the standard components present in the config', function() {
|
||||
var config = fixture.config(
|
||||
['input', 'input', 'select', 'custom', ['color']],
|
||||
false
|
||||
);
|
||||
var clay = fixture.clay(config);
|
||||
assert.deepEqual(clay.components, {
|
||||
input: standardComponents['input'],
|
||||
select: standardComponents['select'],
|
||||
color: standardComponents['color']
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the "showConfiguration" event if autoHandleEvents is not false',
|
||||
function() {
|
||||
stubPebble();
|
||||
var clay = fixture.clay([]);
|
||||
Pebble.addEventListener.withArgs('showConfiguration').callArg(1);
|
||||
|
||||
assert(Pebble.addEventListener.calledWith('showConfiguration'));
|
||||
assert(Pebble.openURL.calledWith(clay.generateUrl()));
|
||||
});
|
||||
|
||||
it('handles the "webviewclosed" event if autoHandleEvents is not false',
|
||||
function() {
|
||||
stubPebble();
|
||||
fixture.clay([]);
|
||||
var logStub = sinon.stub(console, 'log');
|
||||
Pebble.addEventListener
|
||||
.withArgs('webviewclosed')
|
||||
.callArgWith(1, { response: '%7B%22appKey%22%3A%22value%22%7D' });
|
||||
|
||||
assert(Pebble.addEventListener.calledWith('webviewclosed'));
|
||||
assert(Pebble.sendAppMessage.calledWith({ appKey: 'value' }));
|
||||
|
||||
Pebble.sendAppMessage.callArg(1);
|
||||
assert(logStub.calledWith('Sent config data to Pebble'));
|
||||
|
||||
Pebble.sendAppMessage.callArgWith(2, {some: 'error'});
|
||||
assert(logStub.calledWith('Failed to send config data!'));
|
||||
assert(logStub.calledWith('{"some":"error"}'));
|
||||
|
||||
logStub.restore();
|
||||
});
|
||||
|
||||
it('handles an empty response in the "webviewclosed" handler', function() {
|
||||
stubPebble();
|
||||
fixture.clay([]);
|
||||
Pebble.addEventListener.withArgs('webviewclosed').callArgWith(1, undefined);
|
||||
|
||||
assert(Pebble.addEventListener.calledWith('webviewclosed'));
|
||||
assert.strictEqual(Pebble.sendAppMessage.callCount, 0);
|
||||
});
|
||||
|
||||
it('does not handle the "webviewclosed" or "showConfiguration" events ' +
|
||||
'if autoHandleEvents is false', function() {
|
||||
stubPebble();
|
||||
fixture.clay([], null, { autoHandleEvents: false });
|
||||
assert.strictEqual(Pebble.addEventListener.called, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.registerComponent()', function() {
|
||||
it('adds the component to the this.components', function() {
|
||||
var clay = fixture.clay([]);
|
||||
var customComponent = {
|
||||
name: 'custom',
|
||||
template: '<div></div>',
|
||||
manipulator: 'val'
|
||||
};
|
||||
clay.registerComponent(customComponent);
|
||||
assert.strictEqual(clay.components[customComponent.name], customComponent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.generateUrl()', function() {
|
||||
it('does not replace $$RETURN_TO$$ if in the emulator', function() {
|
||||
var clay = fixture.clay([]);
|
||||
stubPebble();
|
||||
Pebble.platform = 'pypkjs';
|
||||
var decodedUrl =
|
||||
atob(decodeURIComponent(clay.generateUrl().replace(/^.*?#/, '')));
|
||||
assert.match(decodedUrl, /\$\$RETURN_TO\$\$/);
|
||||
});
|
||||
|
||||
it('returns the emulator URL if inside emulator', function() {
|
||||
var clay = fixture.clay([]);
|
||||
stubPebble();
|
||||
Pebble.platform = 'pypkjs';
|
||||
assert.match(
|
||||
clay.generateUrl(),
|
||||
/^http:\/\/clay\.pebble\.com\.s3-website-us-west-2\.amazonaws.com\/#/
|
||||
);
|
||||
});
|
||||
|
||||
it('doesn\'t throw and logs an error if settings in localStorage are broken',
|
||||
function() {
|
||||
var clay = fixture.clay([]);
|
||||
var errorStub = sinon.stub(console, 'error');
|
||||
localStorage.setItem('clay-settings', 'not valid JSON');
|
||||
assert.doesNotThrow(function() {
|
||||
clay.generateUrl();
|
||||
});
|
||||
assert(errorStub.calledWithMatch(/SyntaxError/i));
|
||||
errorStub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
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'});
|
||||
});
|
||||
|
||||
it('does not store the response if it is invalid JSON and logs an error',
|
||||
function() {
|
||||
var clay = fixture.clay([]);
|
||||
localStorage.setItem('clay-settings', '{"appKey":"value"}');
|
||||
|
||||
assert.throws(function() {
|
||||
clay.getSettings('not valid JSON');
|
||||
}, /Not Valid JSON/i);
|
||||
assert.equal(localStorage.getItem('clay-settings'), '{"appKey":"value"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Clay.encodeDataUri()', function() {
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
function testEncodeDataUri() {
|
||||
it('adds the correct prefix', function() {
|
||||
assert.equal(Clay.encodeDataUri('test', 'prefix:'), 'prefix:dGVzdA%3D%3D');
|
||||
assert.equal(
|
||||
Clay.encodeDataUri('test'),
|
||||
'data:text/html;base64,dGVzdA%3D%3D'
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes the data correctly', function() {
|
||||
assert.equal(Clay.encodeDataUri('test', ''), 'dGVzdA%3D%3D');
|
||||
assert.equal(Clay.encodeDataUri('test{2}', ''), 'dGVzdHsyfQ%3D%3D');
|
||||
assert.equal(Clay.encodeDataUri('test{10}', ''), 'dGVzdHsxMH0%3D');
|
||||
});
|
||||
|
||||
it('throws if the input is invalid', function() {
|
||||
assert.throws(function() {
|
||||
Clay.encodeDataUri('♥');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('native', function() {
|
||||
testEncodeDataUri();
|
||||
});
|
||||
|
||||
describe('polyfill', function() {
|
||||
var btoaOriginal = window.btoa;
|
||||
|
||||
before(function() {
|
||||
window.btoa = undefined;
|
||||
});
|
||||
|
||||
testEncodeDataUri();
|
||||
|
||||
after(function() {
|
||||
window.btoa = btoaOriginal;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user