about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/composer/options/dropdown
diff options
context:
space:
mode:
authorkibigo! <marrus-sh@users.noreply.github.com>2017-12-23 22:16:45 -0800
committerkibigo! <marrus-sh@users.noreply.github.com>2018-01-04 18:21:59 -0800
commit924ffe81d477a8cf890c8117efb94b908760bccc (patch)
treeacefef7362929f4495424fbb037c3be59cca318f /app/javascript/flavours/glitch/features/composer/options/dropdown
parentfc884d015a1a2d6c31976af3d63039390fa15939 (diff)
WIPgit status <Compose> Refactor; <Composer> ed.
Diffstat (limited to 'app/javascript/flavours/glitch/features/composer/options/dropdown')
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/index.js243
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js126
2 files changed, 369 insertions, 0 deletions
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 (
+      <div
+        className={computedClass}
+        onKeyDown={keyDown}
+        ref={ref}
+      >
+        <IconButton
+          active={open || active}
+          className='value'
+          disabled={disabled}
+          icon={icon}
+          onClick={toggle}
+          size={18}
+          style={{
+            height: null,
+            lineHeight: '27px',
+          }}
+          title={title}
+        />
+        <Overlay
+          placement='bottom'
+          show={open}
+          target={this}
+        >
+          <Motion
+            defaultStyle={{
+              opacity: 0,
+              scaleX: 0.85,
+              scaleY: 0.75,
+            }}
+            style={{
+              opacity: springMotion,
+              scaleX: springMotion,
+              scaleY: springMotion,
+            }}
+          >
+            {({ opacity, scaleX, scaleY }) => (
+              <div
+                className='dropdown'
+                ref={this.setRef}
+                style={{
+                  opacity: opacity,
+                  transform: `scale(${scaleX}, ${scaleY})`,
+                }}
+              >
+                {items.map(
+                  ({
+                    name,
+                    ...rest
+                  }) => (
+                    <ComposerOptionsDropdownItem
+                      active={name === value}
+                      key={name}
+                      name={name}
+                      onChange={onChange}
+                      onClose={close}
+                      options={rest}
+                    />
+                  )
+                )}
+              </div>
+            )}
+          </Motion>
+        </Overlay>
+      </div>
+    );
+  }
+
+}
+
+//  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 (
+      <div
+        className={computedClass}
+        onClick={activate}
+        onKeyDown={activate}
+        role='button'
+        tabIndex='0'
+      >
+        {function () {
+
+          //  We render a `<Toggle>` if we were provided an `on`
+          //  property, and otherwise show an `<Icon>` if available.
+          switch (true) {
+          case on !== null && typeof on !== 'undefined':
+            return (
+              <Toggle
+                checked={on}
+                onChange={activate}
+              />
+            );
+          case !!icon:
+            return (
+              <Icon
+                fullwidth
+                icon={icon}
+              />
+            );
+          default:
+            return null;
+          }
+        }()}
+        {meta ? (
+          <div>
+            <strong>{text}</strong>
+            {meta}
+          </div>
+        ) : <div>{text}</div>}
+      </div>
+    );
+  }
+
+};
+
+//  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,
+  }),
+};