From 1cde41b9946c5157681c9945a47eae76795b84d9 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Wed, 6 Feb 2013 14:03:52 -0800 Subject: [PATCH] Conpherence - add crop Summary: mainly, this adds the image cropper - yay! - also removes the file image from the handle stuff I added in V1. now we do all this crazy photo stuff. Test Plan: - uploaded a photo by dragging to header and noted 120 x 80 showed up on reload - uploaded a photo by dragging to edit dialogue spot and noted 120 x 80 showed up on reload - cropped a photo - noted it cropped right - cropped a photo again and again and again - seems like it crops okay Reviewers: epriestley, chad Reviewed By: epriestley CC: aran, Korvin Maniphest Tasks: T2418, T2399 Differential Revision: https://secure.phabricator.com/D4790 --- .../sql/patches/20130131.conpherencepics.sql | 6 + src/__celerity_resource_map__.php | 114 ++++++++------ src/__phutil_library_map__.php | 6 + .../constants/ConpherenceImageData.php | 11 ++ .../constants/ConpherenceTransactionType.php | 1 + .../controller/ConpherenceController.php | 13 +- .../ConpherenceUpdateController.php | 116 ++++++++++---- .../controller/ConpherenceViewController.php | 22 ++- .../conpherence/editor/ConpherenceEditor.php | 17 ++- .../query/ConpherenceThreadQuery.php | 53 +++++++ .../conpherence/storage/ConpherenceThread.php | 144 +++++++++++++----- .../storage/ConpherenceTransaction.php | 5 +- ...onpherenceFormDragAndDropUploadControl.php | 42 +++++ .../view/ConpherenceTransactionView.php | 13 +- .../files/PhabricatorImageTransformer.php | 84 +++++++++- .../handle/PhabricatorObjectHandleData.php | 3 - .../form/control/AphrontFormCropControl.php | 99 ++++++++++++ webroot/rsrc/css/aphront/form-view.css | 12 ++ .../application/conpherence/header-pane.css | 48 ++++++ .../css/application/conpherence/update.css | 10 ++ .../behavior-drag-and-drop-photo.js | 39 +++++ .../rsrc/js/application/core/behavior-crop.js | 94 ++++++++++++ 22 files changed, 803 insertions(+), 149 deletions(-) create mode 100644 resources/sql/patches/20130131.conpherencepics.sql create mode 100644 src/applications/conpherence/constants/ConpherenceImageData.php create mode 100644 src/applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php create mode 100644 src/view/form/control/AphrontFormCropControl.php create mode 100644 webroot/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js create mode 100644 webroot/rsrc/js/application/core/behavior-crop.js diff --git a/resources/sql/patches/20130131.conpherencepics.sql b/resources/sql/patches/20130131.conpherencepics.sql new file mode 100644 index 0000000000..421291225f --- /dev/null +++ b/resources/sql/patches/20130131.conpherencepics.sql @@ -0,0 +1,6 @@ +ALTER TABLE {$NAMESPACE}_conpherence.conpherence_thread + DROP imagePHID, + ADD imagePHIDs LONGTEXT COLLATE utf8_bin NOT NULL AFTER title; + +UPDATE {$NAMESPACE}_conpherence.conpherence_thread + SET imagePHIDs = '{}' WHERE imagePHIDs = ''; diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 9bdd9fc488..cad7602664 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -639,7 +639,7 @@ celerity_register_resource_map(array( ), 'aphront-form-view-css' => array( - 'uri' => '/res/428fbcd8/rsrc/css/aphront/form-view.css', + 'uri' => '/res/ec323d34/rsrc/css/aphront/form-view.css', 'type' => 'css', 'requires' => array( @@ -748,7 +748,7 @@ celerity_register_resource_map(array( ), 'conpherence-header-pane-css' => array( - 'uri' => '/res/4b8aebd2/rsrc/css/application/conpherence/header-pane.css', + 'uri' => '/res/11c32adc/rsrc/css/application/conpherence/header-pane.css', 'type' => 'css', 'requires' => array( @@ -775,7 +775,7 @@ celerity_register_resource_map(array( ), 'conpherence-update-css' => array( - 'uri' => '/res/8e4757b5/rsrc/css/application/conpherence/update.css', + 'uri' => '/res/92094ed7/rsrc/css/application/conpherence/update.css', 'type' => 'css', 'requires' => array( @@ -1042,6 +1042,19 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-tokenizer.js', ), + 'javelin-behavior-aphront-crop' => + array( + 'uri' => '/res/cda1eace/rsrc/js/application/core/behavior-crop.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-vector', + 3 => 'javelin-magical-init', + ), + 'disk' => '/rsrc/js/application/core/behavior-crop.js', + ), 'javelin-behavior-aphront-drag-and-drop' => array( 'uri' => '/res/3d809b40/rsrc/js/application/core/behavior-drag-and-drop.js', @@ -1106,6 +1119,19 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/diffusion/behavior-audit-preview.js', ), + 'javelin-behavior-conpherence-drag-and-drop-photo' => + array( + 'uri' => '/res/9e3eb1cd/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-workflow', + 3 => 'phabricator-drag-and-drop-file-upload', + ), + 'disk' => '/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js', + ), 'javelin-behavior-conpherence-init' => array( 'uri' => '/res/bd911b43/rsrc/js/application/conpherence/behavior-init.js', @@ -1863,7 +1889,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-pholio-mock-view' => array( - 'uri' => '/res/1364872e/rsrc/js/application/pholio/behavior-pholio-mock-view.js', + 'uri' => '/res/5dd61f73/rsrc/js/application/pholio/behavior-pholio-mock-view.js', 'type' => 'js', 'requires' => array( @@ -3396,7 +3422,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - '7d347135' => + '23503cb9' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -3440,7 +3466,7 @@ celerity_register_resource_map(array( 36 => 'phabricator-object-item-list-view-css', 37 => 'global-drag-and-drop-css', ), - 'uri' => '/res/pkg/7d347135/core.pkg.css', + 'uri' => '/res/pkg/23503cb9/core.pkg.css', 'type' => 'css', ), '58743fec' => @@ -3630,19 +3656,19 @@ celerity_register_resource_map(array( 'reverse' => array( 'aphront-attached-file-view-css' => 'e30a3fa8', - 'aphront-crumbs-view-css' => '7d347135', - 'aphront-dialog-view-css' => '7d347135', - 'aphront-error-view-css' => '7d347135', - 'aphront-form-view-css' => '7d347135', + 'aphront-crumbs-view-css' => '23503cb9', + 'aphront-dialog-view-css' => '23503cb9', + 'aphront-error-view-css' => '23503cb9', + 'aphront-form-view-css' => '23503cb9', 'aphront-headsup-action-list-view-css' => 'ec01d039', - 'aphront-headsup-view-css' => '7d347135', - 'aphront-list-filter-view-css' => '7d347135', - 'aphront-pager-view-css' => '7d347135', - 'aphront-panel-view-css' => '7d347135', - 'aphront-table-view-css' => '7d347135', - 'aphront-tokenizer-control-css' => '7d347135', - 'aphront-tooltip-css' => '7d347135', - 'aphront-typeahead-control-css' => '7d347135', + 'aphront-headsup-view-css' => '23503cb9', + 'aphront-list-filter-view-css' => '23503cb9', + 'aphront-pager-view-css' => '23503cb9', + 'aphront-panel-view-css' => '23503cb9', + 'aphront-table-view-css' => '23503cb9', + 'aphront-tokenizer-control-css' => '23503cb9', + 'aphront-tooltip-css' => '23503cb9', + 'aphront-typeahead-control-css' => '23503cb9', 'differential-changeset-view-css' => 'ec01d039', 'differential-core-view-css' => 'ec01d039', 'differential-inline-comment-editor' => '95d0d865', @@ -3656,7 +3682,7 @@ celerity_register_resource_map(array( 'differential-table-of-contents-css' => 'ec01d039', 'diffusion-commit-view-css' => 'c8ce2d88', 'diffusion-icons-css' => 'c8ce2d88', - 'global-drag-and-drop-css' => '7d347135', + 'global-drag-and-drop-css' => '23503cb9', 'inline-comment-summary-css' => 'ec01d039', 'javelin-aphlict' => '58743fec', 'javelin-behavior' => '88225b70', @@ -3726,48 +3752,48 @@ celerity_register_resource_map(array( 'javelin-util' => '88225b70', 'javelin-vector' => '88225b70', 'javelin-workflow' => '88225b70', - 'lightbox-attachment-css' => '7d347135', + 'lightbox-attachment-css' => '23503cb9', 'maniphest-task-summary-css' => 'e30a3fa8', 'maniphest-transaction-detail-css' => 'e30a3fa8', 'phabricator-busy' => '58743fec', 'phabricator-content-source-view-css' => 'ec01d039', - 'phabricator-core-buttons-css' => '7d347135', - 'phabricator-core-css' => '7d347135', - 'phabricator-crumbs-view-css' => '7d347135', - 'phabricator-directory-css' => '7d347135', + 'phabricator-core-buttons-css' => '23503cb9', + 'phabricator-core-css' => '23503cb9', + 'phabricator-crumbs-view-css' => '23503cb9', + 'phabricator-directory-css' => '23503cb9', 'phabricator-drag-and-drop-file-upload' => '95d0d865', 'phabricator-dropdown-menu' => '58743fec', 'phabricator-file-upload' => '58743fec', - 'phabricator-filetree-view-css' => '7d347135', - 'phabricator-flag-css' => '7d347135', - 'phabricator-form-view-css' => '7d347135', - 'phabricator-header-view-css' => '7d347135', - 'phabricator-jump-nav' => '7d347135', + 'phabricator-filetree-view-css' => '23503cb9', + 'phabricator-flag-css' => '23503cb9', + 'phabricator-form-view-css' => '23503cb9', + 'phabricator-header-view-css' => '23503cb9', + 'phabricator-jump-nav' => '23503cb9', 'phabricator-keyboard-shortcut' => '58743fec', 'phabricator-keyboard-shortcut-manager' => '58743fec', - 'phabricator-main-menu-view' => '7d347135', + 'phabricator-main-menu-view' => '23503cb9', 'phabricator-menu-item' => '58743fec', - 'phabricator-nav-view-css' => '7d347135', + 'phabricator-nav-view-css' => '23503cb9', 'phabricator-notification' => '58743fec', - 'phabricator-notification-css' => '7d347135', - 'phabricator-notification-menu-css' => '7d347135', - 'phabricator-object-item-list-view-css' => '7d347135', + 'phabricator-notification-css' => '23503cb9', + 'phabricator-notification-menu-css' => '23503cb9', + 'phabricator-object-item-list-view-css' => '23503cb9', 'phabricator-object-selector-css' => 'ec01d039', 'phabricator-paste-file-upload' => '58743fec', 'phabricator-prefab' => '58743fec', 'phabricator-project-tag-css' => 'e30a3fa8', - 'phabricator-remarkup-css' => '7d347135', + 'phabricator-remarkup-css' => '23503cb9', 'phabricator-shaped-request' => '95d0d865', - 'phabricator-side-menu-view-css' => '7d347135', - 'phabricator-standard-page-view' => '7d347135', + 'phabricator-side-menu-view-css' => '23503cb9', + 'phabricator-standard-page-view' => '23503cb9', 'phabricator-textareautils' => '58743fec', 'phabricator-tooltip' => '58743fec', - 'phabricator-transaction-view-css' => '7d347135', - 'phabricator-zindex-css' => '7d347135', - 'sprite-apps-large-css' => '7d347135', - 'sprite-gradient-css' => '7d347135', - 'sprite-icon-css' => '7d347135', - 'sprite-menu-css' => '7d347135', - 'syntax-highlighting-css' => '7d347135', + 'phabricator-transaction-view-css' => '23503cb9', + 'phabricator-zindex-css' => '23503cb9', + 'sprite-apps-large-css' => '23503cb9', + 'sprite-gradient-css' => '23503cb9', + 'sprite-icon-css' => '23503cb9', + 'sprite-menu-css' => '23503cb9', + 'syntax-highlighting-css' => '23503cb9', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a1bc47e859..a738ea4e8a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -32,6 +32,7 @@ phutil_register_library_map(array( 'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', 'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', 'AphrontFormControl' => 'view/form/control/AphrontFormControl.php', + 'AphrontFormCropControl' => 'view/form/control/AphrontFormCropControl.php', 'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php', 'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php', 'AphrontFormDragAndDropUploadControl' => 'view/form/control/AphrontFormDragAndDropUploadControl.php', @@ -201,6 +202,8 @@ phutil_register_library_map(array( 'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php', 'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php', 'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php', + 'ConpherenceFormDragAndDropUploadControl' => 'applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php', + 'ConpherenceImageData' => 'applications/conpherence/constants/ConpherenceImageData.php', 'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php', 'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php', 'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php', @@ -1534,6 +1537,7 @@ phutil_register_library_map(array( 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormCheckboxControl' => 'AphrontFormControl', 'AphrontFormControl' => 'AphrontView', + 'AphrontFormCropControl' => 'AphrontFormControl', 'AphrontFormDateControl' => 'AphrontFormControl', 'AphrontFormDividerControl' => 'AphrontFormControl', 'AphrontFormDragAndDropUploadControl' => 'AphrontFormControl', @@ -1689,6 +1693,8 @@ phutil_register_library_map(array( 'ConpherenceController' => 'PhabricatorController', 'ConpherenceDAO' => 'PhabricatorLiskDAO', 'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor', + 'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl', + 'ConpherenceImageData' => 'ConpherenceConstants', 'ConpherenceListController' => 'ConpherenceController', 'ConpherenceMenuItemView' => 'AphrontTagView', 'ConpherenceNewController' => 'ConpherenceController', diff --git a/src/applications/conpherence/constants/ConpherenceImageData.php b/src/applications/conpherence/constants/ConpherenceImageData.php new file mode 100644 index 0000000000..618acb4b35 --- /dev/null +++ b/src/applications/conpherence/constants/ConpherenceImageData.php @@ -0,0 +1,11 @@ +getRequest()->getUser(); foreach ($conpherences as $conpherence) { $uri = $this->getApplicationURI('view/'.$conpherence->getID().'/'); - $data = $conpherence->getDisplayData($user); + $data = $conpherence->getDisplayData( + $user, + null + ); $title = $data['title']; $subtitle = $data['subtitle']; $unread_count = $data['unread_count']; @@ -220,6 +223,14 @@ abstract class ConpherenceController extends PhabricatorController { 'form_pane' => 'conpherence-form' ) ); + Javelin::initBehavior('conpherence-drag-and-drop-photo', + array( + 'target' => 'conpherence-header-pane', + 'form_pane' => 'conpherence-form', + 'upload_uri' => '/file/dropupload/', + 'activated_class' => 'conpherence-header-upload-photo', + ) + ); } } diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index 2d8deee715..ad3bf86fa1 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -30,6 +30,8 @@ final class ConpherenceUpdateController extends $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) + ->needOrigPics(true) + ->needHeaderPics(true) ->executeOne(); $supported_formats = PhabricatorFile::getTransformableImageFormats(); @@ -59,31 +61,63 @@ final class ConpherenceUpdateController extends break; case 'metadata': $xactions = array(); - $images = $request->getArr('image'); - if ($images) { - // just take the first one - $file_phid = reset($images); - $file = id(new PhabricatorFileQuery()) + $top = $request->getInt('image_y'); + $left = $request->getInt('image_x'); + $file_id = $request->getInt('file_id'); + if ($file_id) { + $orig_file = id(new PhabricatorFileQuery()) ->setViewer($user) - ->withPHIDs(array($file_phid)) + ->withIDs(array($file_id)) ->executeOne(); - $okay = $file->isTransformableImage(); + $okay = $orig_file->isTransformableImage(); if ($okay) { - $xformer = new PhabricatorImageTransformer(); - $xformed = $xformer->executeThumbTransform( - $file, - $x = 50, - $y = 50); - $image_phid = $xformed->getPHID(); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE) - ->setNewValue($image_phid); + ->setNewValue($orig_file->getPHID()); + // do 2 transformations "crudely" + $xformer = new PhabricatorImageTransformer(); + $header_file = $xformer->executeConpherenceTransform( + $orig_file, + 0, + 0, + ConpherenceImageData::HEAD_WIDTH, + ConpherenceImageData::HEAD_HEIGHT + ); + // this is handled outside the editor for now. no particularly + // good reason to move it inside + $conpherence->setImagePHIDs( + array( + ConpherenceImageData::SIZE_HEAD => $header_file->getPHID(), + ) + ); + $conpherence->setImages( + array( + ConpherenceImageData::SIZE_HEAD => $header_file, + ) + ); } else { - $e_file[] = $file; + $e_file[] = $orig_file; $errors[] = pht('This server only supports these image formats: %s.', implode(', ', $supported_formats)); } + } else if ($top !== null || $left !== null) { + $file = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); + $xformer = new PhabricatorImageTransformer(); + $xformed = $xformer->executeConpherenceTransform( + $file, + $top, + $left, + ConpherenceImageData::HEAD_WIDTH, + ConpherenceImageData::HEAD_HEIGHT + ); + $image_phid = $xformed->getPHID(); + + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType( + ConpherenceTransactionType::TYPE_PICTURE_CROP + ) + ->setNewValue($image_phid); } $title = $request->getStr('title'); if ($title != $conpherence->getTitle()) { @@ -131,24 +165,42 @@ final class ConpherenceUpdateController extends ->setLabel(pht('Title')) ->setName('title') ->setValue($conpherence->getTitle()) - ) - ->appendChild( - id(new AphrontFormMarkupControl()) - ->setLabel(pht('Image')) - ->setValue(phutil_tag( - 'img', - array( - 'src' => $conpherence->loadImageURI(), - )) - ) - ) - ->appendChild( - id(new AphrontFormDragAndDropUploadControl()) - ->setLabel(pht('Change Image')) - ->setName('image') - ->setValue($e_file) - ->setCaption('Supported formats: '.implode(', ', $supported_formats)) + ); + + $image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); + if ($image) { + $form + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Image')) + ->setValue(phutil_tag( + 'img', + array( + 'src' => + $conpherence->loadImageURI(ConpherenceImageData::SIZE_HEAD), + )) + ) + ) + ->appendChild( + id(new AphrontFormCropControl()) + ->setLabel(pht('Crop Image')) + ->setValue($image) + ->setWidth(ConpherenceImageData::HEAD_WIDTH) + ->setHeight(ConpherenceImageData::HEAD_HEIGHT) + ) + ->appendChild( + id(new ConpherenceFormDragAndDropUploadControl()) + ->setLabel(pht('Change Image')) + ); + + } else { + + $form + ->appendChild( + id(new ConpherenceFormDragAndDropUploadControl()) + ->setLabel(pht('Image')) ); + } require_celerity_resource('conpherence-update-css'); return id(new AphrontDialogResponse()) diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index f082ccf78c..b57ba021a2 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -45,6 +45,7 @@ final class ConpherenceViewController extends ->setViewer($user) ->withIDs(array($conpherence_id)) ->needWidgetData(true) + ->needHeaderPics(true) ->executeOne(); $this->setConpherence($conpherence); @@ -67,23 +68,34 @@ final class ConpherenceViewController extends require_celerity_resource('conpherence-header-pane-css'); $user = $this->getRequest()->getUser(); $conpherence = $this->getConpherence(); - $display_data = $conpherence->getDisplayData($user); + $display_data = $conpherence->getDisplayData( + $user, + ConpherenceImageData::SIZE_HEAD + ); $edit_href = $this->getApplicationURI('update/'.$conpherence->getID().'/'); + $class_mod = $display_data['image_class']; $header = + phutil_tag( + 'div', + array( + 'class' => 'upload-photo' + ), + pht('Drop photo here to change this Conpherence photo.') + ). javelin_tag( 'a', array( 'class' => 'edit', 'href' => $edit_href, - 'sigil' => 'workflow', + 'sigil' => 'workflow edit-action', ), '' ). phutil_tag( 'div', array( - 'class' => 'header-image', + 'class' => $class_mod.'header-image', 'style' => 'background-image: url('.$display_data['image'].');' ), '' @@ -91,14 +103,14 @@ final class ConpherenceViewController extends phutil_tag( 'div', array( - 'class' => 'title', + 'class' => $class_mod.'title', ), $display_data['title'] ). phutil_tag( 'div', array( - 'class' => 'subtitle', + 'class' => $class_mod.'subtitle', ), $display_data['subtitle'] ); diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php index a5b42b93ce..feabfd810b 100644 --- a/src/applications/conpherence/editor/ConpherenceEditor.php +++ b/src/applications/conpherence/editor/ConpherenceEditor.php @@ -48,6 +48,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { $types[] = ConpherenceTransactionType::TYPE_TITLE; $types[] = ConpherenceTransactionType::TYPE_PICTURE; + $types[] = ConpherenceTransactionType::TYPE_PICTURE_CROP; $types[] = ConpherenceTransactionType::TYPE_PARTICIPANTS; $types[] = ConpherenceTransactionType::TYPE_FILES; @@ -62,7 +63,9 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { case ConpherenceTransactionType::TYPE_TITLE: return $object->getTitle(); case ConpherenceTransactionType::TYPE_PICTURE: - return $object->getImagePHID(); + return $object->getImagePHID(ConpherenceImageData::SIZE_ORIG); + case ConpherenceTransactionType::TYPE_PICTURE_CROP: + return $object->getImagePHID(ConpherenceImageData::SIZE_HEAD); case ConpherenceTransactionType::TYPE_PARTICIPANTS: return $object->getParticipantPHIDs(); case ConpherenceTransactionType::TYPE_FILES: @@ -77,6 +80,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { switch ($xaction->getTransactionType()) { case ConpherenceTransactionType::TYPE_TITLE: case ConpherenceTransactionType::TYPE_PICTURE: + case ConpherenceTransactionType::TYPE_PICTURE_CROP: return $xaction->getNewValue(); case ConpherenceTransactionType::TYPE_PARTICIPANTS: case ConpherenceTransactionType::TYPE_FILES: @@ -93,7 +97,16 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { $object->setTitle($xaction->getNewValue()); break; case ConpherenceTransactionType::TYPE_PICTURE: - $object->setImagePHID($xaction->getNewValue()); + $object->setImagePHID( + $xaction->getNewValue(), + ConpherenceImageData::SIZE_ORIG + ); + break; + case ConpherenceTransactionType::TYPE_PICTURE_CROP: + $object->setImagePHID( + $xaction->getNewValue(), + ConpherenceImageData::SIZE_HEAD + ); break; } } diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index d22fd03820..e21951cf09 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -9,6 +9,18 @@ final class ConpherenceThreadQuery private $phids; private $ids; private $needWidgetData; + private $needHeaderPics; + private $needOrigPics; + + public function needOrigPics($need_orig_pics) { + $this->needOrigPics = $need_orig_pics; + return $this; + } + + public function needHeaderPics($need_header_pics) { + $this->needHeaderPics = $need_header_pics; + return $this; + } public function needWidgetData($need_widget_data) { $this->needWidgetData = $need_widget_data; @@ -47,6 +59,12 @@ final class ConpherenceThreadQuery if ($this->needWidgetData) { $this->loadWidgetData($conpherences); } + if ($this->needOrigPics) { + $this->loadOrigPics($conpherences); + } + if ($this->needHeaderPics) { + $this->loadHeaderPics($conpherences); + } } return $conpherences; @@ -187,4 +205,39 @@ final class ConpherenceThreadQuery return $this; } + private function loadOrigPics(array $conpherences) { + return $this->loadPics( + $conpherences, + ConpherenceImageData::SIZE_ORIG + ); + } + + private function loadHeaderPics(array $conpherences) { + return $this->loadPics( + $conpherences, + ConpherenceImageData::SIZE_HEAD + ); + } + + private function loadPics(array $conpherences, $size) { + $conpherence_pic_phids = array(); + foreach ($conpherences as $conpherence) { + $phid = $conpherence->getImagePHID($size); + if ($phid) { + $conpherence_pic_phids[$conpherence->getPHID()] = $phid; + } + } + $files = id(new PhabricatorFileQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($conpherence_pic_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + foreach ($conpherence_pic_phids as $conpherence_phid => $pic_phid) { + $conpherences[$conpherence_phid]->setImage($files[$pic_phid], $size); + } + + return $this; + } + } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 1b80aec1a5..e824ad6207 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -9,7 +9,7 @@ final class ConpherenceThread extends ConpherenceDAO protected $id; protected $phid; protected $title; - protected $imagePHID; + protected $imagePHIDs = array(); protected $mailKey; private $participants; @@ -17,10 +17,14 @@ final class ConpherenceThread extends ConpherenceDAO private $handles; private $filePHIDs; private $widgetData; + private $images = array(); public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'imagePHIDs' => self::SERIALIZATION_JSON, + ), ) + parent::getConfiguration(); } @@ -36,6 +40,33 @@ final class ConpherenceThread extends ConpherenceDAO return parent::save(); } + public function getImagePHID($size) { + $image_phids = $this->getImagePHIDs(); + return idx($image_phids, $size); + } + public function setImagePHID($phid, $size) { + $image_phids = $this->getImagePHIDs(); + $image_phids[$size] = $phid; + return $this->setImagePHIDs($image_phids); + } + + public function getImage($size) { + $images = $this->getImages(); + return idx($images, $size); + } + public function setImage(PhabricatorFile $file, $size) { + $files = $this->getImages(); + $files[$size] = $file; + return $this->setImages($files); + } + public function setImages(array $files) { + $this->images = $files; + return $this; + } + private function getImages() { + return $this->images; + } + public function attachParticipants(array $participants) { assert_instances_of($participants, 'ConpherenceParticipant'); $this->participants = $participants; @@ -112,59 +143,36 @@ final class ConpherenceThread extends ConpherenceDAO return $this->widgetData; } - public function loadImageURI() { - $src_phid = $this->getImagePHID(); + public function loadImageURI($size) { + $file = $this->getImage($size); - if ($src_phid) { - $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); - if ($file) { - return $file->getBestURI(); - } + if ($file) { + return $file->getBestURI(); } return PhabricatorUser::getDefaultProfileImageURI(); } - public function getDisplayData(PhabricatorUser $user) { + public function getDisplayData(PhabricatorUser $user, $size) { $transactions = $this->getTransactions(); - $latest_transaction = end($transactions); - $latest_participant = $latest_transaction->getAuthorPHID(); - $handles = $this->getHandles(); - $latest_handle = $handles[$latest_participant]; - if ($this->getImagePHID()) { - $img_src = $this->loadImageURI(); - } else { - $img_src = $latest_handle->getImageURI(); - } - $title = $this->getTitle(); - if (!$title) { - $title = $latest_handle->getName(); - unset($handles[$latest_participant]); - } - unset($handles[$user->getPHID()]); - $subtitle = ''; - $count = 0; - $final = false; - foreach ($handles as $handle) { - if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) { - continue; - } - if ($subtitle) { - if ($final) { - $subtitle .= '...'; - break; - } else { - $subtitle .= ', '; - } - } - $subtitle .= $handle->getName(); - $count++; - $final = $count == 3; + $handles = $this->getHandles(); + // we don't want to show the user unless they are babbling to themselves + if (count($handles) > 1) { + unset($handles[$user->getPHID()]); } $participants = $this->getParticipants(); $user_participation = $participants[$user->getPHID()]; + $latest_transaction = null; + $title = $this->getTitle(); + $subtitle = ''; + $img_src = null; + $img_class = null; + if ($this->getImagePHID($size)) { + $img_src = $this->getImage($size)->getBestURI(); + $img_class = 'custom-'; + } $unread_count = 0; $max_count = 10; $snippet = null; @@ -186,9 +194,28 @@ final class ConpherenceThread extends ConpherenceDAO $transaction->getComment()->getContent(), 48 ); + if ($transaction->getAuthorPHID() == $user->getPHID()) { + $snippet = "\xE2\x86\xB0 " . $snippet; + } } // fallthrough intentionally here case ConpherenceTransactionType::TYPE_FILES: + if (!$latest_transaction) { + $latest_transaction = $transaction; + } + $latest_participant_phid = $transaction->getAuthorPHID(); + if ((!$title || !$img_src) && + $latest_participant_phid != $user->getPHID()) { + $latest_handle = $handles[$latest_participant_phid]; + if (!$img_src) { + $img_src = $latest_handle->getImageURI(); + } + if (!$title) { + $title = $latest_handle->getName(); + // (maybs) used the pic, definitely used the name -- discard + unset($handles[$latest_participant_phid]); + } + } if ($behind_transaction_phid) { $unread_count++; if ($transaction->getPHID() == $behind_transaction_phid) { @@ -210,12 +237,45 @@ final class ConpherenceThread extends ConpherenceDAO $unread_count = $max_count.'+'; } + // This happens if the user has been babbling, maybs just to themselves, + // but enough un-responded to transactions for our SQL limit would + // hit this too... Also happens on new threads since only the first + // author has participated. + // ...so just pick a different handle in these cases. + $some_handle = reset($handles); + if (!$img_src) { + $img_src = $some_handle->getImageURI(); + } + if (!$title) { + $title = $some_handle->getName(); + } + + $count = 0; + $final = false; + foreach ($handles as $handle) { + if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) { + continue; + } + if ($subtitle) { + if ($final) { + $subtitle .= '...'; + break; + } else { + $subtitle .= ', '; + } + } + $subtitle .= $handle->getName(); + $count++; + $final = $count == 3; + } + return array( 'title' => $title, 'subtitle' => $subtitle, 'unread_count' => $unread_count, 'epoch' => $latest_transaction->getDateCreated(), 'image' => $img_src, + 'image_class' => $img_class, 'snippet' => $snippet, ); } diff --git a/src/applications/conpherence/storage/ConpherenceTransaction.php b/src/applications/conpherence/storage/ConpherenceTransaction.php index 9b242275c6..22a4aa666e 100644 --- a/src/applications/conpherence/storage/ConpherenceTransaction.php +++ b/src/applications/conpherence/storage/ConpherenceTransaction.php @@ -31,6 +31,7 @@ final class ConpherenceTransaction extends PhabricatorApplicationTransaction { case ConpherenceTransactionType::TYPE_PICTURE: return false; case ConpherenceTransactionType::TYPE_FILES: + case ConpherenceTransactionType::TYPE_PICTURE_CROP: return true; } @@ -107,12 +108,10 @@ final class ConpherenceTransaction extends PhabricatorApplicationTransaction { $phids[] = $this->getAuthorPHID(); switch ($this->getTransactionType()) { case ConpherenceTransactionType::TYPE_PICTURE: - $phids[] = $new; - break; case ConpherenceTransactionType::TYPE_TITLE: + case ConpherenceTransactionType::TYPE_FILES: break; case ConpherenceTransactionType::TYPE_PARTICIPANTS: - case ConpherenceTransactionType::TYPE_FILES: $phids = array_merge($phids, $this->getOldValue()); $phids = array_merge($phids, $this->getNewValue()); break; diff --git a/src/applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php b/src/applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php new file mode 100644 index 0000000000..032d8a9aa7 --- /dev/null +++ b/src/applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php @@ -0,0 +1,42 @@ +dropID = $drop_id; + return $this; + } + public function getDropID() { + return $this->dropID; + } + + protected function getCustomControlClass() { + return null; + } + + protected function renderInput() { + + $drop_id = celerity_generate_unique_node_id(); + Javelin::initBehavior('conpherence-drag-and-drop-photo', + array( + 'target' => $drop_id, + 'form_pane' => 'conpherence-form', + 'upload_uri' => '/file/dropupload/', + 'activated_class' => 'conpherence-dialogue-upload-photo', + ) + ); + require_celerity_resource('conpherence-update-css'); + + return phutil_tag( + 'div', + array( + 'id' => $drop_id, + 'class' => 'conpherence-dialogue-drag-photo', + ), + pht('Drag and drop an image here to upload it.') + ); + } + +} diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php index d8163b3ecd..72f4b6656b 100644 --- a/src/applications/conpherence/view/ConpherenceTransactionView.php +++ b/src/applications/conpherence/view/ConpherenceTransactionView.php @@ -38,23 +38,14 @@ final class ConpherenceTransactionView extends AphrontView { $content_class = null; switch ($transaction->getTransactionType()) { case ConpherenceTransactionType::TYPE_TITLE: + case ConpherenceTransactionType::TYPE_PICTURE: + case ConpherenceTransactionType::TYPE_PICTURE_CROP: $content = $transaction->getTitle(); $transaction_view->addClass('conpherence-edited'); break; case ConpherenceTransactionType::TYPE_FILES: $content = $transaction->getTitle(); break; - case ConpherenceTransactionType::TYPE_PICTURE: - $img = $transaction->getHandle($transaction->getNewValue()); - $content = $transaction->getTitle() . - phutil_tag( - 'img', - array( - 'src' => $img->getImageURI() - ) - ); - $transaction_view->addClass('conpherence-edited'); - break; case ConpherenceTransactionType::TYPE_PARTICIPANTS: $content = $transaction->getTitle(); $transaction_view->addClass('conpherence-edited'); diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php index a073d14030..8e651eba69 100644 --- a/src/applications/files/PhabricatorImageTransformer.php +++ b/src/applications/files/PhabricatorImageTransformer.php @@ -56,6 +56,29 @@ final class PhabricatorImageTransformer { )); } + public function executeConpherenceTransform( + PhabricatorFile $file, + $top, + $left, + $width, + $height + ) { + + $image = $this->crasslyCropTo( + $file, + $top, + $left, + $width, + $height + ); + + return PhabricatorFile::newFromFileData( + $image, + array( + 'name' => 'conpherence-'.$file->getName(), + ) + ); + } private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) { $data = $file->loadFileData(); @@ -80,6 +103,30 @@ final class PhabricatorImageTransformer { return $this->saveImageDataInAnyFormat($img, $file->getMimeType()); } + private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) { + $data = $file->loadFileData(); + $src = imagecreatefromstring($data); + $dst = $this->getBlankDestinationFile($w, $h); + + $scale = self::getScaleForCrop($file, $w, $h); + $orig_x = $left / $scale; + $orig_y = $top / $scale; + $orig_w = $w / $scale; + $orig_h = $h / $scale; + + imagecopyresampled( + $dst, + $src, + 0, 0, + $orig_x, $orig_y, + $w, $h, + $orig_w, $orig_h + ); + + return $this->saveImageDataInAnyFormat($dst, $file->getMimeType()); + } + + /** * Very crudely scale an image up or down to an exact size. */ @@ -92,15 +139,21 @@ final class PhabricatorImageTransformer { return $this->saveImageDataInAnyFormat($dst, $file->getMimeType()); } + private function getBlankDestinationFile($dx, $dy) { + $dst = imagecreatetruecolor($dx, $dy); + imagesavealpha($dst, true); + imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); + + return $dst; + } + private function applyScaleTo($src, $dx, $dy) { $x = imagesx($src); $y = imagesy($src); $scale = min(($dx / $x), ($dy / $y), 1); - $dst = imagecreatetruecolor($dx, $dy); - imagesavealpha($dst, true); - imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); + $dst = $this->getBlankDestinationFile($dx, $dy); $sdx = $scale * $x; $sdy = $scale * $y; @@ -141,6 +194,27 @@ final class PhabricatorImageTransformer { ); } + public static function getScaleForCrop( + PhabricatorFile $file, + $des_width, + $des_height) { + + $metadata = $file->getMetadata(); + $width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH]; + $height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT]; + + if ($height < $des_height) { + $scale = $height / $des_height; + } else if ($width < $des_width) { + $scale = $width / $des_width; + } else { + $scale_x = $des_width / $width; + $scale_y = $des_height / $height; + $scale = max($scale_x, $scale_y); + } + + return $scale; + } private function generatePreview(PhabricatorFile $file, $size) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); @@ -153,9 +227,7 @@ final class PhabricatorImageTransformer { $sdx = $dimensions['sdx']; $sdy = $dimensions['sdy']; - $dst = imagecreatetruecolor($dx, $dy); - imagesavealpha($dst, true); - imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); + $dst = $this->getBlankDestinationFile($dx, $dy); imagecopyresampled( $dst, diff --git a/src/applications/phid/handle/PhabricatorObjectHandleData.php b/src/applications/phid/handle/PhabricatorObjectHandleData.php index 52c1bb7d0c..67eaaf8e63 100644 --- a/src/applications/phid/handle/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/PhabricatorObjectHandleData.php @@ -413,9 +413,6 @@ final class PhabricatorObjectHandleData { $handle->setName($file->getName()); $handle->setURI($file->getBestURI()); $handle->setComplete(true); - if ($file->isViewableImage()) { - $handle->setImageURI($file->getBestURI()); - } } $handles[$phid] = $handle; } diff --git a/src/view/form/control/AphrontFormCropControl.php b/src/view/form/control/AphrontFormCropControl.php new file mode 100644 index 0000000000..26cb70c0e4 --- /dev/null +++ b/src/view/form/control/AphrontFormCropControl.php @@ -0,0 +1,99 @@ +height = $height; + return $this; + } + public function getHeight() { + return $this->height; + } + + public function setWidth($width) { + $this->width = $width; + return $this; + } + public function getWidth() { + return $this->width; + } + + protected function getCustomControlClass() { + return 'aphront-form-crop'; + } + + protected function renderInput() { + $file = $this->getValue(); + + if ($file === null) { + return phutil_render_tag( + 'img', + array( + 'src' => PhabricatorUser::getDefaultProfileImageURI() + ), + '' + ); + } + + $c_id = celerity_generate_unique_node_id(); + $metadata = $file->getMetadata(); + $scale = PhabricatorImageTransformer::getScaleForCrop( + $file, + $this->getWidth(), + $this->getHeight() + ); + + Javelin::initBehavior( + 'aphront-crop', + array( + 'cropBoxID' => $c_id, + 'width' => $this->getWidth(), + 'height' => $this->getHeight(), + 'scale' => $scale, + 'imageH' => $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT], + 'imageW' => $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH], + ) + ); + + return javelin_render_tag( + 'div', + array( + 'id' => $c_id, + 'sigil' => 'crop-box', + 'mustcapture' => true, + 'class' => 'crop-box' + ), + javelin_render_tag( + 'img', + array( + 'src' => $file->getBestURI(), + 'class' => 'crop-image', + 'sigil' => 'crop-image' + ), + '' + ). + javelin_render_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'image_x', + 'sigil' => 'crop-x', + ), + '' + ). + javelin_render_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'image_y', + 'sigil' => 'crop-y', + ), + '' + ) + ); + } + +} diff --git a/webroot/rsrc/css/aphront/form-view.css b/webroot/rsrc/css/aphront/form-view.css index 52dc73e307..abca6a6b2d 100644 --- a/webroot/rsrc/css/aphront/form-view.css +++ b/webroot/rsrc/css/aphront/form-view.css @@ -231,6 +231,18 @@ table.aphront-form-control-checkbox-layout th { border-color: #669966; } +.aphront-form-crop .crop-box { + cursor: move; + overflow: hidden; +} + +.aphront-form-crop .crop-box .crop-image { + position: relative; + top: 0px; + left: 0px; +} + + .calendar-button { display: inline; background: url(/rsrc/image/icon/fatcow/calendar_edit.png) diff --git a/webroot/rsrc/css/application/conpherence/header-pane.css b/webroot/rsrc/css/application/conpherence/header-pane.css index d007091de3..27198a2879 100644 --- a/webroot/rsrc/css/application/conpherence/header-pane.css +++ b/webroot/rsrc/css/application/conpherence/header-pane.css @@ -29,6 +29,14 @@ width: 50px; } +.conpherence-header-pane .custom-header-image { + position: absolute; + top: 0px; + left: 0px; + height: 80px; + width: 120px; +} + .conpherence-header-pane .title { position: relative; font-size: 16px; @@ -39,6 +47,16 @@ overflow-x: auto; } +.conpherence-header-pane .custom-title { + position: relative; + font-size: 16px; + font-weight: bold; + left: 132px; + top: 21px; + max-width: 80%; + overflow-x: auto; +} + .conpherence-header-pane .subtitle { position: relative; left: 77px; @@ -47,4 +65,34 @@ max-width: 80%; } +.conpherence-header-pane .custom-subtitle { + position: relative; + left: 132px; + top: 21px; + color: #bfbfbf; + max-width: 80%; +} + +.conpherence-header-pane .upload-photo { + display: none; +} + +.conpherence-header-upload-photo .upload-photo { + display: block; + width: 100%; + padding: 32px; + font-size: 16px; + background: #99ff99; + border-color: #669966; +} + +.conpherence-header-upload-photo .edit, +.conpherence-header-upload-photo .header-image, +.conpherence-header-upload-photo .custom-header-image, +.conpherence-header-upload-photo .title, +.conpherence-header-upload-photo .custom-title, +.conpherence-header-upload-photo .subtitle, +.conpherence-header-upload-photo .custom-subtitle { + display: none; +} diff --git a/webroot/rsrc/css/application/conpherence/update.css b/webroot/rsrc/css/application/conpherence/update.css index fcf26c9674..bcd26fb4b4 100644 --- a/webroot/rsrc/css/application/conpherence/update.css +++ b/webroot/rsrc/css/application/conpherence/update.css @@ -5,3 +5,13 @@ .phabricator-standard-page-body .aphront-dialog-view { margin: 20px auto 0px auto; } + +.aphront-dialog-view .conpherence-dialogue-drag-photo { + border: 1px dashed #bfbfbf; + padding: 10px 0px 10px 10px; +} + +.aphront-dialog-view .conpherence-dialogue-upload-photo { + background: #99ff99; + border-color: #669966; +} diff --git a/webroot/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js b/webroot/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js new file mode 100644 index 0000000000..a19bba029f --- /dev/null +++ b/webroot/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js @@ -0,0 +1,39 @@ +/** + * @provides javelin-behavior-conpherence-drag-and-drop-photo + * @requires javelin-behavior + * javelin-dom + * javelin-workflow + * phabricator-drag-and-drop-file-upload + */ + +JX.behavior('conpherence-drag-and-drop-photo', function(config) { + + var target = JX.$(config.target); + var form_pane = JX.$(config.form_pane); + + function onupload(f) { + var data = { + 'file_id' : f.getID(), + 'action' : 'metadata' + }; + + var form = JX.DOM.find(form_pane, 'form'); + var workflow = JX.Workflow.newFromForm(form, data); + workflow.start(); + } + + if (JX.PhabricatorDragAndDropFileUpload.isSupported()) { + var drop = new JX.PhabricatorDragAndDropFileUpload(target) + .setURI(config.upload_uri); + drop.listen('didBeginDrag', function(e) { + JX.DOM.alterClass(target, config.activated_class, true); + }); + drop.listen('didEndDrag', function(e) { + JX.DOM.alterClass(target, config.activated_class, false); + }); + drop.listen('didUpload', onupload); + drop.start(); + } + +}); + diff --git a/webroot/rsrc/js/application/core/behavior-crop.js b/webroot/rsrc/js/application/core/behavior-crop.js new file mode 100644 index 0000000000..965e3336f2 --- /dev/null +++ b/webroot/rsrc/js/application/core/behavior-crop.js @@ -0,0 +1,94 @@ +/** + * @provides javelin-behavior-aphront-crop + * @requires javelin-behavior + * javelin-dom + * javelin-vector + * javelin-magical-init + */ + + JX.behavior('aphront-crop', function(config) { + + var dragging = false; + var startX, startY; + var finalX, finalY; + var dScale = config.scale; + + var cropBox = JX.$(config.cropBoxID); + var basePos = JX.$V(cropBox); + cropBox.style.height = config.height + 'px'; + cropBox.style.width = config.width + 'px'; + var baseD = JX.$V(config.width, config.height); + + var image = JX.DOM.find(cropBox, 'img', 'crop-image'); + image.style.height = (config.imageH * config.scale) + 'px'; + image.style.width = (config.imageW * config.scale) + 'px'; + var imageD = JX.$V( + config.imageW * config.scale, + config.imageH * config.scale + ); + var minLeft = baseD.x - imageD.x; + var minTop = baseD.y - imageD.y; + + var minScale = Math.min( + config.width / config.imageW, + config.height / config.imageH, + 1 + ); + var maxScale = Math.max( + config.imageW / config.width, + config.imageH / config.height, + 2 + ); + + var ondrag = function(e) { + e.kill(); + dragging = true; + var p = JX.$V(e); + startX = p.x; + startY = p.y; + }; + + var onmove = function(e) { + if (!dragging) { + return; + } + e.kill(); + + var p = JX.$V(e); + var dx = startX - p.x; + var dy = startY - p.y; + var imagePos = JX.$V(image); + var moveLeft = imagePos.x - basePos.x - dx; + var moveTop = imagePos.y - basePos.y - dy; + + image.style.left = Math.min(Math.max(minLeft, moveLeft), 0) + 'px'; + image.style.top = Math.min(Math.max(minTop, moveTop), 0) + 'px'; + + // reset these; a new beginning! + startX = p.x; + startY = p.y; + + // save off where we are right now + imagePos = JX.$V(image); + finalX = Math.abs(imagePos.x - basePos.x); + finalY = Math.abs(imagePos.y - basePos.y); + JX.DOM.find(cropBox, 'input', 'crop-x').value = finalX; + JX.DOM.find(cropBox, 'input', 'crop-y').value = finalY; + }; + + var ondrop = function(e) { + if (!dragging) { + return; + } + dragging = false; + }; + + // NOTE: Javelin does not dispatch mousemove by default. + JX.enableDispatch(cropBox, 'mousemove'); + + JX.DOM.listen(cropBox, 'mousedown', [], ondrag); + JX.DOM.listen(cropBox, 'mousemove', [], onmove); + JX.DOM.listen(cropBox, 'mouseup', [], ondrop); + JX.DOM.listen(cropBox, 'mouseout', [], ondrop); + +});