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",