add playground
Summary: This adds a web-based playground to try out Yoga. The playground uses yogas javascript bindings to use yoga within the browser. The layout tree can be modified and shared. Code generators for litho, ComponentKit and React Native allow the layout to be copied into any app. allow-large-files Reviewed By: emilsjolander Differential Revision: D6871601 fbshipit-source-id: 3b97c87e91d6bafe8e1c38b8b7eca8d372324c0b
This commit is contained in:
committed by
Facebook Github Bot
parent
afc215aa66
commit
9718c517d9
269
playground/src/Playground.js
Normal file
269
playground/src/Playground.js
Normal file
@@ -0,0 +1,269 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import yoga from 'yoga-layout';
|
||||
import YogaNode from './YogaNode';
|
||||
import Editor from './Editor';
|
||||
import {List, setIn} from 'immutable';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import LayoutRecord from './LayoutRecord';
|
||||
import Toolbar from './Toolbar';
|
||||
import Code from './Code';
|
||||
import Sidebar from './Sidebar';
|
||||
import type {LayoutRecordT} from './LayoutRecord';
|
||||
import type {Yoga$Direction} from 'yoga-layout';
|
||||
import './Playground.css';
|
||||
|
||||
type Props = {
|
||||
layoutDefinition: LayoutRecordT,
|
||||
direction: Yoga$Direction,
|
||||
maxDepth: number,
|
||||
maxChildren?: number,
|
||||
minChildren?: number,
|
||||
};
|
||||
type State = {
|
||||
selectedNodePath: ?Array<number>,
|
||||
layoutDefinition: LayoutRecordT,
|
||||
direction: Yoga$Direction,
|
||||
showCode: boolean,
|
||||
};
|
||||
|
||||
function getPath(path: Array<number>): Array<mixed> {
|
||||
return path.reduce((acc, cv) => acc.concat('children', cv), []);
|
||||
}
|
||||
|
||||
export default class App extends Component<Props, State> {
|
||||
_containerRef: ?HTMLElement;
|
||||
|
||||
static defaultProps = {
|
||||
layoutDefinition: LayoutRecord({
|
||||
width: 800,
|
||||
height: 400,
|
||||
justifyContent: yoga.JUSTIFY_SPACE_BETWEEN,
|
||||
alignItems: yoga.ALIGN_FLEX_START,
|
||||
children: List([LayoutRecord(), LayoutRecord()]),
|
||||
padding: PositionRecord({
|
||||
left: '10',
|
||||
top: '10',
|
||||
right: '10',
|
||||
bottom: '10',
|
||||
}),
|
||||
margin: PositionRecord({
|
||||
left: '20',
|
||||
top: '70',
|
||||
}),
|
||||
}),
|
||||
direction: yoga.DIRECTION_LTR,
|
||||
maxDepth: 3,
|
||||
showCode: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
selectedNodePath: null,
|
||||
layoutDefinition: this.props.layoutDefinition,
|
||||
direction: this.props.direction,
|
||||
showCode: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
|
||||
// rehydrate
|
||||
if (window.location.hash && window.location.hash.length > 1) {
|
||||
try {
|
||||
const restoredState = JSON.parse(atob(window.location.hash.substr(1)));
|
||||
this.setState({layoutDefinition: this.rehydrate(restoredState)});
|
||||
} catch (e) {
|
||||
window.location.hash = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
rehydrate = (node: Object): LayoutRecord => {
|
||||
let record = LayoutRecord(node);
|
||||
record = record.set('padding', PositionRecord(record.padding));
|
||||
record = record.set('border', PositionRecord(record.border));
|
||||
record = record.set('margin', PositionRecord(record.margin));
|
||||
record = record.set('position', PositionRecord(record.position));
|
||||
record = record.set('children', List(record.children.map(this.rehydrate)));
|
||||
return record;
|
||||
};
|
||||
|
||||
onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.hideSidePanes();
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (e: MouseEvent) => {
|
||||
if (e.target === this._containerRef) {
|
||||
this.hideSidePanes();
|
||||
}
|
||||
};
|
||||
|
||||
hideSidePanes() {
|
||||
this.setState({
|
||||
selectedNodePath: null,
|
||||
showCode: false,
|
||||
});
|
||||
}
|
||||
|
||||
onChangeLayout = (key: string, value: any) => {
|
||||
const {selectedNodePath} = this.state;
|
||||
if (selectedNodePath) {
|
||||
this.modifyAtPath([...getPath(selectedNodePath), key], value);
|
||||
}
|
||||
};
|
||||
|
||||
onRemove = () => {
|
||||
const {selectedNodePath, layoutDefinition} = this.state;
|
||||
if (selectedNodePath) {
|
||||
const index = selectedNodePath.pop();
|
||||
const path = getPath(selectedNodePath).concat('children');
|
||||
const updatedChildren = layoutDefinition.getIn(path).delete(index);
|
||||
this.modifyAtPath(path, updatedChildren);
|
||||
this.setState({selectedNodePath: null});
|
||||
}
|
||||
};
|
||||
|
||||
onAdd = () => {
|
||||
const {selectedNodePath, layoutDefinition} = this.state;
|
||||
if (selectedNodePath) {
|
||||
const path = getPath(selectedNodePath).concat('children');
|
||||
const updatedChildren = layoutDefinition.getIn(path).push(LayoutRecord());
|
||||
this.modifyAtPath(
|
||||
path,
|
||||
updatedChildren,
|
||||
selectedNodePath.concat(updatedChildren.size - 1),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
modifyAtPath(
|
||||
path: Array<any>,
|
||||
value: any,
|
||||
selectedNodePath?: ?Array<number> = this.state.selectedNodePath,
|
||||
) {
|
||||
// $FlowFixMe
|
||||
const layoutDefinition = setIn(this.state.layoutDefinition, path, value);
|
||||
this.setState({
|
||||
layoutDefinition,
|
||||
selectedNodePath,
|
||||
});
|
||||
|
||||
window.location.hash = btoa(
|
||||
JSON.stringify(this.removeUnchangedProperties(layoutDefinition)),
|
||||
);
|
||||
}
|
||||
|
||||
removeUnchangedProperties = (node: LayoutRecordT): Object => {
|
||||
const untouchedLayout = LayoutRecord({});
|
||||
const untouchedPosition = PositionRecord({});
|
||||
const result = {};
|
||||
if (!node.equals(untouchedLayout)) {
|
||||
Object.keys(node.toJS()).forEach(key => {
|
||||
if (key === 'children' && node.children.size > 0) {
|
||||
result.children = node.children
|
||||
.toJSON()
|
||||
.map(this.removeUnchangedProperties);
|
||||
} else if (
|
||||
node[key] instanceof PositionRecord &&
|
||||
!node[key].equals(untouchedPosition)
|
||||
) {
|
||||
result[key] = {};
|
||||
Object.keys(untouchedPosition.toJS()).forEach(position => {
|
||||
if (node[key][position] !== untouchedPosition[position]) {
|
||||
result[key][position] = node[key][position];
|
||||
}
|
||||
});
|
||||
} else if (node[key] !== untouchedLayout[key]) {
|
||||
result[key] = node[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
getChildrenCountForSelectedPath = (): number => {
|
||||
const selectedNode: ?LayoutRecordT = (
|
||||
this.state.selectedNodePath || []
|
||||
).reduce(
|
||||
(node: LayoutRecordT, cv) => node.children.get(cv),
|
||||
this.state.layoutDefinition,
|
||||
);
|
||||
return selectedNode ? selectedNode.children.size : 0;
|
||||
};
|
||||
|
||||
onToggleCode = () => {
|
||||
this.setState({
|
||||
selectedNodePath: this.state.showCode
|
||||
? this.state.selectedNodePath
|
||||
: null,
|
||||
showCode: !this.state.showCode,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {layoutDefinition, selectedNodePath} = this.state;
|
||||
|
||||
const selectedNode: ?LayoutRecordT = selectedNodePath
|
||||
? layoutDefinition.getIn(getPath(selectedNodePath))
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="App"
|
||||
onMouseDown={this.onMouseDown}
|
||||
ref={ref => {
|
||||
this._containerRef = ref;
|
||||
}}>
|
||||
<Toolbar
|
||||
onShowCode={
|
||||
!this.state.showCode
|
||||
? () => this.setState({selectedNodePath: null, showCode: true})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<YogaNode
|
||||
layoutDefinition={layoutDefinition}
|
||||
selectedNodePath={selectedNodePath}
|
||||
onClick={selectedNodePath =>
|
||||
this.setState({selectedNodePath, showCode: false})
|
||||
}
|
||||
onDoubleClick={this.onAdd}
|
||||
direction={this.state.direction}
|
||||
/>
|
||||
<Sidebar visible={Boolean(selectedNodePath) && !this.state.showCode}>
|
||||
<Editor
|
||||
node={selectedNode}
|
||||
onChangeLayout={this.onChangeLayout}
|
||||
onChangeSetting={(key, value) => this.setState({[key]: value})}
|
||||
direction={this.state.direction}
|
||||
onRemove={
|
||||
selectedNodePath && selectedNodePath.length > 0
|
||||
? this.onRemove
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
selectedNodePath && selectedNodePath.length < this.props.maxDepth
|
||||
? this.onAdd
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Sidebar>
|
||||
<Sidebar
|
||||
width={500}
|
||||
visible={this.state.showCode}
|
||||
onClose={() => this.setState({showCode: false})}>
|
||||
<Code
|
||||
layoutDefinition={layoutDefinition}
|
||||
direction={this.state.direction}
|
||||
/>
|
||||
</Sidebar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user