about summary refs log tree commit diff
path: root/app/javascript/mastodon/components/dropdown_menu.js
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-02-09 01:17:07 +0100
committerGitHub <noreply@github.com>2022-02-09 01:17:07 +0100
commitfd3a45e3482e86dad3c1dfc069144864c4ff0b0b (patch)
tree41d497599cf1ecb35f9f2e278b67c90e3a850a46 /app/javascript/mastodon/components/dropdown_menu.js
parent2adcad04ff96fc8e7cb9aeefef1b22ea38e65457 (diff)
Add edit history to web UI (#17390)
* Add edit history to web UI

* Change history reducer to store items per status

* Fix missing loading prop
Diffstat (limited to 'app/javascript/mastodon/components/dropdown_menu.js')
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js149
1 files changed, 106 insertions, 43 deletions
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 7d0588901..4b4ad8355 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay';
 import Motion from '../features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { supportsPassiveEvents } from 'detect-passive-events';
+import classNames from 'classnames';
+import { CircularProgress } from 'mastodon/components/loading_indicator';
 
 const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 let id = 0;
@@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent {
   };
 
   static propTypes = {
-    items: PropTypes.array.isRequired,
+    items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
+    loading: PropTypes.bool,
+    scrollable: PropTypes.bool,
     onClose: PropTypes.func.isRequired,
     style: PropTypes.object,
     placement: PropTypes.string,
     arrowOffsetLeft: PropTypes.string,
     arrowOffsetTop: PropTypes.string,
     openedViaKeyboard: PropTypes.bool,
+    renderItem: PropTypes.func,
+    renderHeader: PropTypes.func,
+    onItemClick: PropTypes.func.isRequired,
   };
 
   static defaultProps = {
@@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent {
     document.addEventListener('click', this.handleDocumentClick, false);
     document.addEventListener('keydown', this.handleKeyDown, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+
     if (this.focusedItem && this.props.openedViaKeyboard) {
       this.focusedItem.focus({ preventScroll: true });
     }
+
     this.setState({ mounted: true });
   }
 
@@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent {
   }
 
   handleKeyDown = e => {
-    const items = Array.from(this.node.getElementsByTagName('a'));
+    const items = Array.from(this.node.querySelectorAll('a, button'));
     const index = items.indexOf(document.activeElement);
     let element = null;
 
@@ -109,21 +118,11 @@ class DropdownMenu extends React.PureComponent {
   }
 
   handleClick = e => {
-    const i = Number(e.currentTarget.getAttribute('data-index'));
-    const { action, to } = this.props.items[i];
-
-    this.props.onClose();
-
-    if (typeof action === 'function') {
-      e.preventDefault();
-      action(e);
-    } else if (to) {
-      e.preventDefault();
-      this.context.router.history.push(to);
-    }
+    const { onItemClick } = this.props;
+    onItemClick(e);
   }
 
-  renderItem (option, i) {
+  renderItem = (option, i) => {
     if (option === null) {
       return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
     }
@@ -140,9 +139,11 @@ class DropdownMenu extends React.PureComponent {
   }
 
   render () {
-    const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
+    const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
     const { mounted } = this.state;
 
+    let renderItem = this.props.renderItem || this.renderItem;
+
     return (
       <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 }) }}>
         {({ opacity, scaleX, scaleY }) => (
@@ -152,9 +153,23 @@ class DropdownMenu extends React.PureComponent {
           <div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
             <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
 
-            <ul>
-              {items.map((option, i) => this.renderItem(option, i))}
-            </ul>
+            <div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}>
+              {loading && (
+                <CircularProgress size={30} strokeWidth={3.5} />
+              )}
+
+              {!loading && renderHeader && (
+                <div className='dropdown-menu__container__header'>
+                  {renderHeader(items)}
+                </div>
+              )}
+
+              {!loading && (
+                <ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
+                  {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
+                </ul>
+              )}
+            </div>
           </div>
         )}
       </Motion>
@@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent {
   };
 
   static propTypes = {
-    icon: PropTypes.string.isRequired,
-    items: PropTypes.array.isRequired,
-    size: PropTypes.number.isRequired,
+    children: PropTypes.node,
+    icon: PropTypes.string,
+    items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
+    loading: PropTypes.bool,
+    size: PropTypes.number,
     title: PropTypes.string,
     disabled: PropTypes.bool,
+    scrollable: PropTypes.bool,
     status: ImmutablePropTypes.map,
     isUserTouching: PropTypes.func,
     onOpen: PropTypes.func.isRequired,
@@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent {
     dropdownPlacement: PropTypes.string,
     openDropdownId: PropTypes.number,
     openedViaKeyboard: PropTypes.bool,
+    renderItem: PropTypes.func,
+    renderHeader: PropTypes.func,
+    onItemClick: PropTypes.func,
   };
 
   static defaultProps = {
@@ -237,17 +258,21 @@ export default class Dropdown extends React.PureComponent {
   }
 
   handleItemClick = e => {
+    const { onItemClick } = this.props;
     const i = Number(e.currentTarget.getAttribute('data-index'));
-    const { action, to } = this.props.items[i];
+    const item = this.props.items[i];
 
     this.handleClose();
 
-    if (typeof action === 'function') {
+    if (typeof onItemClick === 'function') {
+      e.preventDefault();
+      onItemClick(item, i);
+    } else if (item && typeof item.action === 'function') {
       e.preventDefault();
-      action();
-    } else if (to) {
+      item.action();
+    } else if (item && item.to) {
       e.preventDefault();
-      this.context.router.history.push(to);
+      this.context.router.history.push(item.to);
     }
   }
 
@@ -265,29 +290,67 @@ export default class Dropdown extends React.PureComponent {
     }
   }
 
+  close = () => {
+    this.handleClose();
+  }
+
   render () {
-    const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
+    const {
+      icon,
+      items,
+      size,
+      title,
+      disabled,
+      loading,
+      scrollable,
+      dropdownPlacement,
+      openDropdownId,
+      openedViaKeyboard,
+      children,
+      renderItem,
+      renderHeader,
+    } = this.props;
+
     const open = this.state.id === openDropdownId;
 
+    const button = children ? React.cloneElement(React.Children.only(children), {
+      ref: this.setTargetRef,
+      onClick: this.handleClick,
+      onMouseDown: this.handleMouseDown,
+      onKeyDown: this.handleButtonKeyDown,
+      onKeyPress: this.handleKeyPress,
+    }) : (
+      <IconButton
+        icon={icon}
+        title={title}
+        active={open}
+        disabled={disabled}
+        size={size}
+        ref={this.setTargetRef}
+        onClick={this.handleClick}
+        onMouseDown={this.handleMouseDown}
+        onKeyDown={this.handleButtonKeyDown}
+        onKeyPress={this.handleKeyPress}
+      />
+    );
+
     return (
-      <div>
-        <IconButton
-          icon={icon}
-          title={title}
-          active={open}
-          disabled={disabled}
-          size={size}
-          ref={this.setTargetRef}
-          onClick={this.handleClick}
-          onMouseDown={this.handleMouseDown}
-          onKeyDown={this.handleButtonKeyDown}
-          onKeyPress={this.handleKeyPress}
-        />
+      <React.Fragment>
+        {button}
 
         <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
-          <DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
+          <DropdownMenu
+            items={items}
+            loading={loading}
+            scrollable={scrollable}
+            onClose={this.handleClose}
+            openedViaKeyboard={openedViaKeyboard}
+            renderItem={renderItem}
+            renderHeader={renderHeader}
+            onItemClick={this.handleItemClick}
+          />
         </Overlay>
-      </div>
+      </React.Fragment>
     );
   }