From 924ffe81d477a8cf890c8117efb94b908760bccc Mon Sep 17 00:00:00 2001 From: kibigo! Date: Sat, 23 Dec 2017 22:16:45 -0800 Subject: WIPgit status Refactor; ed. --- .../features/composer/options/dropdown/index.js | 243 ++++++++++++++++ .../composer/options/dropdown/item/index.js | 126 ++++++++ .../glitch/features/composer/options/index.js | 321 +++++++++++++++++++++ 3 files changed, 690 insertions(+) create mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/index.js create mode 100644 app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js create mode 100644 app/javascript/flavours/glitch/features/composer/options/index.js (limited to 'app/javascript/flavours/glitch/features/composer/options') diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js new file mode 100644 index 000000000..0f304bc88 --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js @@ -0,0 +1,243 @@ +// Package imports. +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'; + +// 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 () { + 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 }) { + const { + close, + toggle, + } = this.handlers; + switch (key) { + case 'Enter': + toggle(); + break; + case 'Escape': + close(); + break; + } + }, + + // Toggles opening and closing the dropdown. + toggle () { + const { + items, + onChange, + onModalClose, + onModalOpen, + value, + } = 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, + onClick (e) { + e.preventDefault(); // Prevents focus from changing + onModalClose(); + onChange(name); + }, + }) + ), + }); + } + + // Otherwise, we just set our state to open. + } else { + this.setState({ open: !open }); + } + }, + + // Stores our node in `this.node`. + ref (node) { + this.node = node; + }, +}; + +// The component. +export default class ComposerOptionsDropdown extends React.PureComponent { + + // Constructor. + constructor (props) { + super(props); + assignHandlers(this, handlers); + this.state = { open: false }; + + // Instance variables. + this.node = null; + } + + // 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); + } + + // Rendering. + render () { + const { + close, + keyDown, + ref, + toggle, + } = this.handlers; + const { + active, + disabled, + title, + icon, + items, + onChange, + value, + } = this.props; + const { open } = this.state; + const computedClass = classNames('composer--options--dropdown', { + active, + open: open || active, + }); + + // The result. + return ( +
+ + + + {({ opacity, scaleX, scaleY }) => ( +
+ {items.map( + ({ + name, + ...rest + }) => ( + + ) + )} +
+ )} +
+
+
+ ); + } + +} + +// Props. +ComposerOptionsDropdown.propTypes = { + active: PropTypes.bool, + disabled: PropTypes.bool, + icon: PropTypes.string, + 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, + onModalClose: PropTypes.func, + onModalOpen: PropTypes.func, + title: PropTypes.string, + value: PropTypes.string, +}; 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 new file mode 100644 index 000000000..ca4ee393e --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/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. + 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 new file mode 100644 index 000000000..ee633e865 --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/options/index.js @@ -0,0 +1,321 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import { + FormattedMessage, + defineMessages, +} from 'react-intl'; +import spring from 'react-motion/lib/spring'; + +// Components. +import IconButton from 'flavours/glitch/components/icon_button'; +import TextIconButton from 'flavours/glitch/components/text_icon_button'; +import Dropdown from './dropdown'; + +// Utils. +import Motion from 'flavours/glitch/util/optional_motion'; +import { + assignHandlers, + hiddenComponent, +} from 'flavours/glitch/util/react_helpers'; + +// Messages. +const messages = defineMessages({ + advanced_options_icon_title: { + defaultMessage: 'Advanced options', + id: 'advanced_options.icon_title', + }, + attach: { + defaultMessage: 'Attach...', + id: 'compose.attach', + }, + change_privacy: { + defaultMessage: 'Adjust status privacy', + id: 'privacy.change', + }, + direct_long: { + defaultMessage: 'Post to mentioned users only', + id: 'privacy.direct.long', + }, + direct_short: { + defaultMessage: 'Direct', + id: 'privacy.direct.short', + }, + doodle: { + defaultMessage: 'Draw something', + id: 'compose.attach.doodle', + }, + local_only_long: { + defaultMessage: 'Do not post to other instances', + id: 'advanced-options.local-only.long', + }, + local_only_short: { + defaultMessage: 'Local-only', + id: 'advanced-options.local-only.short', + }, + private_long: { + defaultMessage: 'Post to followers only', + id: 'privacy.private.long', + }, + private_short: { + defaultMessage: 'Followers-only', + id: 'privacy.private.short', + }, + public_long: { + defaultMessage: 'Post to public timelines', + id: 'privacy.public.long', + }, + public_short: { + defaultMessage: 'Public', + id: 'privacy.public.short', + }, + sensitive: { + defaultMessage: 'Mark media as sensitive', + id: 'compose_form.sensitive', + }, + spoiler: { + defaultMessage: 'Hide text behind warning', + id: 'compose_form.spoiler', + }, + unlisted_long: { + defaultMessage: 'Do not show in public timelines', + id: 'privacy.unlisted.long', + }, + unlisted_short: { + defaultMessage: 'Unlisted', + id: 'privacy.unlisted.short', + }, + upload: { + defaultMessage: 'Upload a file', + id: 'compose.attach.upload', + }, +}); + +// Handlers. +const handlers = { + + // Handles file selection. + changeFiles ({ target: { files } }) { + const { onUpload } = this.props; + if (files.length && onUpload) { + onUpload(files); + } + }, + + // Handles attachment clicks. + clickAttach (name) { + const { fileElement } = this; + const { onDoodleOpen } = this.props; + + // We switch over the name of the option. + switch (name) { + case 'upload': + if (fileElement) { + fileElement.click(); + } + return; + case 'doodle': + if (onDoodleOpen) { + onDoodleOpen(); + } + return; + } + }, + + // Handles a ref to the file input. + refFileElement (fileElement) { + this.fileElement = fileElement; + }, +}; + +// The component. +export default class ComposerOptions extends React.PureComponent { + + // Constructor. + constructor (props) { + super(props); + assignHandlers(this, handlers); + + // Instance variables. + this.fileElement = null; + } + + // Rendering. + render () { + const { + changeFiles, + clickAttach, + refFileElement, + } = this.handlers; + const { + acceptContentTypes, + disabled, + doNotFederate, + full, + hasMedia, + intl, + onChangeSensitivity, + onChangeVisibility, + onModalClose, + onModalOpen, + onToggleAdvancedOption, + privacy, + resetFileKey, + sensitive, + spoiler, + } = this.props; + + // We predefine our privacy items so that we can easily pick the + // dropdown icon later. + const privacyItems = { + direct: { + icon: 'envelope', + meta: , + name: 'direct', + text: , + }, + private: { + icon: 'lock', + meta: , + name: 'private', + text: , + }, + public: { + icon: 'globe', + meta: , + name: 'public', + text: , + }, + unlisted: { + icon: 'unlock-alt', + meta: , + name: 'unlisted', + text: , + }, + }; + + // The result. + return ( +
+ + , + }, + { + icon: 'paint-brush', + name: 'doodle', + text: , + }, + ]} + onChange={clickAttach} + onModalClose={onModalClose} + onModalOpen={onModalOpen} + title={messages.attach} + /> + + {({ scale }) => ( +
+ +
+ )} +
+
+ + + , + name: 'do_not_federate', + on: doNotFederate, + text: , + }, + ]} + onChange={onToggleAdvancedOption} + onModalClose={onModalClose} + onModalOpen={onModalOpen} + title={intl.formatMessage(messages.advanced_options_icon_title)} + /> +
+ ); + } + +} + +// Props. +ComposerOptions.propTypes = { + acceptContentTypes: PropTypes.string, + disabled: PropTypes.bool, + doNotFederate: PropTypes.bool, + full: PropTypes.bool, + hasMedia: PropTypes.bool, + intl: PropTypes.object.isRequired, + onChangeSensitivity: PropTypes.func, + onChangeVisibility: PropTypes.func, + onDoodleOpen: PropTypes.func, + onModalClose: PropTypes.func, + onModalOpen: PropTypes.func, + onToggleAdvancedOption: PropTypes.func, + onUpload: PropTypes.func, + privacy: PropTypes.string, + resetFileKey: PropTypes.string, + sensitive: PropTypes.bool, + spoiler: PropTypes.bool, +}; -- cgit