]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/features/compose/components/privacy_dropdown.js
import only Overlay from react-overlays (#5235)
[mastodon.git] / app / javascript / mastodon / features / compose / components / privacy_dropdown.js
1 import React from 'react';
2 import PropTypes from 'prop-types';
3 import { injectIntl, defineMessages } from 'react-intl';
4 import IconButton from '../../../components/icon_button';
5 import Overlay from 'react-overlays/lib/Overlay';
6 import { Motion, spring } from 'react-motion';
7 import detectPassiveEvents from 'detect-passive-events';
8 import classNames from 'classnames';
9
10 const messages = defineMessages({
11 public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
12 public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
13 unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
14 unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
15 private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
16 private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
17 direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
18 direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
19 change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
20 });
21
22 const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
23
24 class PrivacyDropdownMenu extends React.PureComponent {
25
26 static propTypes = {
27 style: PropTypes.object,
28 items: PropTypes.array.isRequired,
29 value: PropTypes.string.isRequired,
30 onClose: PropTypes.func.isRequired,
31 onChange: PropTypes.func.isRequired,
32 };
33
34 handleDocumentClick = e => {
35 if (this.node && !this.node.contains(e.target)) {
36 this.props.onClose();
37 }
38 }
39
40 handleClick = e => {
41 if (e.key === 'Escape') {
42 this.props.onClose();
43 } else if (!e.key || e.key === 'Enter') {
44 const value = e.currentTarget.getAttribute('data-index');
45
46 e.preventDefault();
47
48 this.props.onClose();
49 this.props.onChange(value);
50 }
51 }
52
53 componentDidMount () {
54 document.addEventListener('click', this.handleDocumentClick, false);
55 document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
56 }
57
58 componentWillUnmount () {
59 document.removeEventListener('click', this.handleDocumentClick, false);
60 document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
61 }
62
63 setRef = c => {
64 this.node = c;
65 }
66
67 render () {
68 const { style, items, value } = this.props;
69
70 return (
71 <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
72 {({ opacity, scaleX, scaleY }) => (
73 <div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}>
74 {items.map(item =>
75 <div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })}>
76 <div className='privacy-dropdown__option__icon'>
77 <i className={`fa fa-fw fa-${item.icon}`} />
78 </div>
79
80 <div className='privacy-dropdown__option__content'>
81 <strong>{item.text}</strong>
82 {item.meta}
83 </div>
84 </div>
85 )}
86 </div>
87 )}
88 </Motion>
89 );
90 }
91
92 }
93
94 @injectIntl
95 export default class PrivacyDropdown extends React.PureComponent {
96
97 static propTypes = {
98 isUserTouching: PropTypes.func,
99 isModalOpen: PropTypes.bool.isRequired,
100 onModalOpen: PropTypes.func,
101 onModalClose: PropTypes.func,
102 value: PropTypes.string.isRequired,
103 onChange: PropTypes.func.isRequired,
104 intl: PropTypes.object.isRequired,
105 };
106
107 state = {
108 open: false,
109 };
110
111 handleToggle = () => {
112 if (this.props.isUserTouching()) {
113 if (this.state.open) {
114 this.props.onModalClose();
115 } else {
116 this.props.onModalOpen({
117 actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
118 onClick: this.handleModalActionClick,
119 });
120 }
121 } else {
122 this.setState({ open: !this.state.open });
123 }
124 }
125
126 handleModalActionClick = (e) => {
127 e.preventDefault();
128
129 const { value } = this.options[e.currentTarget.getAttribute('data-index')];
130
131 this.props.onModalClose();
132 this.props.onChange(value);
133 }
134
135 handleKeyDown = e => {
136 switch(e.key) {
137 case 'Enter':
138 this.handleToggle();
139 break;
140 case 'Escape':
141 this.handleClose();
142 break;
143 }
144 }
145
146 handleClose = () => {
147 this.setState({ open: false });
148 }
149
150 handleChange = value => {
151 this.props.onChange(value);
152 }
153
154 componentWillMount () {
155 const { intl: { formatMessage } } = this.props;
156
157 this.options = [
158 { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
159 { icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
160 { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
161 { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
162 ];
163 }
164
165 render () {
166 const { value, intl } = this.props;
167 const { open } = this.state;
168
169 const valueOption = this.options.find(item => item.value === value);
170
171 return (
172 <div className={classNames('privacy-dropdown', { active: open })} onKeyDown={this.handleKeyDown}>
173 <div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}>
174 <IconButton
175 className='privacy-dropdown__value-icon'
176 icon={valueOption.icon}
177 title={intl.formatMessage(messages.change_privacy)}
178 size={18}
179 expanded={open}
180 active={open}
181 inverted
182 onClick={this.handleToggle}
183 style={{ height: null, lineHeight: '27px' }}
184 />
185 </div>
186
187 <Overlay show={open} placement='bottom' target={this}>
188 <PrivacyDropdownMenu
189 items={this.options}
190 value={value}
191 onClose={this.handleClose}
192 onChange={this.handleChange}
193 />
194 </Overlay>
195 </div>
196 );
197 }
198
199 }
This page took 0.118976 seconds and 4 git commands to generate.