diff --git a/js/field_set.js b/js/field_set.js index 952e6d789..ee0553aa8 100644 --- a/js/field_set.js +++ b/js/field_set.js @@ -53,15 +53,12 @@ $(function() this.element .bind('field_change', function(oEvent, oData){ - //console.log('field_set: field_change'); me._onFieldChange(oEvent, oData); }) .bind('update_form', function(oEvent, oData){ - //console.log('field_set: update_form'); me._onUpdateForm(oEvent, oData); }) .bind('get_current_values', function(oEvent, oData){ - //console.log('field_set: get_current_values'); return me._onGetCurrentValues(oEvent, oData); }) .bind('validate', function(oEvent, oData){ @@ -69,7 +66,7 @@ $(function() { oData = {}; } - //console.log('field_set: validate'); + return me._onValidate(oEvent, oData); }); @@ -166,7 +163,7 @@ $(function() // Validate the field var oResult = this.getField(oData.name).triggerHandler('validate', {touched_fields_only: true}); - if (!oResult.is_valid) + if ( (oResult !== undefined) && !oResult.is_valid) { this.options.is_valid = false; } diff --git a/sources/autoload.php b/sources/autoload.php index 91d4c0faa..f05bfbc3b 100644 --- a/sources/autoload.php +++ b/sources/autoload.php @@ -23,6 +23,7 @@ require_once APPROOT . 'sources/form/form.class.inc.php'; require_once APPROOT . 'sources/form/formmanager.class.inc.php'; require_once APPROOT . 'sources/form/field/field.class.inc.php'; +require_once APPROOT . 'sources/form/field/fileuploadfield.class.inc.php'; require_once APPROOT . 'sources/form/field/subformfield.class.inc.php'; require_once APPROOT . 'sources/form/field/textfield.class.inc.php'; require_once APPROOT . 'sources/form/field/hiddenfield.class.inc.php'; @@ -52,3 +53,4 @@ require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bssimplefieldre require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php'; require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php'; require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bssubformfieldrenderer.class.inc.php'; +require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bsfileuploadfieldrenderer.class.inc.php'; diff --git a/sources/form/field/fileuploadfield.class.inc.php b/sources/form/field/fileuploadfield.class.inc.php new file mode 100644 index 000000000..a218428b7 --- /dev/null +++ b/sources/form/field/fileuploadfield.class.inc.php @@ -0,0 +1,115 @@ + + +namespace Combodo\iTop\Form\Field; + +use \Combodo\iTop\Form\Field\Field; + +/** + * Description of FileUploadField + * + * @author Guillaume Lajarige + */ +class FileUploadField extends Field +{ + const DEFAULT_ALLOW_DELETE = true; + + protected $sTransactionId; + protected $oObject; + protected $sUploadEndpoint; + protected $sDownloadEndpoint; + protected $bAllowDelete; + + public function __construct($sId, \Closure $onFinalizeCallback = null) + { + $this->sTransactionId = null; + $this->oObject = null; + $this->sUploadEndpoint = null; + $this->sDownloadEndpoint = null; + $this->bAllowDelete = static::DEFAULT_ALLOW_DELETE; + + parent::__construct($sId, $onFinalizeCallback); + } + + /** + * Returns the transaction id for the field. + * + * @return string + */ + public function GetTransactionId() + { + return $this->sTransactionId; + } + + /** + * + * @param string $sTransactionId + * @return \Combodo\iTop\Form\Field\FileUploadField + */ + public function SetTransactionId($sTransactionId) + { + $this->sTransactionId = $sTransactionId; + return $this; + } + + public function GetObject() + { + return $this->oObject; + } + + public function SetObject($oObject) + { + $this->oObject = $oObject; + return $this; + } + + public function GetUploadEndpoint() + { + return $this->sUploadEndpoint; + } + + public function SetUploadEndpoint($sUploadEndpoint) + { + $this->sUploadEndpoint = $sUploadEndpoint; + return $this; + } + + public function GetDownloadEndpoint() + { + return $this->sDownloadEndpoint; + } + + public function SetDownloadEndpoint($sDownloadEndpoint) + { + $this->sDownloadEndpoint = $sDownloadEndpoint; + return $this; + } + + public function GetAllowDelete() + { + return $this->bAllowDelete; + } + + public function SetAllowDelete($bAllowDelete) + { + $this->bAllowDelete = (boolean) $bAllowDelete; + return $this; + } + +} diff --git a/sources/renderer/bootstrap/bsformrenderer.class.inc.php b/sources/renderer/bootstrap/bsformrenderer.class.inc.php index 77a3007d9..87d75c32a 100644 --- a/sources/renderer/bootstrap/bsformrenderer.class.inc.php +++ b/sources/renderer/bootstrap/bsformrenderer.class.inc.php @@ -52,6 +52,7 @@ class BsFormRenderer extends FormRenderer $this->AddSupportedField('SubFormField', 'BsSubFormFieldRenderer'); $this->AddSupportedField('SelectObjectField', 'BsSelectObjectFieldRenderer'); $this->AddSupportedField('LinkedSetField', 'BsLinkedSetFieldRenderer'); + $this->AddSupportedField('FileUploadField', 'BsFileUploadFieldRenderer'); } } diff --git a/sources/renderer/bootstrap/fieldrenderer/bsfileuploadfieldrenderer.class.inc.php b/sources/renderer/bootstrap/fieldrenderer/bsfileuploadfieldrenderer.class.inc.php new file mode 100644 index 000000000..4d5bacbbd --- /dev/null +++ b/sources/renderer/bootstrap/fieldrenderer/bsfileuploadfieldrenderer.class.inc.php @@ -0,0 +1,237 @@ + + +namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer; + +use \utils; +use \Dict; +use \UserRights; +use \InlineImage; +use \DBObjectSet; +use \DBObjectSearch; +use \MetaModel; +use \Combodo\iTop\Renderer\FieldRenderer; +use \Combodo\iTop\Renderer\RenderingOutput; +use \Combodo\iTop\Form\Field\LinkedSetField; + +/** + * Description of BsFileUploadFieldRenderer + * + * @author Guillaume Lajarige + */ +class BsFileUploadFieldRenderer extends FieldRenderer +{ + + /** + * Returns a RenderingOutput for the FieldRenderer's Field + * + * @return \Combodo\iTop\Renderer\RenderingOutput + */ + public function Render() + { + $oOutput = new RenderingOutput(); + + $sObjectClass = get_class($this->oField->GetObject()); + $sIsDeleteAllowed = ($this->oField->GetAllowDelete()) ? 'true' : 'false'; + $sDeleteBtn = Dict::S('Portal:Button:Delete'); + $sTempId = session_id() . '_' . $this->oField->GetTransactionId(); + $sUploadDropZoneLabel = Dict::S('Portal:Attachments:DropZone:Message'); + + // Starting field container + $oOutput->AddHtml('
'); + // Field label + if ($this->oField->GetLabel() !== '') + { + $oOutput->AddHtml(''); + } + // Field feedback + $oOutput->AddHtml('
'); + // Starting files container + $oOutput->AddHtml('
'); + // Files list + $oOutput->AddHtml('
'); + $this->PrepareExistingFiles($oOutput); + $oOutput->Addhtml('
'); + // TODO : Add max upload size when itop attachment has been refactored + $oOutput->AddHtml('
' . Dict::S('Attachments:AddAttachment') . '
'); + // Ending files container + $oOutput->AddHtml('
'); + // Ending field container + $oOutput->AddHtml('
'); + + // JS for file upload + // Note : This is based on itop-attachement/main.attachments.php + $oOutput->AddJs( +<<oField->GetGlobalId()}').fileupload({ + url: '{$this->oField->GetUploadEndpoint()}', + formData: { operation: 'add', temp_id: '{$sTempId}', object_class: '{$sObjectClass}', 'field_name': '{$this->oField->GetId()}' }, + dataType: 'json', + pasteZone: null, // Don't accept files via Chrome's copy/paste + done: function (e, data) { + if(data.result.error !== undefined) + { + console.log(data.result.error); + } + else + { + var sDownloadLink = '{$this->oField->GetDownloadEndpoint()}'.replace(/-sAttId-/, data.result.att_id); + + $(this).closest('.fileupload_field_content').find('.attachments_container').append( + '' + ); + + $('#display_attachment_'+data.result.att_id+' :button').click(function(oEvent){ + oEvent.preventDefault(); + RemoveAttachment(data.result.att_id); + }); + + $('#display_attachment_'+data.result.att_id).hover( function(){ + $(this).children(':button').toggleClass('hidden'); + }); + } + }, + start: function() { + // Scrolling to dropzone so the user can see that attachments are uploaded + $(this)[0].scrollIntoView(); + // Showing loader + $(this).closest('.upload_container').find('.loader').css('visibility', 'visible'); + }, + stop: function() { + // Hiding the loader + $(this).closest('.upload_container').find('.loader').css('visibility', 'hidden'); + // Adding this field to the touched fields of the field set so the cancel event is called if necessary + $(this).closest(".field_set").trigger("field_change", { + id: '{$this->oField->GetGlobalId()}', + name: '{$this->oField->GetId()}' + }); + } + }); + + $('.attachments_container .attachment :button').click(function(oEvent){ + oEvent.preventDefault(); + RemoveAttachment($(this).closest('.attachment').find(':input[name="attachments[]"]').val()); + }); + + if($sIsDeleteAllowed) + { + $('.attachment').hover( function(){ + $(this).find(':button').toggleClass('hidden'); + }); + } + + // Handles a drag / drop overlay + if($('#drag_overlay').length === 0) + { + $('body').append( $('
{$sUploadDropZoneLabel}
') ); + } + + // Handles highlighting of the drop zone + // Note : This is inspired by itop-attachments/main.attachments.php + $(document).on('dragover', function(oEvent){ + var bFiles = false; + if (oEvent.dataTransfer && oEvent.dataTransfer.types) + { + for (var i = 0; i < oEvent.dataTransfer.types.length; i++) + { + if (oEvent.dataTransfer.types[i] == "application/x-moz-nativeimage") + { + bFiles = false; // mozilla contains "Files" in the types list when dragging images inside the page, but it also contains "application/x-moz-nativeimage" before + break; + } + + if (oEvent.dataTransfer.types[i] == "Files") + { + bFiles = true; + break; + } + } + } + + if (!bFiles) return; // Not dragging files + + var oDropZone = $('#drag_overlay'); + var oTimeout = window.dropZoneTimeout; + // This is to detect when there is no drag over because there is no "drag out" event + if (!oTimeout) { + oDropZone.removeClass('drag_out').addClass('drag_in'); + } else { + clearTimeout(oTimeout); + } + window.dropZoneTimeout = setTimeout(function () { + window.dropZoneTimeout = null; + oDropZone.removeClass('drag_in').addClass('drag_out'); + }, 200); + }); + +EOF + ); + + return $oOutput; + } + + /** + * + * @param RenderingOutput $oOutput + */ + protected function PrepareExistingFiles(RenderingOutput &$oOutput) + { + $sObjectClass = get_class($this->oField->GetObject()); + $sDeleteBtn = Dict::S('Portal:Button:Delete'); + + $oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE item_class = :class AND item_id = :item_id"); + $oSet = new DBObjectSet($oSearch, array(), array('class' => $sObjectClass, 'item_id' => $this->oField->GetObject()->GetKey())); + while ($oAttachment = $oSet->Fetch()) + { + $iAttId = $oAttachment->GetKey(); + $oDoc = $oAttachment->Get('contents'); + $sFileName = htmlentities($oDoc->GetFileName(), ENT_QUOTES, 'UTF-8'); + $sIcon = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-attachments/icons/image.png'; + $sPreview = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; + $sDownloadLink = str_replace('-sAttachmentId-', $iAttId, $this->oField->GetDownloadEndpoint()); + + $oOutput->Addhtml( +<< + +
+
{$sFileName}
+ +
+ + +EOF + ); + } + } + +} diff --git a/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php b/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php index eb4016147..23046761e 100644 --- a/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php +++ b/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php @@ -30,7 +30,7 @@ use \Combodo\iTop\Renderer\RenderingOutput; use \Combodo\iTop\Form\Field\LinkedSetField; /** - * Description of BsSelectObjectFieldRenderer + * Description of BsLinkedSetFieldRenderer * * @author Guillaume Lajarige */ @@ -182,6 +182,9 @@ EOF // When we have data (meaning that we picked objects from search) if(oData !== undefined && Object.keys(oData.values).length > 0) { + // Showing loader while retrieving informations + $('#page_overlay').fadeIn(200); + // Retrieving new rows ids var aObjectIds = Object.keys(oData.values); @@ -222,6 +225,10 @@ EOF } $('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds)); + }) + .always(function(oData){ + // Hiding loader + $('#page_overlay').fadeOut(200); }); } // We come from a button