about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/composer/upload_form
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features/composer/upload_form')
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/index.js60
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/item/index.js203
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js52
3 files changed, 315 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
new file mode 100644
index 000000000..c2ff66623
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
@@ -0,0 +1,60 @@
+//  Package imports.
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+//  Components.
+import ComposerUploadFormItem from './item';
+import ComposerUploadFormProgress from './progress';
+
+//  The component.
+export default function ComposerUploadForm ({
+  intl,
+  media,
+  onChangeDescription,
+  onOpenFocalPointModal,
+  onRemove,
+  progress,
+  uploading,
+  handleRef,
+}) {
+  const computedClass = classNames('composer--upload_form', { uploading });
+
+  //  The result.
+  return (
+    <div className={computedClass} ref={handleRef}>
+      {uploading ? <ComposerUploadFormProgress progress={progress} /> : null}
+      {media ? (
+        <div className='content'>
+          {media.map(item => (
+            <ComposerUploadFormItem
+              description={item.get('description')}
+              key={item.get('id')}
+              id={item.get('id')}
+              intl={intl}
+              focusX={item.getIn(['meta', 'focus', 'x'])}
+              focusY={item.getIn(['meta', 'focus', 'y'])}
+              mediaType={item.get('type')}
+              preview={item.get('preview_url')}
+              onChangeDescription={onChangeDescription}
+              onOpenFocalPointModal={onOpenFocalPointModal}
+              onRemove={onRemove}
+            />
+          ))}
+        </div>
+      ) : null}
+    </div>
+  );
+}
+
+//  Props.
+ComposerUploadForm.propTypes = {
+  intl: PropTypes.object.isRequired,
+  media: ImmutablePropTypes.list,
+  onChangeDescription: PropTypes.func.isRequired,
+  onRemove: PropTypes.func.isRequired,
+  progress: PropTypes.number,
+  uploading: PropTypes.bool,
+  handleRef: PropTypes.func,
+};
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
new file mode 100644
index 000000000..93fa4e39e
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
@@ -0,0 +1,203 @@
+//  Package imports.
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {
+  FormattedMessage,
+  defineMessages,
+} from 'react-intl';
+import spring from 'react-motion/lib/spring';
+
+//  Components.
+import IconButton from 'flavours/glitch/components/icon_button';
+
+//  Utils.
+import Motion from 'flavours/glitch/util/optional_motion';
+import { assignHandlers } from 'flavours/glitch/util/react_helpers';
+import { isUserTouching } from 'flavours/glitch/util/is_mobile';
+
+//  Messages.
+const messages = defineMessages({
+  undo: {
+    defaultMessage: 'Undo',
+    id: 'upload_form.undo',
+  },
+  description: {
+    defaultMessage: 'Describe for the visually impaired',
+    id: 'upload_form.description',
+  },
+  crop: {
+    defaultMessage: 'Crop',
+    id: 'upload_form.focus',
+  },
+});
+
+//  Handlers.
+const handlers = {
+
+  //  On blur, we save the description for the media item.
+  handleBlur () {
+    const {
+      id,
+      onChangeDescription,
+    } = this.props;
+    const { dirtyDescription } = this.state;
+
+    this.setState({ dirtyDescription: null, focused: false });
+
+    if (id && onChangeDescription && dirtyDescription !== null) {
+      onChangeDescription(id, dirtyDescription);
+    }
+  },
+
+  //  When the value of our description changes, we store it in the
+  //  temp value `dirtyDescription` in our state.
+  handleChange ({ target: { value } }) {
+    this.setState({ dirtyDescription: value });
+  },
+
+  //  Records focus on the media item.
+  handleFocus () {
+    this.setState({ focused: true });
+  },
+
+  //  Records the start of a hover over the media item.
+  handleMouseEnter () {
+    this.setState({ hovered: true });
+  },
+
+  //  Records the end of a hover over the media item.
+  handleMouseLeave () {
+    this.setState({ hovered: false });
+  },
+
+  //  Removes the media item.
+  handleRemove () {
+    const {
+      id,
+      onRemove,
+    } = this.props;
+    if (id && onRemove) {
+      onRemove(id);
+    }
+  },
+
+  //  Opens the focal point modal.
+  handleFocalPointClick () {
+    const {
+      id,
+      onOpenFocalPointModal,
+    } = this.props;
+    if (id && onOpenFocalPointModal) {
+      onOpenFocalPointModal(id);
+    }
+  },
+};
+
+//  The component.
+export default class ComposerUploadFormItem extends React.PureComponent {
+
+  //  Constructor.
+  constructor (props) {
+    super(props);
+    assignHandlers(this, handlers);
+    this.state = {
+      hovered: false,
+      focused: false,
+      dirtyDescription: null,
+    };
+  }
+
+  //  Rendering.
+  render () {
+    const {
+      handleBlur,
+      handleChange,
+      handleFocus,
+      handleMouseEnter,
+      handleMouseLeave,
+      handleRemove,
+      handleFocalPointClick,
+    } = this.handlers;
+    const {
+      intl,
+      preview,
+      focusX,
+      focusY,
+      mediaType,
+    } = this.props;
+    const {
+      focused,
+      hovered,
+      dirtyDescription,
+    } = this.state;
+    const active = hovered || focused || isUserTouching();
+    const computedClass = classNames('composer--upload_form--item', { active });
+    const x = ((focusX /  2) + .5) * 100;
+    const y = ((focusY / -2) + .5) * 100;
+    const description = dirtyDescription || (dirtyDescription !== '' && this.props.description) || '';
+
+    //  The result.
+    return (
+      <div
+        className={computedClass}
+        onMouseEnter={handleMouseEnter}
+        onMouseLeave={handleMouseLeave}
+      >
+        <Motion
+          defaultStyle={{ scale: 0.8 }}
+          style={{
+            scale: spring(1, {
+              stiffness: 180,
+              damping: 12,
+            }),
+          }}
+        >
+          {({ scale }) => (
+            <div
+              style={{
+                transform: `scale(${scale})`,
+                backgroundImage: preview ? `url(${preview})` : null,
+                backgroundPosition: `${x}% ${y}%`
+              }}
+            >
+              <div className={classNames('composer--upload_form--actions', { active })}>
+                <button className='icon-button' onClick={handleRemove}>
+                  <i className='fa fa-times' /> <FormattedMessage {...messages.undo} />
+                </button>
+                {mediaType === 'image' && <button className='icon-button' onClick={handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage {...messages.crop} /></button>}
+              </div>
+              <label>
+                <span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span>
+                <input
+                  maxLength={420}
+                  onBlur={handleBlur}
+                  onChange={handleChange}
+                  onFocus={handleFocus}
+                  placeholder={intl.formatMessage(messages.description)}
+                  type='text'
+                  value={description}
+                />
+              </label>
+            </div>
+          )}
+        </Motion>
+      </div>
+    );
+  }
+
+}
+
+//  Props.
+ComposerUploadFormItem.propTypes = {
+  description: PropTypes.string,
+  id: PropTypes.string,
+  intl: PropTypes.object.isRequired,
+  onChangeDescription: PropTypes.func.isRequired,
+  onOpenFocalPointModal: PropTypes.func.isRequired,
+  onRemove: PropTypes.func.isRequired,
+  focusX: PropTypes.number,
+  focusY: PropTypes.number,
+  mediaType: PropTypes.string,
+  preview: PropTypes.string,
+};
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js
new file mode 100644
index 000000000..8c4b0eea6
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js
@@ -0,0 +1,52 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import {
+  defineMessages,
+  FormattedMessage,
+} from 'react-intl';
+import spring from 'react-motion/lib/spring';
+
+//  Components.
+import Icon from 'flavours/glitch/components/icon';
+
+//  Utils.
+import Motion from 'flavours/glitch/util/optional_motion';
+
+//  Messages.
+const messages = defineMessages({
+  upload: {
+    defaultMessage: 'Uploading...',
+    id: 'upload_progress.label',
+  },
+});
+
+//  The component.
+export default function ComposerUploadFormProgress ({ progress }) {
+
+  //  The result.
+  return (
+    <div className='composer--upload_form--progress'>
+      <Icon icon='upload' />
+      <div className='message'>
+        <FormattedMessage {...messages.upload} />
+        <div className='backdrop'>
+          <Motion
+            defaultStyle={{ width: 0 }}
+            style={{ width: spring(progress) }}
+          >
+            {({ width }) =>
+              (<div
+                className='tracker'
+                style={{ width: `${width}%` }}
+              />)
+            }
+          </Motion>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+//  Props.
+ComposerUploadFormProgress.propTypes = { progress: PropTypes.number };