From 47deb680d5dca389eb71ee756e475d38514c2ba8 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 23 Feb 2024 00:42:51 +0100 Subject: [PATCH] Add Content-Type dropdown back --- .../compose/components/compose_form.jsx | 2 + .../components/content_type_button.jsx | 115 ++++++++++++++++ .../compose/components/dropdown_menu.jsx | 125 ++++++++++++++++++ .../flavours/glitch/locales/en.json | 7 + 4 files changed, 249 insertions(+) create mode 100644 app/javascript/flavours/glitch/features/compose/components/content_type_button.jsx create mode 100644 app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index 12873e2cc0..3e0613b10d 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -27,6 +27,7 @@ import WarningContainer from '../containers/warning_container'; import { countableText } from '../util/counter'; import { CharacterCounter } from './character_counter'; +import { ContentTypeButton } from './content_type_button'; import { EditIndicator } from './edit_indicator'; import { NavigationBar } from './navigation_bar'; import { PollForm } from "./poll_form"; @@ -309,6 +310,7 @@ class ComposeForm extends ImmutablePureComponent { {!this.props.spoilerAlwaysOn && } + diff --git a/app/javascript/flavours/glitch/features/compose/components/content_type_button.jsx b/app/javascript/flavours/glitch/features/compose/components/content_type_button.jsx new file mode 100644 index 0000000000..9ac382091e --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/content_type_button.jsx @@ -0,0 +1,115 @@ +import { useCallback, useState, useRef } from 'react'; + +import { useIntl, defineMessages } from 'react-intl'; + +import Overlay from 'react-overlays/Overlay'; + +import CodeIcon from '@/material-icons/400-24px/code.svg?react'; +import DescriptionIcon from '@/material-icons/400-24px/description.svg?react'; +import MarkdownIcon from '@/material-icons/400-24px/markdown.svg?react'; +import { changeComposeContentType } from 'flavours/glitch/actions/compose'; +import { IconButton } from 'flavours/glitch/components/icon_button'; +import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; + +import DropdownMenu from './dropdown_menu'; + +const messages = defineMessages({ + change_content_type: { id: 'compose.content-type.change', defaultMessage: 'Change advanced formatting options' }, + plain_text_label: { id: 'compose.content-type.plain', defaultMessage: 'Plain text' }, + plain_text_meta: { id: 'compose.content-type.plain_meta', defaultMessage: 'Write with no advanced formatting' }, + markdown_label: { id: 'compose.content-type.markdown', defaultMessage: 'Markdown' }, + markdown_meta: { id: 'compose.content-type.markdown_meta', defaultMessage: 'Format your posts using Markdown' }, + html_label: { id: 'compose.content-type.html', defaultMessage: 'HTML' }, + html_meta: { id: 'compose.content-type.html_meta', defaultMessage: 'Format your posts using HTML' }, +}); + +export const ContentTypeButton = () => { + const intl = useIntl(); + + const showButton = useAppSelector((state) => state.getIn(['local_settings', 'show_content_type_choice'])); + const contentType = useAppSelector((state) => state.getIn(['compose', 'content_type'])); + const dispatch = useAppDispatch(); + + const containerRef = useRef(null); + + const [activeElement, setActiveElement] = useState(null); + const [open, setOpen] = useState(false); + const [placement, setPlacement] = useState('bottom'); + + const handleToggle = useCallback(() => { + if (open && activeElement) { + activeElement.focus({ preventScroll: true }); + setActiveElement(null); + } + + setOpen(!open); + }, [open, setOpen, activeElement, setActiveElement]); + + const handleClose = useCallback(() => { + if (open && activeElement) { + activeElement.focus({ preventScroll: true }); + setActiveElement(null); + } + + setOpen(false); + }, [open, setOpen, activeElement, setActiveElement]); + + const handleChange = useCallback((value) => { + dispatch(changeComposeContentType(value)); + }, [dispatch]); + + const handleOverlayEnter = useCallback((state) => { + setPlacement(state.placement); + }, [setPlacement]); + + if (!showButton) { + return null; + } + + const options = [ + { icon: 'file-text', iconComponent: DescriptionIcon, value: 'text/plain', text: intl.formatMessage(messages.plain_text_label), meta: intl.formatMessage(messages.plain_text_meta) }, + { icon: 'arrow-circle-down', iconComponent: MarkdownIcon, value: 'text/markdown', text: intl.formatMessage(messages.markdown_label), meta: intl.formatMessage(messages.markdown_meta) }, + { icon: 'code', iconComponent: CodeIcon, value: 'text/html', text: intl.formatMessage(messages.html_label), meta: intl.formatMessage(messages.html_meta) }, + ]; + + const icon = { + 'text/plain': 'file-text', + 'text/markdown': 'arrow-circle-down', + 'text/html': 'code', + }[contentType]; + + const iconComponent = { + 'text/plain': DescriptionIcon, + 'text/markdown': MarkdownIcon, + 'text/html': CodeIcon, + }[contentType]; + + return ( +
+ + + + {({ props, placement }) => ( +
+
+ +
+
+ )} +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx new file mode 100644 index 0000000000..2f1adf2fd1 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx @@ -0,0 +1,125 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import classNames from 'classnames'; + +import { supportsPassiveEvents } from 'detect-passive-events'; + +import { Icon } from 'flavours/glitch/components/icon'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; + +// copied from PrivacyDropdown; will require refactor with upstream down the line +class DropdownMenu extends PureComponent { + + static propTypes = { + style: PropTypes.object, + items: PropTypes.array.isRequired, + value: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + e.stopPropagation(); + } + }; + + handleKeyDown = e => { + const { items } = this.props; + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => { + return (item.value === value); + }); + let element = null; + + switch(e.key) { + case 'Escape': + this.props.onClose(); + break; + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = this.node.childNodes[index + 1] || this.node.firstChild; + break; + case 'ArrowUp': + element = this.node.childNodes[index - 1] || this.node.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = this.node.childNodes[index - 1] || this.node.lastChild; + } else { + element = this.node.childNodes[index + 1] || this.node.firstChild; + } + break; + case 'Home': + element = this.node.firstChild; + break; + case 'End': + element = this.node.lastChild; + break; + } + + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + }; + + handleClick = e => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + this.props.onClose(); + this.props.onChange(value); + }; + + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + }; + + setFocusRef = c => { + this.focusedItem = c; + }; + + render () { + const { style, items, value } = this.props; + + return ( +
+ {items.map(item => ( +
+
+ +
+ +
+ {item.text} + {item.meta} +
+
+ ))} +
+ ); + } + +} + +export default DropdownMenu; diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index b513e837b9..1fd648c6e0 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -14,6 +14,13 @@ "column_subheading.lists": "Lists", "column_subheading.navigation": "Navigation", "community.column_settings.allow_local_only": "Show local-only toots", + "compose.content-type.change": "Change advanced formatting options", + "compose.content-type.html": "HTML", + "compose.content-type.html_meta": "Format your posts using HTML", + "compose.content-type.markdown": "Markdown", + "compose.content-type.markdown_meta": "Format your posts using Markdown", + "compose.content-type.plain": "Plain text", + "compose.content-type.plain_meta": "Write with no advanced formatting", "compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}", "compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}", "compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",