From adeb42f58b6ca61e2dff5a88546b1915943f479f Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 10:44:13 -0700 Subject: [PATCH 01/20] Match header colours --- src/index.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.css b/src/index.css index c13be2b0..ab80be26 100644 --- a/src/index.css +++ b/src/index.css @@ -16,9 +16,10 @@ body { } header { - background-color: #fff; + background-color: #596362; box-shadow: 0 1px 2px 0 rgba(0,0,0,0.22); height: 54px; + border-bottom:2px solid #3ab7a5; position: fixed; width: 100%; z-index: 999; From feb824b7a4fc058acf254264f2e827904b9bc732 Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 10:49:46 -0700 Subject: [PATCH 02/20] Match search bar to colour scheme --- src/containers/FindBar.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/containers/FindBar.css b/src/containers/FindBar.css index 5f31c520..7cca55a2 100644 --- a/src/containers/FindBar.css +++ b/src/containers/FindBar.css @@ -1,9 +1,9 @@ :root { - --foregroundColor: #555; - --backgroundColor: rgba(245, 245, 245, 0.98); + --foregroundColor: #fff; + --backgroundColor: #596362; --textFieldBorderColor: #e7e7e7; - --highlightFGColor: #444; - --highlightBGColor: #d2dee4; + --highlightFGColor: #fff; + --highlightBGColor: #3ab7a5; } .root { From 01b113a1d69262a4445c21784c8516ca45c8bd91 Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 10:53:31 -0700 Subject: [PATCH 03/20] Grabbed font declaration list --- src/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.css b/src/index.css index ab80be26..bbf9d6ba 100644 --- a/src/index.css +++ b/src/index.css @@ -3,6 +3,7 @@ html { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; margin: 0; + font-family: Roboto,"Helvetica Neue",HelveticaNeue,Helvetica,Arial,sans-serif; } *, *:before, *:after { box-sizing: inherit; From d972418dae1e5ac6c2ad9891f1412200d9cb713d Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 10:54:12 -0700 Subject: [PATCH 04/20] Set main bg and foreground colours --- src/index.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.css b/src/index.css index bbf9d6ba..8284730b 100644 --- a/src/index.css +++ b/src/index.css @@ -12,7 +12,8 @@ html { body { font-family: 'Roboto', sans-serif; height: 100%; - background-color: #fafafa; + background-color: #f2f5f4; + color:#7c8382; margin: 0; } From ae2b7ad45c7b1788807b123cd9f644e1872002cd Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 11:05:03 -0700 Subject: [PATCH 05/20] Match header styles --- src/index.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.css b/src/index.css index 8284730b..a2d11d46 100644 --- a/src/index.css +++ b/src/index.css @@ -34,3 +34,11 @@ header { h1, h2, h3, h4, h5, h6, p { margin: 0; } + +h1{ + color: #3ab7a5; + border-bottom: 1px solid #3ab7a5; + margin: 30px auto 25px; + padding-bottom: 15px; + font-size: 25px; +} From bf87ac15b529447a0906f47fdeeb842faa5739ff Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 11:08:53 -0700 Subject: [PATCH 06/20] Make cards have h2s not h1s --- src/components/Cards/ImageCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Cards/ImageCard.js b/src/components/Cards/ImageCard.js index 307b63a9..9473ca61 100644 --- a/src/components/Cards/ImageCard.js +++ b/src/components/Cards/ImageCard.js @@ -9,7 +9,7 @@ export default class ImageCard extends React.Component { return ( -

{text}

+

{text}

{description ?

{description}

: null}
From 81e27632becbe1e4b29c655a8a5c6cbc2a3e9c62 Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 12:11:14 -0700 Subject: [PATCH 07/20] Made the image upload button stand out as a drag target --- src/components/EntryEditor.js | 5 ++++- src/components/Widgets/ImageControl.js | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 1ab0f003..30d405cd 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -8,7 +8,7 @@ export default function EntryEditor({ collection, entry, getMedia, onChange, onA

Entry in {collection.get('label')}

{entry && entry.get('title')}

-
+
- - {imageName ? imageName : 'Click or drop image here.'} + + {imageName ? imageName : 'Click here to upload from your file browser, or drag an image directly into this box from your desktop'} Date: Wed, 31 Aug 2016 13:06:23 -0700 Subject: [PATCH 08/20] Copy drag image style a bit more from ember version --- src/components/Widgets/ImageControl.js | 11 ++++++----- src/index.css | 12 ++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index f0c5ee06..6753f91d 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -98,14 +98,15 @@ const styles = { display: 'none' }, imageUpload: { - backgroundColor: '#3ab7a5', + backgroundColor: '#fff', textAlign: 'center', - color: '#fff', - padding: '10px', + color: '#888', + padding: '20px', display: 'block', margin: '10px', - boxShadow: '0px 9px 8px -5px rgba(0,0,0,0.75)', - cursor: 'pointer' + border: '1px dashed #eee', + cursor: 'pointer', + fontSize: '12px' } }; diff --git a/src/index.css b/src/index.css index a2d11d46..af9ba209 100644 --- a/src/index.css +++ b/src/index.css @@ -42,3 +42,15 @@ h1{ padding-bottom: 15px; font-size: 25px; } +input{ + width:100%; + padding:3px; +} +button{ + border: 1px solid #3ab7a5; + padding: 3px 20px; + font-size: 12px; + line-height: 18px; + background-color:#fff; + cursor: pointer; +} \ No newline at end of file From c49cfc5fce92cb689d353bd7a504624386d1cc4b Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 13:08:14 -0700 Subject: [PATCH 09/20] Knock back tip text --- src/components/Widgets/ImageControl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 6753f91d..99a94adb 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -79,7 +79,7 @@ export default class ImageControl extends React.Component { onDrop={this.handleChange} > - {imageName ? imageName : 'Click here to upload from your file browser, or drag an image directly into this box from your desktop'} + {imageName ? imageName : 'Tip: Click here to upload an image from your file browser, or drag an image directly into this box from your desktop'} Date: Wed, 31 Aug 2016 13:18:18 -0700 Subject: [PATCH 10/20] Add a bit of padding to inputs --- src/components/EntryEditor.js | 4 +++- src/components/Widgets/ImageControl.js | 1 - src/containers/FindBar.css | 1 + src/index.css | 6 ++++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 30d405cd..6ae93edd 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -31,7 +31,9 @@ const styles = { display: 'flex' }, controlPane: { - width: '50%' + width: '50%', + paddingLeft: '10px', + paddingRight: '10px' }, pane: { width: '50%' diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 99a94adb..12b250e4 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -103,7 +103,6 @@ const styles = { color: '#999', padding: '20px', display: 'block', - margin: '10px', border: '1px dashed #eee', cursor: 'pointer', fontSize: '12px' diff --git a/src/containers/FindBar.css b/src/containers/FindBar.css index 7cca55a2..e370c63b 100644 --- a/src/containers/FindBar.css +++ b/src/containers/FindBar.css @@ -30,6 +30,7 @@ white-space: nowrap; vertical-align: middle; border-right: 1px solid var(--textFieldBorderColor); + margin:0; } .inputField { diff --git a/src/index.css b/src/index.css index af9ba209..00fd30e3 100644 --- a/src/index.css +++ b/src/index.css @@ -42,9 +42,15 @@ h1{ padding-bottom: 15px; font-size: 25px; } + input{ width:100%; padding:3px; + font-size:14px; + margin-bottom:10px; +} +header input{ + margin-bottom:0; } button{ border: 1px solid #3ab7a5; From 8112a62010021f9e738f724f2e497d655112be43 Mon Sep 17 00:00:00 2001 From: Frances Berriman Date: Wed, 31 Aug 2016 13:24:54 -0700 Subject: [PATCH 11/20] Apply the correct shade of grey to header --- src/containers/FindBar.css | 2 +- src/index.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/FindBar.css b/src/containers/FindBar.css index e370c63b..be0abe88 100644 --- a/src/containers/FindBar.css +++ b/src/containers/FindBar.css @@ -1,6 +1,6 @@ :root { --foregroundColor: #fff; - --backgroundColor: #596362; + --backgroundColor: #272e30; --textFieldBorderColor: #e7e7e7; --highlightFGColor: #fff; --highlightBGColor: #3ab7a5; diff --git a/src/index.css b/src/index.css index 00fd30e3..cd5e3545 100644 --- a/src/index.css +++ b/src/index.css @@ -18,7 +18,7 @@ body { } header { - background-color: #596362; + background-color: #272e30; box-shadow: 0 1px 2px 0 rgba(0,0,0,0.22); height: 54px; border-bottom:2px solid #3ab7a5; From 769d2bd2844abe661fc47458f86efad1cab318dc Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Thu, 8 Sep 2016 20:05:45 +0200 Subject: [PATCH 12/20] Fix content type for file requests --- src/backends/netlify-git/API.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backends/netlify-git/API.js b/src/backends/netlify-git/API.js index 8c072a30..d0bad38c 100644 --- a/src/backends/netlify-git/API.js +++ b/src/backends/netlify-git/API.js @@ -100,7 +100,7 @@ export default class API { if (cached && cached.expires > Date.now()) { return cached.data; } return this.request(`${this.repoURL}/files/${key}.json?ref=refs/meta/_netlify_cms`, { - headers: { Accept: 'application/vnd.github.VERSION.raw' }, + headers: { 'Content-Type': 'application/vnd.netlify.raw' }, cache: 'no-store', }).then((result) => { LocalForage.setItem(`gh.meta.${key}`, { @@ -118,7 +118,7 @@ export default class API { if (cached) { return cached; } return this.request(`${this.repoURL}/files/${path}`, { - headers: { Accept: 'application/vnd.github.VERSION.raw' }, + headers: { 'Content-Type': 'application/vnd.netlify.raw' }, params: { ref: this.branch }, cache: false, raw: true From 08596e906f9cb64eb41c3970c6b795dcedf03b56 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Fri, 9 Sep 2016 16:57:28 +0200 Subject: [PATCH 13/20] Better css module classnames --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index b312cf01..b5dbcf83 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,7 +13,7 @@ module.exports = { { test: /\.json$/, loader: 'json-loader' }, { test: /\.css$/, - loader: ExtractTextPlugin.extract("style", "css?modules&importLoaders=1!postcss"), + loader: ExtractTextPlugin.extract("style", "css?modules&importLoaders=1&&localIdentName=cms__[name]__[local]!postcss"), }, { loader: 'babel', From 102429aa5b8f3c56f2aa64ab5cc09b4c6bb1e7de Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Fri, 9 Sep 2016 17:31:59 +0200 Subject: [PATCH 14/20] Make preview pane render to an iframe --- src/components/PreviewPane.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/PreviewPane.js b/src/components/PreviewPane.js index 2964ae93..731f9c90 100644 --- a/src/components/PreviewPane.js +++ b/src/components/PreviewPane.js @@ -1,8 +1,9 @@ import React, { PropTypes } from 'react'; +import { render } from 'react-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Widgets from './Widgets'; -export default class PreviewPane extends React.Component { +class Preview extends React.Component { previewFor(field) { const { entry, getMedia } = this.props; const widget = Widgets[field.get('widget')] || Widgets._unknown; @@ -17,13 +18,43 @@ export default class PreviewPane extends React.Component { const { collection } = this.props; if (!collection) { return null; } - return
{collection.get('fields').map((field) =>
{this.previewFor(field)}
)}
; } } +export default class PreviewPane extends React.Component { + constructor(props) { + super(props); + this.handleIframeRef = this.handleIframeRef.bind(this); + } + + componentDidUpdate() { + this.renderPreview(); + } + + renderPreview() { + const props = this.props; + render(, this.previewEl); + } + + handleIframeRef(ref) { + if (ref) { + this.previewEl = document.createElement('div'); + ref.contentDocument.body.appendChild(this.previewEl); + this.renderPreview(); + } + } + + render() { + const { collection } = this.props; + if (!collection) { return null; } + + return + } +} + PreviewPane.propTypes = { collection: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired, From c51f42658e667c4420b3668f43b0cfd162fec972 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Fri, 9 Sep 2016 17:32:23 +0200 Subject: [PATCH 15/20] Get rid of double connect in markdown control --- src/components/Widgets/MarkdownControl.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js index a315cb80..13cc4759 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl.js @@ -64,8 +64,6 @@ class MarkdownControl extends React.Component { } } -export default MarkdownControl; - MarkdownControl.propTypes = { editor: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, From 8d63ff0a8804ba7d9f3c144c3688e8abe31ee960 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Sun, 11 Sep 2016 17:53:44 +0200 Subject: [PATCH 16/20] Expose methods for installations to create custom preview components --- package.json | 1 + src/components/ControlPane.js | 19 ++++++----- src/components/PreviewPane.css | 6 ++++ src/components/PreviewPane.js | 34 +++++++++++++++++-- .../VisualEditor/index.js | 1 + src/index.css | 2 +- src/index.js | 6 ++++ src/lib/registry.js | 20 +++++++++++ 8 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 src/components/PreviewPane.css create mode 100644 src/lib/registry.js diff --git a/package.json b/package.json index c1db9538..a0a84167 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "dependencies": { "bricks.js": "^1.7.0", "fuzzy": "^0.1.1", + "html-to-react": "^1.0.0", "js-base64": "^2.1.9", "json-loader": "^0.5.4", "localforage": "^1.4.2", diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index a86ecaf5..59beb698 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -6,14 +6,17 @@ export default class ControlPane extends React.Component { controlFor(field) { const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; const widget = Widgets[field.get('widget')] || Widgets._unknown; - return React.createElement(widget.Control, { - field: field, - value: entry.getIn(['data', field.get('name')]), - onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), - onAddMedia: onAddMedia, - onRemoveMedia: onRemoveMedia, - getMedia: getMedia - }); + return
+ + {React.createElement(widget.Control, { + field: field, + value: entry.getIn(['data', field.get('name')]), + onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), + onAddMedia: onAddMedia, + onRemoveMedia: onRemoveMedia, + getMedia: getMedia + })} +
; } render() { diff --git a/src/components/PreviewPane.css b/src/components/PreviewPane.css new file mode 100644 index 00000000..6bf62a0a --- /dev/null +++ b/src/components/PreviewPane.css @@ -0,0 +1,6 @@ +.frame { + width: 100%; + height: 100%; + border: none; + background: #fff; +} diff --git a/src/components/PreviewPane.js b/src/components/PreviewPane.js index 731f9c90..24af9229 100644 --- a/src/components/PreviewPane.js +++ b/src/components/PreviewPane.js @@ -1,7 +1,9 @@ import React, { PropTypes } from 'react'; import { render } from 'react-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { getPreviewTemplate, getPreviewStyles } from '../lib/registry'; import Widgets from './Widgets'; +import styles from './PreviewPane.css'; class Preview extends React.Component { previewFor(field) { @@ -24,23 +26,49 @@ class Preview extends React.Component { } } +Preview.propTypes = { + collection: ImmutablePropTypes.map.isRequired, + entry: ImmutablePropTypes.map.isRequired, + getMedia: PropTypes.func.isRequired, +}; + export default class PreviewPane extends React.Component { constructor(props) { super(props); this.handleIframeRef = this.handleIframeRef.bind(this); + this.widgetFor = this.widgetFor.bind(this); } componentDidUpdate() { this.renderPreview(); } + widgetFor(name) { + const { collection, entry, getMedia } = this.props; + const field = collection.get('fields').find((field) => field.get('name') === name); + const widget = Widgets[field.get('widget')] || Widgets._unknown; + return React.createElement(widget.Preview, { + field: field, + value: entry.getIn(['data', field.get('name')]), + getMedia: getMedia, + }); + } + renderPreview() { - const props = this.props; - render(, this.previewEl); + const props = Object.assign({}, this.props, {widgetFor: this.widgetFor}); + const component = getPreviewTemplate(props.collection.get('name')) || Preview; + + render(React.createElement(component, props), this.previewEl); } handleIframeRef(ref) { if (ref) { + getPreviewStyles().forEach((style) => { + const linkEl = document.createElement('link'); + linkEl.setAttribute('rel', 'stylesheet'); + linkEl.setAttribute('href', style); + ref.contentDocument.head.appendChild(linkEl); + }); this.previewEl = document.createElement('div'); ref.contentDocument.body.appendChild(this.previewEl); this.renderPreview(); @@ -51,7 +79,7 @@ export default class PreviewPane extends React.Component { const { collection } = this.props; if (!collection) { return null; } - return + return } } diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js index 7525bcb5..0f9eb696 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js @@ -42,6 +42,7 @@ class VisualEditor extends React.Component { let rawJson; if (props.value !== undefined) { const content = this.markdown.toContent(props.value); + console.log('md: %o', content); rawJson = SlateUtils.encode(content, null, ['mediaproxy'].concat(getPlugins().map(plugin => plugin.id))); } else { rawJson = emptyParagraphBlock; diff --git a/src/index.css b/src/index.css index cd5e3545..4bc2c468 100644 --- a/src/index.css +++ b/src/index.css @@ -59,4 +59,4 @@ button{ line-height: 18px; background-color:#fff; cursor: pointer; -} \ No newline at end of file +} diff --git a/src/index.js b/src/index.js index 13dc95ca..5ef69bb4 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; +import { registerPreviewStyle,registerPreviewTemplate } from './lib/registry'; import configureStore from './store/configureStore'; import routes from './routing/routes'; import history, { syncHistory } from './routing/history'; @@ -29,3 +30,8 @@ render(( ), el); + +window.CMS = { + registerPreviewStyle: registerPreviewStyle, + registerPreviewTemplate: registerPreviewTemplate +}; diff --git a/src/lib/registry.js b/src/lib/registry.js new file mode 100644 index 00000000..809b9784 --- /dev/null +++ b/src/lib/registry.js @@ -0,0 +1,20 @@ +const registry = { + templates: {}, + previewStyles: [] +}; + +export function registerPreviewStyle(style) { + registry.previewStyles.push(style); +} + +export function registerPreviewTemplate(name, component) { + registry.templates[name] = component; +} + +export function getPreviewTemplate(name) { + return registry.templates[name]; +} + +export function getPreviewStyles() { + return registry.previewStyles; +} From bbbf3c5621de4598ce37cbc7cab4a5d238427ad0 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Sun, 11 Sep 2016 23:07:48 +0200 Subject: [PATCH 17/20] Add datetime widget --- package.json | 1 + src/components/Widgets/DateTimeControl.js | 22 ++++++++++++++++++++++ src/components/Widgets/DateTimePreview.js | 9 +++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/components/Widgets/DateTimeControl.js create mode 100644 src/components/Widgets/DateTimePreview.js diff --git a/package.json b/package.json index a0a84167..f9371cdb 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "markup-it": "git+https://github.com/cassiozen/markup-it.git", "pluralize": "^3.0.0", "prismjs": "^1.5.1", + "react-datetime": "^2.6.0", "react-portal": "^2.2.1", "selection-position": "^1.0.0", "semaphore": "^1.0.5", diff --git a/src/components/Widgets/DateTimeControl.js b/src/components/Widgets/DateTimeControl.js new file mode 100644 index 00000000..82476a25 --- /dev/null +++ b/src/components/Widgets/DateTimeControl.js @@ -0,0 +1,22 @@ +import React, { PropTypes } from 'react'; +import DateTime from 'react-datetime'; + +export default class DateTimeControl extends React.Component { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + handleChange(datetime) { + this.props.onChange(datetime); + } + + render() { + return ; + } +} + +DateTimeControl.propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.node, +}; diff --git a/src/components/Widgets/DateTimePreview.js b/src/components/Widgets/DateTimePreview.js new file mode 100644 index 00000000..972e068c --- /dev/null +++ b/src/components/Widgets/DateTimePreview.js @@ -0,0 +1,9 @@ +import React, { PropTypes } from 'react'; + +export default function StringPreview({ value }) { + return {value}; +} + +StringPreview.propTypes = { + value: PropTypes.node, +}; From fcd0ce718a3160b8572ffd83a0c730aac0eb8e81 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Sun, 11 Sep 2016 23:08:18 +0200 Subject: [PATCH 18/20] Support for YAML content --- src/components/ControlPane.js | 6 +- src/components/EntryEditor.css | 10 ++ src/components/EntryEditor.js | 21 +--- src/components/EntryListing.js | 3 +- src/components/PreviewPane.js | 18 +-- src/components/Widgets.js | 31 ++--- src/formats/formats.js | 12 +- src/index.css | 212 +++++++++++++++++++++++++++++++++ src/index.js | 11 +- src/lib/registry.js | 40 ++++--- 10 files changed, 291 insertions(+), 73 deletions(-) create mode 100644 src/components/EntryEditor.css diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index 59beb698..fc85243b 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -1,14 +1,14 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Widgets from './Widgets'; +import {resolveWidget} from './Widgets'; export default class ControlPane extends React.Component { controlFor(field) { const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; - const widget = Widgets[field.get('widget')] || Widgets._unknown; + const widget = resolveWidget(field.get('widget')); return
- {React.createElement(widget.Control, { + {React.createElement(widget.control, { field: field, value: entry.getIn(['data', field.get('name')]), onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), diff --git a/src/components/EntryEditor.css b/src/components/EntryEditor.css new file mode 100644 index 00000000..b77e517f --- /dev/null +++ b/src/components/EntryEditor.css @@ -0,0 +1,10 @@ +.container { + display: flex +} +.controlPane { + width: 50%; + padding: 0 10px; +} +.previewPane { + width: 50%; +} diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 6ae93edd..c4a6353b 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -2,13 +2,14 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ControlPane from './ControlPane'; import PreviewPane from './PreviewPane'; +import styles from './EntryEditor.css'; export default function EntryEditor({ collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist }) { return

Entry in {collection.get('label')}

{entry && entry.get('title')}

-
-
+
+
-
+
@@ -26,20 +27,6 @@ export default function EntryEditor({ collection, entry, getMedia, onChange, onA
; } -const styles = { - container: { - display: 'flex' - }, - controlPane: { - width: '50%', - paddingLeft: '10px', - paddingRight: '10px' - }, - pane: { - width: '50%' - } -}; - EntryEditor.propTypes = { collection: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired, diff --git a/src/components/EntryListing.js b/src/components/EntryListing.js index 28baa927..7ca45393 100644 --- a/src/components/EntryListing.js +++ b/src/components/EntryListing.js @@ -60,7 +60,8 @@ export default class EntryListing extends React.Component { cardFor(collection, entry, link) { //const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; - const card = Cards[collection.getIn(['card', 'type'])] || Cards._unknown; + const cartType = collection.getIn(['card', 'type']) || 'alltype'; + const card = Cards[cartType] || Cards._unknown; return React.createElement(card, { key: entry.get('slug'), collection: collection, diff --git a/src/components/PreviewPane.js b/src/components/PreviewPane.js index 24af9229..9ce76ab5 100644 --- a/src/components/PreviewPane.js +++ b/src/components/PreviewPane.js @@ -1,15 +1,15 @@ import React, { PropTypes } from 'react'; import { render } from 'react-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { getPreviewTemplate, getPreviewStyles } from '../lib/registry'; -import Widgets from './Widgets'; +import registry from '../lib/registry'; +import { resolveWidget } from './Widgets'; import styles from './PreviewPane.css'; class Preview extends React.Component { previewFor(field) { const { entry, getMedia } = this.props; - const widget = Widgets[field.get('widget')] || Widgets._unknown; - return React.createElement(widget.Preview, { + const widget = resolveWidget(field.get('widget')); + return React.createElement(widget.preview, { field: field, value: entry.getIn(['data', field.get('name')]), getMedia: getMedia, @@ -46,8 +46,8 @@ export default class PreviewPane extends React.Component { widgetFor(name) { const { collection, entry, getMedia } = this.props; const field = collection.get('fields').find((field) => field.get('name') === name); - const widget = Widgets[field.get('widget')] || Widgets._unknown; - return React.createElement(widget.Preview, { + const widget = resolveWidget(field.get('widget')); + return React.createElement(widget.preview, { field: field, value: entry.getIn(['data', field.get('name')]), getMedia: getMedia, @@ -56,14 +56,14 @@ export default class PreviewPane extends React.Component { renderPreview() { const props = Object.assign({}, this.props, {widgetFor: this.widgetFor}); - const component = getPreviewTemplate(props.collection.get('name')) || Preview; + const component = registry.getPreviewTemplate(props.collection.get('name')) || Preview; render(React.createElement(component, props), this.previewEl); } handleIframeRef(ref) { if (ref) { - getPreviewStyles().forEach((style) => { + registry.getPreviewStyles().forEach((style) => { const linkEl = document.createElement('link'); linkEl.setAttribute('rel', 'stylesheet'); linkEl.setAttribute('href', style); @@ -79,7 +79,7 @@ export default class PreviewPane extends React.Component { const { collection } = this.props; if (!collection) { return null; } - return + return ; } } diff --git a/src/components/Widgets.js b/src/components/Widgets.js index 0a86b6b7..e03bfa8f 100644 --- a/src/components/Widgets.js +++ b/src/components/Widgets.js @@ -1,3 +1,4 @@ +import registry from '../lib/registry'; import UnknownControl from './Widgets/UnknownControl'; import UnknownPreview from './Widgets/UnknownPreview'; import StringControl from './Widgets/StringControl'; @@ -6,25 +7,15 @@ import MarkdownControl from './Widgets/MarkdownControl'; import MarkdownPreview from './Widgets/MarkdownPreview'; import ImageControl from './Widgets/ImageControl'; import ImagePreview from './Widgets/ImagePreview'; +import DateTimeControl from './Widgets/DateTimeControl'; +import DateTimePreview from './Widgets/DateTimePreview'; +registry.registerWidget('string', StringControl, StringPreview); +registry.registerWidget('markdown', MarkdownControl, MarkdownPreview); +registry.registerWidget('image', ImageControl, ImagePreview); +registry.registerWidget('datetime', DateTimeControl, DateTimePreview); +registry.registerWidget('_unknown', UnknownControl, UnknownPreview); -const Widgets = { - _unknown: { - Control: UnknownControl, - Preview: UnknownPreview - }, - string: { - Control: StringControl, - Preview: StringPreview - }, - markdown: { - Control: MarkdownControl, - Preview: MarkdownPreview - }, - image: { - Control: ImageControl, - Preview: ImagePreview - } -}; - -export default Widgets; +export function resolveWidget(name) { + return registry.getWidget(name) || registry.getWidget('_unknown'); +} diff --git a/src/formats/formats.js b/src/formats/formats.js index 9d6f72ad..8e3679b5 100644 --- a/src/formats/formats.js +++ b/src/formats/formats.js @@ -1,5 +1,15 @@ +import YAML from './yaml'; import YAMLFrontmatter from './yaml-frontmatter'; +const yamlFormatter = new YAML(); +const YamlFrontmatterFormatter = new YAMLFrontmatter(); + export function resolveFormat(collection, entry) { - return new YAMLFrontmatter(); + const extension = entry.path.split('.').pop(); + switch (extension) { + case 'yml': + return yamlFormatter; + default: + return YamlFrontmatterFormatter; + } } diff --git a/src/index.css b/src/index.css index 4bc2c468..21a60d09 100644 --- a/src/index.css +++ b/src/index.css @@ -60,3 +60,215 @@ button{ background-color:#fff; cursor: pointer; } + +:global { + & .rdt { + position: relative; + } + & .rdtPicker { + display: none; + position: absolute; + width: 250px; + padding: 4px; + margin-top: 1px; + z-index: 99999 !important; + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,.1); + border: 1px solid #f9f9f9; + } + & .rdtOpen .rdtPicker { + display: block; + } + & .rdtStatic .rdtPicker { + box-shadow: none; + position: static; + } + + & .rdtPicker .rdtTimeToggle { + text-align: center; + } + + & .rdtPicker table { + width: 100%; + margin: 0; + } + & .rdtPicker td, + & .rdtPicker th { + text-align: center; + height: 28px; + } + & .rdtPicker td { + cursor: pointer; + } + & .rdtPicker td.rdtDay:hover, + & .rdtPicker td.rdtHour:hover, + & .rdtPicker td.rdtMinute:hover, + & .rdtPicker td.rdtSecond:hover, + & .rdtPicker .rdtTimeToggle:hover { + background: #eeeeee; + cursor: pointer; + } + & .rdtPicker td.rdtOld, + & .rdtPicker td.rdtNew { + color: #999999; + } + & .rdtPicker td.rdtToday { + position: relative; + } + & .rdtPicker td.rdtToday:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-bottom: 7px solid #428bca; + border-top-color: rgba(0, 0, 0, 0.2); + position: absolute; + bottom: 4px; + right: 4px; + } + & .rdtPicker td.rdtActive, + & .rdtPicker td.rdtActive:hover { + background-color: #428bca; + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + } + & .rdtPicker td.rdtActive.rdtToday:before { + border-bottom-color: #fff; + } + & .rdtPicker td.rdtDisabled, + & .rdtPicker td.rdtDisabled:hover { + background: none; + color: #999999; + cursor: not-allowed; + } + + & .rdtPicker td span.rdtOld { + color: #999999; + } + & .rdtPicker td span.rdtDisabled, + & .rdtPicker td span.rdtDisabled:hover { + background: none; + color: #999999; + cursor: not-allowed; + } + & .rdtPicker th { + border-bottom: 1px solid #f9f9f9; + } + & .rdtPicker .dow { + width: 14.2857%; + border-bottom: none; + } + & .rdtPicker th.rdtSwitch { + width: 100px; + } + & .rdtPicker th.rdtNext, + & .rdtPicker th.rdtPrev { + font-size: 21px; + vertical-align: top; + } + + & .rdtPrev span, + & .rdtNext span { + display: block; + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Chrome/Safari/Opera */ + -khtml-user-select: none; /* Konqueror */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; + } + + & .rdtPicker th.rdtDisabled, + & .rdtPicker th.rdtDisabled:hover { + background: none; + color: #999999; + cursor: not-allowed; + } + & .rdtPicker thead tr:first-child th { + cursor: pointer; + } + & .rdtPicker thead tr:first-child th:hover { + background: #eeeeee; + } + + & .rdtPicker tfoot { + border-top: 1px solid #f9f9f9; + } + + & .rdtPicker button { + border: none; + background: none; + cursor: pointer; + } + & .rdtPicker button:hover { + background-color: #eee; + } + + & .rdtPicker thead button { + width: 100%; + height: 100%; + } + + & td.rdtMonth, + & td.rdtYear { + height: 50px; + width: 25%; + cursor: pointer; + } + & td.rdtMonth:hover, + & td.rdtYear:hover { + background: #eee; + } + + & .rdtCounters { + display: inline-block; + } + + & .rdtCounters > div { + float: left; + } + + & .rdtCounter { + height: 100px; + } + + & .rdtCounter { + width: 40px; + } + + & .rdtCounterSeparator { + line-height: 100px; + } + + & .rdtCounter .rdtBtn { + height: 40%; + line-height: 40px; + cursor: pointer; + display: block; + + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Chrome/Safari/Opera */ + -khtml-user-select: none; /* Konqueror */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; + } + & .rdtCounter .rdtBtn:hover { + background: #eee; + } + & .rdtCounter .rdtCount { + height: 20%; + font-size: 1.2em; + } + + & .rdtMilli { + vertical-align: middle; + padding-left: 8px; + width: 48px; + } + + & .rdtMilli input { + width: 100%; + font-size: 1.2em; + margin-top: 37px; + } +} diff --git a/src/index.js b/src/index.js index 5ef69bb4..e93eaea1 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; -import { registerPreviewStyle,registerPreviewTemplate } from './lib/registry'; +import registry from './lib/registry'; import configureStore from './store/configureStore'; import routes from './routing/routes'; import history, { syncHistory } from './routing/history'; @@ -31,7 +31,8 @@ render(( ), el); -window.CMS = { - registerPreviewStyle: registerPreviewStyle, - registerPreviewTemplate: registerPreviewTemplate -}; +window.CMS = {}; +console.log('reg: ', registry); +for (const method in registry) { + window.CMS[method] = registry[method]; +} diff --git a/src/lib/registry.js b/src/lib/registry.js index 809b9784..84a1eb94 100644 --- a/src/lib/registry.js +++ b/src/lib/registry.js @@ -1,20 +1,26 @@ -const registry = { +const _registry = { templates: {}, - previewStyles: [] + previewStyles: [], + widgets: {} }; -export function registerPreviewStyle(style) { - registry.previewStyles.push(style); -} - -export function registerPreviewTemplate(name, component) { - registry.templates[name] = component; -} - -export function getPreviewTemplate(name) { - return registry.templates[name]; -} - -export function getPreviewStyles() { - return registry.previewStyles; -} +export default { + registerPreviewStyle(style) { + _registry.previewStyles.push(style); + }, + registerPreviewTemplate(name, component) { + _registry.templates[name] = component; + }, + getPreviewTemplate(name) { + return _registry.templates[name]; + }, + getPreviewStyles() { + return _registry.previewStyles; + }, + registerWidget(name, control, preview) { + _registry.widgets[name] = { control, preview }; + }, + getWidget(name) { + return _registry.widgets[name]; + } +}; From 8221c9c170b63d01c304ea7a566901203e4787b8 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Mon, 12 Sep 2016 11:14:21 +0200 Subject: [PATCH 19/20] Maor UI polish for editing with live preview --- package.json | 1 - src/components/ControlPane.js | 2 +- src/components/EntryEditor.css | 19 +++++- src/components/EntryEditor.js | 68 +++++++++++++------ src/components/Widgets.js | 3 + src/components/Widgets/DateTimeControl.js | 2 +- src/components/Widgets/MarkdownControl.js | 12 ++-- .../MarkdownControlElements/plugins.js} | 35 ++++------ src/components/Widgets/TextControl.js | 37 ++++++++++ src/components/Widgets/TextPreview.js | 4 ++ src/components/Widgets/richText.js | 2 - src/containers/App.css | 42 +----------- src/containers/CollectionPage.css | 39 +++++++++++ src/containers/CollectionPage.js | 3 +- src/index.css | 59 ++++++++++++++-- src/index.js | 11 +-- src/lib/registry.js | 12 +++- 17 files changed, 242 insertions(+), 109 deletions(-) rename src/{plugins/index.js => components/Widgets/MarkdownControlElements/plugins.js} (52%) create mode 100644 src/components/Widgets/TextControl.js create mode 100644 src/components/Widgets/TextPreview.js create mode 100644 src/containers/CollectionPage.css diff --git a/package.json b/package.json index f9371cdb..a3c0dd05 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "dependencies": { "bricks.js": "^1.7.0", "fuzzy": "^0.1.1", - "html-to-react": "^1.0.0", "js-base64": "^2.1.9", "json-loader": "^0.5.4", "localforage": "^1.4.2", diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index fc85243b..435aaffa 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -23,7 +23,7 @@ export default class ControlPane extends React.Component { const { collection } = this.props; if (!collection) { return null; } return
- {collection.get('fields').map((field) =>
{this.controlFor(field)}
)} + {collection.get('fields').map((field) =>
{this.controlFor(field)}
)}
; } } diff --git a/src/components/EntryEditor.css b/src/components/EntryEditor.css index b77e517f..03c128bd 100644 --- a/src/components/EntryEditor.css +++ b/src/components/EntryEditor.css @@ -1,9 +1,24 @@ +.entryEditor { + display: flex; + flex-direction: column; + height: 100%; +} .container { - display: flex + display: flex; + height: 100%; +} +.footer { + background: #fff; + height: 45px; + border-top: 1px solid #e8eae8; + padding: 10px 20px; } .controlPane { width: 50%; - padding: 0 10px; + max-height: 100%; + overflow: auto; + padding: 0 20px; + border-right: 1px solid #e8eae8; } .previewPane { width: 50%; diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index c4a6353b..ea83f7bb 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -4,27 +4,57 @@ import ControlPane from './ControlPane'; import PreviewPane from './PreviewPane'; import styles from './EntryEditor.css'; -export default function EntryEditor({ collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist }) { - return
-

Entry in {collection.get('label')}

-

{entry && entry.get('title')}

-
-
- +export default class EntryEditor extends React.Component { + constructor(props) { + super(props); + this.state = {}; + this.handleResize = this.handleResize.bind(this); + } + + componentDidMount() { + this.calculateHeight(); + window.addEventListener('resize', this.handleResize, false); + } + + componengWillUnmount() { + window.removeEventListener('resize', this.handleResize); + } + + handleResize() { + this.calculateHeight(); + } + + calculateHeight() { + const height = window.innerHeight - 54; + console.log('setting height to %s', height); + this.setState({height}); + } + + render() { + const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; + const {height} = this.state; + + return
+
+
+ +
+
+ +
-
- +
+
-
- -
; +
; + } } EntryEditor.propTypes = { diff --git a/src/components/Widgets.js b/src/components/Widgets.js index e03bfa8f..e731ed78 100644 --- a/src/components/Widgets.js +++ b/src/components/Widgets.js @@ -3,6 +3,8 @@ import UnknownControl from './Widgets/UnknownControl'; import UnknownPreview from './Widgets/UnknownPreview'; import StringControl from './Widgets/StringControl'; import StringPreview from './Widgets/StringPreview'; +import TextControl from './Widgets/TextControl'; +import TextPreview from './Widgets/TextPreview'; import MarkdownControl from './Widgets/MarkdownControl'; import MarkdownPreview from './Widgets/MarkdownPreview'; import ImageControl from './Widgets/ImageControl'; @@ -11,6 +13,7 @@ import DateTimeControl from './Widgets/DateTimeControl'; import DateTimePreview from './Widgets/DateTimePreview'; registry.registerWidget('string', StringControl, StringPreview); +registry.registerWidget('text', TextControl, TextPreview); registry.registerWidget('markdown', MarkdownControl, MarkdownPreview); registry.registerWidget('image', ImageControl, ImagePreview); registry.registerWidget('datetime', DateTimeControl, DateTimePreview); diff --git a/src/components/Widgets/DateTimeControl.js b/src/components/Widgets/DateTimeControl.js index 82476a25..7f49868a 100644 --- a/src/components/Widgets/DateTimeControl.js +++ b/src/components/Widgets/DateTimeControl.js @@ -18,5 +18,5 @@ export default class DateTimeControl extends React.Component { DateTimeControl.propTypes = { onChange: PropTypes.func.isRequired, - value: PropTypes.node, + value: PropTypes.object, }; diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js index 13cc4759..54abac8b 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl.js @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import registry from '../../lib/registry'; import RawEditor from './MarkdownControlElements/RawEditor'; import VisualEditor from './MarkdownControlElements/VisualEditor'; import { processEditorPlugins } from './richText'; @@ -13,7 +14,8 @@ class MarkdownControl extends React.Component { } componentWillMount() { - processEditorPlugins(this.context.plugins.editor); + this.useRawEditor(); + processEditorPlugins(registry.getEditorComponents()); } useVisualEditor() { @@ -28,8 +30,8 @@ class MarkdownControl extends React.Component { const { editor, onChange, onAddMedia, getMedia, value } = this.props; if (editor.get('useVisualMode')) { return ( -
- +
+ {null && } - +
+ {null && } { - const configObj = new EditorComponent({ - id: config.id || config.label.replace(/[^A-Z0-9]+/ig, '_'), - label: config.label, - icon: config.icon, - fields: config.fields, - pattern: config.pattern, - fromBlock: _.isFunction(config.fromBlock) ? config.fromBlock.bind(null) : null, - toBlock: _.isFunction(config.toBlock) ? config.toBlock.bind(null) : null, - toPreview: _.isFunction(config.toPreview) ? config.toPreview.bind(null) : config.toBlock.bind(null) - }); - - plugins.editor = plugins.editor.push(configObj); - }; -} - class Plugin extends Component { getChildContext() { @@ -51,8 +34,18 @@ Plugin.childContextTypes = { plugins: PropTypes.object }; +export function newEditorPlugin(config) { + const configObj = new EditorComponent({ + id: config.id || config.label.replace(/[^A-Z0-9]+/ig, '_'), + label: config.label, + icon: config.icon, + fields: config.fields, + pattern: config.pattern, + fromBlock: _.isFunction(config.fromBlock) ? config.fromBlock.bind(null) : null, + toBlock: _.isFunction(config.toBlock) ? config.toBlock.bind(null) : null, + toPreview: _.isFunction(config.toPreview) ? config.toPreview.bind(null) : config.toBlock.bind(null) + }); -export const initPluginAPI = () => { - window.CMS = new CMS(); - return Plugin; -}; + + return configObj; +} diff --git a/src/components/Widgets/TextControl.js b/src/components/Widgets/TextControl.js new file mode 100644 index 00000000..aaeec4e3 --- /dev/null +++ b/src/components/Widgets/TextControl.js @@ -0,0 +1,37 @@ +import React, { PropTypes } from 'react'; + +export default class StringControl extends React.Component { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + this.handleRef = this.handleRef.bind(this); + } + + componentDidMount() { + this.updateHeight(); + } + + handleChange(e) { + this.props.onChange(e.target.value); + this.updateHeight(); + } + + updateHeight() { + if (this.element.scrollHeight > this.element.clientHeight) { + this.element.style.height = this.element.scrollHeight + 'px'; + } + } + + handleRef(ref) { + this.element = ref; + } + + render() { + return