about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/drawer/search
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features/drawer/search')
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/index.js152
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/popout/index.js109
2 files changed, 261 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/features/drawer/search/index.js b/app/javascript/flavours/glitch/features/drawer/search/index.js
new file mode 100644
index 000000000..8cbb0906c
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/search/index.js
@@ -0,0 +1,152 @@
+//  Package imports.
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {
+  FormattedMessage,
+  defineMessages,
+} from 'react-intl';
+import Overlay from 'react-overlays/lib/Overlay';
+
+//  Components.
+import Icon from 'flavours/glitch/components/icon';
+import DrawerSearchPopout from './popout';
+
+//  Utils.
+import { focusRoot } from 'flavours/glitch/util/dom_helpers';
+import {
+  assignHandlers,
+  hiddenComponent,
+} from 'flavours/glitch/util/react_helpers';
+
+//  Messages.
+const messages = defineMessages({
+  placeholder: {
+    defaultMessage: 'Search',
+    id: 'search.placeholder',
+  },
+});
+
+//  Handlers.
+const handlers = {
+
+  handleBlur () {
+    this.setState({ expanded: false });
+  },
+
+  handleChange ({ target: { value } }) {
+    const { onChange } = this.props;
+    if (onChange) {
+      onChange(value);
+    }
+  },
+
+  handleClear (e) {
+    const {
+      onClear,
+      submitted,
+      value,
+    } = this.props;
+    e.preventDefault();  //  Prevents focus change ??
+    if (onClear && (submitted || value && value.length)) {
+      onClear();
+    }
+  },
+
+  handleFocus () {
+    const { onShow } = this.props;
+    this.setState({ expanded: true });
+    if (onShow) {
+      onShow();
+    }
+  },
+
+  handleKeyUp (e) {
+    const { onSubmit } = this.props;
+    switch (e.key) {
+    case 'Enter':
+      if (onSubmit) {
+        onSubmit();
+      }
+      break;
+    case 'Escape':
+      focusRoot();
+    }
+  },
+};
+
+//  The component.
+export default class DrawerSearch extends React.PureComponent {
+
+  //  Constructor.
+  constructor (props) {
+    super(props);
+    assignHandlers(this, handlers);
+    this.state = { expanded: false };
+  }
+
+  //  Rendering.
+  render () {
+    const {
+      handleBlur,
+      handleChange,
+      handleClear,
+      handleFocus,
+      handleKeyUp,
+    } = this.handlers;
+    const {
+      intl,
+      submitted,
+      value,
+    } = this.props;
+    const { expanded } = this.state;
+    const active = value && value.length || submitted;
+    const computedClass = classNames('drawer--search', { active });
+
+    return (
+      <div className={computedClass}>
+        <label>
+          <span {...hiddenComponent}>
+            <FormattedMessage {...messages.placeholder} />
+          </span>
+          <input
+            type='text'
+            placeholder={intl.formatMessage(messages.placeholder)}
+            value={value || ''}
+            onChange={handleChange}
+            onKeyUp={handleKeyUp}
+            onFocus={handleFocus}
+            onBlur={handleBlur}
+          />
+        </label>
+        <div
+          aria-label={intl.formatMessage(messages.placeholder)}
+          className='icon'
+          onClick={handleClear}
+          role='button'
+          tabIndex='0'
+        >
+          <Icon icon='search' />
+          <Icon icon='times-circle' />
+        </div>
+        <Overlay
+          placement='bottom'
+          show={expanded && !active}
+          target={this}
+        ><DrawerSearchPopout /></Overlay>
+      </div>
+    );
+  }
+
+}
+
+//  Props.
+DrawerSearch.propTypes = {
+  value: PropTypes.string,
+  submitted: PropTypes.bool,
+  onChange: PropTypes.func,
+  onSubmit: PropTypes.func,
+  onClear: PropTypes.func,
+  onShow: PropTypes.func,
+  intl: PropTypes.object,
+};
diff --git a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
new file mode 100644
index 000000000..fec090b64
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
@@ -0,0 +1,109 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import {
+  FormattedMessage,
+  defineMessages,
+} from 'react-intl';
+import spring from 'react-motion/lib/spring';
+
+//  Utils.
+import Motion from 'flavours/glitch/util/optional_motion';
+import { searchEnabled } from 'flavours/glitch/util/initial_state';
+
+//  Messages.
+const messages = defineMessages({
+  format: {
+    defaultMessage: 'Advanced search format',
+    id: 'search_popout.search_format',
+  },
+  hashtag: {
+    defaultMessage: 'hashtag',
+    id: 'search_popout.tips.hashtag',
+  },
+  status: {
+    defaultMessage: 'status',
+    id: 'search_popout.tips.status',
+  },
+  text: {
+    defaultMessage: 'Simple text returns matching display names, usernames and hashtags',
+    id: 'search_popout.tips.text',
+  },
+  full_text: {
+    defaultMessage: 'Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.',
+    id: 'search_popout.tips.full_text',
+  },
+  user: {
+    defaultMessage: 'user',
+    id: 'search_popout.tips.user',
+  },
+});
+
+//  The spring used by our motion.
+const motionSpring = spring(1, { damping: 35, stiffness: 400 });
+
+//  The component.
+export default function DrawerSearchPopout ({ style }) {
+
+  //  The result.
+  return (
+    <div
+      className='drawer--search--popout'
+      style={{
+        ...style,
+        position: 'absolute',
+        width: 285,
+      }}
+    >
+      <Motion
+        defaultStyle={{
+          opacity: 0,
+          scaleX: 0.85,
+          scaleY: 0.75,
+        }}
+        style={{
+          opacity: motionSpring,
+          scaleX: motionSpring,
+          scaleY: motionSpring,
+        }}
+      >
+        {({ opacity, scaleX, scaleY }) => (
+          <div
+            style={{
+              opacity: opacity,
+              transform: `scale(${scaleX}, ${scaleY})`,
+            }}
+          >
+            <h4><FormattedMessage {...messages.format} /></h4>
+            <ul>
+              <li>
+                <em>#example</em>
+                {' '}
+                <FormattedMessage {...messages.hashtag} />
+              </li>
+              <li>
+                <em>@username@domain</em>
+                {' '}
+                <FormattedMessage {...messages.user} />
+              </li>
+              <li>
+                <em>URL</em>
+                {' '}
+                <FormattedMessage {...messages.user} />
+              </li>
+              <li>
+                <em>URL</em>
+                {' '}
+                <FormattedMessage {...messages.status} />
+              </li>
+            </ul>
+            { searchEnabled ? <FormattedMessage {...messages.full_text} /> : <FormattedMessage {...messages.text} /> }
+          </div>
+        )}
+      </Motion>
+    </div>
+  );
+}
+
+//  Props.
+DrawerSearchPopout.propTypes = { style: PropTypes.object };