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
20
playground/src/Code.css
Normal file
20
playground/src/Code.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.Code {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.CodeContainer {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.CodeLanguageSelector.ant-radio-group {
|
||||
display: flex;
|
||||
margin: 10px 15px;
|
||||
}
|
||||
|
||||
.CodeLanguageSelector.ant-radio-group .ant-radio-button-wrapper {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
text-align: center;
|
||||
}
|
103
playground/src/Code.js
Normal file
103
playground/src/Code.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import SyntaxHighlighter, {
|
||||
registerLanguage,
|
||||
} from 'react-syntax-highlighter/prism-light';
|
||||
import styles from 'react-syntax-highlighter/styles/prism/prism';
|
||||
import jsx from 'react-syntax-highlighter/languages/prism/jsx';
|
||||
//import javascript from 'react-syntax-highlighter/languages/prism/javascript';
|
||||
import java from 'react-syntax-highlighter/languages/prism/java';
|
||||
import objectivec from 'react-syntax-highlighter/languages/prism/objectivec';
|
||||
import type {LayoutRecordT} from './LayoutRecord';
|
||||
import {Radio} from 'antd';
|
||||
import CodeJavaScript from './CodeJavaScript';
|
||||
import CodeLitho from './CodeLitho';
|
||||
import CodeReactNative from './CodeReactNative';
|
||||
import CodeComponentKit from './CodeComponentKit';
|
||||
import './Code.css';
|
||||
import type {Yoga$Direction} from 'yoga-layout';
|
||||
const RadioButton = Radio.Button;
|
||||
const RadioGroup = Radio.Group;
|
||||
registerLanguage('jsx', jsx);
|
||||
//registerLanguage('javascript', javascript);
|
||||
registerLanguage('java', java);
|
||||
registerLanguage('objectivec', objectivec);
|
||||
|
||||
type Language = 'JavaScript' | 'Litho' | 'ComponentKit' | 'React Native';
|
||||
|
||||
type Props = {
|
||||
layoutDefinition: LayoutRecordT,
|
||||
direction: Yoga$Direction,
|
||||
languages: Array<Language>,
|
||||
};
|
||||
|
||||
type State = {
|
||||
language: Language,
|
||||
};
|
||||
|
||||
export default class Code extends Component<Props, State> {
|
||||
static defaultProps = {
|
||||
languages: [/*'JavaScript', */ 'Litho', 'ComponentKit', 'React Native'],
|
||||
};
|
||||
|
||||
state = {
|
||||
language: 'Litho',
|
||||
};
|
||||
|
||||
generateCode(lang: Language): string {
|
||||
if (lang === 'JavaScript') {
|
||||
return CodeJavaScript(this.props.layoutDefinition, this.props.direction);
|
||||
} else if (lang === 'Litho') {
|
||||
return CodeLitho(this.props.layoutDefinition, this.props.direction);
|
||||
} else if (lang === 'ComponentKit') {
|
||||
return CodeComponentKit(
|
||||
this.props.layoutDefinition,
|
||||
this.props.direction,
|
||||
);
|
||||
} else if (lang === 'React Native') {
|
||||
return CodeReactNative(this.props.layoutDefinition, this.props.direction);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
getLanguage(): string {
|
||||
if (this.state.language === 'Litho') {
|
||||
return 'java';
|
||||
} else if (this.state.language === 'React Native') {
|
||||
return 'javascript';
|
||||
} else if (this.state.language === 'ComponentKit') {
|
||||
return 'objectivec';
|
||||
} else {
|
||||
return this.state.language;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="Code">
|
||||
<RadioGroup
|
||||
className="CodeLanguageSelector"
|
||||
onChange={e => this.setState({language: e.target.value})}
|
||||
value={this.state.language}>
|
||||
{this.props.languages.map(lang => (
|
||||
<RadioButton key={lang} value={lang}>
|
||||
{lang}
|
||||
</RadioButton>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<div className="CodeContainer">
|
||||
<SyntaxHighlighter
|
||||
language={this.getLanguage()}
|
||||
style={styles}
|
||||
customStyle={{fontSize: '13px', backgroundColor: 'white'}}
|
||||
lineNumberStyle={{userSelect: 'none', opacity: 0.5}}
|
||||
codeTagProps={{style: {tabSize: 4}}}
|
||||
showLineNumbers>
|
||||
{this.generateCode(this.state.language)}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
144
playground/src/CodeComponentKit.js
Normal file
144
playground/src/CodeComponentKit.js
Normal file
@@ -0,0 +1,144 @@
|
||||
// @flow
|
||||
import yoga from 'yoga-layout';
|
||||
import LayoutRecord from './LayoutRecord';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import type {LayoutRecordT} from './LayoutRecord';
|
||||
import type {Yoga$Direction} from 'yoga-layout';
|
||||
|
||||
const enumLookup = {
|
||||
flexDirection: {
|
||||
[yoga.FLEX_DIRECTION_COLUMN]: 'CKFlexboxDirectionVertical',
|
||||
[yoga.FLEX_DIRECTION_ROW]: 'CKFlexboxDirectionHorizontal',
|
||||
[yoga.FLEX_DIRECTION_COLUMN_REVERSE]: 'CKFlexboxDirectionVerticalReverse',
|
||||
[yoga.FLEX_DIRECTION_ROW_REVERSE]: 'CKFlexboxDirectionHorizontalReverse',
|
||||
},
|
||||
alignItems: {
|
||||
[yoga.ALIGN_FLEX_START]: 'CKFlexboxAlignItemsStart',
|
||||
[yoga.ALIGN_FLEX_END]: 'CKFlexboxAlignItemsEnd',
|
||||
[yoga.ALIGN_CENTER]: 'CKFlexboxAlignItemsCenter',
|
||||
[yoga.ALIGN_BASELINE]: 'CKFlexboxAlignItemsBaseline',
|
||||
[yoga.ALIGN_STRETCH]: 'CKFlexboxAlignItemsStretch',
|
||||
},
|
||||
alignSelf: {
|
||||
[yoga.ALIGN_AUTO]: 'CKFlexboxAlignSelfAuto',
|
||||
[yoga.ALIGN_FLEX_START]: 'CKFlexboxAlignSelfStart',
|
||||
[yoga.ALIGN_FLEX_END]: 'CKFlexboxAlignSelfEnd',
|
||||
[yoga.ALIGN_CENTER]: 'CKFlexboxAlignSelfCenter',
|
||||
[yoga.ALIGN_BASELINE]: 'CKFlexboxAlignSelfBaseline',
|
||||
[yoga.ALIGN_STRETCH]: 'CKFlexboxAlignSelfStretch',
|
||||
},
|
||||
alignContent: {
|
||||
[yoga.ALIGN_FLEX_START]: 'CKFlexboxAlignContentStart',
|
||||
[yoga.ALIGN_FLEX_END]: 'CKFlexboxAlignContentEnd',
|
||||
[yoga.ALIGN_CENTER]: 'CKFlexboxAlignContentCenter',
|
||||
[yoga.ALIGN_SPACE_BETWEEN]: 'CKFlexboxAlignContentSpaceBetween',
|
||||
[yoga.ALIGN_SPACE_AROUND]: 'CKFlexboxAlignContentSpaceAround',
|
||||
[yoga.ALIGN_STRETCH]: 'CKFlexboxAlignContentStretch',
|
||||
},
|
||||
justifyContent: {
|
||||
[yoga.JUSTIFY_FLEX_START]: 'CKFlexboxJustifyContentStart',
|
||||
[yoga.JUSTIFY_CENTER]: 'CKFlexboxJustifyContentCenter',
|
||||
[yoga.JUSTIFY_FLEX_END]: 'CKFlexboxJustifyContentEnd',
|
||||
[yoga.JUSTIFY_SPACE_BETWEEN]: 'CKFlexboxJustifyContentSpaceBetween',
|
||||
[yoga.JUSTIFY_SPACE_AROUND]: 'CKFlexboxJustifyContentSpaceAround',
|
||||
},
|
||||
flexWrap: {
|
||||
[yoga.WRAP_NO_WRAP]: 'CKFlexboxWrapNoWrap',
|
||||
[yoga.WRAP_WRAP]: 'CKFlexboxWrapWrap',
|
||||
[yoga.WRAP_WRAP_REVERSE]: 'CKFlexboxWrapWrapReverse',
|
||||
},
|
||||
positionType: {
|
||||
[yoga.POSITION_TYPE_RELATIVE]: 'CKFlexboxPositionTypeRelative',
|
||||
[yoga.POSITION_TYPE_ABSOLUTE]: 'CKFlexboxPositionTypeAbsolute',
|
||||
},
|
||||
};
|
||||
|
||||
function keyLookup(key: string): string {
|
||||
const keyLookup = {
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'direction',
|
||||
};
|
||||
return keyLookup[key] || key;
|
||||
}
|
||||
|
||||
function getLayoutCode(
|
||||
node: LayoutRecordT,
|
||||
indent: string = '',
|
||||
isRoot?: boolean,
|
||||
): string {
|
||||
const lines = [];
|
||||
|
||||
lines.push(indent + `[CKFlexboxComponent`);
|
||||
lines.push(indent + ` newWithView:kWhiteBackgroundView`);
|
||||
lines.push(indent + ` size:{${node.width},${node.height}}`);
|
||||
lines.push(indent + ` style:{`);
|
||||
const untouchedLayout = LayoutRecord({});
|
||||
const untouchedPosition = PositionRecord({});
|
||||
|
||||
Object.keys(node.toJSON()).forEach(key => {
|
||||
if (
|
||||
node[key] instanceof PositionRecord &&
|
||||
!node[key].equals(untouchedPosition)
|
||||
) {
|
||||
lines.push(indent + `\t.${key} = {`);
|
||||
|
||||
if (key === 'positionType') {
|
||||
lines.push(
|
||||
indent +
|
||||
`\t.position = ${enumLookup.positionType[node.positionType]},`,
|
||||
);
|
||||
}
|
||||
|
||||
['top', 'left', 'right', 'bottom'].forEach(pKey => {
|
||||
if (node[key][pKey]) {
|
||||
lines.push(indent + `\t\t.${pKey} = ${node[key][pKey]},`);
|
||||
}
|
||||
});
|
||||
|
||||
lines.push(indent + `\t},`);
|
||||
} else if (
|
||||
key !== 'children' &&
|
||||
key !== 'width' &&
|
||||
key !== 'height' &&
|
||||
node[key] !== untouchedLayout[key]
|
||||
) {
|
||||
if (enumLookup[key]) {
|
||||
lines.push(
|
||||
indent +
|
||||
`\t.${keyLookup(key)} = ${enumLookup[key][node.flexDirection]},`,
|
||||
);
|
||||
} else {
|
||||
console.error(`Unknown property ${key}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
lines.push(indent + ` }`);
|
||||
|
||||
if (node.children.size > 0) {
|
||||
lines.push(indent + ' children:{');
|
||||
lines.push(
|
||||
...node.children
|
||||
.toJSON()
|
||||
.map(
|
||||
child =>
|
||||
`${indent}\t{\n${getLayoutCode(
|
||||
child,
|
||||
indent + '\t\t',
|
||||
)}\n${indent}\t},`,
|
||||
),
|
||||
);
|
||||
lines.push(indent + '}');
|
||||
}
|
||||
lines[lines.length - 1] += `]${isRoot ? ';' : ''}`;
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export default function generateCode(
|
||||
root: LayoutRecordT,
|
||||
direction: Yoga$Direction,
|
||||
): string {
|
||||
return ['CKFlexboxComponent *c =', getLayoutCode(root, '\t', true)].join(
|
||||
'\n',
|
||||
);
|
||||
}
|
127
playground/src/CodeJavaScript.js
Normal file
127
playground/src/CodeJavaScript.js
Normal file
@@ -0,0 +1,127 @@
|
||||
// @flow
|
||||
import yoga from 'yoga-layout';
|
||||
import LayoutRecord from './LayoutRecord';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import type {LayoutRecordT} from './LayoutRecord';
|
||||
import type {Yoga$Direction} from 'yoga-layout';
|
||||
|
||||
export const JSEnumLookup = {
|
||||
justifyContent: 'JUSTIFY_',
|
||||
alignItems: 'ALIGN_',
|
||||
alignContent: 'ALIGN_',
|
||||
alignSelf: 'ALIGN_',
|
||||
position: 'POSITION_',
|
||||
flexDirection: 'DIRECTION_',
|
||||
flexWrap: 'WRAP_',
|
||||
positionType: 'POSITION_TYPE_',
|
||||
direction: 'DIRECTION_',
|
||||
};
|
||||
|
||||
function getEnum(yogaEnum: string, value: string | number): string {
|
||||
return `yoga.${Object.keys(yoga)
|
||||
.filter(key => key.toLowerCase().startsWith(yogaEnum.toLowerCase()))
|
||||
.find(key => yoga[key] === value) || value}`;
|
||||
}
|
||||
|
||||
function setProperty(
|
||||
name: string,
|
||||
key: string,
|
||||
value: string,
|
||||
enumValue?: string,
|
||||
): string {
|
||||
return [
|
||||
name,
|
||||
'.set',
|
||||
key[0].toUpperCase() + key.substr(1),
|
||||
'(',
|
||||
enumValue ? `${enumValue}, ` : '',
|
||||
JSEnumLookup[key] ? getEnum(JSEnumLookup[key], value) : value,
|
||||
');',
|
||||
].join('');
|
||||
}
|
||||
|
||||
function getLayoutCode(
|
||||
node: LayoutRecordT,
|
||||
name: string,
|
||||
index: number,
|
||||
): string {
|
||||
const lines = [];
|
||||
const childName = (i: number) => `${name === 'root' ? 'node' : name}_${i}`;
|
||||
|
||||
lines.push(
|
||||
...node.children.map((node, i) => getLayoutCode(node, childName(i), i)),
|
||||
);
|
||||
|
||||
lines.push('', `// create node ${name}`, `const ${name} = Node.create();`);
|
||||
const untouchedNode = LayoutRecord({width: '', height: ''});
|
||||
Object.keys(untouchedNode.toJS()).forEach(key => {
|
||||
if (key !== 'children' && untouchedNode[key] !== node[key]) {
|
||||
if (node[key] instanceof PositionRecord) {
|
||||
// iterate through position record
|
||||
const {top, left, right, bottom} = node[key].toJS();
|
||||
if (
|
||||
top !== untouchedNode[key].top &&
|
||||
top === left &&
|
||||
top === right &&
|
||||
top === bottom
|
||||
) {
|
||||
// all edges
|
||||
lines.push(setProperty(name, key, node[key].top, getEnum('edge', 8)));
|
||||
return;
|
||||
}
|
||||
const alreadySet = [];
|
||||
if (top !== untouchedNode[key].top && top === bottom) {
|
||||
lines.push(setProperty(name, key, node[key].top, getEnum('edge', 7)));
|
||||
alreadySet.push('top', 'bottom');
|
||||
}
|
||||
if (left !== untouchedNode[key].left && left === right) {
|
||||
lines.push(
|
||||
setProperty(name, key, node[key].left, getEnum('edge', 6)),
|
||||
);
|
||||
alreadySet.push('left', 'right');
|
||||
}
|
||||
['left', 'top', 'right', 'bottom'].forEach((pKey, i) => {
|
||||
if (
|
||||
node[key][pKey] !== untouchedNode[key][pKey] &&
|
||||
alreadySet.indexOf(pKey) === -1
|
||||
) {
|
||||
lines.push(
|
||||
setProperty(name, key, node[key][pKey], getEnum('edge', i)),
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
lines.push(setProperty(name, key, node[key]));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (node.children && node.children.size > 0) {
|
||||
lines.push(
|
||||
'',
|
||||
'// insert children',
|
||||
...node.children.map(
|
||||
(_, i) => `${name}.insertChild(${childName(i)}, ${i});`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export default function generateCode(
|
||||
root: LayoutRecordT,
|
||||
direction: Yoga$Direction,
|
||||
): string {
|
||||
const rootNodeName = 'root';
|
||||
return [
|
||||
`import yoga, {Node} from 'yoga-layout';`,
|
||||
getLayoutCode(root, rootNodeName, 0),
|
||||
'',
|
||||
`${rootNodeName}.calculateLayout(${root.width}, ${root.height}, ${getEnum(
|
||||
'direction',
|
||||
direction,
|
||||
)});`,
|
||||
`${rootNodeName}.getComputedLayout();`,
|
||||
].join('\n');
|
||||
}
|
161
playground/src/CodeLitho.js
Normal file
161
playground/src/CodeLitho.js
Normal file
@@ -0,0 +1,161 @@
|
||||
// @flow
|
||||
import yoga from 'yoga-layout';
|
||||
import LayoutRecord from './LayoutRecord';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import {JSEnumLookup} from './CodeJavaScript';
|
||||
import type {LayoutRecordT} from './LayoutRecord';
|
||||
import type {Yoga$Direction} from 'yoga-layout';
|
||||
|
||||
function getEnum(yogaEnum: string, value: string | number): string {
|
||||
const enumLookup = {
|
||||
justifyContent: 'Justify',
|
||||
alignItems: 'Align',
|
||||
alignContent: 'Align',
|
||||
alignSelf: 'Align',
|
||||
position: 'Position',
|
||||
flexWrap: 'Wrap',
|
||||
positionType: 'PositionType',
|
||||
direction: 'Driection',
|
||||
};
|
||||
|
||||
if (!enumLookup[yogaEnum]) {
|
||||
return String(value);
|
||||
} else {
|
||||
const enumValue = Object.keys(yoga)
|
||||
.filter(key =>
|
||||
key.toLowerCase().startsWith(JSEnumLookup[yogaEnum].toLowerCase()),
|
||||
)
|
||||
.find(key => yoga[key] === value);
|
||||
|
||||
return `Yoga${enumLookup[yogaEnum]}.${
|
||||
enumValue ? enumValue.replace(/^([A-Z]+)_/, '') : value
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getLayoutCode(
|
||||
node: LayoutRecordT,
|
||||
indent: string = '',
|
||||
isReturning?: boolean,
|
||||
): string {
|
||||
const lines = [];
|
||||
const flexDirection = {
|
||||
[yoga.FLEX_DIRECTION_ROW]: 'Row',
|
||||
[yoga.FLEX_DIRECTION_ROW_REVERSE]: 'RowReverse',
|
||||
[yoga.FLEX_DIRECTION_COLUMN]: 'Column',
|
||||
[yoga.FLEX_DIRECTION_COLUMN_REVERSE]: 'ColumnReverse',
|
||||
};
|
||||
|
||||
lines.push(
|
||||
indent +
|
||||
`${isReturning ? 'return ' : ''}${
|
||||
flexDirection[node.flexDirection]
|
||||
}.create(c)`,
|
||||
);
|
||||
if (node.children.size > 0) {
|
||||
lines.push(
|
||||
...node.children
|
||||
.toJSON()
|
||||
.map(
|
||||
child =>
|
||||
`${indent}\t.child(\n${getLayoutCode(child, indent + '\t\t')})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
const untouchedLayout = LayoutRecord({width: '', height: ''});
|
||||
const untouchedPosition = PositionRecord({});
|
||||
Object.keys(node.toJSON()).forEach(key => {
|
||||
if (
|
||||
node[key] instanceof PositionRecord &&
|
||||
!node[key].equals(untouchedPosition)
|
||||
) {
|
||||
if (key === 'border') {
|
||||
lines.push(indent + '\t.border(', indent + '\t\tBorder.create(c)');
|
||||
}
|
||||
|
||||
const {top, left, right, bottom} = node[key].toJS();
|
||||
if (
|
||||
top !== untouchedPosition.top &&
|
||||
top === left &&
|
||||
top === right &&
|
||||
top === bottom
|
||||
) {
|
||||
// all edges
|
||||
lines.push(
|
||||
indent +
|
||||
(key === 'border'
|
||||
? `\t\t\t.widthDip(YogaEdge.ALL, ${node[key].top})`
|
||||
: `\t.${key}Dip(YogaEdge.ALL, ${node[key].top})`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const alreadySet = [];
|
||||
if (top !== untouchedPosition.top && top === bottom) {
|
||||
lines.push(
|
||||
indent +
|
||||
(key === 'border'
|
||||
? `\t\t\t.widthDip(YogaEdge.VERTICAL, ${node[key].top})`
|
||||
: `\t.${key}Dip(YogaEdge.VERTICAL, ${node[key].top})`),
|
||||
);
|
||||
alreadySet.push('top', 'bottom');
|
||||
}
|
||||
if (left !== untouchedPosition.left && left === right) {
|
||||
lines.push(
|
||||
indent +
|
||||
(key === 'border'
|
||||
? `\t\t\t.widthDip(YogaEdge.HORIZONTAL, ${node[key].left})`
|
||||
: `\t.${key}Dip(YogaEdge.HORIZONTAL, ${node[key].left})`),
|
||||
);
|
||||
alreadySet.push('left', 'right');
|
||||
}
|
||||
['left', 'top', 'right', 'bottom'].forEach((pKey, i) => {
|
||||
if (
|
||||
node[key][pKey] !== untouchedPosition[pKey] &&
|
||||
alreadySet.indexOf(pKey) === -1
|
||||
) {
|
||||
lines.push(
|
||||
indent +
|
||||
(key === 'border'
|
||||
? `\t\t\t.widthDip(YogaEdge.${pKey.toUpperCase()}, ${
|
||||
node.border[pKey]
|
||||
})`
|
||||
: `\t.${key}Dip(YogaEdge.${pKey.toUpperCase()}, ${
|
||||
node[key][pKey]
|
||||
})`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (key === 'border') {
|
||||
lines.push(
|
||||
indent + '\t\t\t.color(YogaEdge.ALL, 0xfff36b7f)',
|
||||
indent + '\t\t\t.build())',
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
key !== 'children' &&
|
||||
key !== 'flexDirection' &&
|
||||
node[key] !== untouchedLayout[key]
|
||||
) {
|
||||
lines.push(indent + `\t.${key}(${getEnum(key, node[key])})`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export default function generateCode(
|
||||
root: LayoutRecordT,
|
||||
direction: Yoga$Direction,
|
||||
): string {
|
||||
return [
|
||||
'@LayoutSpec',
|
||||
'public class PlaygroundComponentSpec {',
|
||||
'\t@OnCreateLayout',
|
||||
'\tstatic Component onCreateLayout(ComponentContext c) {',
|
||||
getLayoutCode(root, '\t\t', true),
|
||||
'\t\t\t.build();',
|
||||
'\t}',
|
||||
'}',
|
||||
].join('\n');
|
||||
}
|
122
playground/src/CodeReactNative.js
Normal file
122
playground/src/CodeReactNative.js
Normal file
@@ -0,0 +1,122 @@
|
||||
// @flow
|
||||
import yoga from 'yoga-layout';
|
||||
import LayoutRecord from './LayoutRecord';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import {JSEnumLookup} from './CodeJavaScript';
|
||||
import type {LayoutRecordT} from './LayoutRecord';
|
||||
import type {Yoga$Direction} from 'yoga-layout';
|
||||
|
||||
function getEnum(yogaEnum: string, value: string | number): string {
|
||||
const enumValue = Object.keys(yoga)
|
||||
.filter(
|
||||
key =>
|
||||
JSEnumLookup[yogaEnum] &&
|
||||
key.toLowerCase().startsWith(JSEnumLookup[yogaEnum].toLowerCase()),
|
||||
)
|
||||
.find(key => yoga[key] === value);
|
||||
|
||||
return enumValue
|
||||
? "'" +
|
||||
enumValue
|
||||
.replace(/^([A-Z]+)_/, '')
|
||||
.replace('_', '-')
|
||||
.toLowerCase() +
|
||||
"'"
|
||||
: String(value);
|
||||
}
|
||||
|
||||
function getLayoutCode(node: LayoutRecordT, indent: string = ''): string {
|
||||
const lines = [];
|
||||
const untouchedLayout = LayoutRecord({width: '', height: ''});
|
||||
const untouchedPosition = PositionRecord({});
|
||||
lines.push(indent + '<View style={{');
|
||||
lines.push(indent + ' flex: 1,');
|
||||
Object.keys(node.toJSON()).forEach(key => {
|
||||
if (key === 'border' && !node.border.equals(untouchedPosition)) {
|
||||
['Top', 'Left', 'Right', 'Bottom'].forEach(pKey => {
|
||||
if (
|
||||
untouchedPosition[pKey.toLowerCase()] !==
|
||||
node.border[pKey.toLowerCase()]
|
||||
) {
|
||||
lines.push(
|
||||
indent +
|
||||
` border${pKey}Width: ${node.border[pKey.toLowerCase()]},`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
node[key] instanceof PositionRecord &&
|
||||
!node[key].equals(untouchedPosition)
|
||||
) {
|
||||
const {top, left, right, bottom} = node[key].toJS();
|
||||
if (
|
||||
top !== untouchedPosition.top &&
|
||||
top === left &&
|
||||
top === right &&
|
||||
top === bottom
|
||||
) {
|
||||
// all edges
|
||||
lines.push(indent + ` ${key}: ${node[key].top},`);
|
||||
return;
|
||||
}
|
||||
const alreadySet = [];
|
||||
if (top !== untouchedPosition.top && top === bottom) {
|
||||
lines.push(indent + ` ${key}Vertical: ${node[key].top},`);
|
||||
alreadySet.push('top', 'bottom');
|
||||
}
|
||||
if (left !== untouchedPosition.left && left === right) {
|
||||
lines.push(indent + ` ${key}Horizontal: ${node[key].left},`);
|
||||
alreadySet.push('left', 'right');
|
||||
}
|
||||
['left', 'top', 'right', 'bottom'].forEach((pKey, i) => {
|
||||
if (
|
||||
node[key][pKey] !== untouchedPosition[pKey] &&
|
||||
alreadySet.indexOf(pKey) === -1
|
||||
) {
|
||||
lines.push(
|
||||
indent +
|
||||
` ${key}${pKey[0].toUpperCase()}${pKey.substr(1)}: ${
|
||||
node[key][pKey]
|
||||
},`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if (key !== 'children' && node[key] !== untouchedLayout[key]) {
|
||||
lines.push(indent + ` ${key}: ${getEnum(key, node[key])},`);
|
||||
}
|
||||
});
|
||||
if (node.children.size > 0) {
|
||||
lines.push(indent + '}}>');
|
||||
} else {
|
||||
lines.push(indent + '}} />');
|
||||
}
|
||||
if (node.children.size > 0) {
|
||||
lines.push(
|
||||
...node.children
|
||||
.toJSON()
|
||||
.map(child => getLayoutCode(child, indent + ' ')),
|
||||
);
|
||||
}
|
||||
if (node.children.size > 0) {
|
||||
lines.push(indent + '</View>');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export default function generateCode(
|
||||
root: LayoutRecordT,
|
||||
direction: Yoga$Direction,
|
||||
): string {
|
||||
return [
|
||||
`import React, {Component} from 'react';`,
|
||||
`import {View} from 'react-native';`,
|
||||
'',
|
||||
'export default class MyLayout extends Component {',
|
||||
' render() {',
|
||||
' return (',
|
||||
getLayoutCode(root, ' '),
|
||||
' );',
|
||||
' }',
|
||||
'};',
|
||||
].join('\n');
|
||||
}
|
10
playground/src/Demo.js
Normal file
10
playground/src/Demo.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
export default class Demo extends Component<Props> {
|
||||
render() {
|
||||
return <div className="Demo">hey i am a demo</div>;
|
||||
}
|
||||
}
|
49
playground/src/Editor.css
Normal file
49
playground/src/Editor.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.Editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.Editor .field {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Editor .ant-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Editor h2 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.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: scroll;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.Editor .EditorButtons {
|
||||
padding: 15px;
|
||||
}
|
273
playground/src/Editor.js
Normal file
273
playground/src/Editor.js
Normal file
@@ -0,0 +1,273 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import {Row, Col, Button, Tabs, Input} from 'antd';
|
||||
import YogaEnumSelect from './YogaEnumSelect';
|
||||
import type {LayoutRecordT} from './LayoutRecord';
|
||||
import type {Yoga$Direction} from 'yoga-layout';
|
||||
import InfoText from './InfoText';
|
||||
import YogaPositionEditor from './YogaPositionEditor';
|
||||
import './Editor.css';
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
type Props = {
|
||||
node: ?LayoutRecordT,
|
||||
onChangeLayout: (key: string, value: any) => void,
|
||||
onChangeSetting: (key: string, value: any) => void,
|
||||
direction: Yoga$Direction,
|
||||
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} = this.props;
|
||||
const disabled = !Boolean(node);
|
||||
return (
|
||||
<div className="Editor">
|
||||
<Tabs defaultActiveKey="1" className="EditorTabs">
|
||||
<TabPane tab="Flex" key="1">
|
||||
<h2>
|
||||
Direction
|
||||
<InfoText>
|
||||
The direction property specifies the text direction/writing
|
||||
direction
|
||||
</InfoText>
|
||||
</h2>
|
||||
<YogaEnumSelect
|
||||
property="DIRECTION"
|
||||
value={this.props.direction}
|
||||
onChange={e => this.props.onChangeSetting('direction', e)}
|
||||
/>
|
||||
<h2>
|
||||
Flex direction
|
||||
<InfoText>Defining the direction of the main-axis</InfoText>
|
||||
</h2>
|
||||
<YogaEnumSelect
|
||||
disabled={disabled}
|
||||
property="FLEX_DIRECTION"
|
||||
value={node ? node.flexDirection : ''}
|
||||
onChange={e => this.props.onChangeLayout('flexDirection', e)}
|
||||
/>
|
||||
|
||||
<Row gutter={15} style={{marginTop: 30}}>
|
||||
<Col span={12}>
|
||||
<h2>
|
||||
Flex grow
|
||||
<InfoText>
|
||||
Grow factor defined how much space this element should take
|
||||
up, relative to it's siblings
|
||||
</InfoText>
|
||||
</h2>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="flexGrow"
|
||||
disabled={disabled}
|
||||
value={node ? node.flexGrow : ''}
|
||||
onChange={e =>
|
||||
this.props.onChangeLayout('flexGrow', e.target.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<h2>
|
||||
Flex shrink
|
||||
<InfoText>
|
||||
Shrink factor if elements don't fit into the parent node
|
||||
anymore.
|
||||
</InfoText>
|
||||
</h2>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="flexShrink"
|
||||
disabled={disabled}
|
||||
value={node ? node.flexShrink : ''}
|
||||
onChange={e =>
|
||||
this.props.onChangeLayout('flexShrink', e.target.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<h2>
|
||||
Flex wrap
|
||||
<InfoText>
|
||||
Wrapping behaviour when child nodes don't fit into a single line
|
||||
</InfoText>
|
||||
</h2>
|
||||
<YogaEnumSelect
|
||||
disabled={disabled}
|
||||
property="WRAP"
|
||||
value={node ? node.flexWrap : ''}
|
||||
onChange={e => this.props.onChangeLayout('flexWrap', e)}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="Alignment" key="2">
|
||||
<h2>
|
||||
Justify content
|
||||
<InfoText>Aligns child nodes along the main-axis</InfoText>
|
||||
</h2>
|
||||
<YogaEnumSelect
|
||||
disabled={disabled}
|
||||
property="JUSTIFY"
|
||||
value={node ? node.justifyContent : ''}
|
||||
onChange={e => this.props.onChangeLayout('justifyContent', e)}
|
||||
/>
|
||||
|
||||
<h2>
|
||||
Align items
|
||||
<InfoText>Aligns child nodes along the cross-axis</InfoText>
|
||||
</h2>
|
||||
<YogaEnumSelect
|
||||
disabled={disabled}
|
||||
property="ALIGN"
|
||||
value={node ? node.alignItems : ''}
|
||||
onChange={e => this.props.onChangeLayout('alignItems', e)}
|
||||
/>
|
||||
|
||||
<h2>
|
||||
Align self
|
||||
<InfoText>
|
||||
Specifies alignment on the cross-axis for the node itself
|
||||
</InfoText>
|
||||
</h2>
|
||||
<YogaEnumSelect
|
||||
disabled={disabled}
|
||||
property="ALIGN"
|
||||
value={node ? node.alignSelf : ''}
|
||||
onChange={e => this.props.onChangeLayout('alignSelf', e)}
|
||||
/>
|
||||
|
||||
<h2>
|
||||
Align content
|
||||
<InfoText>
|
||||
Alignment of lines along the cross-axis when wrapping
|
||||
</InfoText>
|
||||
</h2>
|
||||
<YogaEnumSelect
|
||||
disabled={disabled}
|
||||
property="ALIGN"
|
||||
value={node ? node.alignContent : ''}
|
||||
onChange={e => this.props.onChangeLayout('alignContent', e)}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="Layout" key="3">
|
||||
<h2>
|
||||
Width × height
|
||||
<InfoText>Dimensions of the node</InfoText>
|
||||
</h2>
|
||||
<Row gutter={15}>
|
||||
<Col span={12}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="width"
|
||||
disabled={disabled}
|
||||
value={node ? node.width : ''}
|
||||
onChange={e =>
|
||||
this.props.onChangeLayout('width', e.target.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="height"
|
||||
disabled={disabled}
|
||||
value={node ? node.height : ''}
|
||||
onChange={e =>
|
||||
this.props.onChangeLayout('height', e.target.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<h2>
|
||||
Aspect ratio
|
||||
<InfoText>
|
||||
Aspect radio is an additon by Yoga which is handy e.g. for nodes
|
||||
displaying videos
|
||||
</InfoText>
|
||||
</h2>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Aspect ratio"
|
||||
disabled={disabled}
|
||||
value={node ? node.aspectRatio : ''}
|
||||
onChange={e =>
|
||||
this.props.onChangeLayout('aspectRatio', e.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<h2>Box model</h2>
|
||||
{['padding', 'border', 'margin'].map(property => (
|
||||
<YogaPositionEditor
|
||||
property={property}
|
||||
key={property}
|
||||
value={node ? node[property] : undefined}
|
||||
onChange={value => this.props.onChangeLayout(property, value)}
|
||||
/>
|
||||
))}
|
||||
<h2>
|
||||
Position
|
||||
<InfoText>
|
||||
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>
|
||||
|
||||
<YogaEnumSelect
|
||||
disabled={disabled}
|
||||
property="POSITION_TYPE"
|
||||
value={node ? node.positionType : ''}
|
||||
onChange={e => this.props.onChangeLayout('positionType', e)}
|
||||
/>
|
||||
<YogaPositionEditor
|
||||
property="position"
|
||||
value={node ? node.position : undefined}
|
||||
onChange={value => this.props.onChangeLayout('position', value)}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
9
playground/src/InfoText.css
Normal file
9
playground/src/InfoText.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.InfoText {
|
||||
max-width: 230px;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
.InfoTextIcon {
|
||||
margin-left: 5px;
|
||||
opacity: 0.5;
|
||||
}
|
20
playground/src/InfoText.js
Normal file
20
playground/src/InfoText.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import {Popover, Icon} from 'antd';
|
||||
import './InfoText.css';
|
||||
|
||||
type Props = {
|
||||
children: any,
|
||||
};
|
||||
|
||||
export default class InfoText extends Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<Popover
|
||||
content={<div className="InfoText">{this.props.children}</div>}
|
||||
trigger="hover">
|
||||
<Icon className="InfoTextIcon" type="info-circle-o" />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
56
playground/src/LayoutRecord.js
Normal file
56
playground/src/LayoutRecord.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import {Record, List} from 'immutable';
|
||||
import type {RecordOf} from 'immutable';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import type {PositionRecordT} from './PositionRecord';
|
||||
import yoga from 'yoga-layout';
|
||||
|
||||
import type {
|
||||
Yoga$Align,
|
||||
Yoga$JustifyContent,
|
||||
Yoga$FlexDirection,
|
||||
Yoga$FlexWrap,
|
||||
Yoga$Yoga$PositionType,
|
||||
} from 'yoga-layout';
|
||||
|
||||
export type LayoutRecordT = RecordOf<{
|
||||
width?: ?number,
|
||||
height?: ?number,
|
||||
justifyContent?: Yoga$JustifyContent,
|
||||
padding: PositionRecordT,
|
||||
border: PositionRecordT,
|
||||
margin: PositionRecordT,
|
||||
position: PositionRecordT,
|
||||
positionType: Yoga$Yoga$PositionType,
|
||||
alignItems?: Yoga$Align,
|
||||
alignSelf?: Yoga$Align,
|
||||
alignContent?: Yoga$Align,
|
||||
flexDirection?: Yoga$FlexDirection,
|
||||
flexGrow?: number,
|
||||
flexShrink?: number,
|
||||
padding?: number,
|
||||
flexWrap?: Yoga$FlexWrap,
|
||||
aspectRatio?: number,
|
||||
children?: List<LayoutDefinition>,
|
||||
}>;
|
||||
|
||||
const r: LayoutRecordT = Record({
|
||||
width: 100,
|
||||
height: 100,
|
||||
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(),
|
||||
positionType: yoga.POSITION_TYPE_RELATIVE,
|
||||
flexWrap: yoga.WRAP_NO_WRAP,
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
children: List(),
|
||||
aspectRatio: 'auto',
|
||||
});
|
||||
|
||||
export default r;
|
25
playground/src/Playground.css
Normal file
25
playground/src/Playground.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.App {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
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;
|
||||
}
|
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>
|
||||
);
|
||||
}
|
||||
}
|
9
playground/src/PositionGuide.css
Normal file
9
playground/src/PositionGuide.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.PositionGuide {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
user-select: none;
|
||||
}
|
137
playground/src/PositionGuide.js
Normal file
137
playground/src/PositionGuide.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import type {PositionRecordT} from './PositionRecord';
|
||||
import './PositionGuide.css';
|
||||
|
||||
type Props = {
|
||||
inset?: boolean,
|
||||
reverse?: boolean,
|
||||
position: PositionRecordT,
|
||||
offset: PositionRecordT,
|
||||
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 (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 (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,
|
||||
];
|
||||
}
|
||||
}
|
18
playground/src/PositionRecord.js
Normal file
18
playground/src/PositionRecord.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import {Record} from 'immutable';
|
||||
import type {RecordOf} from 'immutable';
|
||||
|
||||
export type PositionRecordT = RecordOf<{
|
||||
top: string,
|
||||
right: string,
|
||||
bottom: string,
|
||||
left: string,
|
||||
}>;
|
||||
|
||||
const r: PositionRecordT = Record({
|
||||
top: '',
|
||||
right: '',
|
||||
bottom: '',
|
||||
left: '',
|
||||
});
|
||||
|
||||
export default r;
|
33
playground/src/Sidebar.css
Normal file
33
playground/src/Sidebar.css
Normal file
@@ -0,0 +1,33 @@
|
||||
.Sidebar {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 350px;
|
||||
top: 50px;
|
||||
background: white;
|
||||
border-left: 1px solid #dddfe2;
|
||||
transform: translateX(100%);
|
||||
transition: 0.2s transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Sidebar.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.SidebarClose {
|
||||
text-align: right;
|
||||
padding: 15px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.SidebarClose i {
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.SidebarClose i:hover {
|
||||
opacity: 0.6;
|
||||
}
|
27
playground/src/Sidebar.js
Normal file
27
playground/src/Sidebar.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import {Icon} from 'antd';
|
||||
import './Sidebar.css';
|
||||
|
||||
type Props = {
|
||||
onClose?: () => void,
|
||||
width?: number,
|
||||
children: any,
|
||||
};
|
||||
|
||||
export default class Sidebar extends Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={`Sidebar ${this.props.visible ? 'visible' : ''}`}
|
||||
style={{width: this.props.width}}>
|
||||
{this.props.onClose && (
|
||||
<div className="SidebarClose">
|
||||
<Icon type="close" onClick={this.props.onClose} />
|
||||
</div>
|
||||
)}
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
34
playground/src/Toolbar.css
Normal file
34
playground/src/Toolbar.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.Toolbar {
|
||||
border-bottom: 1px solid #dddfe2;
|
||||
box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.1);
|
||||
background: white;
|
||||
height: 50px;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-bottom: 1px;
|
||||
align-items: center;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Toolbar h1 {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.Toolbar .ToolbarSpacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Toolbar .ToolbarToggle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.Toolbar .ToolbarToggle i {
|
||||
margin-right: 5px;
|
||||
}
|
30
playground/src/Toolbar.js
Normal file
30
playground/src/Toolbar.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import {Icon} from 'antd';
|
||||
import './Toolbar.css';
|
||||
|
||||
type Props = {
|
||||
onShowCode?: () => void,
|
||||
};
|
||||
|
||||
export default class Toolbar extends Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div className="Toolbar">
|
||||
<img
|
||||
src="https://facebook.github.io/yoga/static/logo.svg"
|
||||
width="42"
|
||||
alt="Yoga logo"
|
||||
/>
|
||||
<h1>Yoga Playground</h1>
|
||||
<div className="ToolbarSpacer" />
|
||||
{this.props.onShowCode && (
|
||||
<a className="ToolbarToggle" onClick={this.props.onShowCode}>
|
||||
<Icon type={'code-o'} />
|
||||
Code
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
9
playground/src/YogaEnumSelect.css
Normal file
9
playground/src/YogaEnumSelect.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.YogaEnumSelect.ant-radio-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.YogaEnumSelect.ant-radio-group .ant-radio-button-wrapper {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
text-align: center;
|
||||
}
|
70
playground/src/YogaEnumSelect.js
Normal file
70
playground/src/YogaEnumSelect.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import yoga from 'yoga-layout';
|
||||
import {Radio, Menu, Dropdown, Button, Icon} from 'antd';
|
||||
import './YogaEnumSelect.css';
|
||||
const RadioButton = Radio.Button;
|
||||
const RadioGroup = Radio.Group;
|
||||
|
||||
type Props = {
|
||||
property: string,
|
||||
disabled?: boolean,
|
||||
value: string | number,
|
||||
onChange: (value: number) => void,
|
||||
};
|
||||
|
||||
export default class YogaEnumSelect extends Component<Props> {
|
||||
values: Array<{key: string, value: number}>;
|
||||
|
||||
constructor({property}: Props) {
|
||||
super();
|
||||
// $FlowFixMe
|
||||
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(yoga[key]);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {property, ...props} = this.props;
|
||||
const selected = this.values.find(
|
||||
({key, value}) => value === this.props.value,
|
||||
);
|
||||
const replacer = new RegExp(`^${property}_`);
|
||||
|
||||
return this.values.length > 3 ? (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu onClick={this.handleMenuClick}>
|
||||
{this.values.map(({key, value}) => (
|
||||
<Menu.Item key={key} value={value}>
|
||||
{key.replace(replacer, '')}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
}>
|
||||
<Button>
|
||||
{selected ? selected.key.replace(replacer, '') : 'undefiend'}{' '}
|
||||
<Icon type="down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<RadioGroup
|
||||
{...props}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
defaultValue="a"
|
||||
className="YogaEnumSelect">
|
||||
{this.values.map(({key, value}) => (
|
||||
<RadioButton key={key} value={value}>
|
||||
{key.replace(new RegExp(`^${property}_`), '')}
|
||||
</RadioButton>
|
||||
))}
|
||||
</RadioGroup>
|
||||
);
|
||||
}
|
||||
}
|
51
playground/src/YogaNode.css
Normal file
51
playground/src/YogaNode.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.YogaNode .YogaNode {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.YogaNode > .info {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.YogaNode:hover > .info {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.YogaNode:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.YogaNode.focused {
|
||||
box-shadow: 0 0 0 2px #0894fb, 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;
|
||||
}
|
267
playground/src/YogaNode.js
Normal file
267
playground/src/YogaNode.js
Normal file
@@ -0,0 +1,267 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import yoga, {Node} from 'yoga-layout';
|
||||
import PositionGuide from './PositionGuide';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import type {LayoutRecordT} from './LayoutRecord';
|
||||
import type {Yoga$Direction} from 'yoga-layout';
|
||||
|
||||
import './YogaNode.css';
|
||||
|
||||
type Yoga$Node = any;
|
||||
|
||||
type ComputedLayout = {|
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number,
|
||||
children: Array<ComputedLayout>,
|
||||
node: Yoga$Node,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
layoutDefinition: LayoutRecordT,
|
||||
className?: string,
|
||||
computedLayout?: ComputedLayout,
|
||||
path: Array<number>,
|
||||
selectedNodePath?: ?Array<number>,
|
||||
direction?: Yoga$Direction,
|
||||
label?: string,
|
||||
onClick?: (path: Array<number>) => void,
|
||||
onDoubleClick?: (path: Array<number>) => void,
|
||||
|};
|
||||
|
||||
type State = {
|
||||
visible?: boolean,
|
||||
};
|
||||
|
||||
export default class YogaNode extends Component<Props, State> {
|
||||
node: Yoga$Node;
|
||||
|
||||
static defaultProps = {
|
||||
path: [],
|
||||
label: 'root',
|
||||
};
|
||||
|
||||
state = {};
|
||||
computedLayout: ?ComputedLayout;
|
||||
rootNode: ?Yoga$Node;
|
||||
|
||||
constructor(props: Props) {
|
||||
super();
|
||||
if (!props.computedLayout) {
|
||||
// is root node
|
||||
this.calculateLayout(props);
|
||||
this.state = {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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: LayoutRecordT): Yoga$Node => {
|
||||
const root = Node.create();
|
||||
root.setWidth(layoutDefinition.width);
|
||||
root.setHeight(layoutDefinition.height);
|
||||
root.setJustifyContent(layoutDefinition.justifyContent);
|
||||
root.setAlignItems(layoutDefinition.alignItems);
|
||||
root.setAlignSelf(layoutDefinition.alignSelf);
|
||||
root.setFlexGrow(layoutDefinition.flexGrow);
|
||||
root.setFlexShrink(layoutDefinition.flexShrink);
|
||||
|
||||
root.setPadding(yoga.EDGE_TOP, layoutDefinition.padding.top);
|
||||
root.setPadding(yoga.EDGE_RIGHT, layoutDefinition.padding.right);
|
||||
root.setPadding(yoga.EDGE_BOTTOM, layoutDefinition.padding.bottom);
|
||||
root.setPadding(yoga.EDGE_LEFT, layoutDefinition.padding.left);
|
||||
|
||||
root.setBorder(yoga.EDGE_TOP, layoutDefinition.border.top);
|
||||
root.setBorder(yoga.EDGE_RIGHT, layoutDefinition.border.right);
|
||||
root.setBorder(yoga.EDGE_BOTTOM, layoutDefinition.border.bottom);
|
||||
root.setBorder(yoga.EDGE_LEFT, layoutDefinition.border.left);
|
||||
|
||||
root.setMargin(yoga.EDGE_TOP, layoutDefinition.margin.top);
|
||||
root.setMargin(yoga.EDGE_RIGHT, layoutDefinition.margin.right);
|
||||
root.setMargin(yoga.EDGE_BOTTOM, layoutDefinition.margin.bottom);
|
||||
root.setMargin(yoga.EDGE_LEFT, layoutDefinition.margin.left);
|
||||
|
||||
root.setPosition(yoga.EDGE_TOP, layoutDefinition.position.top);
|
||||
root.setPosition(yoga.EDGE_RIGHT, layoutDefinition.position.right);
|
||||
root.setPosition(yoga.EDGE_BOTTOM, layoutDefinition.position.bottom);
|
||||
root.setPosition(yoga.EDGE_LEFT, layoutDefinition.position.left);
|
||||
|
||||
root.setPositionType(layoutDefinition.positionType);
|
||||
|
||||
root.setDisplay(yoga.DISPLAY_FLEX);
|
||||
root.setAspectRatio(layoutDefinition.aspectRatio);
|
||||
root.setFlexWrap(layoutDefinition.flexWrap);
|
||||
root.setFlexDirection(layoutDefinition.flexDirection);
|
||||
(layoutDefinition.children || [])
|
||||
.map(this.createYogaNodes)
|
||||
.forEach((node, i) => {
|
||||
root.insertChild(node, i);
|
||||
});
|
||||
|
||||
return root;
|
||||
};
|
||||
|
||||
getComputedLayout = (node: Yoga$Node): ComputedLayout => {
|
||||
return {
|
||||
...node.getComputedLayout(),
|
||||
node,
|
||||
children: Array.apply(null, Array(node.getChildCount())).map((_, i) =>
|
||||
this.getComputedLayout(node.getChild(i)),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
onClick = (e: SyntheticMouseEvent<>) => {
|
||||
const {onClick} = this.props;
|
||||
if (onClick) {
|
||||
e.stopPropagation();
|
||||
onClick(this.props.path);
|
||||
}
|
||||
};
|
||||
|
||||
onDoubleClick = (e: SyntheticMouseEvent<>) => {
|
||||
const {onDoubleClick} = this.props;
|
||||
if (onDoubleClick) {
|
||||
e.stopPropagation();
|
||||
onDoubleClick(this.props.path);
|
||||
}
|
||||
};
|
||||
|
||||
showPositionGuides({node}: ComputedLayout) {
|
||||
const padding = PositionRecord({
|
||||
top: node.getComputedPadding(yoga.EDGE_TOP),
|
||||
left: node.getComputedPadding(yoga.EDGE_LEFT),
|
||||
right: node.getComputedPadding(yoga.EDGE_RIGHT),
|
||||
bottom: node.getComputedPadding(yoga.EDGE_BOTTOM),
|
||||
});
|
||||
const border = PositionRecord({
|
||||
top: node.getComputedBorder(yoga.EDGE_TOP),
|
||||
left: node.getComputedBorder(yoga.EDGE_LEFT),
|
||||
right: node.getComputedBorder(yoga.EDGE_RIGHT),
|
||||
bottom: node.getComputedBorder(yoga.EDGE_BOTTOM),
|
||||
});
|
||||
const margin = PositionRecord({
|
||||
top: node.getComputedMargin(yoga.EDGE_TOP),
|
||||
left: node.getComputedMargin(yoga.EDGE_LEFT),
|
||||
right: node.getComputedMargin(yoga.EDGE_RIGHT),
|
||||
bottom: node.getComputedMargin(yoga.EDGE_BOTTOM),
|
||||
});
|
||||
const position = PositionRecord({
|
||||
top: node.getPosition(yoga.EDGE_TOP).value,
|
||||
left: node.getPosition(yoga.EDGE_LEFT).value,
|
||||
right: node.getPosition(yoga.EDGE_RIGHT).value,
|
||||
bottom: node.getPosition(yoga.EDGE_BOTTOM).value,
|
||||
});
|
||||
|
||||
return [
|
||||
<PositionGuide
|
||||
key="border"
|
||||
inset
|
||||
position={border}
|
||||
color="rgba(251, 170, 51, 0.15)"
|
||||
reverse={node.getFlexWrap() === yoga.WRAP_WRAP_REVERSE}
|
||||
/>,
|
||||
<PositionGuide
|
||||
key="padding"
|
||||
inset
|
||||
offset={border}
|
||||
position={padding}
|
||||
color="rgba(123, 179, 41, 0.1)"
|
||||
reverse={node.getFlexWrap() === yoga.WRAP_WRAP_REVERSE}
|
||||
/>,
|
||||
<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;
|
||||
|
||||
// $FlowFixMe
|
||||
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'
|
||||
}`}
|
||||
style={{left, top, width, height}}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
onClick={this.onClick}>
|
||||
{label && <div className="label">{label}</div>}
|
||||
{isFocused && 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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
22
playground/src/YogaPositionEditor.css
Normal file
22
playground/src/YogaPositionEditor.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.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: 16px;
|
||||
font-weight: bold;
|
||||
}
|
49
playground/src/YogaPositionEditor.js
Normal file
49
playground/src/YogaPositionEditor.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
import React, {Component} from 'react';
|
||||
import {Input} from 'antd';
|
||||
import PositionRecord from './PositionRecord';
|
||||
import type {PositionRecordT} from './PositionRecord';
|
||||
import './YogaPositionEditor.css';
|
||||
|
||||
type Props = {
|
||||
value: PositionRecordT,
|
||||
property: string,
|
||||
onChange: (value: PositionRecordT) => void,
|
||||
};
|
||||
|
||||
export default class YogaPositionEditor extends Component<Props> {
|
||||
static defaultProps = {
|
||||
value: PositionRecord(),
|
||||
};
|
||||
|
||||
render() {
|
||||
const {onChange, value, property} = this.props;
|
||||
return (
|
||||
<div className="YogaPositionEditor">
|
||||
<Input
|
||||
type="text"
|
||||
value={value.top}
|
||||
onChange={e => onChange(value.set('top', e.target.value))}
|
||||
/>
|
||||
<div className="YogaPositionEditorRow">
|
||||
<Input
|
||||
type="text"
|
||||
value={value.left}
|
||||
onChange={e => onChange(value.set('left', e.target.value))}
|
||||
/>
|
||||
{property}
|
||||
<Input
|
||||
type="text"
|
||||
value={value.right}
|
||||
onChange={e => onChange(value.set('right', e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
value={value.bottom}
|
||||
onChange={e => onChange(value.set('bottom', e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
12
playground/src/index.css
Normal file
12
playground/src/index.css
Normal file
@@ -0,0 +1,12 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Barlow', sans-serif;
|
||||
color: #303845;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-weight: 500;
|
||||
}
|
30
playground/src/index.js
Normal file
30
playground/src/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import Playground from './Playground';
|
||||
import Demo from './Demo';
|
||||
|
||||
const components = {Playground, Demo};
|
||||
window.onload = function() {
|
||||
document.querySelectorAll('react').forEach(node => {
|
||||
const props = JSON.parse(node.getAttribute('props'));
|
||||
if (components[node.getAttribute('component')]) {
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
components[node.getAttribute('component')],
|
||||
props,
|
||||
node.innerHTML,
|
||||
),
|
||||
node,
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`Component "${node.getAttribute(
|
||||
'component',
|
||||
)}" could not be found. Exported components are ${Object.keys(
|
||||
components,
|
||||
).join(', ')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
Reference in New Issue
Block a user