web: components: first version of the avatar editor
This commit is contained in:
parent
676532c303
commit
d54ba9f9d3
@ -33,6 +33,7 @@
|
||||
"prop-types": "^15.7.2",
|
||||
"rc-tree": "^2.1.2",
|
||||
"react-autosize-textarea": "^7.0.0",
|
||||
"react-avatar-edit": "^0.8.3",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-datepicker": "^2.8.0",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
|
218
web/ASC.Web.Components/src/components/avatar-editor/index.js
Normal file
218
web/ASC.Web.Components/src/components/avatar-editor/index.js
Normal file
@ -0,0 +1,218 @@
|
||||
import React, { memo } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import PropTypes from 'prop-types'
|
||||
import ModalDialog from '../modal-dialog'
|
||||
import Button from '../button'
|
||||
import { Text } from '../text'
|
||||
import Avatar from 'react-avatar-edit'
|
||||
import { default as ASCAvatar } from '../avatar/index'
|
||||
|
||||
const StyledASCAvatar = styled(ASCAvatar)`
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
`;
|
||||
const StyledAvatarContainer = styled.div`
|
||||
text-align: center;
|
||||
div:first-child {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
class AvatarEditorBody extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
croppedImage: null,
|
||||
src: this.props.image,
|
||||
hasMaxSizeError: false
|
||||
}
|
||||
this.onCrop = this.onCrop.bind(this)
|
||||
this.onClose = this.onClose.bind(this)
|
||||
this.onBeforeFileLoad = this.onBeforeFileLoad.bind(this)
|
||||
this.onFileLoad = this.onFileLoad.bind(this)
|
||||
|
||||
}
|
||||
onClose() {
|
||||
this.props.onCloseEditor();
|
||||
this.setState({ croppedImage: null })
|
||||
}
|
||||
onCrop(croppedImage) {
|
||||
this.props.onCropImage(croppedImage);
|
||||
this.setState({ croppedImage })
|
||||
}
|
||||
onBeforeFileLoad(elem) {
|
||||
if (elem.target.files[0].size > this.props.maxSize * 1000000) {
|
||||
this.setState({
|
||||
hasMaxSizeError: true
|
||||
});
|
||||
elem.target.value = "";
|
||||
}else if(this.state.hasMaxSizeError){
|
||||
this.setState({
|
||||
hasMaxSizeError: false
|
||||
});
|
||||
};
|
||||
}
|
||||
onFileLoad(file){
|
||||
let reader = new FileReader();
|
||||
let _this = this;
|
||||
reader.onloadend = () => {
|
||||
_this.props.onFileLoad(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<StyledAvatarContainer>
|
||||
<Avatar
|
||||
width={400}
|
||||
height={295}
|
||||
imageWidth={400}
|
||||
cropRadius={50}
|
||||
onCrop={this.onCrop}
|
||||
onClose={this.onClose}
|
||||
onBeforeFileLoad={this.onBeforeFileLoad}
|
||||
onFileLoad={this.onFileLoad}
|
||||
label={this.props.label}
|
||||
src={this.state.src}
|
||||
/>
|
||||
{this.state.croppedImage && (
|
||||
<div>
|
||||
<StyledASCAvatar
|
||||
size='max'
|
||||
role='user'
|
||||
source={this.state.croppedImage}
|
||||
editing={false}
|
||||
/>
|
||||
<StyledASCAvatar
|
||||
size='big'
|
||||
role='user'
|
||||
source={this.state.croppedImage}
|
||||
editing={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.hasMaxSizeError &&
|
||||
<Text.Body as='span' color="#ED7309" isBold={true}>
|
||||
{this.props.maxSizeErrorLabel}
|
||||
</Text.Body>
|
||||
}
|
||||
</StyledAvatarContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
defaultImage: null,
|
||||
croppedImage: null,
|
||||
visible: props.value
|
||||
};
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
|
||||
this.onCropImage = this.onCropImage.bind(this);
|
||||
this.onCloseEditor = this.onCloseEditor.bind(this);
|
||||
|
||||
this.onFileLoad = this.onFileLoad.bind(this);
|
||||
this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
|
||||
|
||||
}
|
||||
onFileLoad(file){
|
||||
this.setState({ defaultImage: file });
|
||||
}
|
||||
onSaveButtonClick() {
|
||||
this.props.onSave({
|
||||
defaultImage: this.state.defaultImage,
|
||||
croppedImage: this.state.croppedImage
|
||||
});
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
onCloseEditor() {
|
||||
this.setState({
|
||||
croppedImage: null
|
||||
});
|
||||
}
|
||||
onCropImage(result) {
|
||||
this.setState({
|
||||
croppedImage: result
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.setState({ visible: false });
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.visible !== prevProps.visible) {
|
||||
this.setState({ visible: this.props.visible });
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ModalDialog
|
||||
visible={this.state.visible}
|
||||
headerContent={this.props.headerLabel}
|
||||
bodyContent={
|
||||
<AvatarEditorBody
|
||||
maxSize={this.props.maxSize}
|
||||
image={this.props.image}
|
||||
onCropImage={this.onCropImage}
|
||||
onCloseEditor={this.onCloseEditor}
|
||||
label={this.props.chooseFileLabel}
|
||||
maxSizeErrorLabel={this.props.maxSizeErrorLabel}
|
||||
onFileLoad={this.onFileLoad}
|
||||
/>
|
||||
}
|
||||
footerContent={[
|
||||
<Button
|
||||
key="SaveBtn"
|
||||
label={this.props.saveButtonLabel}
|
||||
primary={true}
|
||||
onClick={this.onSaveButtonClick}
|
||||
/>,
|
||||
<Button
|
||||
key="CancelBtn"
|
||||
label={this.props.cancelButtonLabel}
|
||||
onClick={this.onClose}
|
||||
style={{ marginLeft: "8px" }}
|
||||
/>
|
||||
]}
|
||||
onClose={this.props.onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AvatarEditor.propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
headerLabel: PropTypes.string,
|
||||
chooseFileLabel: PropTypes.string,
|
||||
saveButtonLabel: PropTypes.string,
|
||||
maxSizeErrorLabel: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
cancelButtonLabel: PropTypes.string,
|
||||
maxSize: PropTypes.number,
|
||||
|
||||
onSave: PropTypes.func,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
AvatarEditor.defaultProps = {
|
||||
visible: false,
|
||||
maxSize: 1, //1MB
|
||||
chooseFileLabel: 'Choose a file',
|
||||
headerLabel: 'Edit Photo',
|
||||
saveButtonLabel: 'Save',
|
||||
cancelButtonLabel: 'Cancel',
|
||||
maxSizeErrorLabel: 'File is too big'
|
||||
};
|
||||
|
||||
export default AvatarEditor;
|
||||
|
||||
|
@ -56,7 +56,7 @@ const RoleWrapper = styled.div`
|
||||
`;
|
||||
|
||||
const ImageStyled = styled.img`
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 50%;
|
||||
|
||||
|
@ -12,6 +12,7 @@ export { default as GroupButtonsMenu } from './components/group-buttons-menu'
|
||||
export { default as TreeMenu } from './components/tree-menu'
|
||||
export { default as TreeNode } from './components/tree-menu-node'
|
||||
export { default as Avatar } from './components/avatar'
|
||||
export { default as AvatarEditor } from './components/avatar-editor'
|
||||
export { default as RequestLoader } from './components/request-loader'
|
||||
export { default as MainButton } from './components/main-button'
|
||||
export { default as ContextMenuButton } from './components/context-menu-button'
|
||||
|
@ -4976,6 +4976,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
|
||||
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
|
||||
|
||||
konva@2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/konva/-/konva-2.5.1.tgz#cca611a9522e831e54cf57c508a1aed3f0ceac25"
|
||||
integrity sha512-YdHEWqmbWPieqIZuLx7JFGm9Ui08hSUaSJ2k2Ml8o5giFgJ0WmxAS0DPXIM+Ty2ADRagOHZfXSJ/skwYqqlwgQ==
|
||||
|
||||
lazy-cache@^0.2.3:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
|
||||
@ -6777,6 +6782,13 @@ react-autosize-textarea@^7.0.0:
|
||||
line-height "^0.3.1"
|
||||
prop-types "^15.5.6"
|
||||
|
||||
react-avatar-edit@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-avatar-edit/-/react-avatar-edit-0.8.3.tgz#0ebf21391328fc255429bdfbc782f795827109bf"
|
||||
integrity sha512-QEedh6DjDCSI7AUsUHHtfhxApCWC5hJAoywxUA5PtUdw03iIjEurgVqPOIt1UBHhU/Zk/9amElRF3oepN9JZSg==
|
||||
dependencies:
|
||||
konva "2.5.1"
|
||||
|
||||
react-custom-scrollbars@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db"
|
||||
|
34
web/ASC.Web.Storybook/stories/avatar-editor/README.md
Normal file
34
web/ASC.Web.Storybook/stories/avatar-editor/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Avatar Editor
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { AvatarEditor } from 'asc-web-components';
|
||||
```
|
||||
|
||||
#### Description
|
||||
|
||||
Required to display user avatar editor on page.
|
||||
|
||||
#### Usage
|
||||
|
||||
```js
|
||||
<AvatarEditor
|
||||
visible={true}
|
||||
onSave={(data) =>{console.log(data.croppedImage, data.defaultImage)}}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Properties
|
||||
|
||||
| Props | Type | Required | Values | Default | Description |
|
||||
| ------------------ | -------- | :------: | ----------------------------------------- | ------------------ | ----------------------------------------------------- |
|
||||
| `visible` | `bool` | - | | `false` | Display avatar editor or not |
|
||||
| `chooseFileLabel` | `string` | - | | `Choose a file` | |
|
||||
| `headerLabel` | `string` | - | | `Edit Photo` | |
|
||||
| `saveButtonLabel` | `string` | - | | `Save` | |
|
||||
| `cancelButtonLabel` | `string` | - | | `Cancel` | |
|
||||
| `maxSizeErrorLabel` | `string` | - | | `File is too big` | |
|
||||
| `maxSize` | `number` | - | | `1` | Max size of image |
|
||||
| `onSave` | `function` | - | | | |
|
||||
| `onClose` | `function` | - | | | |
|
72
web/ASC.Web.Storybook/stories/avatar-editor/index.stories.js
Normal file
72
web/ASC.Web.Storybook/stories/avatar-editor/index.stories.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withKnobs, boolean, text, select } from '@storybook/addon-knobs/react';
|
||||
import withReadme from 'storybook-readme/with-readme';
|
||||
import Readme from './README.md';
|
||||
import { AvatarEditor, Avatar } from 'asc-web-components';
|
||||
import Section from '../../.storybook/decorators/section';
|
||||
|
||||
class AvatarEditorStory extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
userImage: null
|
||||
}
|
||||
|
||||
this.openEditor = this.openEditor.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onSave = this.onSave.bind(this);
|
||||
|
||||
}
|
||||
onSave(result){
|
||||
action('onSave')(result);
|
||||
this.setState({
|
||||
userImage: result.croppedImage,
|
||||
isOpen: false
|
||||
})
|
||||
}
|
||||
openEditor(){
|
||||
this.setState({
|
||||
isOpen: true
|
||||
})
|
||||
}
|
||||
onClose(){
|
||||
action('onClose');
|
||||
this.setState({
|
||||
isOpen: false
|
||||
})
|
||||
}
|
||||
render(){
|
||||
return(
|
||||
<div>
|
||||
<Avatar
|
||||
size='max'
|
||||
role='user'
|
||||
source={this.state.userImage }
|
||||
editing={true}
|
||||
editAction={this.openEditor}
|
||||
/>
|
||||
<AvatarEditor
|
||||
visible={this.state.isOpen}
|
||||
onClose={this.onClose}
|
||||
onSave={this.onSave}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
storiesOf('Components|AvatarEditor', module)
|
||||
.addDecorator(withKnobs)
|
||||
.addDecorator(withReadme(Readme))
|
||||
.add('avatar editor', () => {
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<AvatarEditorStory />
|
||||
</Section>
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue
Block a user