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';