diff --git a/package.json b/package.json index f35f9d71..cfc2add9 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "bootstrap": "^4.1.1", "chart.js": "^2.7.2", "copy-webpack-plugin": "^5.0.3", + "cropper": "^4.0.0", + "font-awesome": "^4.7.0", "interactjs": "^1.4.0-rc.13", "jquery": "^1.9.1", "jsplumb": "^2.10.1", @@ -22,6 +24,7 @@ "react-chartjs-2": "^2.7.2", "react-dom": "^16.4.0", "react-intl": "^2.4.0", + "react-modal": "^3.9.1", "react-redux": "^5.0.7", "react-router-dom": "^4.3.1", "react-scripts": "1.1.4", diff --git a/src/components/ImageEditor.js b/src/components/ImageEditor.js new file mode 100644 index 00000000..ccd4e226 --- /dev/null +++ b/src/components/ImageEditor.js @@ -0,0 +1,245 @@ +import React, {Component} from 'react'; +import Cropper from 'cropperjs'; +import 'font-awesome/css/font-awesome.min.css'; +import 'cropperjs/dist/cropper.css'; +import '../css/ImageEditor.css'; + +class ImageEditor extends Component { + + constructor(props) { + super(props); + + this.state = { + cropped: false, + cropping: false, + previousUrl: '', + type: '', + url: '', + enableEditor: false + } ; + + this.cropper = null; + } + + componentDidMount () { + const {mediaSource} = this.props; + this.setState({ + ...this.state, + url: mediaSource + }) + } + + cropperSetup=()=>{ + + const image = document.getElementsByClassName('image')[0]; + this.cropper = new Cropper(image, { + autoCrop: false, + dragMode: 'move', + background: false, + + crop: ({ detail }) => { + if (detail.width > 0 && detail.height > 0 && !this.state.cropping) { + this.update({ + cropping: true, + }); + } + }, + }); + } + + crop = () => { + const { cropper} = this; + const { cropping, url, type } = this.state; + + if (cropping) { + this.update({ + cropped: true, + cropping: false, + previousUrl: url, + url: cropper.getCroppedCanvas(type === 'image/png' ? {} : { + fillColor: '#fff', + }).toDataURL(type), + }); + cropper.clear(); + } + } + + save = () => { + const { cropper} = this; + const {url, type } = this.state; + + if(cropper) { + this.update({ + previousUrl: url, + url: cropper.getCroppedCanvas(type === 'image/png' ? {} : { + fillColor: '#fff', + }).toDataURL(type), + }); + cropper.clear(); + } + } + + clear = () => { + const { cropping } = this.state; + if (cropping) { + this.cropper.clear(); + this.update({ + cropping: false, + }); + } + } + + restore = () => { + const { previousUrl } = this.state; + if (previousUrl) { + this.setState({ + ...this.state, + cropper: false, + cropping: false, + previousUrl: '', + url: this.state.previousUrl + }, () => { + this.updateCropper(); + } + ); + } + } + + update = (updatedData) => { + this.setState({ + ...this.state, + ...updatedData + }, () => { + if(this.state.cropped) { + this.updateCropper(); + this.props.setMediaSource(this.state.url); + this.props.onClose(); + } + }); + } + + updateCropper = () => { + if(this.cropper) + this.cropper.destroy(); + this.cropper = null; + this.cropperSetup(); + } + + editAction = (e) => { + if(this.state.url!=='') { + const { cropper } = this; + let action = e.currentTarget.dataset.action; + switch (action) { + case 'move': + break; + + case 'crop': + cropper.setDragMode(action); + break; + + case 'zoom-in': + cropper.zoom(0.1); + this.save(); + break; + + case 'zoom-out': + cropper.zoom(-0.1); + this.save(); + break; + + case 'rotate-left': + cropper.rotate(-90); + this.save(); + break; + + case 'rotate-right': + cropper.rotate(90); + this.save(); + break; + + case 'flip-horizontal': + cropper.scaleX(-cropper.getData().scaleX || -1); + this.save(); + break; + + case 'flip-vertical': + cropper.scaleY(-cropper.getData().scaleY || -1); + this.save(); + break; + + default: + } + } + } + + enableEditor = () => { + this.setState({ + ...this.state, + enableEditor: true + }); + this.cropperSetup(); + } + + render () { + return ( +
+
+ editable-img + +
+ {!this.state.enableEditor && +
+ , + +
+ } + {this.state.enableEditor && +
+ + +
+ } + {this.state.enableEditor && +
+ + + + + + + + + +
+ } +
+ ) + } +} + +export default ImageEditor; diff --git a/src/components/MultimediaJSX.js b/src/components/MultimediaJSX.js index 18791c9d..49fa7222 100644 --- a/src/components/MultimediaJSX.js +++ b/src/components/MultimediaJSX.js @@ -40,7 +40,7 @@ export function QuestionOptionsJSX(props){ export function QuestionJSX(props){ let question; - let {questionType, questionData, handleChangeQues, showMedia, speak} = props; + let {questionType, questionData, handleChangeQues, showMedia, speak, setImageEditorSource} = props; if( questionType === MULTIMEDIA.text) question = ( {showMedia(questionData)}} + onClick = {()=>{showMedia(questionData, 'img', setImageEditorSource)}} alt="Question"/> ); @@ -93,7 +93,7 @@ export function QuestionJSX(props){ export function AnswerOptionsJSX(props){ - const {selectOptionType, resetOption, showMedia, speak, options, changeOrder, handleChangeOption, templateType} = props; + const {selectOptionType, resetOption, showMedia, speak, options, changeOrder, handleChangeOption, templateType, setImageEditorSource} = props; // Answer-Options let answerOptions = options.map((option, i) => { if(!option.type) @@ -165,7 +165,7 @@ export function AnswerOptionsJSX(props){
{showMedia(option.data)}} + onClick = {()=>{showMedia(option.data, 'img', setImageEditorSource(i))}} alt="Option"/>
`), - closeButton: false, - modalStyles: { - backgroundColor: "#e5e5e5", - height: "400px", - width: "600px", - maxWidth: "90%" - } - }) - .afterShow(function(modal) { - let closeButton = document.getElementById('close-button'); - closeButton.addEventListener('click', function() { - modal.close(); - }); - }) - .afterClose((modal) => { - modal.destroy(); + closeModal = () => { + this.setState({ + ...this.state, + modalIsOpen: false + }); + } + + showMedia = (imageSource, mediaType = 'img', setImageEditorSource = null) => { + this.setState({ + ...this.state, + modalSource: imageSource, + modalMediaType: mediaType, + modalIsOpen: true, + setImageEditorSource: setImageEditorSource }) - .show(); - }; + } + + showModalWindow = () => { + return ( + + {this.state.modalMediaType === 'img' && + non-editable img + } + {this.state.modalMediaType === 'video' && + } + + + ); + } + + showEditableModalWindow = () => { + return ( + + {this.state.modalMediaType === 'img' && + + } + {this.state.modalMediaType === 'video' && +
+ + +
+ } +
+ ); + } deleteThumbnail = () => { this.setState({ @@ -126,6 +175,13 @@ const withMultimedia = (defaultThumbnail) => (Component) => { }); } + setThumbnail = (url) => { + this.setState({ + ...this.state, + thumbnail: url + }); + } + render() { // Thumbnail @@ -135,7 +191,7 @@ const withMultimedia = (defaultThumbnail) => (Component) => {
{this.showMedia(defaultThumbnail)}} + onClick = {() => {this.showMedia(defaultThumbnail, 'img', this.setThumbnail)}} alt="Thumbnail"/>
); @@ -144,9 +200,9 @@ const withMultimedia = (defaultThumbnail) => (Component) => {
{this.showMedia(this.state.thumbnail)}} + onClick = {() => {this.showMedia(this.state.thumbnail, 'img', this.setThumbnail)}} alt="Thumbnail"/> -
@@ -158,9 +214,11 @@ const withMultimedia = (defaultThumbnail) => (Component) => { {...this.props} srcThumbnail={this.state.thumbnail} userLanguage={this.state.userLanguage} - thumbnail={thumbnail} - insertThumbnail={this.insertThumbnail} + thumbnail={thumbnail} + insertThumbnail={this.insertThumbnail} showMedia={this.showMedia} + ShowModalWindow = {this.showModalWindow} + ShowEditableModalWindow = {this.showEditableModalWindow} /> ); } @@ -169,4 +227,4 @@ const withMultimedia = (defaultThumbnail) => (Component) => { return MultimediaHoc; } -export default withMultimedia \ No newline at end of file +export default withMultimedia diff --git a/src/containers/Builders/CLOZEForm.js b/src/containers/Builders/CLOZEForm.js index 379c33d7..4d384989 100644 --- a/src/containers/Builders/CLOZEForm.js +++ b/src/containers/Builders/CLOZEForm.js @@ -377,6 +377,8 @@ class CLOZEForm extends Component { } var dataentry = new datastore.DatastoreObject(entry.objectId); dataentry.loadAsText((err, metadata, text) => { + if(mediaType === MULTIMEDIA.image) + this.props.showMedia(text, 'img', this.setSourceFromImageEditor); this.setState({ ...this.state, question:{ @@ -422,9 +424,21 @@ class CLOZEForm extends Component { } } + setSourceFromImageEditor = (url) => { + this.setState({ + ...this.state, + question: { + ...this.state.question, + data: url + } + }, () => { + this.checkFormValidation(); + }) + } + render() { const {errors, answers} = this.state; - const { thumbnail, insertThumbnail, showMedia} = this.props; + const { thumbnail, insertThumbnail, showMedia, ShowEditableModalWindow} = this.props; let questionType = this.state.question.type; let inputs = answers.map((ans, i) => { @@ -524,6 +538,7 @@ class CLOZEForm extends Component { showMedia = {showMedia} handleChangeQues = {this.handleChangeQues} speak = {this.speak} + setImageEditorSource = {this.setSourceFromImageEditor} /> } {question_error} @@ -630,6 +645,7 @@ class CLOZEForm extends Component { + ) } diff --git a/src/containers/Builders/FreeTextInputForm.js b/src/containers/Builders/FreeTextInputForm.js index cb98c575..bc77bf00 100644 --- a/src/containers/Builders/FreeTextInputForm.js +++ b/src/containers/Builders/FreeTextInputForm.js @@ -331,6 +331,8 @@ class FreeTextInputForm extends Component { } var dataentry = new datastore.DatastoreObject(entry.objectId); dataentry.loadAsText((err, metadata, text) => { + if(mediaType === MULTIMEDIA.image) + this.props.showMedia(text, 'img', this.setSourceFromImageEditor); this.setState({ ...this.state, currentQuestion:{ @@ -385,6 +387,21 @@ class FreeTextInputForm extends Component { } } + setSourceFromImageEditor = (url) => { + this.setState({ + ...this.state, + currentQuestion: { + ...this.state.currentQuestion, + question: { + ...this.state.currentQuestion.question, + data: url + } + } + }, ()=>{ + this.checkFormValidation(); + }) + } + onDeleteQuestion = () => { const {currentQuestion, questions} = this.state; let updatedQuestions = []; @@ -437,7 +454,7 @@ class FreeTextInputForm extends Component { render() { const {currentQuestion, errors} = this.state; - const {thumbnail, insertThumbnail, showMedia} = this.props; + const {thumbnail, insertThumbnail, showMedia, ShowEditableModalWindow} = this.props; const {id} = currentQuestion; let questionType = currentQuestion.question.type; let placeholder_string = ENTER_ANSWER; @@ -506,6 +523,7 @@ class FreeTextInputForm extends Component { showMedia = {showMedia} handleChangeQues = {this.handleChangeQues} speak = {this.speak} + setImageEditorSource = {this.setSourceFromImageEditor} /> } {question_error} @@ -569,6 +587,7 @@ class FreeTextInputForm extends Component { + ) } diff --git a/src/containers/Builders/GroupAssignmentForm.js b/src/containers/Builders/GroupAssignmentForm.js index 8b2476cd..78d3ecf0 100644 --- a/src/containers/Builders/GroupAssignmentForm.js +++ b/src/containers/Builders/GroupAssignmentForm.js @@ -454,7 +454,10 @@ class GroupAssignmentForm extends Component { } var dataentry = new datastore.DatastoreObject(entry.objectId); dataentry.loadAsText((err, metadata, text) => { - if(groups){ + if(groups) { + if(mediaType === MULTIMEDIA.image) + this.props.showMedia(text, 'img', this.setGroupSourceFromImageEditor(groupNo)); + let {groups} = this.state; groups[groupNo] = {type: mediaType, data: text}; this.setState({ @@ -463,7 +466,10 @@ class GroupAssignmentForm extends Component { },() => { this.checkFormValidation(); }); - } else{ + } else { + if(mediaType === MULTIMEDIA.image) + this.props.showMedia(text, 'img', this.setQuestionSourceFromImageEditor); + let {currentQuestion} = this.state; this.setState({ ...this.state, @@ -552,6 +558,33 @@ class GroupAssignmentForm extends Component { } } + setQuestionSourceFromImageEditor = (url) => { + this.setState({ + ...this.state, + currentQuestion: { + ...this.state.currentQuestion, + question: { + ...this.state.currentQuestion.question, + data: url + } + } + }, () => { + this.checkFormValidation(); + }); + } + + setGroupSourceFromImageEditor = (index) => (url) => { + const {groups} = this.state; + const updatedGroups = groups; + updatedGroups[index].data = url; + this.setState({ + ...this.state, + groups: updatedGroups + }, () => { + this.checkFormValidation(); + }); + } + onDeleteQuestion = () => { const {currentQuestion, questions} = this.state; let updatedQuestions = []; @@ -603,7 +636,7 @@ class GroupAssignmentForm extends Component { render() { const {currentQuestion, errors, groups} = this.state; const {id} = currentQuestion; - const {thumbnail, insertThumbnail, showMedia} = this.props; + const {thumbnail, insertThumbnail, showMedia, ShowEditableModalWindow} = this.props; let questionType = currentQuestion.question.type; let groupOptions = groups.map((group, i) => { @@ -612,10 +645,10 @@ class GroupAssignmentForm extends Component { question = ( [ ,