diff options
Diffstat (limited to 'app/javascript/flavours/glitch/features/composer/options')
-rw-r--r-- | app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js | 138 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js (renamed from app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js) | 17 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/features/composer/options/dropdown/index.js | 221 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/features/composer/options/index.js | 25 |
4 files changed, 259 insertions, 142 deletions
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 ( + <Motion + defaultStyle={{ + opacity: 0, + scaleX: 0.85, + scaleY: 0.75, + }} + style={{ + opacity: springMotion, + scaleX: springMotion, + scaleY: springMotion, + }} + > + {({ opacity, scaleX, scaleY }) => ( + <div + className='composer--options--dropdown--content' + ref={handleRef} + style={{ + ...style, + opacity: opacity, + transform: `scale(${scaleX}, ${scaleY})`, + }} + > + {items.map( + ({ + name, + ...rest + }) => ( + <ComposerOptionsDropdownContentItem + active={name === value} + key={name} + name={name} + onChange={onChange} + onClose={onClose} + options={rest} + /> + ) + )} + </div> + )} + </Motion> + ); + } + +} + +// 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/item/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js index e9047dc50..605c945bd 100644 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js @@ -14,7 +14,7 @@ import { assignHandlers } from 'flavours/glitch/util/react_helpers'; const handlers = { // This function activates the dropdown item. - activate (e) { + handleActivate (e) { const { name, onChange, @@ -35,11 +35,10 @@ const handlers = { onChange(name); } }, - }; // The component. -export default class ComposerOptionsDropdownItem extends React.PureComponent { +export default class ComposerOptionsDropdownContentItem extends React.PureComponent { // Constructor. constructor (props) { @@ -49,7 +48,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { // Rendering. render () { - const { activate } = this.handlers; + const { handleActivate } = this.handlers; const { active, options: { @@ -59,7 +58,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { text, }, } = this.props; - const computedClass = classNames('composer--options--dropdown_item', { + const computedClass = classNames('composer--options--dropdown--content--item', { active, lengthy: meta, 'toggled-off': !on && on !== null && typeof on !== 'undefined', @@ -71,8 +70,8 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { return ( <div className={computedClass} - onClick={activate} - onKeyDown={activate} + onClick={handleActivate} + onKeyDown={handleActivate} role='button' tabIndex='0' > @@ -85,7 +84,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { return ( <Toggle checked={on} - onChange={activate} + onChange={handleActivate} /> ); case !!icon: @@ -113,7 +112,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { }; // Props. -ComposerOptionsDropdownItem.propTypes = { +ComposerOptionsDropdownContentItem.propTypes = { active: PropTypes.bool, name: PropTypes.string, onChange: PropTypes.func, 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 ( <div className={computedClass} - onKeyDown={keyDown} - ref={ref} + onKeyDown={handleKeyDown} > <IconButton active={open || active} className='value' disabled={disabled} icon={icon} - onClick={toggle} + onClick={handleToggle} size={18} style={{ height: null, @@ -178,49 +187,17 @@ export default class ComposerOptionsDropdown extends React.PureComponent { title={title} /> <Overlay + containerPadding={20} placement='bottom' show={open} target={this} > - <Motion - defaultStyle={{ - opacity: 0, - scaleX: 0.85, - scaleY: 0.75, - }} - style={{ - opacity: springMotion, - scaleX: springMotion, - scaleY: springMotion, - }} - > - {({ opacity, scaleX, scaleY }) => ( - <div - className='composer--options--dropdown__dropdown' - ref={this.setRef} - style={{ - opacity: opacity, - transform: `scale(${scaleX}, ${scaleY})`, - }} - > - {items.map( - ({ - name, - ...rest - }) => ( - <ComposerOptionsDropdownItem - active={name === value} - key={name} - name={name} - onChange={onChange} - onClose={close} - options={rest} - /> - ) - )} - </div> - )} - </Motion> + <ComposerOptionsDropdownContent + items={items} + onChange={onChange} + onClose={handleClose} + value={value} + /> </Overlay> </div> ); 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: <FormattedMessage {...messages.doodle} />, }, ]} - onChange={clickAttach} + onChange={handleClickAttach} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={messages.attach} + title={intl.formatMessage(messages.attach)} /> <Motion defaultStyle={{ scale: 0.87 }} @@ -279,6 +280,7 @@ export default class ComposerOptions extends React.PureComponent { active={spoiler} ariaControls='glitch.composer.spoiler.input' label='CW' + onClick={onToggleSpoiler} title={intl.formatMessage(messages.spoiler)} /> <Dropdown @@ -318,9 +320,10 @@ ComposerOptions.propTypes = { onModalClose: PropTypes.func, onModalOpen: PropTypes.func, onToggleAdvancedOption: PropTypes.func, + onToggleSpoiler: PropTypes.func, onUpload: PropTypes.func, privacy: PropTypes.string, - resetFileKey: PropTypes.string, + resetFileKey: PropTypes.number, sensitive: PropTypes.bool, spoiler: PropTypes.bool, }; |