From 42f50049ff29ccdc484c22f8a5a19cda2dd03a5b Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 3 Jan 2018 12:36:21 -0800 Subject: WIP Refactor; 1000 tiny edits --- .../composer/options/dropdown/content/index.js | 138 +++++++++++++ .../options/dropdown/content/item/index.js | 126 ++++++++++++ .../features/composer/options/dropdown/index.js | 221 +++++++++------------ .../composer/options/dropdown/item/index.js | 127 ------------ .../glitch/features/composer/options/index.js | 25 ++- 5 files changed, 377 insertions(+), 260 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js create mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js delete mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js (limited to 'app/javascript/flavours/glitch/features/composer/options') diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js new file mode 100644 index 000000000..28bdfc0db --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js @@ -0,0 +1,138 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import spring from 'react-motion/lib/spring'; + +// Components. +import ComposerOptionsDropdownContentItem from './item'; + +// Utils. +import { withPassive } from 'flavours/glitch/util/dom_helpers'; +import Motion from 'flavours/glitch/util/optional_motion'; +import { assignHandlers } from 'flavours/glitch/util/react_helpers'; + +// Handlers. +const handlers = { + + // When the document is clicked elsewhere, we close the dropdown. + handleDocumentClick ({ target }) { + const { node } = this; + const { onClose } = this.props; + if (onClose && node && !node.contains(target)) { + onClose(); + } + }, + + // Stores our node in `this.node`. + handleRef (node) { + this.node = node; + }, +}; + +// The spring to use with our motion. +const springMotion = spring(1, { + damping: 35, + stiffness: 400, +}); + +// The component. +export default class ComposerOptionsDropdownContent extends React.PureComponent { + + // Constructor. + constructor (props) { + super(props); + assignHandlers(this, handlers); + + // Instance variables. + this.node = null; + } + + // On mounting, we add our listeners. + componentDidMount () { + const { handleDocumentClick } = this.handlers; + document.addEventListener('click', handleDocumentClick, false); + document.addEventListener('touchend', handleDocumentClick, withPassive); + } + + // On unmounting, we remove our listeners. + componentWillUnmount () { + const { handleDocumentClick } = this.handlers; + document.removeEventListener('click', handleDocumentClick, false); + document.removeEventListener('touchend', handleDocumentClick, withPassive); + } + + // Rendering. + render () { + const { handleRef } = this.handlers; + const { + items, + onChange, + onClose, + style, + value, + } = this.props; + + // The result. + return ( + + {({ opacity, scaleX, scaleY }) => ( +
+ {items.map( + ({ + name, + ...rest + }) => ( + + ) + )} +
+ )} +
+ ); + } + +} + +// Props. +ComposerOptionsDropdownContent.propTypes = { + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.string, + meta: PropTypes.node, + name: PropTypes.string.isRequired, + on: PropTypes.bool, + text: PropTypes.node, + })).isRequired, + onChange: PropTypes.func, + onClose: PropTypes.func, + style: PropTypes.object, + value: PropTypes.string, +}; + +// Default props. +ComposerOptionsDropdownContent.defaultProps = { style: {} }; diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js new file mode 100644 index 000000000..605c945bd --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js @@ -0,0 +1,126 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Toggle from 'react-toggle'; + +// Components. +import Icon from 'flavours/glitch/components/icon'; + +// Utils. +import { assignHandlers } from 'flavours/glitch/util/react_helpers'; + +// Handlers. +const handlers = { + + // This function activates the dropdown item. + handleActivate (e) { + const { + name, + onChange, + onClose, + options: { on }, + } = this.props; + + // If the escape key was pressed, we close the dropdown. + if (e.key === 'Escape' && onClose) { + onClose(); + + // Otherwise, we both close the dropdown and change the value. + } else if (onChange && (!e.key || e.key === 'Enter')) { + e.preventDefault(); // Prevents change in focus on click + if ((on === null || typeof on === 'undefined') && onClose) { + onClose(); + } + onChange(name); + } + }, +}; + +// The component. +export default class ComposerOptionsDropdownContentItem extends React.PureComponent { + + // Constructor. + constructor (props) { + super(props); + assignHandlers(this, handlers); + } + + // Rendering. + render () { + const { handleActivate } = this.handlers; + const { + active, + options: { + icon, + meta, + on, + text, + }, + } = this.props; + const computedClass = classNames('composer--options--dropdown--content--item', { + active, + lengthy: meta, + 'toggled-off': !on && on !== null && typeof on !== 'undefined', + 'toggled-on': on, + 'with-icon': icon, + }); + + // The result. + return ( +
+ {function () { + + // We render a `` if we were provided an `on` + // property, and otherwise show an `` if available. + switch (true) { + case on !== null && typeof on !== 'undefined': + return ( + + ); + case !!icon: + return ( + + ); + default: + return null; + } + }()} + {meta ? ( +
+ {text} + {meta} +
+ ) :
{text}
} +
+ ); + } + +}; + +// Props. +ComposerOptionsDropdownContentItem.propTypes = { + active: PropTypes.bool, + name: PropTypes.string, + onChange: PropTypes.func, + onClose: PropTypes.func, + options: PropTypes.shape({ + icon: PropTypes.string, + meta: PropTypes.node, + on: PropTypes.bool, + text: PropTypes.node, + }), +}; diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js index daed4ec8a..d63d90a9f 100644 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js @@ -2,108 +2,120 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import spring from 'react-motion/lib/spring'; import Overlay from 'react-overlays/lib/Overlay'; // Components. import IconButton from 'flavours/glitch/components/icon_button'; -import ComposerOptionsDropdownItem from './item'; +import ComposerOptionsDropdownContent from './content'; // Utils. -import { withPassive } from 'flavours/glitch/util/dom_helpers'; import { isUserTouching } from 'flavours/glitch/util/is_mobile'; -import Motion from 'flavours/glitch/util/optional_motion'; import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -// We'll use this to define our various transitions. -const springMotion = spring(1, { - damping: 35, - stiffness: 400, -}); - // Handlers. const handlers = { // Closes the dropdown. - close () { + handleClose () { this.setState({ open: false }); }, - // When the document is clicked elsewhere, we close the dropdown. - documentClick ({ target }) { - const { node } = this; - const { onClose } = this.props; - if (onClose && node && !node.contains(target)) { - onClose(); - } - }, - // The enter key toggles the dropdown's open state, and the escape // key closes it. - keyDown ({ key }) { + handleKeyDown ({ key }) { const { - close, - toggle, + handleClose, + handleToggle, } = this.handlers; switch (key) { case 'Enter': - toggle(); + handleToggle(); break; case 'Escape': - close(); + handleClose(); break; } }, - // Toggles opening and closing the dropdown. - toggle () { + // Creates an action modal object. + handleMakeModal () { + const component = this; const { items, onChange, - onModalClose, onModalOpen, + onModalClose, value, } = this.props; + + // Required props. + if (!(onChange && onModalOpen && onModalClose && items)) { + return null; + } + + // The object. + return { + actions: items.map( + ({ + name, + ...rest + }) => ({ + ...rest, + active: value && name === value, + name, + onClick (e) { + e.preventDefault(); // Prevents focus from changing + onModalClose(); + onChange(name); + }, + onPassiveClick (e) { + e.preventDefault(); // Prevents focus from changing + onChange(name); + component.setState({ needsModalUpdate: true }); + }, + }) + ), + }; + }, + + // Toggles opening and closing the dropdown. + handleToggle () { + const { handleMakeModal } = this.handlers; + const { onModalOpen } = this.props; const { open } = this.state; // If this is a touch device, we open a modal instead of the // dropdown. - if (onModalClose && isUserTouching()) { - if (open) { - onModalClose(); - } else if (onChange && onModalOpen) { - onModalOpen({ - actions: items.map( - ({ - name, - ...rest - }) => ({ - ...rest, - active: value && name === value, - name, - onClick (e) { - e.preventDefault(); // Prevents focus from changing - onModalClose(); - onChange(name); - }, - onPassiveClick (e) { - e.preventDefault(); // Prevents focus from changing - onChange(name); - }, - }) - ), - }); + if (isUserTouching()) { + + // This gets the modal to open. + const modal = handleMakeModal(); + + // If we can, we then open the modal. + if (modal && onModalOpen) { + onModalOpen(modal); + return; } + } // Otherwise, we just set our state to open. - } else { - this.setState({ open: !open }); - } + this.setState({ open: !open }); }, - // Stores our node in `this.node`. - ref (node) { - this.node = node; + // If our modal is open and our props update, we need to also update + // the modal. + handleUpdate () { + const { handleMakeModal } = this.handlers; + const { onModalOpen } = this.props; + const { needsModalUpdate } = this.state; + + // Gets our modal object. + const modal = handleMakeModal(); + + // Reopens the modal with the new object. + if (needsModalUpdate && modal && onModalOpen) { + onModalOpen(modal); + } }, }; @@ -114,33 +126,31 @@ export default class ComposerOptionsDropdown extends React.PureComponent { constructor (props) { super(props); assignHandlers(this, handlers); - this.state = { open: false }; - - // Instance variables. - this.node = null; + this.state = { + needsModalUpdate: false, + open: false, + }; } - // On mounting, we add our listeners. - componentDidMount () { - const { documentClick } = this.handlers; - document.addEventListener('click', documentClick, false); - document.addEventListener('touchend', documentClick, withPassive); - } - - // On unmounting, we remove our listeners. - componentWillUnmount () { - const { documentClick } = this.handlers; - document.removeEventListener('click', documentClick, false); - document.removeEventListener('touchend', documentClick, withPassive); + // Updates our modal as necessary. + componentDidUpdate (prevProps) { + const { handleUpdate } = this.handlers; + const { items } = this.props; + const { needsModalUpdate } = this.state; + if (needsModalUpdate && items.find( + (item, i) => item.on !== prevProps.items[i].on + )) { + handleUpdate(); + this.setState({ needsModalUpdate: false }); + } } // Rendering. render () { const { - close, - keyDown, - ref, - toggle, + handleClose, + handleKeyDown, + handleToggle, } = this.handlers; const { active, @@ -154,22 +164,21 @@ export default class ComposerOptionsDropdown extends React.PureComponent { const { open } = this.state; const computedClass = classNames('composer--options--dropdown', { active, - open: open || active, + open, }); // The result. return (
- - {({ opacity, scaleX, scaleY }) => ( -
- {items.map( - ({ - name, - ...rest - }) => ( - - ) - )} -
- )} -
+
); diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js deleted file mode 100644 index e9047dc50..000000000 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js +++ /dev/null @@ -1,127 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Toggle from 'react-toggle'; - -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; - -// Handlers. -const handlers = { - - // This function activates the dropdown item. - activate (e) { - const { - name, - onChange, - onClose, - options: { on }, - } = this.props; - - // If the escape key was pressed, we close the dropdown. - if (e.key === 'Escape' && onClose) { - onClose(); - - // Otherwise, we both close the dropdown and change the value. - } else if (onChange && (!e.key || e.key === 'Enter')) { - e.preventDefault(); // Prevents change in focus on click - if ((on === null || typeof on === 'undefined') && onClose) { - onClose(); - } - onChange(name); - } - }, - -}; - -// The component. -export default class ComposerOptionsDropdownItem extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - } - - // Rendering. - render () { - const { activate } = this.handlers; - const { - active, - options: { - icon, - meta, - on, - text, - }, - } = this.props; - const computedClass = classNames('composer--options--dropdown_item', { - active, - lengthy: meta, - 'toggled-off': !on && on !== null && typeof on !== 'undefined', - 'toggled-on': on, - 'with-icon': icon, - }); - - // The result. - return ( -
- {function () { - - // We render a `` if we were provided an `on` - // property, and otherwise show an `` if available. - switch (true) { - case on !== null && typeof on !== 'undefined': - return ( - - ); - case !!icon: - return ( - - ); - default: - return null; - } - }()} - {meta ? ( -
- {text} - {meta} -
- ) :
{text}
} -
- ); - } - -}; - -// Props. -ComposerOptionsDropdownItem.propTypes = { - active: PropTypes.bool, - name: PropTypes.string, - onChange: PropTypes.func, - onClose: PropTypes.func, - options: PropTypes.shape({ - icon: PropTypes.string, - meta: PropTypes.node, - on: PropTypes.bool, - text: PropTypes.node, - }), -}; diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js index ea998a421..e805372ab 100644 --- a/app/javascript/flavours/glitch/features/composer/options/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/index.js @@ -95,7 +95,7 @@ const messages = defineMessages({ const handlers = { // Handles file selection. - changeFiles ({ target: { files } }) { + handleChangeFiles ({ target: { files } }) { const { onUpload } = this.props; if (files.length && onUpload) { onUpload(files); @@ -103,7 +103,7 @@ const handlers = { }, // Handles attachment clicks. - clickAttach (name) { + handleClickAttach (name) { const { fileElement } = this; const { onDoodleOpen } = this.props; @@ -123,7 +123,7 @@ const handlers = { }, // Handles a ref to the file input. - refFileElement (fileElement) { + handleRefFileElement (fileElement) { this.fileElement = fileElement; }, }; @@ -143,9 +143,9 @@ export default class ComposerOptions extends React.PureComponent { // Rendering. render () { const { - changeFiles, - clickAttach, - refFileElement, + handleChangeFiles, + handleClickAttach, + handleRefFileElement, } = this.handlers; const { acceptContentTypes, @@ -159,6 +159,7 @@ export default class ComposerOptions extends React.PureComponent { onModalClose, onModalOpen, onToggleAdvancedOption, + onToggleSpoiler, privacy, resetFileKey, sensitive, @@ -201,8 +202,8 @@ export default class ComposerOptions extends React.PureComponent { accept={acceptContentTypes} disabled={disabled || full} key={resetFileKey} - onChange={changeFiles} - ref={refFileElement} + onChange={handleChangeFiles} + ref={handleRefFileElement} type='file' {...hiddenComponent} /> @@ -221,10 +222,10 @@ export default class ComposerOptions extends React.PureComponent { text: , }, ]} - onChange={clickAttach} + onChange={handleClickAttach} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={messages.attach} + title={intl.formatMessage(messages.attach)} />