diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c6416ff5..e3bf73d20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
+### Added
+- [#692](https://github.com/plotly/dash-core-components/pull/692) Async DatePickerSingle, DatePickerRange, Dropdown, Markdown, Upload components
+
### Updated
- [#693](https://github.com/plotly/dash-core-components/pull/693) Upgraded plotly.js to 1.51.1
- [Feature release 1.51.0](https://github.com/plotly/plotly.js/releases/tag/v1.51.0) which contains:
diff --git a/dash_core_components_base/__init__.py b/dash_core_components_base/__init__.py
index 1088dbc19..c01ec03ec 100644
--- a/dash_core_components_base/__init__.py
+++ b/dash_core_components_base/__init__.py
@@ -39,7 +39,37 @@
_this_module = _sys.modules[__name__]
-_js_dist = [
+async_resources = [
+ 'datepicker',
+ 'dropdown',
+ 'graph',
+ 'markdown',
+ 'upload'
+]
+
+_js_dist = []
+
+_js_dist.extend([{
+ 'relative_package_path': 'async~{}.js'.format(async_resource),
+ 'external_url': (
+ 'https://unpkg.com/dash-core-components@{}'
+ '/dash_core_components/async~{}.js'
+ ).format(__version__, async_resource),
+ 'namespace': 'dash_core_components',
+ 'async': True
+ } for async_resource in async_resources])
+
+_js_dist.extend([{
+ 'relative_package_path': 'async~{}.js.map'.format(async_resource),
+ 'external_url': (
+ 'https://unpkg.com/dash-core-components@{}'
+ '/dash_core_components/async~{}.js.map'
+ ).format(__version__, async_resource),
+ 'namespace': 'dash_core_components',
+ 'dynamic': True
+ } for async_resource in async_resources])
+
+_js_dist.extend([
{
'relative_package_path': 'highlight.pack.js',
'namespace': 'dash_core_components'
@@ -106,7 +136,7 @@
'namespace': 'dash_core_components',
'dynamic': True
},
-]
+])
for _component in __all__:
setattr(locals()[_component], '_js_dist', _js_dist)
\ No newline at end of file
diff --git a/src/components/DatePickerRange.react.js b/src/components/DatePickerRange.react.js
index caffb3e4c..e6957eecc 100644
--- a/src/components/DatePickerRange.react.js
+++ b/src/components/DatePickerRange.react.js
@@ -1,10 +1,8 @@
-import 'react-dates/initialize';
-import {DateRangePicker} from 'react-dates';
import PropTypes from 'prop-types';
-import React, {Component} from 'react';
-import uniqid from 'uniqid';
+import React, {Component, lazy, Suspense} from 'react';
+import LazyLoader from '../utils/LazyLoader';
-import convertToMoment from '../utils/convertToMoment';
+const RealDatePickerRange = lazy(LazyLoader.datePickerRange);
/**
* DatePickerRange is a tailor made component designed for selecting
@@ -18,176 +16,11 @@ import convertToMoment from '../utils/convertToMoment';
* which can be found here: https://github.com/airbnb/react-dates
*/
export default class DatePickerRange extends Component {
- constructor(props) {
- super(props);
- this.propsToState = this.propsToState.bind(this);
- this.onDatesChange = this.onDatesChange.bind(this);
- this.isOutsideRange = this.isOutsideRange.bind(this);
- this.state = {
- focused: false,
- start_date_id: props.start_date_id || uniqid(),
- end_date_id: props.end_date_id || uniqid(),
- };
- }
-
- propsToState(newProps) {
- this.setState({
- start_date: newProps.start_date,
- end_date: newProps.end_date,
- });
- }
-
- componentWillReceiveProps(newProps) {
- this.propsToState(newProps);
- }
-
- componentWillMount() {
- this.propsToState(this.props);
- }
-
- onDatesChange({startDate: start_date, endDate: end_date}) {
- const {setProps, updatemode, clearable} = this.props;
-
- const oldMomentDates = convertToMoment(this.state, [
- 'start_date',
- 'end_date',
- ]);
-
- if (start_date && !start_date.isSame(oldMomentDates.start_date)) {
- if (updatemode === 'singledate') {
- setProps({start_date: start_date.format('YYYY-MM-DD')});
- } else {
- this.setState({start_date: start_date.format('YYYY-MM-DD')});
- }
- }
-
- if (end_date && !end_date.isSame(oldMomentDates.end_date)) {
- if (updatemode === 'singledate') {
- setProps({end_date: end_date.format('YYYY-MM-DD')});
- } else if (updatemode === 'bothdates') {
- setProps({
- start_date: this.state.start_date,
- end_date: end_date.format('YYYY-MM-DD'),
- });
- }
- }
-
- if (
- clearable &&
- !start_date &&
- !end_date &&
- (oldMomentDates.start_date !== start_date ||
- oldMomentDates.end_date !== end_date)
- ) {
- setProps({start_date: null, end_date: null});
- }
- }
-
- isOutsideRange(date) {
- const {min_date_allowed, max_date_allowed} = this.props;
-
- return (
- (min_date_allowed && date.isBefore(min_date_allowed)) ||
- (max_date_allowed && date.isAfter(max_date_allowed))
- );
- }
-
render() {
- const {focusedInput} = this.state;
-
- const {
- calendar_orientation,
- clearable,
- day_size,
- disabled,
- display_format,
- end_date_placeholder_text,
- first_day_of_week,
- is_RTL,
- minimum_nights,
- month_format,
- number_of_months_shown,
- reopen_calendar_on_clear,
- show_outside_days,
- start_date_placeholder_text,
- stay_open_on_select,
- with_full_screen_portal,
- with_portal,
- loading_state,
- id,
- style,
- className,
- start_date_id,
- end_date_id,
- } = this.props;
-
- const {initial_visible_month} = convertToMoment(this.props, [
- 'initial_visible_month',
- ]);
-
- const {start_date, end_date} = convertToMoment(this.state, [
- 'start_date',
- 'end_date',
- ]);
- const verticalFlag = calendar_orientation !== 'vertical';
-
- const DatePickerWrapperStyles = {
- position: 'relative',
- display: 'inline-block',
- ...style,
- };
-
return (
-
- {
- if (initial_visible_month) {
- return initial_visible_month;
- } else if (end_date && focusedInput === 'endDate') {
- return end_date;
- } else if (start_date && focusedInput === 'startDate') {
- return start_date;
- }
- return start_date;
- }}
- isOutsideRange={this.isOutsideRange}
- isRTL={is_RTL}
- keepOpenOnDateSelect={stay_open_on_select}
- minimumNights={minimum_nights}
- monthFormat={month_format}
- numberOfMonths={number_of_months_shown}
- onDatesChange={this.onDatesChange}
- onFocusChange={focusedInput =>
- this.setState({focusedInput})
- }
- orientation={calendar_orientation}
- reopenPickerOnClearDates={reopen_calendar_on_clear}
- showClearDates={clearable}
- startDate={start_date}
- startDatePlaceholderText={start_date_placeholder_text}
- withFullScreenPortal={
- with_full_screen_portal && verticalFlag
- }
- withPortal={with_portal && verticalFlag}
- startDateId={start_date_id || this.state.start_date_id}
- endDateId={end_date_id || this.state.end_date_id}
- />
-
+
+
+
);
}
}
@@ -446,3 +279,6 @@ DatePickerRange.defaultProps = {
persisted_props: ['start_date', 'end_date'],
persistence_type: 'local',
};
+
+export const propTypes = DatePickerRange.propTypes;
+export const defaultProps = DatePickerRange.defaultProps;
diff --git a/src/components/DatePickerSingle.react.js b/src/components/DatePickerSingle.react.js
index f15f1fbae..df7a41bc2 100644
--- a/src/components/DatePickerSingle.react.js
+++ b/src/components/DatePickerSingle.react.js
@@ -1,11 +1,8 @@
-import 'react-dates/initialize';
-
-import {SingleDatePicker} from 'react-dates';
-import moment from 'moment';
import PropTypes from 'prop-types';
-import React, {Component} from 'react';
+import React, {Component, lazy, Suspense} from 'react';
+import LazyLoader from '../utils/LazyLoader';
-import convertToMoment from '../utils/convertToMoment';
+const RealDateSingleRange = lazy(LazyLoader.datePickerSingle);
/**
* DatePickerSingle is a tailor made component designed for selecting
@@ -18,108 +15,12 @@ import convertToMoment from '../utils/convertToMoment';
* This component is based off of Airbnb's react-dates react component
* which can be found here: https://github.com/airbnb/react-dates
*/
-
export default class DatePickerSingle extends Component {
- constructor() {
- super();
- this.isOutsideRange = this.isOutsideRange.bind(this);
- this.onDateChange = this.onDateChange.bind(this);
- this.state = {focused: false};
- }
-
- isOutsideRange(date) {
- const {max_date_allowed, min_date_allowed} = convertToMoment(
- this.props,
- ['max_date_allowed', 'min_date_allowed']
- );
-
- return (
- (min_date_allowed && date.isBefore(min_date_allowed)) ||
- (max_date_allowed && date.isAfter(max_date_allowed))
- );
- }
-
- onDateChange(date) {
- const {setProps} = this.props;
- const payload = {date: date ? date.format('YYYY-MM-DD') : null};
- setProps(payload);
- }
-
render() {
- const {focused} = this.state;
-
- const {
- calendar_orientation,
- clearable,
- day_size,
- disabled,
- display_format,
- first_day_of_week,
- is_RTL,
- month_format,
- number_of_months_shown,
- placeholder,
- reopen_calendar_on_clear,
- show_outside_days,
- stay_open_on_select,
- with_full_screen_portal,
- with_portal,
- loading_state,
- id,
- style,
- className,
- } = this.props;
-
- const {date, initial_visible_month} = convertToMoment(this.props, [
- 'date',
- 'initial_visible_month',
- ]);
-
- const verticalFlag = calendar_orientation !== 'vertical';
-
- const DatePickerWrapperStyles = {
- position: 'relative',
- display: 'inline-block',
- ...style,
- };
-
return (
-
- this.setState({focused})}
- initialVisibleMonth={() =>
- date || initial_visible_month || moment()
- }
- isOutsideRange={this.isOutsideRange}
- numberOfMonths={number_of_months_shown}
- withPortal={with_portal && verticalFlag}
- withFullScreenPortal={
- with_full_screen_portal && verticalFlag
- }
- firstDayOfWeek={first_day_of_week}
- enableOutsideDays={show_outside_days}
- monthFormat={month_format}
- displayFormat={display_format}
- placeholder={placeholder}
- showClearDate={clearable}
- disabled={disabled}
- keepOpenOnDateSelect={stay_open_on_select}
- reopenPickerOnClearDate={reopen_calendar_on_clear}
- isRTL={is_RTL}
- orientation={calendar_orientation}
- daySize={day_size}
- />
-
+
+
+
);
}
}
@@ -335,3 +236,6 @@ DatePickerSingle.defaultProps = {
persisted_props: ['date'],
persistence_type: 'local',
};
+
+export const propTypes = DatePickerSingle.propTypes;
+export const defaultProps = DatePickerSingle.defaultProps;
diff --git a/src/components/Dropdown.react.js b/src/components/Dropdown.react.js
index f3f7195d3..24669b335 100644
--- a/src/components/Dropdown.react.js
+++ b/src/components/Dropdown.react.js
@@ -1,24 +1,8 @@
import PropTypes from 'prop-types';
-import {isNil, pluck, omit, type} from 'ramda';
-import React, {Component} from 'react';
-import ReactDropdown from 'react-virtualized-select';
-import createFilterOptions from 'react-select-fast-filter-options';
-import './css/react-virtualized-select@3.1.0.css';
-import './css/react-virtualized@9.9.0.css';
+import React, {Component, lazy, Suspense} from 'react';
+import LazyLoader from '../utils/LazyLoader';
-// Custom tokenizer, see https://github.com/bvaughn/js-search/issues/43
-// Split on spaces
-const REGEX = /\s+/;
-const TOKENIZER = {
- tokenize(text) {
- return text.split(REGEX).filter(
- // Filter empty tokens
- text => text
- );
- },
-};
-
-const DELIMETER = ',';
+const RealDropdown = lazy(LazyLoader.dropdown);
/**
* Dropdown is an interactive dropdown element for selecting one or more
@@ -31,79 +15,11 @@ const DELIMETER = ',';
* which have the benefit of showing the users all of the items at once.
*/
export default class Dropdown extends Component {
- constructor(props) {
- super(props);
- this.state = {
- filterOptions: createFilterOptions({
- options: props.options,
- tokenizer: TOKENIZER,
- }),
- };
- }
-
- componentWillReceiveProps(newProps) {
- if (newProps.options !== this.props.options) {
- this.setState({
- filterOptions: createFilterOptions({
- options: newProps.options,
- tokenizer: TOKENIZER,
- }),
- });
- }
- }
-
render() {
- const {
- id,
- multi,
- options,
- setProps,
- style,
- loading_state,
- value,
- } = this.props;
- const {filterOptions} = this.state;
- let selectedValue;
- if (type(value) === 'array') {
- selectedValue = value.join(DELIMETER);
- } else {
- selectedValue = value;
- }
return (
-
- {
- if (multi) {
- let value;
- if (isNil(selectedOption)) {
- value = [];
- } else {
- value = pluck('value', selectedOption);
- }
- setProps({value});
- } else {
- let value;
- if (isNil(selectedOption)) {
- value = null;
- } else {
- value = selectedOption.value;
- }
- setProps({value});
- }
- }}
- onInputChange={search_value => setProps({search_value})}
- {...omit(['setProps', 'value'], this.props)}
- />
-
+
+
+
);
}
}
@@ -268,3 +184,6 @@ Dropdown.defaultProps = {
persisted_props: ['value'],
persistence_type: 'local',
};
+
+export const propTypes = Dropdown.propTypes;
+export const defaultProps = Dropdown.defaultProps;
diff --git a/src/components/Graph.react.js b/src/components/Graph.react.js
index f87ae39d1..793d4571e 100644
--- a/src/components/Graph.react.js
+++ b/src/components/Graph.react.js
@@ -2,21 +2,7 @@ import React, {Component, PureComponent, Suspense} from 'react';
import PropTypes from 'prop-types';
import {asyncDecorator} from '@plotly/dash-component-plugins';
-
-const loader = {
- plotly: () =>
- Promise.resolve(
- window.Plotly ||
- import(/* webpackChunkName: "plotlyjs" */ 'plotly.js').then(
- ({default: Plotly}) => {
- window.Plotly = Plotly;
- return Plotly;
- }
- )
- ),
- graph: () =>
- import(/* webpackChunkName: "graph" */ '../fragments/Graph.react'),
-};
+import LazyLoader from '../utils/LazyLoader';
const EMPTY_EXTEND_DATA = [];
@@ -101,7 +87,7 @@ class PlotlyGraph extends Component {
}
const RealPlotlyGraph = asyncDecorator(PlotlyGraph, () =>
- loader.plotly().then(() => loader.graph())
+ LazyLoader.plotly().then(LazyLoader.graph)
);
class ControlledPlotlyGraph extends PureComponent {
@@ -114,7 +100,7 @@ class ControlledPlotlyGraph extends PureComponent {
}
}
-export const graphPropTypes = {
+PlotlyGraph.propTypes = {
/**
* The ID of this component, used to identify dash components
* in callbacks. The ID needs to be unique across all of the
@@ -492,7 +478,7 @@ export const graphPropTypes = {
}),
};
-export const graphDefaultProps = {
+PlotlyGraph.defaultProps = {
clickData: null,
clickAnnotationData: null,
hoverData: null,
@@ -519,8 +505,7 @@ export const graphDefaultProps = {
config: {},
};
-PlotlyGraph.propTypes = graphPropTypes;
-
-PlotlyGraph.defaultProps = graphDefaultProps;
+export const graphPropTypes = PlotlyGraph.propTypes;
+export const graphDefaultProps = PlotlyGraph.defaultProps;
export default PlotlyGraph;
diff --git a/src/components/Markdown.react.js b/src/components/Markdown.react.js
index 3b98d720f..95552addb 100644
--- a/src/components/Markdown.react.js
+++ b/src/components/Markdown.react.js
@@ -1,122 +1,15 @@
-import React, {Component} from 'react';
import PropTypes from 'prop-types';
-import {type} from 'ramda';
-import Markdown from 'react-markdown';
-import './css/highlight.css';
+import React, {Component, lazy, Suspense} from 'react';
+import LazyLoader from '../utils/LazyLoader';
-// eslint-disable-next-line valid-jsdoc
-/**
- * A component that renders Markdown text as specified by the
- * GitHub Markdown spec. These component uses
- * [react-markdown](https://rexxars.github.io/react-markdown/) under the hood.
- */
-class DashMarkdown extends Component {
- constructor(props) {
- super(props);
- this.highlightCode = this.highlightCode.bind(this);
- this.dedent = this.dedent.bind(this);
- }
-
- componentDidMount() {
- this.highlightCode();
- }
-
- componentDidUpdate() {
- this.highlightCode();
- }
-
- highlightCode() {
- if (!window.hljs) {
- // skip highlighting if highlight.js isn't found
- return;
- }
- if (this.mdContainer) {
- const nodes = this.mdContainer.querySelectorAll('pre code');
-
- for (let i = 0; i < nodes.length; i++) {
- window.hljs.highlightBlock(nodes[i]);
- }
- }
- }
-
- dedent(text) {
- const lines = text.split(/\r\n|\r|\n/);
- let commonPrefix = null;
- for (const line of lines) {
- const preMatch = line && line.match(/^\s*(?=\S)/);
- if (preMatch) {
- const prefix = preMatch[0];
- if (commonPrefix !== null) {
- for (let i = 0; i < commonPrefix.length; i++) {
- // Like Python's textwrap.dedent, we'll remove both
- // space and tab characters, but only if they match
- if (prefix[i] !== commonPrefix[i]) {
- commonPrefix = commonPrefix.substr(0, i);
- break;
- }
- }
- } else {
- commonPrefix = prefix;
- }
-
- if (!commonPrefix) {
- break;
- }
- }
- }
-
- const commonLen = commonPrefix ? commonPrefix.length : 0;
- return lines
- .map(line => {
- return line.match(/\S/) ? line.substr(commonLen) : '';
- })
- .join('\n');
- }
+const RealDashMarkdown = lazy(LazyLoader.markdown);
+export default class DashMarkdown extends Component {
render() {
- const {
- id,
- style,
- className,
- highlight_config,
- loading_state,
- dangerously_allow_html,
- children,
- dedent,
- } = this.props;
-
- const textProp =
- type(children) === 'Array' ? children.join('\n') : children;
- const displayText =
- dedent && textProp ? this.dedent(textProp) : textProp;
-
return (
- {
- this.mdContainer = node;
- }}
- style={style}
- className={
- ((highlight_config && highlight_config.theme) ||
- className) &&
- `${className ? className : ''} ${
- highlight_config &&
- highlight_config.theme &&
- highlight_config.theme === 'dark'
- ? 'hljs-dark'
- : ''
- }`
- }
- data-dash-is-loading={
- (loading_state && loading_state.is_loading) || undefined
- }
- >
-
-
+
+
+
);
}
}
@@ -197,4 +90,5 @@ DashMarkdown.defaultProps = {
dedent: true,
};
-export default DashMarkdown;
+export const propTypes = DashMarkdown.propTypes;
+export const defaultProps = DashMarkdown.defaultProps;
diff --git a/src/components/Upload.react.js b/src/components/Upload.react.js
index c5599c19a..66fb5326a 100644
--- a/src/components/Upload.react.js
+++ b/src/components/Upload.react.js
@@ -1,98 +1,18 @@
import PropTypes from 'prop-types';
-import React, {Component} from 'react';
-import Dropzone from 'react-dropzone';
+import React, {Component, lazy, Suspense} from 'react';
+import LazyLoader from '../utils/LazyLoader';
+
+const RealUpload = lazy(LazyLoader.upload);
/**
* Upload components allow your app to accept user-uploaded files via drag'n'drop
*/
export default class Upload extends Component {
- constructor() {
- super();
- this.onDrop = this.onDrop.bind(this);
- }
-
- onDrop(files) {
- const {multiple, setProps} = this.props;
- const newProps = {
- contents: [],
- filename: [],
- last_modified: [],
- };
- files.forEach(file => {
- const reader = new FileReader();
- reader.onload = () => {
- /*
- * I'm not sure if reader.onload will be executed in order.
- * For example, if the 1st file is larger than the 2nd one,
- * the 2nd file might load first.
- */
- newProps.contents.push(reader.result);
- newProps.filename.push(file.name);
- // eslint-disable-next-line no-magic-numbers
- newProps.last_modified.push(file.lastModified / 1000);
- if (newProps.contents.length === files.length) {
- if (multiple) {
- setProps(newProps);
- } else {
- setProps({
- contents: newProps.contents[0],
- filename: newProps.filename[0],
- last_modified: newProps.last_modified[0],
- });
- }
- }
- };
- reader.readAsDataURL(file);
- });
- }
-
render() {
- const {
- id,
- children,
- accept,
- disabled,
- disable_click,
- max_size,
- min_size,
- multiple,
- className,
- className_active,
- className_reject,
- className_disabled,
- style,
- style_active,
- style_reject,
- style_disabled,
- loading_state,
- } = this.props;
return (
-
-
- {children}
-
-
+
+
+
);
}
}
@@ -279,3 +199,6 @@ Upload.defaultProps = {
backgroundColor: '#eee',
},
};
+
+export const propTypes = Upload.propTypes;
+export const defaultProps = Upload.defaultProps;
diff --git a/src/fragments/DatePickerRange.react.js b/src/fragments/DatePickerRange.react.js
new file mode 100644
index 000000000..a53b9fb18
--- /dev/null
+++ b/src/fragments/DatePickerRange.react.js
@@ -0,0 +1,194 @@
+import 'react-dates/initialize';
+import {DateRangePicker} from 'react-dates';
+import React, {Component} from 'react';
+import uniqid from 'uniqid';
+
+import {propTypes, defaultProps} from '../components/DatePickerRange.react';
+import convertToMoment from '../utils/convertToMoment';
+
+export default class DatePickerRange extends Component {
+ constructor(props) {
+ super(props);
+ this.propsToState = this.propsToState.bind(this);
+ this.onDatesChange = this.onDatesChange.bind(this);
+ this.isOutsideRange = this.isOutsideRange.bind(this);
+ this.state = {
+ focused: false,
+ start_date_id: props.start_date_id || uniqid(),
+ end_date_id: props.end_date_id || uniqid(),
+ };
+ }
+
+ propsToState(newProps, force = false) {
+ const state = {};
+
+ if (force || newProps.start_date !== this.props.start_date) {
+ state.start_date = newProps.start_date;
+ }
+
+ if (force || newProps.end_date !== this.props.end_date) {
+ state.end_date = newProps.end_date;
+ }
+
+ if (Object.keys(state).length) {
+ this.setState(state);
+ }
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.propsToState(newProps);
+ }
+
+ componentWillMount() {
+ this.propsToState(this.props, true);
+ }
+
+ onDatesChange({startDate: start_date, endDate: end_date}) {
+ const {setProps, updatemode, clearable} = this.props;
+
+ const oldMomentDates = convertToMoment(this.state, [
+ 'start_date',
+ 'end_date',
+ ]);
+
+ if (start_date && !start_date.isSame(oldMomentDates.start_date)) {
+ if (updatemode === 'singledate') {
+ setProps({start_date: start_date.format('YYYY-MM-DD')});
+ } else {
+ this.setState({start_date: start_date.format('YYYY-MM-DD')});
+ }
+ }
+
+ if (end_date && !end_date.isSame(oldMomentDates.end_date)) {
+ if (updatemode === 'singledate') {
+ setProps({end_date: end_date.format('YYYY-MM-DD')});
+ } else if (updatemode === 'bothdates') {
+ setProps({
+ start_date: this.state.start_date,
+ end_date: end_date.format('YYYY-MM-DD'),
+ });
+ }
+ }
+
+ if (
+ clearable &&
+ !start_date &&
+ !end_date &&
+ (oldMomentDates.start_date !== start_date ||
+ oldMomentDates.end_date !== end_date)
+ ) {
+ setProps({start_date: null, end_date: null});
+ }
+ }
+
+ isOutsideRange(date) {
+ const {min_date_allowed, max_date_allowed} = this.props;
+
+ return (
+ (min_date_allowed && date.isBefore(min_date_allowed)) ||
+ (max_date_allowed && date.isAfter(max_date_allowed))
+ );
+ }
+
+ render() {
+ const {focusedInput} = this.state;
+
+ const {
+ calendar_orientation,
+ clearable,
+ day_size,
+ disabled,
+ display_format,
+ end_date_placeholder_text,
+ first_day_of_week,
+ is_RTL,
+ minimum_nights,
+ month_format,
+ number_of_months_shown,
+ reopen_calendar_on_clear,
+ show_outside_days,
+ start_date_placeholder_text,
+ stay_open_on_select,
+ with_full_screen_portal,
+ with_portal,
+ loading_state,
+ id,
+ style,
+ className,
+ start_date_id,
+ end_date_id,
+ } = this.props;
+
+ const {initial_visible_month} = convertToMoment(this.props, [
+ 'initial_visible_month',
+ ]);
+
+ const {start_date, end_date} = convertToMoment(this.state, [
+ 'start_date',
+ 'end_date',
+ ]);
+ const verticalFlag = calendar_orientation !== 'vertical';
+
+ const DatePickerWrapperStyles = {
+ position: 'relative',
+ display: 'inline-block',
+ ...style,
+ };
+
+ return (
+
+ {
+ if (initial_visible_month) {
+ return initial_visible_month;
+ } else if (end_date && focusedInput === 'endDate') {
+ return end_date;
+ } else if (start_date && focusedInput === 'startDate') {
+ return start_date;
+ }
+ return start_date;
+ }}
+ isOutsideRange={this.isOutsideRange}
+ isRTL={is_RTL}
+ keepOpenOnDateSelect={stay_open_on_select}
+ minimumNights={minimum_nights}
+ monthFormat={month_format}
+ numberOfMonths={number_of_months_shown}
+ onDatesChange={this.onDatesChange}
+ onFocusChange={focusedInput =>
+ this.setState({focusedInput})
+ }
+ orientation={calendar_orientation}
+ reopenPickerOnClearDates={reopen_calendar_on_clear}
+ showClearDates={clearable}
+ startDate={start_date}
+ startDatePlaceholderText={start_date_placeholder_text}
+ withFullScreenPortal={
+ with_full_screen_portal && verticalFlag
+ }
+ withPortal={with_portal && verticalFlag}
+ startDateId={start_date_id || this.state.start_date_id}
+ endDateId={end_date_id || this.state.end_date_id}
+ />
+
+ );
+ }
+}
+
+DatePickerRange.propTypes = propTypes;
+DatePickerRange.defaultProps = defaultProps;
diff --git a/src/fragments/DatePickerSingle.react.js b/src/fragments/DatePickerSingle.react.js
new file mode 100644
index 000000000..841e40565
--- /dev/null
+++ b/src/fragments/DatePickerSingle.react.js
@@ -0,0 +1,116 @@
+import 'react-dates/initialize';
+
+import {SingleDatePicker} from 'react-dates';
+import moment from 'moment';
+import React, {Component} from 'react';
+
+import {propTypes, defaultProps} from '../components/DatePickerRange.react';
+import convertToMoment from '../utils/convertToMoment';
+
+export default class DatePickerSingle extends Component {
+ constructor() {
+ super();
+ this.isOutsideRange = this.isOutsideRange.bind(this);
+ this.onDateChange = this.onDateChange.bind(this);
+ this.state = {focused: false};
+ }
+
+ isOutsideRange(date) {
+ const {max_date_allowed, min_date_allowed} = convertToMoment(
+ this.props,
+ ['max_date_allowed', 'min_date_allowed']
+ );
+
+ return (
+ (min_date_allowed && date.isBefore(min_date_allowed)) ||
+ (max_date_allowed && date.isAfter(max_date_allowed))
+ );
+ }
+
+ onDateChange(date) {
+ const {setProps} = this.props;
+ const payload = {date: date ? date.format('YYYY-MM-DD') : null};
+ setProps(payload);
+ }
+
+ render() {
+ const {focused} = this.state;
+
+ const {
+ calendar_orientation,
+ clearable,
+ day_size,
+ disabled,
+ display_format,
+ first_day_of_week,
+ is_RTL,
+ month_format,
+ number_of_months_shown,
+ placeholder,
+ reopen_calendar_on_clear,
+ show_outside_days,
+ stay_open_on_select,
+ with_full_screen_portal,
+ with_portal,
+ loading_state,
+ id,
+ style,
+ className,
+ } = this.props;
+
+ const {date, initial_visible_month} = convertToMoment(this.props, [
+ 'date',
+ 'initial_visible_month',
+ ]);
+
+ const verticalFlag = calendar_orientation !== 'vertical';
+
+ const DatePickerWrapperStyles = {
+ position: 'relative',
+ display: 'inline-block',
+ ...style,
+ };
+
+ return (
+
+ this.setState({focused})}
+ initialVisibleMonth={() =>
+ date || initial_visible_month || moment()
+ }
+ isOutsideRange={this.isOutsideRange}
+ numberOfMonths={number_of_months_shown}
+ withPortal={with_portal && verticalFlag}
+ withFullScreenPortal={
+ with_full_screen_portal && verticalFlag
+ }
+ firstDayOfWeek={first_day_of_week}
+ enableOutsideDays={show_outside_days}
+ monthFormat={month_format}
+ displayFormat={display_format}
+ placeholder={placeholder}
+ showClearDate={clearable}
+ disabled={disabled}
+ keepOpenOnDateSelect={stay_open_on_select}
+ reopenPickerOnClearDate={reopen_calendar_on_clear}
+ isRTL={is_RTL}
+ orientation={calendar_orientation}
+ daySize={day_size}
+ />
+
+ );
+ }
+}
+
+DatePickerSingle.propTypes = propTypes;
+DatePickerSingle.defaultProps = defaultProps;
diff --git a/src/fragments/Dropdown.react.js b/src/fragments/Dropdown.react.js
new file mode 100644
index 000000000..a4a94844b
--- /dev/null
+++ b/src/fragments/Dropdown.react.js
@@ -0,0 +1,103 @@
+import {isNil, pluck, omit, type} from 'ramda';
+import React, {Component} from 'react';
+import ReactDropdown from 'react-virtualized-select';
+import createFilterOptions from 'react-select-fast-filter-options';
+import '../components/css/react-virtualized-select@3.1.0.css';
+import '../components/css/react-virtualized@9.9.0.css';
+
+import {propTypes, defaultProps} from '../components/Dropdown.react';
+
+// Custom tokenizer, see https://github.com/bvaughn/js-search/issues/43
+// Split on spaces
+const REGEX = /\s+/;
+const TOKENIZER = {
+ tokenize(text) {
+ return text.split(REGEX).filter(
+ // Filter empty tokens
+ text => text
+ );
+ },
+};
+
+const DELIMETER = ',';
+
+export default class Dropdown extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ filterOptions: createFilterOptions({
+ options: props.options,
+ tokenizer: TOKENIZER,
+ }),
+ };
+ }
+
+ componentWillReceiveProps(newProps) {
+ if (newProps.options !== this.props.options) {
+ this.setState({
+ filterOptions: createFilterOptions({
+ options: newProps.options,
+ tokenizer: TOKENIZER,
+ }),
+ });
+ }
+ }
+
+ render() {
+ const {
+ id,
+ multi,
+ options,
+ setProps,
+ style,
+ loading_state,
+ value,
+ } = this.props;
+ const {filterOptions} = this.state;
+ let selectedValue;
+ if (type(value) === 'array') {
+ selectedValue = value.join(DELIMETER);
+ } else {
+ selectedValue = value;
+ }
+ return (
+
+ {
+ if (multi) {
+ let value;
+ if (isNil(selectedOption)) {
+ value = [];
+ } else {
+ value = pluck('value', selectedOption);
+ }
+ setProps({value});
+ } else {
+ let value;
+ if (isNil(selectedOption)) {
+ value = null;
+ } else {
+ value = selectedOption.value;
+ }
+ setProps({value});
+ }
+ }}
+ onInputChange={search_value => setProps({search_value})}
+ {...omit(['setProps', 'value'], this.props)}
+ />
+
+ );
+ }
+}
+
+Dropdown.propTypes = propTypes;
+Dropdown.defaultProps = defaultProps;
diff --git a/src/fragments/Markdown.react.js b/src/fragments/Markdown.react.js
new file mode 100644
index 000000000..544389ba3
--- /dev/null
+++ b/src/fragments/Markdown.react.js
@@ -0,0 +1,126 @@
+import React, {Component} from 'react';
+import {type} from 'ramda';
+import Markdown from 'react-markdown';
+
+import {propTypes, defaultProps} from '../components/Markdown.react';
+import '../components/css/highlight.css';
+
+// eslint-disable-next-line valid-jsdoc
+/**
+ * A component that renders Markdown text as specified by the
+ * GitHub Markdown spec. These component uses
+ * [react-markdown](https://rexxars.github.io/react-markdown/) under the hood.
+ */
+export default class DashMarkdown extends Component {
+ constructor(props) {
+ super(props);
+ this.highlightCode = this.highlightCode.bind(this);
+ this.dedent = this.dedent.bind(this);
+ }
+
+ componentDidMount() {
+ this.highlightCode();
+ }
+
+ componentDidUpdate() {
+ this.highlightCode();
+ }
+
+ highlightCode() {
+ if (!window.hljs) {
+ // skip highlighting if highlight.js isn't found
+ return;
+ }
+ if (this.mdContainer) {
+ const nodes = this.mdContainer.querySelectorAll('pre code');
+
+ for (let i = 0; i < nodes.length; i++) {
+ window.hljs.highlightBlock(nodes[i]);
+ }
+ }
+ }
+
+ dedent(text) {
+ const lines = text.split(/\r\n|\r|\n/);
+ let commonPrefix = null;
+ for (const line of lines) {
+ const preMatch = line && line.match(/^\s*(?=\S)/);
+ if (preMatch) {
+ const prefix = preMatch[0];
+ if (commonPrefix !== null) {
+ for (let i = 0; i < commonPrefix.length; i++) {
+ // Like Python's textwrap.dedent, we'll remove both
+ // space and tab characters, but only if they match
+ if (prefix[i] !== commonPrefix[i]) {
+ commonPrefix = commonPrefix.substr(0, i);
+ break;
+ }
+ }
+ } else {
+ commonPrefix = prefix;
+ }
+
+ if (!commonPrefix) {
+ break;
+ }
+ }
+ }
+
+ const commonLen = commonPrefix ? commonPrefix.length : 0;
+ return lines
+ .map(line => {
+ return line.match(/\S/) ? line.substr(commonLen) : '';
+ })
+ .join('\n');
+ }
+
+ render() {
+ const {
+ id,
+ style,
+ className,
+ highlight_config,
+ loading_state,
+ dangerously_allow_html,
+ children,
+ dedent,
+ } = this.props;
+
+ const textProp =
+ type(children) === 'Array' ? children.join('\n') : children;
+ const displayText =
+ dedent && textProp ? this.dedent(textProp) : textProp;
+
+ return (
+ {
+ this.mdContainer = node;
+ }}
+ style={style}
+ className={
+ ((highlight_config && highlight_config.theme) ||
+ className) &&
+ `${className ? className : ''} ${
+ highlight_config &&
+ highlight_config.theme &&
+ highlight_config.theme === 'dark'
+ ? 'hljs-dark'
+ : ''
+ }`
+ }
+ data-dash-is-loading={
+ (loading_state && loading_state.is_loading) || undefined
+ }
+ >
+
+
+ );
+ }
+}
+
+DashMarkdown.propTypes = propTypes;
+DashMarkdown.defaultProps = defaultProps;
diff --git a/src/fragments/Upload.react.js b/src/fragments/Upload.react.js
new file mode 100644
index 000000000..286516449
--- /dev/null
+++ b/src/fragments/Upload.react.js
@@ -0,0 +1,99 @@
+import React, {Component} from 'react';
+import Dropzone from 'react-dropzone';
+
+import {propTypes, defaultProps} from '../components/Upload.react';
+
+export default class Upload extends Component {
+ constructor() {
+ super();
+ this.onDrop = this.onDrop.bind(this);
+ }
+
+ onDrop(files) {
+ const {multiple, setProps} = this.props;
+ const newProps = {
+ contents: [],
+ filename: [],
+ last_modified: [],
+ };
+ files.forEach(file => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ /*
+ * I'm not sure if reader.onload will be executed in order.
+ * For example, if the 1st file is larger than the 2nd one,
+ * the 2nd file might load first.
+ */
+ newProps.contents.push(reader.result);
+ newProps.filename.push(file.name);
+ // eslint-disable-next-line no-magic-numbers
+ newProps.last_modified.push(file.lastModified / 1000);
+ if (newProps.contents.length === files.length) {
+ if (multiple) {
+ setProps(newProps);
+ } else {
+ setProps({
+ contents: newProps.contents[0],
+ filename: newProps.filename[0],
+ last_modified: newProps.last_modified[0],
+ });
+ }
+ }
+ };
+ reader.readAsDataURL(file);
+ });
+ }
+
+ render() {
+ const {
+ id,
+ children,
+ accept,
+ disabled,
+ disable_click,
+ max_size,
+ min_size,
+ multiple,
+ className,
+ className_active,
+ className_reject,
+ className_disabled,
+ style,
+ style_active,
+ style_reject,
+ style_disabled,
+ loading_state,
+ } = this.props;
+ return (
+
+
+ {children}
+
+
+ );
+ }
+}
+
+Upload.propTypes = propTypes;
+Upload.defaultProps = defaultProps;
diff --git a/src/utils/LazyLoader.js b/src/utils/LazyLoader.js
new file mode 100644
index 000000000..485a5dc75
--- /dev/null
+++ b/src/utils/LazyLoader.js
@@ -0,0 +1,24 @@
+export default {
+ datePickerRange: () =>
+ import(/* webpackChunkName: "datepicker" */ '../fragments/DatePickerRange.react'),
+ datePickerSingle: () =>
+ import(/* webpackChunkName: "datepicker" */ '../fragments/DatePickerSingle.react'),
+ dropdown: () =>
+ import(/* webpackChunkName: "dropdown" */ '../fragments/Dropdown.react'),
+ graph: () =>
+ import(/* webpackChunkName: "graph" */ '../fragments/Graph.react'),
+ markdown: () =>
+ import(/* webpackChunkName: "markdown" */ '../fragments/Markdown.react'),
+ plotly: () =>
+ Promise.resolve(
+ window.Plotly ||
+ import(/* webpackChunkName: "plotlyjs" */ 'plotly.js').then(
+ ({default: Plotly}) => {
+ window.Plotly = Plotly;
+ return Plotly;
+ }
+ )
+ ),
+ upload: () =>
+ import(/* webpackChunkName: "upload" */ '../fragments/Upload.react'),
+};
diff --git a/tests/unit/DatePickerRange.test.js b/tests/unit/DatePickerRange.test.js
index 760f18f52..3d848ba51 100644
--- a/tests/unit/DatePickerRange.test.js
+++ b/tests/unit/DatePickerRange.test.js
@@ -1,4 +1,4 @@
-import DatePickerRange from '../../src/components/DatePickerRange.react';
+import DatePickerRange from '../../src/fragments/DatePickerRange.react';
import {merge} from 'ramda';
import React from 'react';
import {mount, render} from 'enzyme';
diff --git a/tests/unit/DatePickerSingle.test.js b/tests/unit/DatePickerSingle.test.js
index 1b3681ace..5b4fbb464 100644
--- a/tests/unit/DatePickerSingle.test.js
+++ b/tests/unit/DatePickerSingle.test.js
@@ -1,4 +1,4 @@
-import DatePickerSingle from '../../src/components/DatePickerSingle.react';
+import DatePickerSingle from '../../src/fragments/DatePickerSingle.react';
import {merge} from 'ramda';
import React from 'react';
import {mount, render} from 'enzyme';
diff --git a/tests/unit/Dropdown.test.js b/tests/unit/Dropdown.test.js
index 251a988a7..c10fd24d3 100644
--- a/tests/unit/Dropdown.test.js
+++ b/tests/unit/Dropdown.test.js
@@ -1,4 +1,4 @@
-import Dropdown from '../../src/components/Dropdown.react.js';
+import Dropdown from '../../src/fragments/Dropdown.react.js';
import React from 'react';
import {mount, render} from 'enzyme';
import {validate} from './utils';
diff --git a/tests/unit/Markdown.test.js b/tests/unit/Markdown.test.js
index 454c56ce9..c6f7ad17d 100644
--- a/tests/unit/Markdown.test.js
+++ b/tests/unit/Markdown.test.js
@@ -1,4 +1,4 @@
-import Markdown from '../../src/components/Markdown.react.js';
+import Markdown from '../../src/fragments/Markdown.react.js';
import React from 'react';
import {shallow, render} from 'enzyme';