Docusaurus: Replant Playground (#1331)
Summary: Pull Request resolved: https://github.com/facebook/yoga/pull/1331 This lifts and copies code from the Yoga Playground component of the old website, to the new one, using a live workspace version of JS Yoga. This may eventually be used if the new website is fleshed out, but this also gives us real-world validation of the Yoga bindings in a web scenario, compared to the Node test runner. There is still some cleanup here needed, but we are able to bundle, typecheck, and render the playground now. The code here is mostly the same as before, but I removed some of the cruftier bits (e.g. URL shortener that goes to some private Heroku instance), and needed to do some tweaks for the new Yoga package, and TypeScript. This is currently using `yoga-layout/sync`, the asmjs bindings. But I had previously checked bundling against the wasm ones. Reviewed By: cortinico Differential Revision: D46884439 fbshipit-source-id: f53f0855c131cd2b81975bf05f71c43713600616
This commit is contained in:
committed by
Facebook GitHub Bot
parent
6ae890dccd
commit
45088187a7
300
website-next/src/components/Playground/index.tsx
Normal file
300
website-next/src/components/Playground/index.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {Direction} from 'yoga-layout/sync';
|
||||
import YogaNode from './YogaNode';
|
||||
import Editor from './Editor';
|
||||
import {List, setIn} from 'immutable';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import LayoutRecord from './LayoutRecord';
|
||||
import Sidebar from './Sidebar';
|
||||
import {Row, Col} from 'antd';
|
||||
import type {LayoutRecordType} from './LayoutRecord';
|
||||
import './index.css';
|
||||
|
||||
type Props = {
|
||||
layoutDefinition: Object,
|
||||
direction: Direction,
|
||||
maxDepth: number,
|
||||
maxChildren?: number,
|
||||
minChildren?: number,
|
||||
selectedNodePath?: Array<number>,
|
||||
showGuides: boolean,
|
||||
className?: string,
|
||||
height?: string | number,
|
||||
persist?: boolean,
|
||||
renderSidebar?: (layoutDefinition: LayoutRecordType, onChange: Function) => any,
|
||||
};
|
||||
|
||||
type State = {
|
||||
selectedNodePath?: Array<number>,
|
||||
layoutDefinition: LayoutRecordType,
|
||||
direction: Direction,
|
||||
};
|
||||
|
||||
function getPath(path: Array<number>): Array<unknown> {
|
||||
return path.reduce((acc, cv) => acc.concat('children', cv), []);
|
||||
}
|
||||
|
||||
export default class Playground extends Component<Props, State> {
|
||||
_containerRef?: HTMLElement;
|
||||
|
||||
static defaultProps = {
|
||||
layoutDefinition: {
|
||||
width: 500,
|
||||
height: 500,
|
||||
children: [
|
||||
{width: 100, height: 100},
|
||||
{width: 100, height: 100},
|
||||
{width: 100, height: 100},
|
||||
],
|
||||
},
|
||||
direction: Direction.LTR,
|
||||
maxDepth: 3,
|
||||
showGuides: true,
|
||||
persist: false,
|
||||
};
|
||||
|
||||
rehydrate = (node: Object): LayoutRecordType => {
|
||||
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;
|
||||
};
|
||||
|
||||
state = {
|
||||
selectedNodePath: this.props.selectedNodePath,
|
||||
layoutDefinition: this.rehydrate(this.props.layoutDefinition),
|
||||
direction: this.props.direction,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
|
||||
// rehydrate
|
||||
if (window.location.search && window.location.search.length > 1) {
|
||||
try {
|
||||
const restoredState = JSON.parse(
|
||||
atob(window.location.search.substr(1)),
|
||||
);
|
||||
this.setState({layoutDefinition: this.rehydrate(restoredState)});
|
||||
} catch (e) {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
null,
|
||||
window.location.origin + window.location.pathname,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.hideSidePanes();
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (e: React.MouseEvent) => {
|
||||
if (e.target === this._containerRef) {
|
||||
this.hideSidePanes();
|
||||
}
|
||||
};
|
||||
|
||||
hideSidePanes() {
|
||||
if (!Boolean(this.props.renderSidebar)) {
|
||||
// only unselect if we don't have an external sidebar, otherwise the
|
||||
// sidebar may rely on a certain node to be selected
|
||||
this.setState({
|
||||
selectedNodePath: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
// @ts-ignore
|
||||
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)
|
||||
// @ts-ignore
|
||||
.push(LayoutRecord({width: 100, height: 100}));
|
||||
this.modifyAtPath(path, updatedChildren);
|
||||
}
|
||||
};
|
||||
|
||||
modifyAtPath(
|
||||
path: Array<any>,
|
||||
value: any,
|
||||
selectedNodePath: Array<number> = this.state.selectedNodePath,
|
||||
) {
|
||||
const layoutDefinition = setIn(this.state.layoutDefinition, path, value);
|
||||
this.setState({
|
||||
layoutDefinition,
|
||||
selectedNodePath,
|
||||
});
|
||||
|
||||
if (this.props.persist) {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
null,
|
||||
window.location.origin +
|
||||
window.location.pathname +
|
||||
'?' +
|
||||
this.getHash(layoutDefinition),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getHash = (
|
||||
layoutDefinition: LayoutRecordType = this.state.layoutDefinition,
|
||||
): string =>
|
||||
btoa(JSON.stringify(this.removeUnchangedProperties(layoutDefinition)));
|
||||
|
||||
removeUnchangedProperties = (node: LayoutRecordType): Object => {
|
||||
const untouchedLayout = LayoutRecord({});
|
||||
const untouchedPosition = PositionRecord({});
|
||||
const result: {children?: unknown} = {};
|
||||
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: LayoutRecordType = (
|
||||
this.state.selectedNodePath || []
|
||||
).reduce(
|
||||
(node: LayoutRecordType, cv) => node.children.get(cv),
|
||||
this.state.layoutDefinition,
|
||||
);
|
||||
return selectedNode ? selectedNode.children.size : 0;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {layoutDefinition, selectedNodePath, direction} = this.state;
|
||||
const {height} = this.props;
|
||||
|
||||
// @ts-ignore
|
||||
const selectedNode: LayoutRecordType | null = selectedNodePath
|
||||
? layoutDefinition.getIn(getPath(selectedNodePath))
|
||||
: null;
|
||||
|
||||
const playground = (
|
||||
<div
|
||||
className={`Playground ${this.props.renderSidebar ? '' : 'standalone'}`}
|
||||
onMouseDown={this.onMouseDown}
|
||||
style={{height, maxHeight: height}}
|
||||
ref={ref => {
|
||||
this._containerRef = ref;
|
||||
}}>
|
||||
<YogaNode
|
||||
layoutDefinition={layoutDefinition}
|
||||
selectedNodePath={selectedNodePath}
|
||||
onClick={selectedNodePath => this.setState({selectedNodePath})}
|
||||
onDoubleClick={this.onAdd}
|
||||
direction={direction}
|
||||
showGuides={this.props.showGuides}
|
||||
/>
|
||||
{!this.props.renderSidebar && (
|
||||
<Sidebar>
|
||||
{this.state.selectedNodePath ? (
|
||||
<Editor
|
||||
node={selectedNode}
|
||||
selectedNodeIsRoot={
|
||||
selectedNodePath ? selectedNodePath.length === 0 : false
|
||||
}
|
||||
onChangeLayout={this.onChangeLayout}
|
||||
// @ts-ignore
|
||||
onChangeSetting={(key, value) => this.setState({[key]: value})}
|
||||
direction={direction}
|
||||
onRemove={
|
||||
selectedNodePath && selectedNodePath.length > 0
|
||||
? this.onRemove
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
selectedNodePath &&
|
||||
selectedNodePath.length < this.props.maxDepth
|
||||
? this.onAdd
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="NoContent">
|
||||
Select a node to edit its properties
|
||||
</div>
|
||||
)}
|
||||
</Sidebar>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.renderSidebar) {
|
||||
return (
|
||||
<div className={`PlaygroundContainer ${this.props.className || ''}`}>
|
||||
<div>
|
||||
{this.props.renderSidebar(
|
||||
// @ts-ignore
|
||||
layoutDefinition.getIn(getPath(selectedNodePath)),
|
||||
this.onChangeLayout,
|
||||
)}
|
||||
</div>
|
||||
{playground}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return playground;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user