diff --git a/js/app.js b/js/app.js
index 3d33991..7131a7a 100644
--- a/js/app.js
+++ b/js/app.js
@@ -2,11 +2,73 @@
var MainComponent = require('./components/MainComponent');
var React = require('react');
-
+var qs = require('qs');
var attachFastClick = require('fastclick');
+var LocationBar = require('location-bar');
+var serialization = require('./serialization');
+var Options = require('./how/Options');
+
attachFastClick(document.body);
-React.render(
- ,
+var locationBar = new LocationBar();
+
+function onOptionsChange(
+ content: Options.Content,
+ container: Options.Container,
+ horizontalAlignment: Options.HorizontalAlignment,
+ verticalAlignment: Options.VerticalAlignment,
+ browserSupport: Options.BrowserSupport
+) {
+ var serialized = serialization.serializeOptions(
+ content,
+ container,
+ horizontalAlignment,
+ verticalAlignment,
+ browserSupport
+ );
+ var serializedString = qs.stringify(serialized);
+ // qs uses brackets[] for sub objects, but that's ugly. It supports parsing
+ // dot notation, but not generating it. Let's just convert it over, since we
+ // don't have any real user-inputable data that can contain [] anyway.
+ var serializedString = serializedString
+ .replace(/%5B/g, '.')
+ .replace(/%5D/g, '');
+ locationBar.update(serializedString);
+}
+
+var component = React.render(
+ ,
document.getElementById('app')
);
+
+locationBar.onChange((path) => {
+ var serialized = qs.parse(path);
+ if (!serialized) {
+ return;
+ }
+
+ var options = serialization.deserializeOptions(serialized);
+ if (!options) {
+ return;
+ }
+
+ var {
+ content,
+ container,
+ horizontalAlignment,
+ verticalAlignment,
+ browserSupport,
+ } = options;
+
+ component.setOptions(
+ content,
+ container,
+ horizontalAlignment,
+ verticalAlignment,
+ browserSupport
+ );
+});
+
+locationBar.start();
diff --git a/js/components/AlignmentComponent.js b/js/components/AlignmentComponent.js
index 5f1e399..b16ae8d 100644
--- a/js/components/AlignmentComponent.js
+++ b/js/components/AlignmentComponent.js
@@ -14,10 +14,18 @@ class AlignmentComponent extends React.Component {
return this._horizontal.getValue();
}
+ setHorizontalAlignment(alignment: Options.HorizontalAlignment) {
+ this._horizontal.select(alignment);
+ }
+
getVerticalAlignment(): ?Options.VerticalAlignment {
return this._vertical.getValue();
}
+ setVerticalAlignment(alignment: Options.VerticalAlignment) {
+ this._vertical.select(alignment);
+ }
+
render(): ?ReactElement {
return (
diff --git a/js/components/BrowserSupportComponent.js b/js/components/BrowserSupportComponent.js
index 207ec53..706a73c 100644
--- a/js/components/BrowserSupportComponent.js
+++ b/js/components/BrowserSupportComponent.js
@@ -14,6 +14,8 @@ class BrowserSupportComponent extends React.Component {
};
}
+ _radioList: RadioListComponent;
+
state: {
browserSupport: Options.BrowserSupport;
};
@@ -22,6 +24,18 @@ class BrowserSupportComponent extends React.Component {
return this.state.browserSupport;
}
+ setBrowserSupport(browserSupport: Options.BrowserSupport) {
+ this.setState({browserSupport});
+ // Only do IE for now
+ var browserVersion = browserSupport.browserVersionsRequired[0];
+ if (browserVersion) {
+ this._radioList.select({
+ browser: browserVersion.browser,
+ version: browserVersion.minVersion,
+ });
+ }
+ }
+
_handleBrowserSupportChange(
support: { browser: Options.Browser; version: ?string; }
) {
@@ -31,6 +45,13 @@ class BrowserSupportComponent extends React.Component {
this.setState({browserSupport: this.state.browserSupport});
}
+ _compareBrowserSupports(
+ s1: {browser: Options.Browser; version: ?string;},
+ s2: {browser: Options.Browser; version: ?string;}
+ ): bool {
+ return s1.browser === s2.browser && s1.version === s2.version;
+ }
+
render(): ?ReactElement {
var browser = Options.Browser.IE;
var noSupport = {
@@ -44,6 +65,8 @@ class BrowserSupportComponent extends React.Component {
What is the minimum version of {browser.name} you need to support?
this._radioList = c}
+ compareValues={this._compareBrowserSupports}
onChange={this._handleBrowserSupportChange.bind(this)}>
{browser.versions.map(version => {
diff --git a/js/components/ContainerComponent.js b/js/components/ContainerComponent.js
index a2e0ec5..2526e3b 100644
--- a/js/components/ContainerComponent.js
+++ b/js/components/ContainerComponent.js
@@ -15,6 +15,11 @@ class ContainerComponent extends React.Component {
);
}
+ setContainer(container: Options.Container) {
+ this._divSize.setWidth(container.width);
+ this._divSize.setHeight(container.height);
+ }
+
render(): ?ReactElement {
return (
diff --git a/js/components/ContentComponent.js b/js/components/ContentComponent.js
index e2cc134..20e2b15 100644
--- a/js/components/ContentComponent.js
+++ b/js/components/ContentComponent.js
@@ -1,5 +1,7 @@
/** @flow */
+var invariant = require('invariant');
+
var React = require('react');
var LengthComponent = require('./LengthComponent');
var DivSizeComponent = require('./DivSizeComponent');
@@ -25,6 +27,7 @@ class ContentComponent extends React.Component {
contentType: ?ContentType;
textLines: ?number;
};
+ _typeRadioList: ?RadioListComponent
;
_divSize: ?DivSizeComponent;
_textLines: ?TextLinesComponent;
_textFontSize: ?TextFontSizeComponent;
@@ -65,6 +68,48 @@ class ContentComponent extends React.Component {
return null;
}
+ setContent(content: Options.Content) {
+ var contentText = content.text;
+
+ var contentType = contentText ? ContentType.TEXT : ContentType.DIV;
+ var typeRadioList = this._typeRadioList;
+ invariant(typeRadioList, 'should have this');
+ typeRadioList.select(contentType);
+
+ if (contentText) {
+ this.setState({
+ contentType: contentType,
+ textLines: contentText.lines,
+ }, () => {
+ invariant(contentText, 'flow');
+
+ var textLinesComponent = this._textLines;
+ invariant(textLinesComponent, 'should have text lines component');
+ textLinesComponent.setLines(contentText.lines);
+
+ var fontSizeComponent = this._textFontSize;
+ if (fontSizeComponent) {
+ fontSizeComponent.setFontSize(contentText.fontSize);
+ }
+
+ var lineHeightComponent = this._textLineHeight;
+ if (lineHeightComponent) {
+ lineHeightComponent.setLineHeight(contentText.lineHeight);
+ }
+ });
+ } else {
+ this.setState({
+ contentType: contentType,
+ }, () => {
+ var divSizeComponent = this._divSize;
+ invariant(divSizeComponent, 'should have div size component');
+
+ divSizeComponent.setWidth(content.width);
+ divSizeComponent.setHeight(content.height);
+ });
+ }
+ }
+
_handleTypeChange(contentType: ContentType) {
this.setState({contentType});
}
@@ -111,7 +156,9 @@ class ContentComponent extends React.Component {
Content
What do you want to center?
-
+ this._typeRadioList = c}
+ onChange={this._handleTypeChange.bind(this)}>
Just text, or an inline-level block of text and images.
diff --git a/js/components/DivSizeComponent.js b/js/components/DivSizeComponent.js
index 1a5f2ae..a91b2ac 100644
--- a/js/components/DivSizeComponent.js
+++ b/js/components/DivSizeComponent.js
@@ -7,6 +7,8 @@ var RadioComponent = require('./RadioComponent');
var RadioListComponent = require('./RadioListComponent');
class DivSizeComponent extends React.Component {
+ _widthRadioList: RadioListComponent;
+ _heightRadioList: RadioListComponent;
_width: LengthComponent;
_height: LengthComponent;
@@ -14,10 +16,28 @@ class DivSizeComponent extends React.Component {
return this._width.getLength();
}
+ setWidth(length: ?Options.Length) {
+ if (length) {
+ this._widthRadioList.select(true);
+ this._width.setLength(length);
+ } else {
+ this._widthRadioList.select(false);
+ }
+ }
+
getHeight(): ?Options.Length {
return this._height.getLength();
}
+ setHeight(length: ?Options.Length) {
+ if (length) {
+ this._heightRadioList.select(true);
+ this._height.setLength(length);
+ } else {
+ this._heightRadioList.select(false);
+ }
+ }
+
_handleWidthKnown(known: bool) {
if (!known) {
if (this.props.onWidthChange) {
@@ -44,7 +64,9 @@ class DivSizeComponent extends React.Component {
return (
Width
-
+ this._widthRadioList = c}
+ onChange={this._handleWidthKnown.bind(this)}>
Height
-
+ this._heightRadioList = c}
+ onChange={this._handleHeightKnown.bind(this)}>
this._content = c} />
diff --git a/js/components/RadioListComponent.js b/js/components/RadioListComponent.js
index db029fb..ce4cbfe 100644
--- a/js/components/RadioListComponent.js
+++ b/js/components/RadioListComponent.js
@@ -38,14 +38,16 @@ class RadioListComponent extends React.Component {
}
_selectOption(option: ?RadioComponent) {
- if (this.state.selectedOption) {
- this.state.selectedOption.setIsSelected(false);
- }
- if (option) {
- option.setIsSelected(true);
- }
- this.setState({
- selectedOption: option,
+ this.setState((prevState, currentProps) => {
+ if (prevState.selectedOption) {
+ prevState.selectedOption.setIsSelected(false);
+ }
+ if (option) {
+ option.setIsSelected(true);
+ }
+ return {
+ selectedOption: option,
+ };
});
if (this.props.onChange) {
this.props.onChange(option ? option.props.value : null);
@@ -61,7 +63,7 @@ class RadioListComponent extends React.Component {
for (var i = 0; i < childrenRefKeys.length; i++) {
var child = this.refs[childrenRefKeys[i]];
if (child instanceof RadioComponent) {
- if (child.props.value === value) {
+ if (this._compareValues(child.props.value, value)) {
this._selectOption(child);
return;
}
@@ -70,6 +72,13 @@ class RadioListComponent extends React.Component {
throw new Error('No value found for ' + value);
}
+ _compareValues(value1: T, value2: T): bool {
+ if (this.props.compareValues) {
+ return this.props.compareValues(value1, value2);
+ }
+ return value1 === value2;
+ }
+
render(): ?ReactElement {
var classes = classnames({
'radioList': true,
@@ -93,6 +102,7 @@ class RadioListComponent extends React.Component {
}
}
RadioListComponent.propTypes = {
+ compareValues: React.PropTypes.func,
onChange: React.PropTypes.func,
direction: React.PropTypes.oneOf(['vertical', 'horizontal']),
};
diff --git a/js/components/TextFontSizeComponent.js b/js/components/TextFontSizeComponent.js
index 0dba49c..6ea414c 100644
--- a/js/components/TextFontSizeComponent.js
+++ b/js/components/TextFontSizeComponent.js
@@ -7,12 +7,22 @@ var RadioComponent = require('./RadioComponent');
var RadioListComponent = require('./RadioListComponent');
class TextFontSizeComponent extends React.Component {
+ _radioList: RadioListComponent;
_fontSize: LengthComponent;
getFontSize(): ?Options.Length {
return this._fontSize.getLength();
}
+ setFontSize(fontSize: ?Options.Length) {
+ if (fontSize) {
+ this._radioList.select(true);
+ this._fontSize.setLength(fontSize);
+ } else {
+ this._radioList.select(false);
+ }
+ }
+
_handleFontSizeKnownChange(known: bool) {
if (!known) {
if (this.props.onChange) {
@@ -28,7 +38,9 @@ class TextFontSizeComponent extends React.Component {
return (
Do you know the font-size
?
-
+ this._radioList = c}
+ onChange={this._handleFontSizeKnownChange.bind(this)}>
;
_lineHeight: LengthComponent;
getLineHeight(): ?Options.Length {
return this._lineHeight.getLength();
}
+ setLineHeight(lineHeight: ?Options.Length) {
+ if (lineHeight) {
+ this._radioList.select(true);
+ this._lineHeight.setLength(lineHeight);
+ } else {
+ this._radioList.select(false);
+ }
+ }
+
_handleLineHeightKnownChange(known: bool) {
if (!known) {
if (this.props.onChange) {
@@ -28,7 +38,9 @@ class TextLineHeightComponent extends React.Component {
return (
Do you know the line-height
of each line?
-
+ this._radioList = c}
+ onChange={this._handleLineHeightKnownChange.bind(this)}>
;
_linesInput: React.Component;
constructor(props: mixed) {
@@ -22,6 +23,15 @@ class TextLinesComponent extends React.Component {
return this.state.lines;
}
+ setLines(lines: ?number) {
+ if (lines != null) {
+ this._radioList.select(true);
+ this._setLines(lines);
+ } else {
+ this._radioList.select(false);
+ }
+ }
+
_handleTextLinesKnownChange(known: bool) {
if (!known) {
this._setLines(null);
@@ -44,7 +54,9 @@ class TextLinesComponent extends React.Component {
return (
Do you know how many lines of text it'll be?
-
+ this._radioList = c}
+ onChange={this._handleTextLinesKnownChange.bind(this)}>
;
}
Browser.IE = new Browser(
'Internet Explorer',
@@ -177,6 +178,9 @@ Browser.IE = new Browser(
'11',
]
);
+Browser.AllBrowsers = [
+ Browser.IE,
+];
class BrowserVersionRequired {
browser: Browser;
diff --git a/js/serialization.js b/js/serialization.js
new file mode 100644
index 0000000..c618544
--- /dev/null
+++ b/js/serialization.js
@@ -0,0 +1,243 @@
+/** @flow */
+
+var Options = require('./how/Options');
+
+function serializeLength(length: Options.Length): mixed {
+ return length.toFilenameString();
+}
+
+function deserializeLength(serialized: string): ?Options.Length {
+ var number = parseInt(serialized, 10);
+ var unitSerialized = serialized.replace(/\d*/, '');
+ var unit;
+ if (unitSerialized === Options.LengthType.PIXEL.toFilenameString()) {
+ unit = Options.LengthType.PIXEL;
+ } else if (unitSerialized === Options.LengthType.PERCENTAGE.toFilenameString()) {
+ unit = Options.LengthType.PERCENTAGE;
+ } else if (unitSerialized === Options.LengthType.EM.toFilenameString()) {
+ unit = Options.LengthType.EM;
+ }
+
+ if (!unit || isNaN(number)) {
+ return null;
+ }
+
+ return new Options.Length(number, unit);
+}
+
+function serializeOptions(
+ content: Options.Content,
+ container: Options.Container,
+ horizontalAlignment: Options.HorizontalAlignment,
+ verticalAlignment: Options.VerticalAlignment,
+ browserSupport: Options.BrowserSupport
+): mixed {
+ var contentSerial = {};
+
+ var contentType;
+ var contentText = content.text;
+ if (contentText) {
+ contentType = 'text';
+
+ contentSerial.text = {};
+
+ if (contentText.fontSize) {
+ contentSerial.text.fontSize = serializeLength(contentText.fontSize);
+ }
+
+ if (contentText.lines) {
+ contentSerial.text.lines = contentText.lines;
+ }
+
+ if (contentText.lineHeight) {
+ contentSerial.text.lineHeight = serializeLength(contentText.lineHeight);
+ }
+
+ } else {
+ contentType = 'div';
+
+ if (content.width) {
+ contentSerial.width = serializeLength(content.width);
+ }
+
+ if (content.height) {
+ contentSerial.height = serializeLength(content.height);
+ }
+ }
+
+ var containerSerial = {};
+
+ if (container.width) {
+ containerSerial.width = serializeLength(container.width);
+ }
+
+ if (container.height) {
+ containerSerial.height = serializeLength(container.height);
+ }
+
+ var horizontalSerial;
+ if (horizontalAlignment === Options.HorizontalAlignment.LEFT) {
+ horizontalSerial = 'left';
+ } else if (horizontalAlignment === Options.HorizontalAlignment.CENTER) {
+ horizontalSerial = 'center';
+ } else if (horizontalAlignment === Options.HorizontalAlignment.RIGHT) {
+ horizontalSerial = 'right';
+ }
+
+ var verticalSerial;
+ if (verticalAlignment === Options.VerticalAlignment.TOP) {
+ verticalSerial = 'top';
+ } else if (verticalAlignment === Options.VerticalAlignment.MIDDLE) {
+ verticalSerial = 'middle';
+ } else if (verticalAlignment === Options.VerticalAlignment.BOTTOM) {
+ verticalSerial = 'bottom';
+ }
+
+ var browserSupportSerial = {};
+ browserSupport.browserVersionsRequired.forEach(browserVersion => {
+ browserSupportSerial[browserVersion.browser.shortName] =
+ browserVersion.minVersion || 'none';
+ });
+
+ return {
+ contentType,
+ content: contentSerial,
+ container: containerSerial,
+ horizontal: horizontalSerial,
+ vertical: verticalSerial,
+ browser: browserSupportSerial,
+ };
+}
+
+function deserializeOptions(serialized: any): ?{
+ content: Options.Content;
+ container: Options.Container;
+ horizontalAlignment: Options.HorizontalAlignment;
+ verticalAlignment: Options.VerticalAlignment;
+ browserSupport: Options.BrowserSupport;
+} {
+ var contentTypeSerial = serialized.contentType;
+ var contentSerial = serialized.content;
+ var content;
+ if (contentTypeSerial === 'text') {
+ var fontSize;
+ var lines;
+ var lineHeight;
+
+ var contentTextSerial = contentSerial && contentSerial.text;
+ if (contentTextSerial) {
+ var fontSizeSerial = contentTextSerial.fontSize;
+ if (fontSizeSerial) {
+ fontSize = deserializeLength(fontSizeSerial);
+ }
+
+ var linesSerial = contentTextSerial.lines;
+ if (linesSerial) {
+ lines = parseInt(linesSerial, 10) || null;
+ }
+
+ var lineHeightSerial = contentTextSerial.lineHeight;
+ if (lineHeightSerial) {
+ lineHeight = deserializeLength(lineHeightSerial);
+ }
+ }
+
+ content = Options.Content.text(fontSize, lines, lineHeight);
+
+ } else if (contentTypeSerial === 'div') {
+ var contentWidthSerial = contentSerial && contentSerial.width;
+ var width;
+ if (contentWidthSerial) {
+ width = deserializeLength(contentWidthSerial);
+ }
+
+ var contentHeightSerial = contentSerial && contentSerial.height;
+ var height;
+ if (contentHeightSerial) {
+ height = deserializeLength(contentHeightSerial);
+ }
+
+ content = new Options.Content(width, height, null);
+ }
+
+ var containerSerial = serialized.container;
+ var container;
+ if (containerSerial) {
+ var containerWidthSerial = containerSerial.width;
+ var width;
+ if (containerWidthSerial) {
+ width = deserializeLength(containerWidthSerial);
+ }
+
+ var containerHeightSerial = containerSerial.height;
+ var height;
+ if (containerHeightSerial) {
+ height = deserializeLength(containerHeightSerial);
+ }
+
+ container = new Options.Container(width, height);
+ }
+ if (!container) {
+ container = new Options.Container(null, null, null);
+ }
+
+ var horizontalSerial = serialized.horizontal;
+ var horizontalAlignment;
+ if (horizontalSerial === 'left') {
+ horizontalAlignment = Options.HorizontalAlignment.LEFT;
+ } else if (horizontalSerial === 'center') {
+ horizontalAlignment = Options.HorizontalAlignment.CENTER;
+ } else if (horizontalSerial === 'right') {
+ horizontalAlignment = Options.HorizontalAlignment.RIGHT;
+ }
+
+ var verticalSerial = serialized.vertical;
+ var verticalAlignment;
+ if (verticalSerial === 'top') {
+ verticalAlignment = Options.VerticalAlignment.TOP;
+ } else if (verticalSerial === 'middle') {
+ verticalAlignment = Options.VerticalAlignment.MIDDLE;
+ } else if (verticalSerial === 'bottom') {
+ verticalAlignment = Options.VerticalAlignment.BOTTOM;
+ }
+
+ var browserSerial = serialized.browser;
+ var browserSupport;
+ if (browserSerial) {
+ var browserVersions: Array = [];
+ Object.keys(browserSerial).forEach((browserShortName) => {
+ var browser = Options.Browser.AllBrowsers.filter(
+ (browser) => browser.shortName === browserShortName
+ )[0];
+ if (browser) {
+ var version = browserSerial[browserShortName];
+ if (version === 'none') {
+ version = null;
+ }
+ browserVersions.push(
+ new Options.BrowserVersionRequired(browser, version)
+ );
+ }
+ });
+ browserSupport = new Options.BrowserSupport(browserVersions);
+ } else {
+ browserSupport = new Options.BrowserSupport([]);
+ }
+
+ if (!content || !container || !horizontalAlignment || !verticalAlignment) {
+ return null;
+ }
+
+ return {
+ content,
+ container,
+ horizontalAlignment,
+ verticalAlignment,
+ browserSupport,
+ };
+}
+
+module.exports = {
+ serializeOptions,
+ deserializeOptions,
+}
diff --git a/package.json b/package.json
index b448bdd..9bec5ad 100644
--- a/package.json
+++ b/package.json
@@ -61,11 +61,13 @@
"js-string-escape": "^1.0.0",
"keymirror": "^0.1.1",
"less": "^2.3.1",
+ "location-bar": "^2.0.0",
"mocha": "^2.2.4",
"mustache": "^1.1.0",
"node-jsx": "^0.13.3",
"pngjs-image": "^0.11.4",
"q": "^1.4.1",
+ "qs": "^3.1.0",
"react": "^0.13.1",
"react-router": "^0.13.1",
"react-tools": "^0.13.2",