/* Copyright 2008-2018 Clipperz Srl This file is part of Clipperz, the online password manager. For further information about its features and functionalities please refer to http://www.clipperz.com. * Clipperz is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * Clipperz is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License along with Clipperz. If not, see http://www.gnu.org/licenses/. */ "use strict"; Clipperz.Base.module('Clipperz.PM.UI.Components.Cards'); Clipperz.PM.UI.Components.Cards.EditClass = React.createClass({ //============================================================================ displayName: 'Clipperz.PM.UI.Components.Cards.Edit', propTypes: { 'allTags': React.PropTypes.array, // 'label': React.PropTypes.string /*.isRequired */ , // 'loading': React.PropTypes.bool, }, getInitialState: function() { return { 'draggedFieldReference': null, 'passwordGeneratorFieldReference': null, 'fromFieldPosition': -1, 'toFieldPosition': -1, 'dropPosition': -1, 'skippedFiles': [], }; }, //---------------------------------------------------------------------------- record: function () { return this.props['_record']; }, fields: function () { return this.props['fields']; }, reference: function() { return this.props['_reference']; }, //============================================================================ positionOfField: function (aFieldReference) { return MochiKit.Base.map(MochiKit.Base.itemgetter('_reference'), this.fields()).indexOf(aFieldReference); }, //============================================================================ fieldMoveStart: function(aFieldReference, aFieldPosition, touchStyle) { MochiKit.Async.callLater(0.1, MochiKit.Base.method(this, 'setState', { 'draggedFieldReference': aFieldReference, 'fromFieldPosition': aFieldPosition, 'touchStyle': touchStyle, 'toFieldPosition': -1, 'dropPosition': -1 })); }, dragStart: function (anEvent) { var fieldReference = anEvent.currentTarget.dataset['reference']; var fieldPosition = this.positionOfField(fieldReference); var dragElement = MochiKit.DOM.getElement(fieldReference); var x = anEvent.clientX - dragElement.getBoundingClientRect().left; var y = anEvent.clientY - dragElement.getBoundingClientRect().top; // anEvent.dataTransfer.setDragImage(anEvent.currentTarget, x, y); anEvent.dataTransfer.setDragImage(dragElement, x, y); anEvent.dataTransfer.setData('Text', ""); // Firefox wants this to be defined this.fieldMoveStart(fieldReference, fieldPosition, false); }, touchStart: function (anEvent) { var fieldReference = anEvent.currentTarget.dataset['reference']; var fieldPosition = this.positionOfField(fieldReference); var dragElement = MochiKit.DOM.getElement(fieldReference); this.fieldMoveStart(fieldReference, fieldPosition, true); anEvent.preventDefault(); anEvent.stopPropagation(); }, /* drag: function (anEvent) { //console.log("DRAG", anEvent); }, drop: function (anEvent) { console.log("DROP"); //, anEvent); }, */ fieldMoveEnd: function (anEvent) { var draggedElement = MochiKit.DOM.getElement(anEvent.target.dataset['reference']); var dragPosition = this.state['dropPosition']; // this.state['toFieldPosition'] draggedElement.style.top = ''; if (dragPosition != -1) { var reference = this.props['_reference']; //console.log("MOVE FIELD POSITION", this.state['toFieldPosition'], this.state['draggedFieldReference']); Clipperz.Async.callbacks("Clipperz.PM.UI.Components.Cards.Edit.fieldMoveEnd-moveFieldToPosition", [ MochiKit.Base.method(this.record(), 'moveFieldToPosition', this.state['draggedFieldReference'], dragPosition), MochiKit.Base.partial(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', reference), ], {trace:false}); } else { //console.log("CANCELED FIELD MOVE"); } // Delayed because a quick touch would prevent the state to update correctly otherwise (don't know why) MochiKit.Async.callLater(0.1, MochiKit.Base.method(this, 'setState', { 'draggedFieldReference': null, 'fromFieldPosition': -1, 'toFieldPosition': -1, 'dropPosition': -1 }) ); }, //............................................................................ /* dragEnter: function (anEvent) { //console.log("DRAG ENTER", anEvent.currentTarget.dataset['reference'], this.positionOfField(anEvent.currentTarget.dataset['reference'])); // this.setState({'toFieldPosition': this.positionOfField(anEvent.currentTarget.dataset['reference'])}); }, */ handleOverField: function(aTarget, aClientY) { // console.log("OVER FIELD", aTarget.dataset['index']); var toFieldPosition; var dropPosition; var dragElement = MochiKit.DOM.getElement(aTarget.dataset['reference']); // console.log('target', aTarget); // console.log('element', dragElement); var y = aClientY - dragElement.getBoundingClientRect().top; var h = dragElement.getBoundingClientRect().height; var hoveringIndex; var draggingIndex; var isHoveringTopPart; hoveringIndex = +aTarget.dataset['index']; draggingIndex = +this.state['fromFieldPosition']; isHoveringTopPart = (y < h/2); if (isHoveringTopPart) { dropPosition = hoveringIndex; } else { dropPosition = hoveringIndex + 1; } if (hoveringIndex > draggingIndex) { dropPosition = dropPosition - 1; } toFieldPosition = -1; //console.log("-- ", dropPosition, this.state['dropPosition'], toFieldPosition, this.state['toFieldPosition']); if ((dropPosition != this.state['dropPosition']) || (toFieldPosition != this.state['toFieldPosition'])) { this.setState({'dropPosition': dropPosition, 'toFieldPosition': toFieldPosition}); } }, dragOver: function (anEvent) { //console.log("DRAG OVER", anEvent); //console.log("DRAG OVER", anEvent.currentTarget.dataset['index']); var toFieldPosition; var dropPosition; if (typeof(anEvent.currentTarget.dataset['index']) != 'undefined') { this.handleOverField(anEvent.currentTarget, anEvent.clientY); //console.log(hoveringIndex, draggingIndex, isHoveringTopPart, dropPosition); //console.log("isHoveringTopPart", isHoveringTopPart); } else { // TODO: this case is not handled by touch events // (shouldn't this never happen anyway?) dropPosition = anEvent.currentTarget.dataset['dropIndex']; toFieldPosition = dropPosition; //console.log("-- ", dropPosition, this.state['dropPosition'], toFieldPosition, this.state['toFieldPosition']); if ((dropPosition != this.state['dropPosition']) || (toFieldPosition != this.state['toFieldPosition'])) { this.setState({'dropPosition': dropPosition, 'toFieldPosition': toFieldPosition}); } } anEvent.stopPropagation(); }, touchMove: function(anEvent) { var hoveredElement; var touch = anEvent.touches[0]; var draggedElement = MochiKit.DOM.getElement(anEvent.target.dataset['reference']); this.setDraggedElementTopValue(draggedElement, touch.clientY); hoveredElement = this.getElementBelowDraggedElement(touch.clientX, touch.clientY, draggedElement); if (hoveredElement) { if (hoveredElement.className == 'dropArea') { this.handleOverDropTarget(hoveredElement.dataset['dropIndex']); } else { var cardFieldElement = this.getCardFieldFromElement(hoveredElement); if (cardFieldElement) { this.handleOverField(cardFieldElement, anEvent.touches[0].clientY); } else { // console.log(hoveredElement); } } } anEvent.preventDefault(); anEvent.stopPropagation(); }, getElementBelowDraggedElement: function(aClientX, aClientY, aDraggedElement) { var result; aDraggedElement.style.display = 'none'; result = document.elementFromPoint(aClientX, aClientY); aDraggedElement.style.display = ''; return result; }, // TODO: This method uses empirical values, it could meke sense to reference actual // elements' measures setDraggedElementTopValue: function(aDraggedElement, aClientY) { var topValue; topValue = aClientY - aDraggedElement.getBoundingClientRect().height; topValue = (this.props.style != 'narrow') ? topValue-40 : topValue+54; aDraggedElement.style.top = topValue+'px'; }, /** Returns anElement itself if it is a card field, or the closest ancestor of * anElement holding the data-index property, or false if no such an ancestor * exists. */ getCardFieldFromElement: function(anElement) { var result; if (typeof(anElement.dataset['index']) != 'undefined') { result = anElement; } else { var parentElement = anElement.parentElement; if (parentElement) { result = this.getCardFieldFromElement(parentElement); } else { result = false; } } return result; }, /* dragLeave: function (anEvent) { //console.log("DRAG LEAVE", anEvent.currentTarget.dataset['reference'], this.positionOfField(anEvent.currentTarget.dataset['reference'])); // this.setState({'dropPosition': -1}); }, */ //============================================================================ /* dragStartDropTarget: function (anEvent) { //console.log("TARGET: DRAG START"); }, dragDropTarget: function (anEvent) { //console.log("TARGET: DRAG"); }, dropDropTarget: function (anEvent) { //console.log("TARGET: DROP"); }, dragEndDropTarget: function (anEvent) { //console.log("TARGET: DRAG END"); }, //............................................................................ dragEnterDropTarget: function (anEvent) { //console.log("TARGET: DRAG ENTER"); }, */ handleOverDropTarget: function(toFieldPosition) { //console.log("OVER DROP TARGET", toFieldPosition); if (toFieldPosition != this.state['toFieldPosition']) { //console.log("TARGET: DRAG OVER - READY TO DROP", anEvent.currentTarget.dataset['dropIndex']); this.setState({'toFieldPosition':toFieldPosition}); } }, dragOverDropTarget: function (anEvent) { //console.log("DRAG OVER DROP TARGET", anEvent.currentTarget.dataset['dropIndex']/*, anEvent*/); var toFieldPosition = anEvent.currentTarget.dataset['dropIndex']; this.handleOverDropTarget(toFieldPosition); anEvent.stopPropagation(); }, dragLeaveDropTarget: function (anEvent) { //console.log("TARGET: DRAG LEAVE"); if (-1 != this.state['toFieldPosition']) { //console.log("READY TO DROP", anEvent.currentTarget.dataset['dropIndex']); MochiKit.Async.callLater(0.5, MochiKit.Base.bind(function () { //console.log("TARGET: DRAG LEAVE #####"); this.setState({'toFieldPosition':-1}); }, this)) } }, //============================================================================ setValueFromPasswordGenerator: function (aField, aTextAreaRef) { var reference = this.props['_reference']; var self = this; return function (aValue) { aField.setValue(aValue); React.findDOMNode(self.refs[aTextAreaRef]).value = aValue; self.setState({'passwordGeneratorFieldReference':null}); MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', reference); }; }, handleChange: function (anObject , aMethodName) { var reference = this.props['_reference']; var method = MochiKit.Base.method(anObject, aMethodName); return function (anEvent) { method(anEvent.target.value); MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', reference); }; }, handleKeyDown: function (aField) { var self = this; return function (anEvent) { switch (anEvent.keyCode) { case 9: // tab var fieldReferences = MochiKit.Base.map(function (aValue) { return aValue['_reference']}, self.fields()); var fieldIndex = fieldReferences.indexOf(aField.reference()); if (fieldIndex == fieldReferences.length - 1) { Clipperz.Async.callbacks("Clipperz.PM.UI.Components.Cards.Edit.handleKeyDown", [ MochiKit.Base.method(aField, 'isEmpty'), Clipperz.Async.deferredIf('isEmpty',[ ], [ MochiKit.Base.method(anEvent, 'preventDefault'), MochiKit.Base.method(self, 'addNewField'), // TODO: set the focus to the newly created field // hints: http://stackoverflow.com/questions/24248234/react-js-set-input-value-from-sibling-component ]) ], {trace:false}); } break; } }; }, removeField: function (aField) { var reference = this.props['_reference']; var record = this.record(); return function (anEvent) { record.removeField(aField); MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', reference); }; }, addNewField: function (anEvent) { var reference = this.props['_reference']; this.record().addField({'label':"", 'value':"", 'isHidden':false}); MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', reference); }, showPasswordGenerator: function (aField) { var result; if (aField['actionType'] == 'PASSWORD') { var reference = this.props['_reference']; var self = this; result = function (anEvent) { self.setState({'passwordGeneratorFieldReference':aField['_reference']}); }; } else { result = null; } return result; }, toggleLock: function (aField) { var reference = this.props['_reference']; return function (anEvent) { //console.log("FIELD", aField.isHidden(), aField); // aField.setIsHidden(!aField.isHidden()); // MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', reference); return Clipperz.Async.callbacks("Clipperz.PM.UI.Components.Cards.Edit.toggleLock", [ MochiKit.Base.method(aField, 'isHidden'), MochiKit.Base.operator.lognot, MochiKit.Base.method(aField, 'setIsHidden'), function (aValue) { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', reference); }, ], {trace:false}); }; }, closePasswordGenerator: function () { this.setState({'passwordGeneratorFieldReference': null}); }, removeDirectLogin: function(aDirectLoginReference) { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'removeDirectLogin', {'record':this.record(), 'directLoginReference':aDirectLoginReference}); }, //============================================================================ renderLabel: function (aLabel) { return React.DOM.input({'className':'cardLabel', 'autoFocus':true, 'onChange':this.handleChange(this.record(), 'setLabel'), 'defaultValue':aLabel, 'key':this.props['_reference'] + '_label', 'placeholder': "card title"}); }, renderNotes: function (someNotes) { // return React.DOM.textarea({'className':'cardNotes', 'onChange':this.handleChange(this.record(), 'setNotes'), 'defaultValue':someNotes, 'key':this.props['_reference'] + '_notes', 'placeholder': "notes"}); return React.DOM.div({'className':'cardNotes'}, [ Clipperz.PM.UI.Components.Cards.TextArea({'onChange':this.handleChange(this.record(), 'setNotes'), 'defaultValue':someNotes, 'key':this.props['_reference'] + '_notes', 'placeholder': "notes"}) ]); }, //............................................................................ cleanupTags: function (someTags) { return MochiKit.Base.filter(Clipperz.PM.DataModel.Record.isRegularTag, someTags || []).sort(Clipperz.Base.caseInsensitiveCompare); }, renderTags: function (someTags) { return Clipperz.PM.UI.Components.Cards.TagEditor({'selectedTags':this.cleanupTags(someTags), 'allTags':this.cleanupTags(this.props['allTags']), 'readOnly':false }); }, //............................................................................ renderField: function (aField) { var ref = aField['_reference']; var cardFieldClasses = {}; var cardFieldValueClasses = {}; var field = aField['_field']; var fieldValueRef = ref + '_textarea'; //console.log("RENDER FIELD", aField); cardFieldClasses['cardField'] = true; cardFieldClasses[aField['actionType']] = true; cardFieldClasses['hidden'] = aField['isHidden']; if (this.state['draggedFieldReference'] == aField['_reference']) { if (this.state.touchStyle) { cardFieldClasses['draggedTouch'] = true; } else { cardFieldClasses['dragged'] = true; } } cardFieldValueClasses['fieldValue'] = true; cardFieldValueClasses[aField['actionType']] = true; cardFieldValueClasses['hidden'] = aField['isHidden']; return React.DOM.div({'className':Clipperz.PM.UI.Components.classNames(cardFieldClasses), 'id':ref, 'key':ref, 'data-reference':ref, 'data-index':this.positionOfField(ref), 'onDragOver':this.dragOver, }, [ React.DOM.div({'className':'fieldEditAction'}, [ React.DOM.span({'className':'removeField', 'onClick':this.removeField(field)}, "remove field"), React.DOM.div({'className':'dragHandlerContainer', 'draggable':true, 'data-reference':ref, 'data-document-id':ref, 'data-index':this.positionOfField(ref), 'onDragStart':this.dragStart, 'onDragEnd':this.fieldMoveEnd, 'onTouchStart': this.touchStart, 'onTouchMove': this.touchMove, 'onTouchEnd': this.fieldMoveEnd, }, React.DOM.span({ 'className':'dragHandler', 'data-reference':ref, 'data-document-id':ref, 'data-index':this.positionOfField(ref), }, ' ')) ]), React.DOM.div({'className':'fieldValues'}, [ React.DOM.div({'className':'fieldLabel'}, [ React.DOM.input({'_className_':'_fieldLabel_', 'onChange':this.handleChange(field, 'setLabel'), 'defaultValue':aField['label'], 'placeholder': "label"}), ]), React.DOM.div({'className':'fieldValue'}, [ (ref == this.state['passwordGeneratorFieldReference']) ? Clipperz.PM.UI.Components.Cards.PasswordGenerator({'field':aField, 'setValueCallback':this.setValueFromPasswordGenerator(field, fieldValueRef), 'closeClallback':this.closePasswordGenerator, 'preferences':this.props['preferences']}) : null, Clipperz.PM.UI.Components.Cards.TextArea({'className':Clipperz.PM.UI.Components.classNames(cardFieldValueClasses), 'onChange':this.handleChange(field, 'setValue'), 'onKeyDown':this.handleKeyDown(field), 'defaultValue':aField['value'], 'placeholder':(aField['actionType'].toLowerCase() == 'password')?'':"value", 'ref':fieldValueRef}), ]) ]), React.DOM.div({'className':'fieldAction'}, [ React.DOM.span({'className':'action ' + aField['actionType'], 'onClick':this.showPasswordGenerator(aField)}, aField['actionType'].toLowerCase() == 'password' ? 'password generator' : aField['actionType'].toLowerCase()), React.DOM.span({'className':'toggleLock', 'onClick':this.toggleLock(field)}, aField['isHidden'] ? "locked" : "unlocked") ]) ]); }, updateRenderedFieldsWithDropArea: function (someRenderedFields) { var dragFrom = this.state['fromFieldPosition'] var dropTo = this.state['dropPosition']; var dropAreaPositionIndex = dropTo != -1 ? dropTo : dragFrom; var dropArea = React.DOM.div({'className':'dropArea', 'key':'fieldDropArea', 'data-drop-index':dropAreaPositionIndex, // 'onDragStart':this.dragStartDropTarget, // 'onDrag':this.dragDropTarget, // 'onDragEnter':this.dragEnterDropTarget, 'onDragOver': this.dragOverDropTarget, 'onDragLeave': this.dragLeaveDropTarget, // 'onDrop': this.dropDropTarget, // 'onDragEnd':this.dragEndDropTarget }); var dropAreaNodeIndex = (dropAreaPositionIndex < dragFrom) ? dropAreaPositionIndex : dropAreaPositionIndex + 1; //console.log("DROP", dropTo, dropAreaPositionIndex); someRenderedFields.splice(dropAreaNodeIndex, 0, dropArea); return someRenderedFields; }, renderFields: function (someFields) { var renderedFields; renderedFields = MochiKit.Base.map(this.renderField, someFields); if (this.state['draggedFieldReference'] != null) { renderedFields = this.updateRenderedFieldsWithDropArea(renderedFields); } return React.DOM.div({'className':'cardFields' /*, 'dropzone':'move'*/}, renderedFields); }, renderAddNewField: function () { // return React.DOM.div({'className':'newCardField', 'onClick':this.addNewField}, "add new field"); return React.DOM.div({'className':'newCardField', 'onClick':this.addNewField}, [ React.DOM.div({'className':'fieldGhostShadow'}, [ React.DOM.div({'className':'label'}, ""), React.DOM.div({'className':'value'}, ""), ]), React.DOM.div({'className':'addNewFieldButton'}, "add new field"), ]); }, //............................................................................ renderDirectLogin: function (aDirectLogin) { return React.DOM.div({'className':'cardDirectLogin', 'key':aDirectLogin['_reference']}, [ React.DOM.a({ 'className': 'removeDirectLogin', 'onClick': MochiKit.Base.method(this, 'removeDirectLogin', aDirectLogin['_reference']) }, "remove field"), React.DOM.span({'className':'directLoginLabel'}, aDirectLogin['label']), // React.DOM.div({'className':'directLoginAction action'}, 'DIRECT LOGIN') ]); }, renderDirectLogins: function (someDirectLogins) { return React.DOM.div({'className':'cardDirectLogins'}, MochiKit.Base.map(this.renderDirectLogin, someDirectLogins)); }, //............................................................................ handleRemoveAttachment: function(anAttachment) { // MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'cancelAttachment', anAttachment); MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'removeAttachment', {'record':this.record(), 'attachment': anAttachment}); }, //............................................................................ uploadFiles: function(someFiles) { var i; //console.log("uploadFiles", someFiles); var newSkippedFiles = []; for (i = 0; i < someFiles.length; i++) { var file = someFiles[i]; //console.log("uploadFiles - file", file); if (file.size <= Clipperz.PM.DataModel.Attachment.MAX_ATTACHMENT_SIZE) { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'addAttachment', {'record':this.record(), 'file':file}); } else { newSkippedFiles.push(file); this.setState({'skippedFiles': newSkippedFiles}); } } // TODO: check compatibility with all browsers this.refs['attachmentInput'].value = null; }, //............................................................................ handleFileSelect: function(anEvent) { this.uploadFiles(anEvent.target.files); }, //............................................................................ handleOnDrop: function (anEvent) { anEvent.preventDefault(); this.uploadFiles(anEvent.dataTransfer.files); }, handleOnDragOver: function (anEvent) { // Somehow necessary: // http://enome.github.io/javascript/2014/03/24/drag-and-drop-with-react-js.html // https://code.google.com/p/chromium/issues/detail?id=168387 // http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html anEvent.stopPropagation(); anEvent.preventDefault(); anEvent.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy. }, //............................................................................ renderSkippedFiles: function () { var result; result = null; if (this.state['skippedFiles'].length > 0) { result = React.DOM.div({'className': 'skippedFiles'},[ React.DOM.p({}, "The following files exceed the size limit of " + filesize(Clipperz.PM.DataModel.Attachment.MAX_ATTACHMENT_SIZE)), React.DOM.ul({}, MochiKit.Base.map(function(aFile) { return React.DOM.li({}, [ React.DOM.span({'className': 'filename'}, aFile.name), React.DOM.span({}, " (" + filesize(aFile.size) + ")"), ]); }, this.state['skippedFiles']) ), React.DOM.a({ 'onClick': MochiKit.Base.method(this, 'setState', {'skippedFiles': []}), }, 'close'), ]); } return result; }, renderAttachmentProgress: function (aStatus, aServerStatus, aProgress) { var result; var queueOperationsInProgress = (aStatus && aStatus != 'DONE' && aStatus != 'CANCELED' && aStatus != 'FAILED'); result = null; if (aStatus == 'UPLOADING' || aStatus == 'DOWNLOADING') { result = Clipperz.PM.UI.Components.RadialProgressIndicator({ 'progress': aProgress, 'border': 1 }); } else if (queueOperationsInProgress) { result = Clipperz.PM.UI.Components.RadialProgressIndicator({ 'progress': 0, 'border': 1, 'additionalClasses': ['waiting'], }); } return result; }, renderAttachmentStatus: function (aStatus, aServerStatus, aProgress) { var result; var status = aStatus ? aStatus : false; var queueOperationsInProgress = (status && (status != 'DONE' && status != 'CANCELED' && status != 'FAILED')); result = null; if (status == 'FAILED') { result = React.DOM.span({'className': 'failed'}, "failed"); } else if (status == 'UPLOADING' || status == 'DOWNLOADING') { var actionSymbol = (status == 'UPLOADING') ? "\u2b06" : "\u2b07"; result = React.DOM.span({'className': 'progressStatus'}, actionSymbol + Math.floor(aProgress*100) + '%'); } else if (aServerStatus != 'AVAILABLE' && ! this.props['_isBrandNew']) { switch(status) { case 'CANCELED': result = React.DOM.span({'className': 'broken'}, "canceled"); break; case 'DONE': result = React.DOM.span({'className': 'done'}, "done"); break; case false: result = React.DOM.span({'className': 'broken'}, "failed"); break; default: result = React.DOM.span({'className': 'waiting'}, "\u2b06waiting"); } } else if (queueOperationsInProgress) { result = React.DOM.span({'className': 'waiting'}, "\u2b07waiting"); } else if (this.props['_isBrandNew']) { result = React.DOM.span({'className': 'waiting'}, "waiting save"); } return result; }, renderAttachmentActions: function (aStatus, aServerStatus, anAttachment) { var result; result = null; if (aStatus != 'DOWNLOADING') { result = React.DOM.a({ 'className': 'remove', 'onClick': MochiKit.Base.method(this, 'handleRemoveAttachment', anAttachment), }, "remove field"); } return result; }, renderAttachment: function (anAttachment) { var queueInfo = this.props['attachmentQueueInfo'].elementFetchCallback(anAttachment._reference) || []; var queueStatus = queueInfo['status']; var serverStatus = this.props['attachmentServerStatus'][anAttachment._reference]; var queueOperationsInProgress = (queueStatus && queueStatus != 'DONE' && queueStatus != 'CANCELED' && queueStatus != 'FAILED'); var broken = (! serverStatus && ! queueOperationsInProgress && ! this.props['_isBrandNew']); return React.DOM.li({ 'className': (broken) ? 'broken' : '', 'key': anAttachment._reference }, [ React.DOM.span({'className': 'contentType'}, Clipperz.PM.DataModel.Attachment.contentTypeIcon(anAttachment.contentType)), React.DOM.span({'className': 'meta'}, [ React.DOM.span({'className': 'name'}, anAttachment.name), React.DOM.span({'className': 'size'}, filesize(anAttachment.size)), ]), React.DOM.span({'className': 'status'}, this.renderAttachmentStatus (queueStatus, serverStatus, queueInfo['requestProgress'])), React.DOM.span({'className': 'progress'}, this.renderAttachmentProgress(queueStatus, serverStatus, queueInfo['requestProgress'])), React.DOM.span({'className': 'actions'}, this.renderAttachmentActions (queueStatus, serverStatus, anAttachment['_attachment'])), ]) }, renderAttachments: function(someAttachments) { return React.DOM.div({'className':'cardAttachmentWrapper'}, [ React.DOM.div({'className': 'cardAttachments'}, [ React.DOM.h3({'className': 'summaryText'}, "Attachments"), // React.DOM.p({'className': 'summaryText'}, someAttachments.length + ' files attached'), this.renderSkippedFiles(), React.DOM.ul({'className': 'attachmentList'}, MochiKit.Base.map(MochiKit.Base.method(this, 'renderAttachment'), someAttachments) ) ]), React.DOM.div({ 'className': 'cardUploadAttachments', 'onClick': MochiKit.Base.bind(function() { this.refs['attachmentInput'].click() }, this), 'onDragOver': this.handleOnDragOver, 'onDrop': this.handleOnDrop, },[ React.DOM.p({}, "Drag and drop your files here"), React.DOM.p({}, "or"), React.DOM.input({ 'type': 'file', 'id': 'attachmentInput', 'className': 'attachmentInput', 'name': 'attachmentInput', 'ref': 'attachmentInput', 'onChange': this.handleFileSelect, 'multiple': true }), React.DOM.a({ 'className': 'button', 'onDragOver': this.handleOnDragOver, 'onDrop': this.handleOnDrop, }, "select files"), ]) ]) }, //............................................................................ render: function () { var classes = { 'edit': true } return React.DOM.div({'className':'editWrapper'}, [ this.props['showGlobalMask'] ? null : React.DOM.div({'className':'mask'}), React.DOM.div({'className':Clipperz.PM.UI.Components.classNames(classes)},[ Clipperz.PM.UI.Components.Cards.CommandToolbar(this.props), Clipperz.PM.UI.Components.Cards.EditToolbar(this.props), React.DOM.div({'className':'content'}, [ this.renderLabel(this.props['label']), this.renderTags(this.props['tags']), this.renderFields(this.fields()), this.renderAddNewField(), this.renderAttachments(MochiKit.Base.values(this.props['attachments'])), this.renderNotes(this.props['notes']), this.renderDirectLogins(this.props['directLogins']), ]) ]), this.props['ask'] ? Clipperz.PM.UI.Components.DialogBox(this.props['ask']) : null ]); }, //========================================================================= }); Clipperz.PM.UI.Components.Cards.Edit = React.createFactory(Clipperz.PM.UI.Components.Cards.EditClass);