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
@@ -18,10 +18,14 @@
|
|||||||
"@docusaurus/core": "2.4.1",
|
"@docusaurus/core": "2.4.1",
|
||||||
"@docusaurus/preset-classic": "2.4.1",
|
"@docusaurus/preset-classic": "2.4.1",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
|
"antd": "^3.6.5",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
"immutable": "^4.0.0",
|
||||||
"prism-react-renderer": "^1.3.5",
|
"prism-react-renderer": "^1.3.5",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2",
|
||||||
|
"react-syntax-highlighter": "^8.0.0",
|
||||||
|
"yoga-layout": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "2.4.1",
|
"@docusaurus/module-type-aliases": "2.4.1",
|
||||||
|
44
website-next/src/components/Playground/EditValue.tsx
Normal file
44
website-next/src/components/Playground/EditValue.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import YogaEnumSelect from './YogaEnumSelect';
|
||||||
|
import YogaPositionEditor from './YogaPositionEditor';
|
||||||
|
import {Input} from 'antd';
|
||||||
|
|
||||||
|
type Props<T> = {
|
||||||
|
property: string,
|
||||||
|
disabled?: boolean,
|
||||||
|
value?: T,
|
||||||
|
onChange: (property: string, value: T) => void,
|
||||||
|
placeholder?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (props: Props<any>) => {
|
||||||
|
if (YogaEnumSelect.availableProperties.indexOf(props.property) > -1) {
|
||||||
|
// @ts-ignore
|
||||||
|
return <YogaEnumSelect {...props} />;
|
||||||
|
} else if (
|
||||||
|
YogaPositionEditor.availableProperties.indexOf(props.property) > -1
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
return <YogaPositionEditor {...props} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
{...props}
|
||||||
|
onChange={e => props.onChange(props.property, e.target.value)}
|
||||||
|
placeholder={props.placeholder || 'undefined'}
|
||||||
|
onFocus={e => e.target.select()}
|
||||||
|
value={Number.isNaN(props.value) ? '' : props.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
63
website-next/src/components/Playground/Editor.css
Normal file
63
website-next/src/components/Playground/Editor.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.Editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
border-top: 1px solid #E8E8E8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor .field {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor .ant-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor h2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #444950;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor h2:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor .ant-tabs-nav .ant-tabs-tab {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor .EditorTabs {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor .ant-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor .ant-tabs-bar {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor .ant-tabs-tabpane {
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 320px;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 25vh;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Editor .EditorButtons {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
369
website-next/src/components/Playground/Editor.tsx
Normal file
369
website-next/src/components/Playground/Editor.tsx
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
/**
|
||||||
|
* 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 {Row, Col, Button, Tabs} from 'antd';
|
||||||
|
import EditValue from './EditValue';
|
||||||
|
import type {LayoutRecordType} from './LayoutRecord';
|
||||||
|
import type {Direction} from 'yoga-layout/sync';
|
||||||
|
import InfoText from './InfoText';
|
||||||
|
import './Editor.css';
|
||||||
|
const TabPane = Tabs.TabPane;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
node: LayoutRecordType,
|
||||||
|
onChangeLayout: (key: string, value: any) => void,
|
||||||
|
onChangeSetting: (key: string, value: any) => void,
|
||||||
|
direction: Direction,
|
||||||
|
selectedNodeIsRoot: boolean,
|
||||||
|
onRemove?: () => void,
|
||||||
|
onAdd?: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Editor extends Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
(e.key === 'Delete' || e.key === 'Backspace') &&
|
||||||
|
this.props.onRemove &&
|
||||||
|
!(e.target instanceof HTMLInputElement)
|
||||||
|
) {
|
||||||
|
this.props.onRemove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {node, selectedNodeIsRoot} = this.props;
|
||||||
|
const disabled = !Boolean(node);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Editor">
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<Tabs defaultActiveKey="1" className="EditorTabs">
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<TabPane tab="Flex" key="1">
|
||||||
|
<h2>
|
||||||
|
Direction
|
||||||
|
<InfoText doclink="/docs/layout-direction">
|
||||||
|
Defines the direction of which text and items are laid out
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
property="direction"
|
||||||
|
value={this.props.direction}
|
||||||
|
onChange={this.props.onChangeSetting}
|
||||||
|
/>
|
||||||
|
<h2>
|
||||||
|
Flex Direction
|
||||||
|
<InfoText doclink="/docs/flex-direction">
|
||||||
|
Defines the direction of the main-axis
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
disabled={disabled}
|
||||||
|
property="flexDirection"
|
||||||
|
value={node ? node.flexDirection : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Row gutter={15} style={{marginTop: 30}}>
|
||||||
|
<Col span={8}>
|
||||||
|
<h2>
|
||||||
|
Basis
|
||||||
|
<InfoText doclink="/docs/flex">
|
||||||
|
Default size of a node along the main axis
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
property="flexBasis"
|
||||||
|
placeholder="auto"
|
||||||
|
disabled={disabled || selectedNodeIsRoot}
|
||||||
|
value={node ? node.flexBasis : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<h2>
|
||||||
|
Grow
|
||||||
|
<InfoText doclink="/docs/flex">
|
||||||
|
The factor of remaining space should be given to this node
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
property="flexGrow"
|
||||||
|
placeholder="0"
|
||||||
|
disabled={disabled || selectedNodeIsRoot}
|
||||||
|
value={node ? node.flexGrow : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<h2>
|
||||||
|
Shrink
|
||||||
|
<InfoText doclink="/docs/flex">
|
||||||
|
The shrink factor of this element if parent has no space
|
||||||
|
left
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
property="flexShrink"
|
||||||
|
placeholder="1"
|
||||||
|
disabled={disabled || selectedNodeIsRoot}
|
||||||
|
value={node ? node.flexShrink : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Flex Wrap
|
||||||
|
<InfoText doclink="/docs/flex-wrap">
|
||||||
|
Wrapping behaviour when child nodes don't fit into a single line
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
disabled={disabled}
|
||||||
|
property="flexWrap"
|
||||||
|
value={node ? node.flexWrap : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</TabPane>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<TabPane tab="Alignment" key="2">
|
||||||
|
<h2>
|
||||||
|
Justify Content
|
||||||
|
<InfoText doclink="/docs/justify-content">
|
||||||
|
Aligns child nodes along the main-axis
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
disabled={disabled}
|
||||||
|
property="justifyContent"
|
||||||
|
value={node ? node.justifyContent : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Align Items
|
||||||
|
<InfoText doclink="/docs/align-items">
|
||||||
|
Aligns child nodes along the cross-axis
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
disabled={disabled}
|
||||||
|
property="alignItems"
|
||||||
|
value={node ? node.alignItems : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Align Self
|
||||||
|
<InfoText doclink="/docs/align-items">
|
||||||
|
Override align items of parent
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
disabled={disabled || selectedNodeIsRoot}
|
||||||
|
property="alignSelf"
|
||||||
|
value={node ? node.alignSelf : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Align Content
|
||||||
|
<InfoText doclink="/docs/align-content">
|
||||||
|
Alignment of lines along the cross-axis when wrapping
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
disabled={disabled}
|
||||||
|
property="alignContent"
|
||||||
|
value={node ? node.alignContent : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</TabPane>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<TabPane tab="Layout" key="3" className="ant-tabs-tabpane">
|
||||||
|
<h2>
|
||||||
|
Width × Height
|
||||||
|
<InfoText doclink="/docs/width-height">
|
||||||
|
Dimensions of the node
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<Row gutter={15}>
|
||||||
|
<Col span={12}>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
placeholder="auto"
|
||||||
|
property="width"
|
||||||
|
disabled={disabled}
|
||||||
|
value={node ? node.width : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
placeholder="auto"
|
||||||
|
property="height"
|
||||||
|
disabled={disabled}
|
||||||
|
value={node ? node.height : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<h2>
|
||||||
|
Max-Width × Max-Height
|
||||||
|
<InfoText doclink="/docs/min-max">
|
||||||
|
Maximum dimensions of the node
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<Row gutter={15}>
|
||||||
|
<Col span={12}>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
placeholder="none"
|
||||||
|
property="maxWidth"
|
||||||
|
disabled={disabled}
|
||||||
|
value={node ? node.maxWidth : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
placeholder="none"
|
||||||
|
property="maxHeight"
|
||||||
|
disabled={disabled}
|
||||||
|
value={node ? node.maxHeight : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<h2>
|
||||||
|
Min-Width × Min-Height
|
||||||
|
<InfoText doclink="/docs/min-max">
|
||||||
|
Minimum dimensions of the node
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<Row gutter={15}>
|
||||||
|
<Col span={12}>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
placeholder="0"
|
||||||
|
property="minWidth"
|
||||||
|
disabled={disabled}
|
||||||
|
value={node ? node.minWidth : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
placeholder="0"
|
||||||
|
property="minHeight"
|
||||||
|
disabled={disabled}
|
||||||
|
value={node ? node.minHeight : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Aspect Ratio
|
||||||
|
<InfoText doclink="/docs/aspect-ratio">
|
||||||
|
Width/Height aspect ratio of node
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
<EditValue
|
||||||
|
// @ts-ignore
|
||||||
|
type="text"
|
||||||
|
placeholder="auto"
|
||||||
|
property="aspectRatio"
|
||||||
|
disabled={disabled}
|
||||||
|
value={node ? node.aspectRatio : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{['padding', 'border', 'margin'].map(property => (
|
||||||
|
<EditValue
|
||||||
|
property={property}
|
||||||
|
key={property}
|
||||||
|
value={node ? node[property] : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
disabled={property === 'margin' && selectedNodeIsRoot}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<h2>
|
||||||
|
Position Type
|
||||||
|
<InfoText doclink="/docs/absolute-relative-layout">
|
||||||
|
Relative position offsets the node from it's calculated
|
||||||
|
position. Absolute position removes the node from the flexbox
|
||||||
|
flow and positions it at the given position.
|
||||||
|
</InfoText>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<EditValue
|
||||||
|
disabled={disabled || selectedNodeIsRoot}
|
||||||
|
property="positionType"
|
||||||
|
value={node ? node.positionType : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
<EditValue
|
||||||
|
disabled={selectedNodeIsRoot}
|
||||||
|
property="position"
|
||||||
|
value={node ? node.position : undefined}
|
||||||
|
onChange={this.props.onChangeLayout}
|
||||||
|
/>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Row gutter={15} className="EditorButtons">
|
||||||
|
<Col span={12}>
|
||||||
|
<Button
|
||||||
|
icon="plus-circle-o"
|
||||||
|
disabled={!Boolean(this.props.onAdd)}
|
||||||
|
onClick={this.props.onAdd}
|
||||||
|
type="primary">
|
||||||
|
add child node
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Button
|
||||||
|
icon="close-circle-o"
|
||||||
|
disabled={!Boolean(this.props.onRemove)}
|
||||||
|
onClick={this.props.onRemove}
|
||||||
|
type="danger">
|
||||||
|
remove node
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
22
website-next/src/components/Playground/InfoText.css
Normal file
22
website-next/src/components/Playground/InfoText.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.InfoText {
|
||||||
|
max-width: 230px;
|
||||||
|
line-height: 130%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.InfoText .docs-link {
|
||||||
|
margin-top: 0px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.InfoTextIcon {
|
||||||
|
margin-left: 5px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
38
website-next/src/components/Playground/InfoText.tsx
Normal file
38
website-next/src/components/Playground/InfoText.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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 {Popover, Icon} from 'antd';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import './InfoText.css';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: any,
|
||||||
|
doclink: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class InfoText extends Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<div className="InfoText">
|
||||||
|
<p>{this.props.children}</p>
|
||||||
|
<Link className="docs-link" to={this.props.doclink}>
|
||||||
|
DOCUMENTATION
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
trigger="hover">
|
||||||
|
{/*@ts-ignore*/}
|
||||||
|
<Icon className="InfoTextIcon" type="info-circle-o" />
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
81
website-next/src/components/Playground/LayoutRecord.tsx
Normal file
81
website-next/src/components/Playground/LayoutRecord.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 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 {Record, List} from 'immutable';
|
||||||
|
import PositionRecord from './PositionRecord';
|
||||||
|
import type {PositionRecordType} from './PositionRecord';
|
||||||
|
import yoga from 'yoga-layout/sync';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Align,
|
||||||
|
Justify,
|
||||||
|
FlexDirection,
|
||||||
|
Wrap,
|
||||||
|
PositionType,
|
||||||
|
} from 'yoga-layout/sync';
|
||||||
|
|
||||||
|
export type LayoutRecordType = ReturnType<LayoutRecordFactory>;
|
||||||
|
|
||||||
|
export type LayoutRecordFactory = Record.Factory<{
|
||||||
|
width?: number | 'auto',
|
||||||
|
height?: number | 'auto',
|
||||||
|
minWidth?: number,
|
||||||
|
minHeight?: number,
|
||||||
|
maxWidth?: number,
|
||||||
|
maxHeight?: number,
|
||||||
|
justifyContent?: Justify,
|
||||||
|
padding: PositionRecordType,
|
||||||
|
border: PositionRecordType,
|
||||||
|
margin: PositionRecordType,
|
||||||
|
position: PositionRecordType,
|
||||||
|
positionType: PositionType,
|
||||||
|
alignItems?: Align,
|
||||||
|
alignSelf?: Align,
|
||||||
|
alignContent?: Align,
|
||||||
|
flexDirection?: FlexDirection,
|
||||||
|
flexBasis?: number | 'auto',
|
||||||
|
flexGrow?: number,
|
||||||
|
flexShrink?: number,
|
||||||
|
flexWrap?: Wrap,
|
||||||
|
aspectRatio?: number | 'auto',
|
||||||
|
children?: List<LayoutRecordType>,
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const r: LayoutRecordFactory = Record({
|
||||||
|
width: 'auto',
|
||||||
|
height: 'auto',
|
||||||
|
justifyContent: yoga.JUSTIFY_FLEX_START,
|
||||||
|
alignItems: yoga.ALIGN_STRETCH,
|
||||||
|
alignSelf: yoga.ALIGN_AUTO,
|
||||||
|
alignContent: yoga.ALIGN_STRETCH,
|
||||||
|
flexDirection: yoga.FLEX_DIRECTION_ROW,
|
||||||
|
padding: PositionRecord(),
|
||||||
|
margin: PositionRecord(),
|
||||||
|
border: PositionRecord(),
|
||||||
|
position: PositionRecord({
|
||||||
|
left: NaN,
|
||||||
|
top: NaN,
|
||||||
|
right: NaN,
|
||||||
|
bottom: NaN,
|
||||||
|
}),
|
||||||
|
positionType: yoga.POSITION_TYPE_RELATIVE,
|
||||||
|
flexWrap: yoga.WRAP_NO_WRAP,
|
||||||
|
flexBasis: 'auto',
|
||||||
|
flexGrow: 0,
|
||||||
|
flexShrink: 1,
|
||||||
|
children: List(),
|
||||||
|
aspectRatio: 'auto',
|
||||||
|
minWidth: NaN,
|
||||||
|
maxWidth: NaN,
|
||||||
|
minHeight: NaN,
|
||||||
|
maxHeight: NaN,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default r;
|
16
website-next/src/components/Playground/PositionGuide.css
Normal file
16
website-next/src/components/Playground/PositionGuide.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PositionGuide {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
161
website-next/src/components/Playground/PositionGuide.tsx
Normal file
161
website-next/src/components/Playground/PositionGuide.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* 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 PositionRecord from './PositionRecord';
|
||||||
|
import type {PositionRecordType} from './PositionRecord';
|
||||||
|
import './PositionGuide.css';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
inset?: boolean,
|
||||||
|
reverse?: boolean,
|
||||||
|
position: PositionRecordType,
|
||||||
|
offset: PositionRecordType,
|
||||||
|
color: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class PositionGuide extends Component<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
offset: PositionRecord({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {position, offset, inset, color, reverse} = this.props;
|
||||||
|
let {top, left, right, bottom} = position;
|
||||||
|
let {top: oTop, left: oLeft, right: oRight, bottom: oBottom} = offset;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof top !== 'number' ||
|
||||||
|
typeof left !== 'number' ||
|
||||||
|
typeof right !== 'number' ||
|
||||||
|
typeof bottom !== 'number' ||
|
||||||
|
typeof oTop !== 'number' ||
|
||||||
|
typeof oLeft !== 'number' ||
|
||||||
|
typeof oRight !== 'number' ||
|
||||||
|
typeof oBottom !== 'number'
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
let temp1 = left;
|
||||||
|
left = right;
|
||||||
|
right = temp1;
|
||||||
|
temp1 = oLeft;
|
||||||
|
oLeft = oRight;
|
||||||
|
oRight = temp1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!top) {
|
||||||
|
top = 0;
|
||||||
|
}
|
||||||
|
if (!left) {
|
||||||
|
left = 0;
|
||||||
|
}
|
||||||
|
if (!right) {
|
||||||
|
right = 0;
|
||||||
|
}
|
||||||
|
if (!bottom) {
|
||||||
|
bottom = 0;
|
||||||
|
}
|
||||||
|
if (!oTop) {
|
||||||
|
oTop = 0;
|
||||||
|
}
|
||||||
|
if (!oLeft) {
|
||||||
|
oLeft = 0;
|
||||||
|
}
|
||||||
|
if (!oRight) {
|
||||||
|
oRight = 0;
|
||||||
|
}
|
||||||
|
if (!oBottom) {
|
||||||
|
oBottom = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inset) {
|
||||||
|
if (typeof top === 'number' && typeof bottom === 'number') {
|
||||||
|
if (top < 0) {
|
||||||
|
bottom -= top;
|
||||||
|
top = 0;
|
||||||
|
}
|
||||||
|
if (bottom < 0) {
|
||||||
|
top -= bottom;
|
||||||
|
bottom = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (left < 0) {
|
||||||
|
right -= left;
|
||||||
|
left = 0;
|
||||||
|
}
|
||||||
|
if (right < 0) {
|
||||||
|
left -= right;
|
||||||
|
right = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
top !== 0 ? (
|
||||||
|
<div
|
||||||
|
key="top"
|
||||||
|
className="PositionGuide"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
height: top,
|
||||||
|
top: inset ? oTop : -top - oTop,
|
||||||
|
left: inset ? left + oLeft : -left - oLeft,
|
||||||
|
right: inset ? right + oRight : -right - oRight,
|
||||||
|
}}>
|
||||||
|
{top}
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
left !== 0 ? (
|
||||||
|
<div
|
||||||
|
key="left"
|
||||||
|
className="PositionGuide"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
width: left,
|
||||||
|
top: inset ? oTop : -oTop,
|
||||||
|
bottom: inset ? oBottom : -oBottom,
|
||||||
|
left: inset ? oLeft : -left - oLeft,
|
||||||
|
}}>
|
||||||
|
{left}
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
right !== 0 ? (
|
||||||
|
<div
|
||||||
|
key="right"
|
||||||
|
className="PositionGuide"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
width: right,
|
||||||
|
top: inset ? oTop : -oTop,
|
||||||
|
bottom: inset ? oBottom : -oBottom,
|
||||||
|
right: inset ? oRight : -right - oRight,
|
||||||
|
}}>
|
||||||
|
{right}
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
bottom !== 0 ? (
|
||||||
|
<div
|
||||||
|
key="bottom"
|
||||||
|
className="PositionGuide"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
height: bottom,
|
||||||
|
bottom: inset ? oBottom : -bottom - oBottom,
|
||||||
|
left: inset ? left + oLeft : -left - oLeft,
|
||||||
|
right: inset ? right + oRight : -right - oRight,
|
||||||
|
}}>
|
||||||
|
{bottom}
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
28
website-next/src/components/Playground/PositionRecord.tsx
Normal file
28
website-next/src/components/Playground/PositionRecord.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 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 {Record} from 'immutable';
|
||||||
|
|
||||||
|
export type PositionRecordType = ReturnType<PositionRecordFactory>;
|
||||||
|
|
||||||
|
export type PositionRecordFactory = Record.Factory<{
|
||||||
|
top: string | number,
|
||||||
|
right: string | number,
|
||||||
|
bottom: string | number,
|
||||||
|
left: string | number,
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const r: PositionRecordFactory = Record({
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default r;
|
22
website-next/src/components/Playground/Sidebar.css
Normal file
22
website-next/src/components/Playground/Sidebar.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.Sidebar {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 350px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 25px;
|
||||||
|
max-height: calc(100% - 50px);
|
||||||
|
border-radius: 6px;
|
||||||
|
bottom: auto;
|
||||||
|
box-shadow: 3px 3px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
26
website-next/src/components/Playground/Sidebar.tsx
Normal file
26
website-next/src/components/Playground/Sidebar.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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 './Sidebar.css';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width?: number,
|
||||||
|
children: any,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Sidebar extends Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="Sidebar" style={{width: this.props.width}}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
website-next/src/components/Playground/YogaEnumSelect.css
Normal file
25
website-next/src/components/Playground/YogaEnumSelect.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.YogaEnumSelect {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaEnumSelect.ant-radio-group .ant-radio-button-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaEnumSelect .ant-btn {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaEnumSelect .ant-radio-button-wrapper {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
110
website-next/src/components/Playground/YogaEnumSelect.tsx
Normal file
110
website-next/src/components/Playground/YogaEnumSelect.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 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 Yoga from 'yoga-layout/sync';
|
||||||
|
import {Radio, Menu, Dropdown, Button, Icon} from 'antd';
|
||||||
|
import './YogaEnumSelect.css';
|
||||||
|
const RadioButton = Radio.Button;
|
||||||
|
const RadioGroup = Radio.Group;
|
||||||
|
|
||||||
|
const PROPERTY_LOOKUP = {
|
||||||
|
flexDirection: 'FLEX_DIRECTION',
|
||||||
|
direction: 'DIRECTION',
|
||||||
|
justifyContent: 'JUSTIFY',
|
||||||
|
alignSelf: 'ALIGN',
|
||||||
|
alignContent: 'ALIGN',
|
||||||
|
alignItems: 'ALIGN',
|
||||||
|
positionType: 'POSITION_TYPE',
|
||||||
|
flexWrap: 'WRAP',
|
||||||
|
};
|
||||||
|
|
||||||
|
type Property = keyof(typeof PROPERTY_LOOKUP);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
property: Property,
|
||||||
|
disabled?: boolean,
|
||||||
|
value: string | number,
|
||||||
|
onChange: (property: Property, value: number) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class YogaEnumSelect extends Component<Props> {
|
||||||
|
static availableProperties = Object.keys(PROPERTY_LOOKUP);
|
||||||
|
|
||||||
|
values: Array<{key: string, value: number}>;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const property = PROPERTY_LOOKUP[props.property];
|
||||||
|
|
||||||
|
this.values = Object.keys(Yoga)
|
||||||
|
.map(key => ({key, value: Yoga[key]}))
|
||||||
|
.filter(
|
||||||
|
({key}) => key.startsWith(property) && key !== `${property}_COUNT`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMenuClick = ({key}: {key: string}) => {
|
||||||
|
this.props.onChange(this.props.property, Yoga[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
getTitle = (property: string, key: string): string => {
|
||||||
|
const replacer = new RegExp(`^${property}_`);
|
||||||
|
return key
|
||||||
|
.replace(replacer, '')
|
||||||
|
.replace('_', ' ')
|
||||||
|
.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const property = PROPERTY_LOOKUP[this.props.property];
|
||||||
|
const selected = this.values.find(
|
||||||
|
({key, value}) => value === this.props.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.values.length > 3 ? (
|
||||||
|
<div className="YogaEnumSelect">
|
||||||
|
{/*@ts-ignore*/}
|
||||||
|
<Dropdown
|
||||||
|
trigger={['click']}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
overlay={
|
||||||
|
// @ts-ignore
|
||||||
|
<Menu onClick={this.handleMenuClick}>
|
||||||
|
{this.values.map(({key, value}) => (
|
||||||
|
// @ts-ignore
|
||||||
|
<Menu.Item key={key} value={value}>
|
||||||
|
{this.getTitle(property, key)}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
}>
|
||||||
|
<Button>
|
||||||
|
{selected ? this.getTitle(property, selected.key) : ''}
|
||||||
|
{/*@ts-ignore*/}
|
||||||
|
<Icon type="down" />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<RadioGroup
|
||||||
|
{...this.props}
|
||||||
|
onChange={e => this.props.onChange(this.props.property, e.target.value)}
|
||||||
|
defaultValue="a"
|
||||||
|
className="YogaEnumSelect">
|
||||||
|
{this.values.map(({key, value}) => (
|
||||||
|
<RadioButton key={key} value={value}>
|
||||||
|
{this.getTitle(property, key)}
|
||||||
|
</RadioButton>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
57
website-next/src/components/Playground/YogaNode.css
Normal file
57
website-next/src/components/Playground/YogaNode.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.YogaNode {
|
||||||
|
background: white;
|
||||||
|
position: absolute;
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: inset 0 0 0px 1px rgba(48, 56, 69, 0.2);
|
||||||
|
transition: 0.2s all, 0s outline, 0s box-shadow;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaNode.hover {
|
||||||
|
background-color: #F0FFF9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaNode .YogaNode {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaNode .YogaNode.hover{
|
||||||
|
background: rgba(240, 255, 249, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaNode:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaNode.focused {
|
||||||
|
box-shadow: 0 0 0 2px #68CFBB, 0 0 15px rgba(0, 0, 0, 0.2),
|
||||||
|
inset 0 0 0px 1px rgba(255, 255, 255, 0.2);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaNode.invisible {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaNode .label {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
302
website-next/src/components/Playground/YogaNode.tsx
Normal file
302
website-next/src/components/Playground/YogaNode.tsx
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
/**
|
||||||
|
* 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 Yoga from 'yoga-layout/sync';
|
||||||
|
import PositionGuide from './PositionGuide';
|
||||||
|
import PositionRecord from './PositionRecord';
|
||||||
|
import LayoutRecord from './LayoutRecord';
|
||||||
|
import type {LayoutRecordType} from './LayoutRecord';
|
||||||
|
import {Direction, Display, Edge, Node, Wrap} from 'yoga-layout/sync';
|
||||||
|
|
||||||
|
import './YogaNode.css';
|
||||||
|
|
||||||
|
type ComputedLayout = {
|
||||||
|
left: number,
|
||||||
|
top: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
children: Array<ComputedLayout>,
|
||||||
|
node: Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layoutDefinition: LayoutRecordType,
|
||||||
|
className?: string,
|
||||||
|
computedLayout?: ComputedLayout,
|
||||||
|
path: Array<number>,
|
||||||
|
selectedNodePath?: Array<number>,
|
||||||
|
direction?: Direction,
|
||||||
|
label?: string,
|
||||||
|
showGuides: boolean,
|
||||||
|
onClick?: (path: Array<number>) => void,
|
||||||
|
onDoubleClick?: (path: Array<number>) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
visible?: boolean,
|
||||||
|
hovered: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class YogaNode extends Component<Props, State> {
|
||||||
|
node: Node;
|
||||||
|
_ref: HTMLDivElement;
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
path: [],
|
||||||
|
label: 'root',
|
||||||
|
showGuides: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hovered: false,
|
||||||
|
visible: false,
|
||||||
|
};
|
||||||
|
computedLayout?: ComputedLayout;
|
||||||
|
rootNode?: Node;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
if (!props.computedLayout) {
|
||||||
|
// is root node
|
||||||
|
this.calculateLayout(props);
|
||||||
|
this.state = {
|
||||||
|
hovered: false,
|
||||||
|
visible: !Boolean(props.computedLayout),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
setTimeout(() => this.setState({visible: true}), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
|
if (
|
||||||
|
!nextProps.computedLayout &&
|
||||||
|
(!this.props.layoutDefinition.equals(nextProps.layoutDefinition) ||
|
||||||
|
this.props.direction !== nextProps.direction)
|
||||||
|
) {
|
||||||
|
// is root node and the layout definition or settings changed
|
||||||
|
this.calculateLayout(nextProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.rootNode) {
|
||||||
|
this.rootNode.freeRecursive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove = e => {
|
||||||
|
this.setState({hovered: e.target === this._ref});
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateLayout(props: Props) {
|
||||||
|
const root = this.createYogaNodes(props.layoutDefinition);
|
||||||
|
root.calculateLayout(
|
||||||
|
props.layoutDefinition.width,
|
||||||
|
props.layoutDefinition.height,
|
||||||
|
props.direction,
|
||||||
|
);
|
||||||
|
this.computedLayout = this.getComputedLayout(root);
|
||||||
|
this.rootNode = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
createYogaNodes = (layoutDefinition: LayoutRecordType): Node => {
|
||||||
|
const root = Yoga.Node.create();
|
||||||
|
|
||||||
|
const defaultLayout = LayoutRecord({});
|
||||||
|
[
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'minWidth',
|
||||||
|
'maxWidth',
|
||||||
|
'minHeight',
|
||||||
|
'maxHeight',
|
||||||
|
'justifyContent',
|
||||||
|
'alignItems',
|
||||||
|
'alignSelf',
|
||||||
|
'alignContent',
|
||||||
|
'flexGrow',
|
||||||
|
'flexShrink',
|
||||||
|
'positionType',
|
||||||
|
'aspectRatio',
|
||||||
|
'flexWrap',
|
||||||
|
'flexDirection',
|
||||||
|
].forEach(key => {
|
||||||
|
try {
|
||||||
|
const value =
|
||||||
|
layoutDefinition[key] === ''
|
||||||
|
? defaultLayout[key]
|
||||||
|
: layoutDefinition[key];
|
||||||
|
root[`set${key[0].toUpperCase()}${key.substr(1)}`](value);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
['padding', 'margin', 'position', 'border'].forEach(key => {
|
||||||
|
['top', 'right', 'bottom', 'left'].forEach(direction => {
|
||||||
|
try {
|
||||||
|
root[`set${key[0].toUpperCase()}${key.substr(1)}`](
|
||||||
|
Yoga[`EDGE_${direction.toUpperCase()}`],
|
||||||
|
layoutDefinition[key][direction],
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
root.setDisplay(Display.Flex);
|
||||||
|
|
||||||
|
(layoutDefinition.children || [])
|
||||||
|
.map(this.createYogaNodes)
|
||||||
|
.forEach((node, i) => {
|
||||||
|
root.insertChild(node, i);
|
||||||
|
});
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
|
||||||
|
getComputedLayout = (node: Node): ComputedLayout => {
|
||||||
|
return {
|
||||||
|
...node.getComputedLayout(),
|
||||||
|
node,
|
||||||
|
children: Array.apply(null, Array(node.getChildCount())).map((_, i) =>
|
||||||
|
this.getComputedLayout(node.getChild(i)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onClick = (e: React.MouseEvent) => {
|
||||||
|
const {onClick} = this.props;
|
||||||
|
if (onClick) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick(this.props.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onDoubleClick = (e: React.MouseEvent) => {
|
||||||
|
const {onDoubleClick} = this.props;
|
||||||
|
if (onDoubleClick) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDoubleClick(this.props.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMouseLeave = (e: React.MouseEvent) => this.setState({hovered: false});
|
||||||
|
|
||||||
|
showPositionGuides({node}: ComputedLayout) {
|
||||||
|
const padding = PositionRecord({
|
||||||
|
top: node.getComputedPadding(Edge.Top),
|
||||||
|
left: node.getComputedPadding(Edge.Left),
|
||||||
|
right: node.getComputedPadding(Edge.Right),
|
||||||
|
bottom: node.getComputedPadding(Edge.Bottom),
|
||||||
|
});
|
||||||
|
const border = PositionRecord({
|
||||||
|
top: node.getComputedBorder(Edge.Top),
|
||||||
|
left: node.getComputedBorder(Edge.Left),
|
||||||
|
right: node.getComputedBorder(Edge.Right),
|
||||||
|
bottom: node.getComputedBorder(Edge.Bottom),
|
||||||
|
});
|
||||||
|
const margin = PositionRecord({
|
||||||
|
top: node.getComputedMargin(Edge.Top),
|
||||||
|
left: node.getComputedMargin(Edge.Left),
|
||||||
|
right: node.getComputedMargin(Edge.Right),
|
||||||
|
bottom: node.getComputedMargin(Edge.Bottom),
|
||||||
|
});
|
||||||
|
const position = PositionRecord({
|
||||||
|
top: node.getPosition(Edge.Top).value,
|
||||||
|
left: node.getPosition(Edge.Left).value,
|
||||||
|
right: node.getPosition(Edge.Right).value,
|
||||||
|
bottom: node.getPosition(Edge.Bottom).value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
<PositionGuide
|
||||||
|
key="border"
|
||||||
|
inset
|
||||||
|
position={border}
|
||||||
|
color="rgba(251, 170, 51, 0.15)"
|
||||||
|
reverse={node.getFlexWrap() === Wrap.WrapReverse}
|
||||||
|
/>,
|
||||||
|
<PositionGuide
|
||||||
|
key="padding"
|
||||||
|
inset
|
||||||
|
offset={border}
|
||||||
|
position={padding}
|
||||||
|
color="rgba(123, 179, 41, 0.1)"
|
||||||
|
reverse={node.getFlexWrap() === Wrap.WrapReverse}
|
||||||
|
/>,
|
||||||
|
<PositionGuide
|
||||||
|
key="margin"
|
||||||
|
position={margin}
|
||||||
|
color="rgba(214, 43, 28, 0.1)"
|
||||||
|
/>,
|
||||||
|
<PositionGuide
|
||||||
|
key="position"
|
||||||
|
offset={margin}
|
||||||
|
position={position}
|
||||||
|
color="rgba(115, 51, 205, 0.1)"
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
layoutDefinition,
|
||||||
|
className,
|
||||||
|
path,
|
||||||
|
selectedNodePath,
|
||||||
|
label,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const computedLayout: ComputedLayout =
|
||||||
|
this.props.computedLayout || this.computedLayout;
|
||||||
|
const {left, top, width, height, children} = computedLayout;
|
||||||
|
|
||||||
|
const isFocused = selectedNodePath && selectedNodePath.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`YogaNode ${isFocused ? 'focused' : ''} ${className || ''} ${
|
||||||
|
this.state.visible ? '' : 'invisible'
|
||||||
|
} ${this.state.hovered ? 'hover' : ''}`}
|
||||||
|
style={path.length == 0 ? {width, height} : {left, top, width, height}}
|
||||||
|
onDoubleClick={this.onDoubleClick}
|
||||||
|
onMouseMove={this.onMouseMove}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
ref={ref => {
|
||||||
|
this._ref = ref;
|
||||||
|
}}
|
||||||
|
onClick={this.onClick}>
|
||||||
|
{label && <div className="label">{label}</div>}
|
||||||
|
{isFocused &&
|
||||||
|
this.props.showGuides &&
|
||||||
|
this.showPositionGuides(computedLayout)}
|
||||||
|
{(children || []).map((child: ComputedLayout, i) => (
|
||||||
|
<YogaNode
|
||||||
|
key={i}
|
||||||
|
computedLayout={child}
|
||||||
|
label={String(i + 1)}
|
||||||
|
layoutDefinition={layoutDefinition.children.get(i)}
|
||||||
|
selectedNodePath={
|
||||||
|
selectedNodePath &&
|
||||||
|
selectedNodePath.length > 0 &&
|
||||||
|
selectedNodePath[0] === i
|
||||||
|
? selectedNodePath.slice(1)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
path={path.concat(i)}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
onDoubleClick={this.props.onDoubleClick}
|
||||||
|
showGuides={this.props.showGuides}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.YogaPositionEditor {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 60%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaPositionEditor input {
|
||||||
|
width: 55px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YogaPositionEditorRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #444950;
|
||||||
|
}
|
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* 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 {Input} from 'antd';
|
||||||
|
import PositionRecord from './PositionRecord';
|
||||||
|
import type {PositionRecordType} from './PositionRecord';
|
||||||
|
import './YogaPositionEditor.css';
|
||||||
|
|
||||||
|
type Property = 'position' | 'margin' | 'padding' | 'border';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: PositionRecordType,
|
||||||
|
property: Property,
|
||||||
|
disabled?: boolean,
|
||||||
|
onChange: (property: Property, value: PositionRecordType) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class YogaPositionEditor extends Component<Props> {
|
||||||
|
static availableProperties = ['position', 'margin', 'padding', 'border'];
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
value: PositionRecord(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {onChange, value, property, disabled} = this.props;
|
||||||
|
return (
|
||||||
|
<div className="YogaPositionEditor">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={Number.isNaN(value.top) ? '' : value.top}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={e => onChange(property, value.set('top', e.target.value))}
|
||||||
|
/>
|
||||||
|
<div className="YogaPositionEditorRow">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={Number.isNaN(value.left) ? '' : value.left}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={e =>
|
||||||
|
onChange(property, value.set('left', e.target.value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{property.toUpperCase()}
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={Number.isNaN(value.right) ? '' : value.right}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={e =>
|
||||||
|
onChange(property, value.set('right', e.target.value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={Number.isNaN(value.bottom) ? '' : value.bottom}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={e =>
|
||||||
|
onChange(property, value.set('bottom', e.target.value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
74
website-next/src/components/Playground/index.css
Normal file
74
website-next/src/components/Playground/index.css
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PlaygroundContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Playground {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(-90deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px),
|
||||||
|
linear-gradient(rgba(0, 0, 0, 0.02) 1px, transparent 1px),
|
||||||
|
linear-gradient(-90deg, rgba(0, 0, 0, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(
|
||||||
|
transparent 4px,
|
||||||
|
#f5f5f5 4px,
|
||||||
|
#f5f5f5 97px,
|
||||||
|
transparent 97px
|
||||||
|
),
|
||||||
|
linear-gradient(-90deg, #e5e5e5 1px, transparent 1px),
|
||||||
|
linear-gradient(
|
||||||
|
-90deg,
|
||||||
|
transparent 4px,
|
||||||
|
#f5f5f5 4px,
|
||||||
|
#f5f5f5 97px,
|
||||||
|
transparent 97px
|
||||||
|
),
|
||||||
|
linear-gradient(#e5e5e5 1px, transparent 1px), #f5f5f5;
|
||||||
|
background-size: 10px 10px, 10px 10px, 100px 100px, 100px 100px, 100px 100px,
|
||||||
|
100px 100px, 100px 100px, 100px 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Playground > .YogaNode {
|
||||||
|
margin: auto;
|
||||||
|
position: static;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Playground.standalone > .YogaNode {
|
||||||
|
transform: translateX(-175px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Playground .Actions {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Playground .Actions .ant-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Playground .NoContent {
|
||||||
|
border-top: 1px solid #E8E8E8;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 30px 50px;
|
||||||
|
text-align: center;
|
||||||
|
color: #D9D9D9;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 130%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,7 @@ import Link from '@docusaurus/Link';
|
|||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import HomepageFeatures from '@site/src/components/HomepageFeatures';
|
import HomepageFeatures from '@site/src/components/HomepageFeatures';
|
||||||
|
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||||
|
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
|
||||||
@@ -42,6 +43,12 @@ export default function Home(): JSX.Element {
|
|||||||
<HomepageHeader />
|
<HomepageHeader />
|
||||||
<main>
|
<main>
|
||||||
<HomepageFeatures />
|
<HomepageFeatures />
|
||||||
|
<BrowserOnly fallback={null}>
|
||||||
|
{() => {
|
||||||
|
const Playground = require('../components/Playground');
|
||||||
|
return <Playground />;
|
||||||
|
}}
|
||||||
|
</BrowserOnly>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user