]>
cat aescling's git repositories - mastodon.git/blob - 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
from '../../ui/util/optional_motion';
7 import spring
from 'react-motion/lib/spring';
8 import { supportsPassiveEvents
} from 'detect-passive-events';
9 import classNames
from 'classnames';
10 import Icon
from 'mastodon/components/icon';
12 const messages
= defineMessages({
13 public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
14 public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' },
15 unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
16 unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' },
17 private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
18 private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
19 direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
20 direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
21 change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
24 const listenerOptions
= supportsPassiveEvents
? { passive: true } : false;
26 class PrivacyDropdownMenu
extends React
.PureComponent
{
29 style: PropTypes
.object
,
30 items: PropTypes
.array
.isRequired
,
31 value: PropTypes
.string
.isRequired
,
32 placement: PropTypes
.string
.isRequired
,
33 onClose: PropTypes
.func
.isRequired
,
34 onChange: PropTypes
.func
.isRequired
,
41 handleDocumentClick
= e
=> {
42 if (this.node
&& !this.node
.contains(e
.target
)) {
47 handleKeyDown
= e
=> {
48 const { items
} = this.props
;
49 const value
= e
.currentTarget
.getAttribute('data-index');
50 const index
= items
.findIndex(item
=> {
51 return (item
.value
=== value
);
63 element
= this.node
.childNodes
[index
+ 1] || this.node
.firstChild
;
66 element
= this.node
.childNodes
[index
- 1] || this.node
.lastChild
;
70 element
= this.node
.childNodes
[index
- 1] || this.node
.lastChild
;
72 element
= this.node
.childNodes
[index
+ 1] || this.node
.firstChild
;
76 element
= this.node
.firstChild
;
79 element
= this.node
.lastChild
;
85 this.props
.onChange(element
.getAttribute('data-index'));
92 const value
= e
.currentTarget
.getAttribute('data-index');
97 this.props
.onChange(value
);
100 componentDidMount () {
101 document
.addEventListener('click', this.handleDocumentClick
, false);
102 document
.addEventListener('touchend', this.handleDocumentClick
, listenerOptions
);
103 if (this.focusedItem
) this.focusedItem
.focus({ preventScroll: true });
104 this.setState({ mounted: true });
107 componentWillUnmount () {
108 document
.removeEventListener('click', this.handleDocumentClick
, false);
109 document
.removeEventListener('touchend', this.handleDocumentClick
, listenerOptions
);
117 this.focusedItem
= c
;
121 const { mounted
} = this.state
;
122 const { style
, items
, placement
, value
} = this.props
;
125 <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 }) }}>
126 {({ opacity
, scaleX
, scaleY
}) => (
127 // It should not be transformed when mounting because the resulting
128 // size will be used to determine the coordinate of the menu by
130 <div className
={`privacy-dropdown__dropdown ${placement}`} style
={{ ...style
, opacity: opacity
, transform: mounted
? `scale(${scaleX}, ${scaleY})` : null }} role
='listbox' ref
={this.setRef
}>
132 <div role
='option' tabIndex
='0' key
={item
.value
} data
-index
={item
.value
} onKeyDown
={this.handleKeyDown
} onClick
={this.handleClick
} className
={classNames('privacy-dropdown__option', { active: item
.value
=== value
})} aria
-selected
={item
.value
=== value
} ref
={item
.value
=== value
? this.setFocusRef : null}>
133 <div className
='privacy-dropdown__option__icon'>
134 <Icon id
={item
.icon
} fixedWidth
/>
137 <div className
='privacy-dropdown__option__content'>
138 <strong
>{item
.text
}</strong
>
151 export default @injectIntl
152 class PrivacyDropdown
extends React
.PureComponent
{
155 isUserTouching: PropTypes
.func
,
156 onModalOpen: PropTypes
.func
,
157 onModalClose: PropTypes
.func
,
158 value: PropTypes
.string
.isRequired
,
159 onChange: PropTypes
.func
.isRequired
,
160 noDirect: PropTpes
.bool
,
161 container: PropTypes
.func
,
162 intl: PropTypes
.object
.isRequired
,
170 handleToggle
= ({ target
}) => {
171 if (this.props
.isUserTouching
&& this.props
.isUserTouching()) {
172 if (this.state
.open
) {
173 this.props
.onModalClose();
175 this.props
.onModalOpen({
176 actions: this.options
.map(option
=> ({ ...option
, active: option
.value
=== this.props
.value
})),
177 onClick: this.handleModalActionClick
,
181 const { top
} = target
.getBoundingClientRect();
182 if (this.state
.open
&& this.activeElement
) {
183 this.activeElement
.focus({ preventScroll: true });
185 this.setState({ placement: top
* 2 < innerHeight
? 'bottom' : 'top' });
186 this.setState({ open: !this.state
.open
});
190 handleModalActionClick
= (e
) => {
193 const { value
} = this.options
[e
.currentTarget
.getAttribute('data-index')];
195 this.props
.onModalClose();
196 this.props
.onChange(value
);
199 handleKeyDown
= e
=> {
207 handleMouseDown
= () => {
208 if (!this.state
.open
) {
209 this.activeElement
= document
.activeElement
;
213 handleButtonKeyDown
= (e
) => {
217 this.handleMouseDown();
222 handleClose
= () => {
223 if (this.state
.open
&& this.activeElement
) {
224 this.activeElement
.focus({ preventScroll: true });
226 this.setState({ open: false });
229 handleChange
= value
=> {
230 this.props
.onChange(value
);
233 componentWillMount () {
234 const { intl: { formatMessage
} } = this.props
;
237 { icon: 'globe', value: 'public', text: formatMessage(messages
.public_short
), meta: formatMessage(messages
.public_long
) },
238 { icon: 'unlock', value: 'unlisted', text: formatMessage(messages
.unlisted_short
), meta: formatMessage(messages
.unlisted_long
) },
239 { icon: 'lock', value: 'private', text: formatMessage(messages
.private_short
), meta: formatMessage(messages
.private_long
) },
242 if (!this.props
.noDirect
) {
244 { icon: 'envelope', value: 'direct', text: formatMessage(messages
.direct_short
), meta: formatMessage(messages
.direct_long
) },
250 const { value
, container
, intl
} = this.props
;
251 const { open
, placement
} = this.state
;
253 const valueOption
= this.options
.find(item
=> item
.value
=== value
);
256 <div className
={classNames('privacy-dropdown', placement
, { active: open
})} onKeyDown
={this.handleKeyDown
}>
257 <div className
={classNames('privacy-dropdown__value', { active: this.options
.indexOf(valueOption
) === (placement
=== 'bottom' ? 0 : (this.options
.length
- 1)) })}>
259 className
='privacy-dropdown__value-icon'
260 icon
={valueOption
.icon
}
261 title
={intl
.formatMessage(messages
.change_privacy
)}
266 onClick
={this.handleToggle
}
267 onMouseDown
={this.handleMouseDown
}
268 onKeyDown
={this.handleButtonKeyDown
}
269 style
={{ height: null, lineHeight: '27px' }}
273 <Overlay show
={open
} placement
={placement
} target
={this} container
={container
}>
277 onClose
={this.handleClose
}
278 onChange
={this.handleChange
}
279 placement
={placement
}
This page took 0.169575 seconds and 4 git commands to generate.