Skip to content

Commit 562449f

Browse files
authored
Merge pull request blocks#16 from blocks/generic-jsx-type
Proof of concept generic JSX block types
2 parents df6e4da + 49b5ab1 commit 562449f

6 files changed

Lines changed: 170 additions & 224 deletions

File tree

packages/editor/src/lib/schema.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ export default {
2222
hr: {
2323
isVoid: true
2424
},
25-
youtube: {
26-
isVoid: true
27-
},
28-
gist: {
25+
'jsx-void': {
2926
isVoid: true
3027
}
3128
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/** @jsx jsx */
2+
// generic block form
3+
import { jsx } from '@emotion/core'
4+
import { useState } from 'react'
5+
import { Flex } from 'theme-ui/layout'
6+
7+
import Label from '../../components/Label'
8+
import Input from '../../components/Input'
9+
import Button from '../../components/Button'
10+
11+
export default ({ fields, value, onSubmit }) => {
12+
const [state, setState] = useState(value)
13+
const keys = Object.keys(fields)
14+
return (
15+
<form
16+
onClick={e => {
17+
e.stopPropagation()
18+
}}
19+
onSubmit={e => {
20+
e.preventDefault()
21+
onSubmit(state)
22+
}}
23+
>
24+
<Flex flexWrap="wrap" alignItems="flex-end">
25+
{keys.map(key => (
26+
<Label key={key} mr={2}>
27+
{fields[key].title || key}
28+
<Input
29+
type="text"
30+
name={key}
31+
value={state[key] || ''}
32+
onChange={e => {
33+
const format = fields[key].formatValue
34+
const value =
35+
typeof format === 'function'
36+
? format(e.target.value)
37+
: e.target.value
38+
setState({ ...state, [key]: value })
39+
}}
40+
/>
41+
</Label>
42+
))}
43+
<Button>Apply</Button>
44+
</Flex>
45+
</form>
46+
)
47+
}
Lines changed: 17 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,26 @@
11
/** @jsx jsx */
22
import { jsx } from '@emotion/core'
3-
import { useState } from 'react'
4-
import Gist from 'react-gist'
5-
import { Flex } from 'theme-ui/layout'
3+
import ReactGist from 'react-gist'
64

7-
import Overlay from './Overlay'
8-
import Card from '../../components/Card'
9-
import Label from '../../components/Label'
10-
import Input from '../../components/Input'
11-
import Button from '../../components/Button'
12-
13-
const Form = ({ value, onSubmit }) => {
14-
const [state, setState] = useState({ id: value.id || '' })
15-
return (
16-
<Card
17-
css={{
18-
marginBottom: 8
19-
}}
20-
>
21-
<form
22-
onClick={e => {
23-
e.stopPropagation()
24-
}}
25-
onSubmit={e => {
26-
e.preventDefault()
27-
onSubmit(state)
28-
}}
29-
>
30-
<Flex alignItems="flex-end">
31-
<Label mr={2}>
32-
Gist ID:
33-
<Input
34-
type="text"
35-
name="id"
36-
value={state.id}
37-
onChange={e => {
38-
setState({ ...state, id: e.target.value })
39-
}}
40-
/>
41-
</Label>
42-
<Button>Apply</Button>
43-
</Flex>
44-
</form>
45-
</Card>
46-
)
47-
}
48-
49-
export default ({ editor, node, attributes, props, isSelected }) => {
5+
const Gist = props => {
506
return (
517
<div>
52-
<div
53-
{...attributes}
54-
style={{
55-
position: 'relative',
56-
outline: isSelected ? '2px solid blue' : null
57-
}}
58-
>
59-
{props.id ? (
60-
<div style={{ marginLeft: -8, marginRight: -8 }}>
61-
<Gist {...props} />
62-
</div>
63-
) : (
64-
<pre>Enter a Gist ID</pre>
65-
)}
66-
{!isSelected && <Overlay />}
67-
</div>
68-
{isSelected && (
69-
<Form
70-
value={props}
71-
onSubmit={data => {
72-
editor.setJSXProps(data)
73-
}}
74-
/>
8+
{props.id ? (
9+
<div style={{ marginLeft: -8, marginRight: -8 }}>
10+
<ReactGist {...props} />
11+
</div>
12+
) : (
13+
<pre>Enter a Gist ID</pre>
7514
)}
7615
</div>
7716
)
7817
}
18+
19+
Gist.propertyControls = {
20+
id: {
21+
type: 'string',
22+
title: 'Gist ID'
23+
}
24+
}
25+
26+
export default Gist
Lines changed: 15 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,9 @@
11
/** @jsx jsx */
22
import { jsx } from '@emotion/core'
3-
import React, { useState } from 'react'
43
import Player from 'react-youtube'
5-
import { Flex } from 'theme-ui/layout'
64
import isURL from 'is-url'
75
import getYouTubeID from 'get-youtube-id'
86

9-
import Overlay from './Overlay'
10-
import Label from '../../components/Label'
11-
import Input from '../../components/Input'
12-
import Button from '../../components/Button'
13-
14-
export const YouTubeForm = ({ value, onSubmit }) => {
15-
const [state, setState] = useState({
16-
videoId: value.videoId || ''
17-
})
18-
return (
19-
<form
20-
css={{
21-
padding: 8
22-
}}
23-
onClick={e => {
24-
e.stopPropagation()
25-
}}
26-
onSubmit={e => {
27-
e.preventDefault()
28-
onSubmit(state)
29-
}}
30-
>
31-
<Flex flexWrap="wrap" alignItems="flex-end">
32-
<Label mr={2}>
33-
Video ID:
34-
<Input
35-
type="text"
36-
name="videoId"
37-
value={state.videoId}
38-
onChange={e => {
39-
const videoId = isURL(e.target.value)
40-
? getYouTubeID(e.target.value)
41-
: e.target.value
42-
setState({ ...state, videoId })
43-
}}
44-
/>
45-
</Label>
46-
<Button>Apply</Button>
47-
</Flex>
48-
</form>
49-
)
50-
}
51-
527
const Wrapper = props => (
538
<div
549
{...props}
@@ -72,32 +27,21 @@ const Wrapper = props => (
7227
/>
7328
)
7429

75-
const getProps = node => {
76-
const map = node.data.get('props')
77-
if (!map || typeof map.toJS !== 'function') return map
78-
return map.toJS()
79-
}
80-
81-
export default ({ editor, node, attributes, props, isSelected }) => {
30+
const YouTube = props => {
8231
return (
83-
<div>
84-
<Wrapper
85-
{...attributes}
86-
style={{
87-
outline: isSelected ? '2px solid blue' : null
88-
}}
89-
>
90-
{props.videoId ? <Player {...props} /> : <pre>Enter a YouTube ID</pre>}
91-
{!isSelected && <Overlay />}
92-
</Wrapper>
93-
{isSelected && (
94-
<YouTubeForm
95-
value={getProps(node)}
96-
onSubmit={next => {
97-
editor.setJSXProps(next)
98-
}}
99-
/>
100-
)}
101-
</div>
32+
<Wrapper>
33+
{props.videoId ? <Player {...props} /> : <pre>Enter a YouTube ID</pre>}
34+
</Wrapper>
10235
)
10336
}
37+
38+
// mimicking Framer X - could make this compatible
39+
YouTube.propertyControls = {
40+
videoId: {
41+
type: 'string',
42+
title: 'Video ID',
43+
formatValue: n => (isURL(n) ? getYouTubeID(n) : n)
44+
}
45+
}
46+
47+
export default YouTube
Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
1+
/** @jsx jsx */
2+
import { jsx } from '@emotion/core'
13
import React from 'react'
24
import { Data } from 'slate'
5+
import Form from './Form'
6+
import Overlay from './Overlay'
37
import YouTube from './YouTube'
48
import Gist from './Gist'
59

6-
const setJSXProps = (editor, propsObject) => {
7-
const props = Data.create(propsObject)
8-
editor.setBlocks({ data: { props } })
10+
const setJSXProps = (editor, dataObject) => {
11+
const data = Data.create(dataObject)
12+
editor.setBlocks({ data })
913
}
1014

1115
const insertJSXBlock = (editor, type, props) => {
1216
editor.insertBlock({
13-
type,
17+
type: 'jsx-void',
1418
data: {
19+
type,
1520
props: Data.create(props)
1621
}
1722
})
1823
}
1924

2025
const insertYouTube = editor => {
21-
editor.insertJSXBlock('youtube', {
26+
editor.insertJSXBlock('YouTube', {
2227
videoId: ''
2328
})
2429
}
2530

2631
const insertGist = editor => {
27-
editor.insertJSXBlock('gist', {
32+
editor.insertJSXBlock('Gist', {
2833
id: ''
2934
})
3035
}
@@ -35,6 +40,49 @@ const getProps = node => {
3540
return map.toJS()
3641
}
3742

43+
const Wrapper = ({
44+
editor,
45+
attributes,
46+
isSelected,
47+
component,
48+
Component,
49+
props,
50+
fields
51+
}) => {
52+
return (
53+
<div>
54+
<div
55+
{...attributes}
56+
style={{
57+
position: 'relative',
58+
outline: isSelected ? '2px solid blue' : null
59+
}}
60+
>
61+
<Component {...props} />
62+
{!isSelected && <Overlay />}
63+
</div>
64+
{isSelected && (
65+
<Form
66+
fields={fields}
67+
value={props}
68+
onSubmit={next => {
69+
editor.setJSXProps({
70+
type: component,
71+
props: next
72+
})
73+
}}
74+
/>
75+
)}
76+
</div>
77+
)
78+
}
79+
80+
// placeholder
81+
const components = {
82+
YouTube,
83+
Gist
84+
}
85+
3886
export default (opts = {}) => ({
3987
commands: {
4088
insertJSXBlock,
@@ -44,16 +92,23 @@ export default (opts = {}) => ({
4492
},
4593
renderNode: (props, editor, next) => {
4694
const { node } = props
95+
if (node.type !== 'jsx-void') return next()
96+
const type = node.data.get('type')
4797

48-
switch (node.type) {
49-
case 'youtube':
50-
return <YouTube {...props} editor={editor} props={getProps(node)} />
51-
break
52-
case 'gist':
53-
return <Gist {...props} editor={editor} props={getProps(node)} />
54-
break
55-
default:
56-
return next()
98+
if (components[type]) {
99+
const Component = components[type]
100+
return (
101+
<Wrapper
102+
{...props}
103+
editor={editor}
104+
Component={Component}
105+
component={type}
106+
props={getProps(node)}
107+
fields={Component.propertyControls}
108+
/>
109+
)
57110
}
111+
112+
return next()
58113
}
59114
})

0 commit comments

Comments
 (0)