Kaynağa Gözat

Merge pull request #1570 from weseek/feat/profile-image-crop-modalization

profile image crop modal
Yuki Takei 6 yıl önce
ebeveyn
işleme
4339936116

+ 1 - 1
src/client/js/app.jsx

@@ -34,7 +34,7 @@ import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 import TableOfContents from './components/TableOfContents';
 
-import ProfileImageUploader from './components/ProfileImageUploader';
+import ProfileImageUploader from './components/Me/ProfileImageUploader';
 import AdminHome from './components/Admin/AdminHome/AdminHome';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';

+ 130 - 0
src/client/js/components/Me/ImageCropModal.jsx

@@ -0,0 +1,130 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Modal from 'react-bootstrap/es/Modal';
+import Button from 'react-bootstrap/es/Button';
+import { withTranslation } from 'react-i18next';
+import ReactCrop from 'react-image-crop';
+import AppContainer from '../../services/AppContainer';
+import { createSubscribedElement } from '../UnstatedUtils';
+import 'react-image-crop/dist/ReactCrop.css';
+
+class ImageCropModal extends React.Component {
+
+  // demo: https://codesandbox.io/s/72py4jlll6
+  constructor(props) {
+    super();
+    this.state = {
+      crop: null,
+      imageRef: null,
+    };
+    this.onImageLoaded = this.onImageLoaded.bind(this);
+    this.onCropChange = this.onCropChange.bind(this);
+    this.getCroppedImg = this.getCroppedImg.bind(this);
+    this.crop = this.crop.bind(this);
+    this.reset = this.reset.bind(this);
+    this.imageRef = null;
+  }
+
+  onImageLoaded(image) {
+    this.setState({ imageRef: image }, () => this.reset());
+    return false; // Return false when setting crop state in here.
+  }
+
+  onCropChange(crop) {
+    this.setState({ crop });
+  }
+
+  async getCroppedImg(image, crop, fileName) {
+    const canvas = document.createElement('canvas');
+    const scaleX = image.naturalWidth / image.width;
+    const scaleY = image.naturalHeight / image.height;
+    canvas.width = crop.width;
+    canvas.height = crop.height;
+    const ctx = canvas.getContext('2d');
+    ctx.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
+    return new Promise((resolve, reject) => {
+      canvas.toBlob((blob) => {
+        if (!blob) {
+          reject(new Error('Canvas is empty'));
+          return;
+        }
+        blob.name = fileName;
+        window.URL.revokeObjectURL(this.fileUrl);
+        this.fileUrl = window.URL.createObjectURL(blob);
+        resolve(this.fileUrl);
+      }, 'image/jpeg');
+    });
+  }
+
+  async crop() {
+    // crop immages
+    if (this.state.imageRef && this.state.crop.width && this.state.crop.height) {
+      const croppedImageUrl = await this.getCroppedImg(this.state.imageRef, this.state.crop, '/images/icons/user');
+      this.props.onCropCompleted(croppedImageUrl);
+    }
+  }
+
+  reset() {
+    const size = Math.min(this.state.imageRef.width, this.state.imageRef.height);
+    this.setState({
+      crop: {
+        aspect: 1,
+        unit: 'px',
+        x: this.state.imageRef.width / 2 - size / 2,
+        y: this.state.imageRef.height / 2 - size / 2,
+        width: size,
+        height: size,
+      },
+    });
+  }
+
+  render() {
+    return (
+      <Modal show={this.props.show} onHide={this.props.onModalClose}>
+        <Modal.Header closeButton>
+          <Modal.Title>Image Crop</Modal.Title>
+        </Modal.Header>
+        <Modal.Body className="my-4">
+          <ReactCrop
+            circularCrop
+            src={this.props.src}
+            crop={this.state.crop}
+            onImageLoaded={this.onImageLoaded}
+            onChange={this.onCropChange}
+          />
+        </Modal.Body>
+        <Modal.Footer>
+          <div className="d-flex justify-content-between">
+            <Button bsStyle="danger" onClick={this.reset}>
+              Reset
+            </Button>
+            <div className="d-flex">
+              <Button bsStyle="default" onClick={this.props.onModalClose}>
+                Cancel
+              </Button>
+              <Button bsStyle="primary" onClick={this.crop}>
+                Crop
+              </Button>
+            </div>
+          </div>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const ProfileImageFormWrapper = (props) => {
+  return createSubscribedElement(ImageCropModal, props, [AppContainer]);
+};
+ImageCropModal.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  show: PropTypes.bool.isRequired,
+  src: PropTypes.string,
+  onModalClose: PropTypes.func.isRequired,
+  onCropCompleted: PropTypes.func.isRequired,
+};
+export default withTranslation()(ProfileImageFormWrapper);

+ 89 - 0
src/client/js/components/Me/ProfileImageUploader.jsx

@@ -0,0 +1,89 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import AppContainer from '../../services/AppContainer';
+import { createSubscribedElement } from '../UnstatedUtils';
+import 'react-image-crop/dist/ReactCrop.css';
+import ImageCropModal from './ImageCropModal';
+
+class ProfileImageUploader extends React.Component {
+
+  constructor(props) {
+    super();
+    this.state = {
+      show: false,
+      src: null,
+      croppedImageUrl: null,
+    };
+    this.imageRef = null;
+    this.onSelectFile = this.onSelectFile.bind(this);
+    this.hideModal = this.hideModal.bind(this);
+    this.cancelModal = this.cancelModal.bind(this);
+    this.onCropCompleted = this.onCropCompleted.bind(this);
+  }
+
+  onSelectFile(e) {
+    if (e.target.files && e.target.files.length > 0) {
+      const reader = new FileReader();
+      reader.addEventListener('load', () => this.setState({ src: reader.result }));
+      reader.readAsDataURL(e.target.files[0]);
+      this.setState({ show: true });
+    }
+  }
+
+  onCropCompleted(croppedImageUrl) {
+    this.setState({ croppedImageUrl });
+    this.hideModal();
+  }
+
+  showModal() {
+    this.setState({ show: true });
+  }
+
+  hideModal() {
+    this.setState({ show: false });
+  }
+
+  cancelModal() {
+    this.hideModal();
+  }
+
+  render() {
+    const { t } = this.props;
+    const { croppedImageUrl } = this.state;
+    return (
+      <div className="ProfileImageUploader">
+        <div className="form-group">
+          <label className="col-sm-4 control-label">
+            {t('Upload new image')}
+          </label>
+        </div>
+        <div className="col-sm-8">
+          {croppedImageUrl && (
+            <img src={croppedImageUrl} className="picture picture-lg img-circle" id="settingUserPicture" />
+          )}
+          <input type="file" onChange={this.onSelectFile} name="profileImage" accept="image/*" />
+          <ImageCropModal
+            show={this.state.show}
+            src={this.state.src}
+            onModalClose={this.cancelModal}
+            onCropCompleted={this.onCropCompleted}
+          />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const ProfileImageFormWrapper = (props) => {
+  return createSubscribedElement(ProfileImageUploader, props, [AppContainer]);
+};
+ProfileImageUploader.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+export default withTranslation()(ProfileImageFormWrapper);

+ 0 - 138
src/client/js/components/ProfileImageUploader.jsx

@@ -1,138 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import ReactCrop from 'react-image-crop';
-import AppContainer from '../services/AppContainer';
-import { createSubscribedElement } from './UnstatedUtils';
-import 'react-image-crop/dist/ReactCrop.css';
-
-class ProfileImageUploader extends React.Component {
-
-  // demo: https://codesandbox.io/s/72py4jlll6
-  constructor(props) {
-    super();
-
-    this.state = {
-      src: null,
-      crop: {
-        unit: '%',
-        width: 30,
-        aspect: 1,
-      },
-    };
-
-    this.onSelectFile = this.onSelectFile.bind(this);
-    this.onImageLoaded = this.onImageLoaded.bind(this);
-    this.onCropComplete = this.onCropComplete.bind(this);
-    this.onCropChange = this.onCropChange.bind(this);
-    this.makeClientCrop = this.makeClientCrop.bind(this);
-    this.getCroppedImg = this.getCroppedImg.bind(this);
-    this.hanndleSubmit = this.handleSubmit.bind(this);
-  }
-
-  onSelectFile(e) {
-    if (e.target.files && e.target.files.length > 0) {
-      const reader = new FileReader();
-      reader.addEventListener('load', () => this.setState({ src: reader.result }));
-      reader.readAsDataURL(e.target.files[0]);
-    }
-  }
-
-  // If you setState the crop in here you should return false.
-  onImageLoaded(image) {
-    this.imageRef = image;
-  }
-
-  onCropComplete(crop) {
-    this.makeClientCrop(crop);
-  }
-
-  onCropChange(crop, percentCrop) {
-    // You could also use percentCrop:
-    // this.setState({ crop: percentCrop });
-    this.setState({ crop });
-  }
-
-  async makeClientCrop(crop) {
-    // GW-201 で crop済みの画像を state におくときにコメントを外す(未使用変数の lint エラーが生じるためコメントアウト)
-    // if (this.imageRef && crop.width && crop.height) {
-    //   const croppedImageUrl = await this.getCroppedImg(this.imageRef, crop, 'newFile.jpeg');
-    //   this.setState({ croppedImageUrl });
-    // }
-  }
-
-  async getCroppedImg(image, crop, fileName) {
-    const canvas = document.createElement('canvas');
-    const scaleX = image.naturalWidth / image.width;
-    const scaleY = image.naturalHeight / image.height;
-    canvas.width = crop.width;
-    canvas.height = crop.height;
-    const ctx = canvas.getContext('2d');
-
-    ctx.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY,
-      0, 0, crop.width, crop.height);
-
-    return new Promise((resolve, reject) => {
-      canvas.toBlob((blob) => {
-        if (!blob) {
-          reject(new Error('Canvas is empty'));
-          return;
-        }
-        blob.name = fileName;
-        window.URL.revokeObjectURL(this.fileUrl);
-        this.fileUrl = window.URL.createObjectURL(blob);
-        resolve(this.fileUrl);
-      }, 'image/jpeg');
-    });
-  }
-
-  handleSubmit() {
-    // GW-201 にて、crop された画像をサーバー側に送る処理を記述する
-    // me/index.html の 199~240行目の
-    // `$("#pictureUploadForm input[name=profileImage]").on('change', function(){...}`
-    // の処理を node で記述
-  }
-
-  render() {
-    const { crop, src } = this.state;
-
-    return (
-      <div className="App">
-        <input type="file" onChange={this.onSelectFile} name="profileImage" accept="image/*" />
-        {src
-        && (
-        <div>
-          <ReactCrop
-            src={src}
-            crop={crop}
-            onImageLoaded={this.onImageLoaded}
-            onComplete={this.onCropComplete}
-            onChange={this.onCropChange}
-          />
-          <button
-            type="button"
-            onClick={this.handleSubmit}
-          >
-          完了
-          </button>
-        </div>
-        )}
-      </div>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const ProfileImageFormWrapper = (props) => {
-  return createSubscribedElement(ProfileImageUploader, props, [AppContainer]);
-};
-
-ProfileImageUploader.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(ProfileImageFormWrapper);

+ 1 - 16
src/server/views/me/index.html

@@ -166,22 +166,7 @@
         </div><!-- /.form-group -->
 
         <div class="form-group">
-          <label for="" class="col-sm-4 control-label">
-            {{ t('Upload new image') }}
-          </label>
-          <div class="col-sm-8">
-            {% if fileUploadService.getIsUploadable() %}
-            <form action="/_api/attachments.uploadProfileImage" id="pictureUploadForm" method="post" class="form-horizontal" role="form">
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <div id="profile-image-uploader"></div>
-              <div id="pictureUploadFormProgress" class="d-flex align-items-center">
-              </div>
-            </form>
-            {% else %}
-            * {{ t('page_me.form_help.profile_image1') }}<br>
-            * {{ t('page_me.form_help.profile_image2') }}<br>
-            {% endif %}
-          </div>
+          <div id="profile-image-uploader"></div>
         </div><!-- /.form-group -->
 
       </div><!-- /.col-sm- -->