diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 7c715ca5c1..0c7ae3b8ea 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -7,6 +7,13 @@ */ celerity_register_resource_map(array( + '/rsrc/image/app/app_applications.png' => + array( + 'hash' => '0e83b7bea93bf92777e546ae6c7ac1cb', + 'uri' => '/res/0e83b7be/rsrc/image/app/app_applications.png', + 'disk' => '/rsrc/image/app/app_applications.png', + 'type' => 'png', + ), '/rsrc/image/app/app_audit.png' => array( 'hash' => '53340003d1daf306b64ed5ebb08bc204', @@ -140,6 +147,27 @@ celerity_register_resource_map(array( 'disk' => '/rsrc/image/bolt.png', 'type' => 'png', ), + '/rsrc/image/button_apps.png' => + array( + 'hash' => 'cc29f793afd01b15af613562225118f3', + 'uri' => '/res/cc29f793/rsrc/image/button_apps.png', + 'disk' => '/rsrc/image/button_apps.png', + 'type' => 'png', + ), + '/rsrc/image/button_content.png' => + array( + 'hash' => '87cc5797352097b4b3d7541e6c46f032', + 'uri' => '/res/87cc5797/rsrc/image/button_content.png', + 'disk' => '/rsrc/image/button_content.png', + 'type' => 'png', + ), + '/rsrc/image/button_menu.png' => + array( + 'hash' => '5742857c7734d9d25be1125f5737fe0e', + 'uri' => '/res/5742857c/rsrc/image/button_menu.png', + 'disk' => '/rsrc/image/button_menu.png', + 'type' => 'png', + ), '/rsrc/image/credit_cards.png' => array( 'hash' => '681448de424ea159b6ea68af04c046ae', @@ -1153,7 +1181,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-device' => array( - 'uri' => '/res/bec84986/rsrc/js/application/core/behavior-device.js', + 'uri' => '/res/37669d1a/rsrc/js/application/core/behavior-device.js', 'type' => 'js', 'requires' => array( @@ -1614,7 +1642,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-phabricator-nav' => array( - 'uri' => '/res/adaae8ae/rsrc/js/application/core/behavior-phabricator-nav.js', + 'uri' => '/res/cb8979b2/rsrc/js/application/core/behavior-phabricator-nav.js', 'type' => 'js', 'requires' => array( @@ -1623,6 +1651,9 @@ celerity_register_resource_map(array( 2 => 'javelin-dom', 3 => 'javelin-magical-init', 4 => 'javelin-vector', + 5 => 'javelin-request', + 6 => 'javelin-util', + 7 => 'javelin-fx', ), 'disk' => '/rsrc/js/application/core/behavior-phabricator-nav.js', ), @@ -1819,7 +1850,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-toggle-class' => array( - 'uri' => '/res/35b86b96/rsrc/js/application/core/behavior-toggle-class.js', + 'uri' => '/res/fa818e0f/rsrc/js/application/core/behavior-toggle-class.js', 'type' => 'js', 'requires' => array( @@ -2533,7 +2564,7 @@ celerity_register_resource_map(array( ), 'phabricator-nav-view-css' => array( - 'uri' => '/res/3443576d/rsrc/css/aphront/phabricator-nav-view.css', + 'uri' => '/res/82636b80/rsrc/css/aphront/phabricator-nav-view.css', 'type' => 'css', 'requires' => array( diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6f8afa6705..cf538cb35b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -882,6 +882,7 @@ phutil_register_library_map(array( 'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php', 'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php', 'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php', + 'PhabricatorPeopleAdjustSettingController' => 'applications/people/controller/PhabricatorPeopleAdjustSettingController.php', 'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php', 'PhabricatorPeopleEditController' => 'applications/people/controller/PhabricatorPeopleEditController.php', 'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php', @@ -1964,6 +1965,7 @@ phutil_register_library_map(array( 'PhabricatorPasteListController' => 'PhabricatorPasteController', 'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyQuery', 'PhabricatorPasteViewController' => 'PhabricatorPasteController', + 'PhabricatorPeopleAdjustSettingController' => 'PhabricatorPeopleController', 'PhabricatorPeopleController' => 'PhabricatorController', 'PhabricatorPeopleEditController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController', diff --git a/src/applications/audit/application/PhabricatorApplicationAudit.php b/src/applications/audit/application/PhabricatorApplicationAudit.php index 02b15ab5e9..d71aa0e350 100644 --- a/src/applications/audit/application/PhabricatorApplicationAudit.php +++ b/src/applications/audit/application/PhabricatorApplicationAudit.php @@ -42,6 +42,10 @@ final class PhabricatorApplicationAudit extends PhabricatorApplication { ); } + public function getCoreApplicationOrder() { + return 0.130; + } + public function loadStatus(PhabricatorUser $user) { $status = array(); diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index d91f15f3f9..64d53542ae 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -62,6 +62,10 @@ abstract class PhabricatorApplication { return true; } + public function getCoreApplicationOrder() { + return null; + } + /* -( URI Routing )-------------------------------------------------------- */ diff --git a/src/applications/differential/application/PhabricatorApplicationDifferential.php b/src/applications/differential/application/PhabricatorApplicationDifferential.php index 72de852a8d..4354c800a4 100644 --- a/src/applications/differential/application/PhabricatorApplicationDifferential.php +++ b/src/applications/differential/application/PhabricatorApplicationDifferential.php @@ -66,6 +66,10 @@ final class PhabricatorApplicationDifferential extends PhabricatorApplication { ); } + public function getCoreApplicationOrder() { + return 0.100; + } + public function loadStatus(PhabricatorUser $user) { $revisions = id(new DifferentialRevisionQuery()) ->withResponsibleUsers(array($user->getPHID())) diff --git a/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php b/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php index 1db129df93..fc5a47aaf9 100644 --- a/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php +++ b/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php @@ -72,5 +72,9 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication { ); } + public function getCoreApplicationOrder() { + return 0.120; + } + } diff --git a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php index 5982b69a60..1c5976d56b 100644 --- a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php +++ b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php @@ -34,6 +34,10 @@ final class PhabricatorApplicationManiphest extends PhabricatorApplication { return celerity_get_resource_uri('/rsrc/image/app/app_maniphest.png'); } + public function getCoreApplicationOrder() { + return 0.110; + } + public function getFactObjectsForAnalysis() { return array( new ManiphestTask(), diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php index 471b229fac..224c940fcb 100644 --- a/src/applications/maniphest/controller/ManiphestController.php +++ b/src/applications/maniphest/controller/ManiphestController.php @@ -84,6 +84,11 @@ abstract class ManiphestController extends PhabricatorController { $nav->addLabel('Reports'); $nav->addFilter('report', 'Reports', '/maniphest/report/'); + $nav->setFlexNav(true); + $nav->setShowApplicationMenu(true); + $nav->setCurrentApplication($this->getCurrentApplication()); + $nav->setUser($this->getRequest()->getUser()); + return $nav; } diff --git a/src/applications/meta/application/PhabricatorApplicationApplications.php b/src/applications/meta/application/PhabricatorApplicationApplications.php index f9360be2cf..60f3e106f0 100644 --- a/src/applications/meta/application/PhabricatorApplicationApplications.php +++ b/src/applications/meta/application/PhabricatorApplicationApplications.php @@ -26,6 +26,10 @@ final class PhabricatorApplicationApplications extends PhabricatorApplication { return 'Manage Applications'; } + public function getIconURI() { + return celerity_get_resource_uri('/rsrc/image/app/app_applications.png'); + } + public function getRoutes() { return array( '/applications/' => array( diff --git a/src/applications/people/application/PhabricatorApplicationSettings.php b/src/applications/people/application/PhabricatorApplicationSettings.php index 70f9508e3f..65c2468415 100644 --- a/src/applications/people/application/PhabricatorApplicationSettings.php +++ b/src/applications/people/application/PhabricatorApplicationSettings.php @@ -34,6 +34,7 @@ final class PhabricatorApplicationSettings extends PhabricatorApplication { return array( '/settings/' => array( '(?:page/(?P[^/]+)/)?' => 'PhabricatorUserSettingsController', + 'adjust/' => 'PhabricatorPeopleAdjustSettingController', ), ); } diff --git a/src/applications/people/controller/PhabricatorPeopleAdjustSettingController.php b/src/applications/people/controller/PhabricatorPeopleAdjustSettingController.php new file mode 100644 index 0000000000..b147af1a9e --- /dev/null +++ b/src/applications/people/controller/PhabricatorPeopleAdjustSettingController.php @@ -0,0 +1,34 @@ +getRequest(); + $user = $request->getUser(); + + $prefs = $user->loadPreferences(); + $prefs->setPreference( + $request->getStr('key'), + $request->getStr('value')); + $prefs->save(); + + return id(new AphrontAjaxResponse())->setContent(array()); + } +} diff --git a/src/applications/people/storage/PhabricatorUserPreferences.php b/src/applications/people/storage/PhabricatorUserPreferences.php index 2d4941207b..5f6691ca1d 100644 --- a/src/applications/people/storage/PhabricatorUserPreferences.php +++ b/src/applications/people/storage/PhabricatorUserPreferences.php @@ -33,6 +33,9 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO { const PREFERENCE_DIFFUSION_VIEW = 'diffusion-view'; const PREFERENCE_DIFFUSION_SYMBOLS = 'diffusion-symbols'; + const PREFERENCE_NAV_COLLAPSED = 'nav-collapsed'; + const PREFERENCE_NAV_WIDTH = 'nav-width'; + protected $userPHID; protected $preferences = array(); diff --git a/src/applications/phriction/application/PhabricatorApplicationPhriction.php b/src/applications/phriction/application/PhabricatorApplicationPhriction.php index 02c0d67a41..080cdd6863 100644 --- a/src/applications/phriction/application/PhabricatorApplicationPhriction.php +++ b/src/applications/phriction/application/PhabricatorApplicationPhriction.php @@ -53,5 +53,9 @@ final class PhabricatorApplicationPhriction extends PhabricatorApplication { ); } + public function getCoreApplicationOrder() { + return 0.140; + } + } diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index de662f2d35..a8ebba4c53 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -42,6 +42,24 @@ final class AphrontSideNavFilterView extends AphrontView { private $selectedFilter = false; private $flexNav; private $flexible; + private $showApplicationMenu; + private $user; + private $currentApplication; + + public function setCurrentApplication(PhabricatorApplication $current) { + $this->currentApplication = $current; + return $this; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function setShowApplicationMenu($show_application_menu) { + $this->showApplicationMenu = $show_application_menu; + return $this; + } public function setFlexNav($flex_nav) { $this->flexNav = $flex_nav; @@ -129,6 +147,13 @@ final class AphrontSideNavFilterView extends AphrontView { $view = new AphrontSideNavView(); $view->setFlexNav($this->flexNav); $view->setFlexible($this->flexible); + $view->setShowApplicationMenu($this->showApplicationMenu); + if ($this->user) { + $view->setUser($this->user); + } + if ($this->currentApplication) { + $view->setCurrentApplication($this->currentApplication); + } foreach ($this->items as $item) { list($type, $key, $name) = $item; switch ($type) { diff --git a/src/view/layout/AphrontSideNavView.php b/src/view/layout/AphrontSideNavView.php index 6a43eee791..f4a0bedb2f 100644 --- a/src/view/layout/AphrontSideNavView.php +++ b/src/view/layout/AphrontSideNavView.php @@ -18,9 +18,27 @@ final class AphrontSideNavView extends AphrontView { - protected $items = array(); - protected $flexNav; - protected $isFlexible; + private $items = array(); + private $flexNav; + private $isFlexible; + private $showApplicationMenu; + private $user; + private $currentApplication; + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function setShowApplicationMenu($show_application_menu) { + $this->showApplicationMenu = $show_application_menu; + return $this; + } + + public function setCurrentApplication(PhabricatorApplication $current) { + $this->currentApplication = $current; + return $this; + } public function addNavItem($item) { $this->items[] = $item; @@ -42,20 +60,60 @@ final class AphrontSideNavView extends AphrontView { $view->appendChild($this->items); if ($this->flexNav) { + $user = $this->user; + require_celerity_resource('phabricator-nav-view-css'); - $nav_id = celerity_generate_unique_node_id(); - $drag_id = celerity_generate_unique_node_id(); + $nav_classes = array(); + $nav_classes[] = 'phabricator-nav'; + + $app_id = celerity_generate_unique_node_id(); + $nav_id = null; + $drag_id = null; $content_id = celerity_generate_unique_node_id(); + $collapse_id = null; + $expand_id = null; + $main_id = celerity_generate_unique_node_id(); + + $apps = $this->renderApplications(); + + $key = PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED; + if ($user->loadPreferences()->getPreference($key)) { + $nav_classes[] = 'phabricator-nav-app-collapsed'; + } + + $collapse_id = celerity_generate_unique_node_id(); + $expand_id = celerity_generate_unique_node_id(); + + $collapse_button = phutil_render_tag( + 'a', + array( + 'href' => '#', + 'class' => 'phabricator-nav-app-button-collapse', + 'id' => $collapse_id, + ), + '« Collapse'); + $expand_button = phutil_render_tag( + 'a', + array( + 'href' => '#', + 'class' => 'phabricator-nav-app-button-expand', + 'id' => $expand_id, + ), + '»'); + + $app_menu = phutil_render_tag( + 'div', + array( + 'class' => 'phabricator-nav-col phabricator-nav-app', + 'id' => $app_id, + ), + $apps->render()). + $expand_button. + $collapse_button; if ($this->flexible) { - Javelin::initBehavior( - 'phabricator-nav', - array( - 'navID' => $nav_id, - 'dragID' => $drag_id, - 'contentID' => $content_id, - )); + $drag_id = celerity_generate_unique_node_id(); $flex_bar = phutil_render_tag( 'div', array( @@ -67,24 +125,66 @@ final class AphrontSideNavView extends AphrontView { $flex_bar = null; } - return - '
'. - phutil_render_tag( - 'div', - array( - 'class' => 'phabricator-nav-col', - 'id' => $nav_id, - ), - $view->render()). - $flex_bar. - phutil_render_tag( - 'div', - array( - 'class' => 'phabricator-nav-content', - 'id' => $content_id, - ), - $this->renderChildren()). + $nav_menu = null; + if ($this->items) { + $local_id = celerity_generate_unique_node_id(); + $nav_classes[] = 'has-local-nav'; + $local_menu = phutil_render_tag( + 'div', + array( + 'class' => 'phabricator-nav-col phabricator-nav-local', + 'id' => $local_id, + ), + $view->render()); + } + + Javelin::initBehavior( + 'phabricator-nav', + array( + 'mainID' => $main_id, + 'appID' => $app_id, + 'localID' => $local_id, + 'dragID' => $drag_id, + 'contentID' => $content_id, + 'collapseID' => $collapse_id, + 'expandID' => $expand_id, + 'collapseKey' => $key, + )); + + $header_part = + '
'. + '
'. + ''. + ''. + '
'. + ''. '
'; + + return $header_part.phutil_render_tag( + 'div', + array( + 'class' => implode(' ', $nav_classes), + 'id' => $main_id, + ), + $app_menu. + $local_menu. + $flex_bar. + phutil_render_tag( + 'div', + array( + 'class' => 'phabricator-nav-content', + 'id' => $content_id, + ), + $this->renderChildren())); } else { require_celerity_resource('aphront-side-nav-view-css'); @@ -103,4 +203,52 @@ final class AphrontSideNavView extends AphrontView { } } + private function renderApplications() { + $core = array(); + $current = $this->currentApplication; + + $meta = null; + + $applications = PhabricatorApplication::getAllInstalledApplications(); + foreach ($applications as $application) { + if ($application instanceof PhabricatorApplicationApplications) { + $meta = $application; + continue; + } + if ($application->getCoreApplicationOrder() !== null) { + $core[] = $application; + } + } + + $core = msort($core, 'getCoreApplicationOrder'); + if ($meta) { + $core[] = $meta; + } + $core = mpull($core, null, 'getPHID'); + + if ($current && empty($core[$current->getPHID()])) { + array_unshift($core, $this->current); + } + + $apps = array(); + foreach ($core as $phid => $application) { + $classes = array(); + $classes[] = 'phabricator-nav-app-item'; + if ($current && $phid == $current->getPHID()) { + $classes[] = 'phabricator-nav-app-item-selected'; + } + + $apps[] = phutil_render_tag( + 'a', + array( + 'class' => implode(' ', $classes), + 'href' => $application->getBaseURI(), + 'style' => 'background-image: url('.$application->getIconURI().')', + ), + phutil_escape_html($application->getName())); + } + + return id(new AphrontNullView())->appendChild($apps); + } + } diff --git a/webroot/rsrc/css/aphront/phabricator-nav-view.css b/webroot/rsrc/css/aphront/phabricator-nav-view.css index fecb298970..65a54e1dd2 100644 --- a/webroot/rsrc/css/aphront/phabricator-nav-view.css +++ b/webroot/rsrc/css/aphront/phabricator-nav-view.css @@ -11,11 +11,9 @@ top: 44px; left: 0; bottom: 0; - width: 179px; - background: #e3e3e3; border-right: 1px solid #999c9e; - box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.20); + box-shadow: inset -3px 0 4px rgba(0, 0, 0, 0.05); overflow-y: auto; overflow-x: hidden; @@ -23,6 +21,21 @@ white-space: nowrap; } +.phabricator-nav-app { + width: 149px; + background: #d2d2d2; +} + +.phabricator-nav-local { + width: 179px; + background: #ececec; +} + +.device-tablet .phabricator-nav-local, +.device-phone .phabricator-nav-local { + width: 299px; +} + .phabricator-nav-drag { position: fixed; top: 44px; @@ -37,7 +50,7 @@ border-width: 0 1px 0 1px; border-color: #fff #999c9e #fff #999c9e; - box-shadow: inset -1px 0px 2px rgba(0, 0, 0, 0.15); + box-shadow: inset -1px 0px 1px rgba(0, 0, 0, 0.15); background-image: url(/rsrc/image/divot.png); background-position: center; @@ -54,10 +67,30 @@ display: block; } -.phabricator-nav-content { - margin-left: 180px; +.phabricator-nav-local { + left: 150px; } +.device-desktop .phabricator-nav-app-collapsed .phabricator-nav-local { + left: 34px; +} + +.phabricator-nav-content { + margin-left: 150px; +} + +.has-local-nav .phabricator-nav-content { + margin-left: 330px; +} + +.device-desktop .phabricator-nav-app-collapsed .phabricator-nav-content { + margin-left: 32px; +} + +.device-desktop .has-local-nav.phabricator-nav-app-collapsed + .phabricator-nav-content { + margin-left: 212px; +} .phabricator-nav-col span { display: block; @@ -74,10 +107,183 @@ } .phabricator-nav-col a.aphront-side-nav-selected { - background: #a1bbe5; + background-color: #a1bbe5; } -.phabricator-nav-col a:hover { - background: #3875d7; +.device-desktop .phabricator-nav-col a:hover { + background-color: #3875d7; color: #ffffff; } + +a.phabricator-nav-app-item { + color: #222222; + font-weight: normal; + padding: 4px; + padding-left: 37px; + vertical-align: middle; + line-height: 25px; + + border-width: 1px 0px; + border-style: solid; + border-color: transparent; + + background-size: auto 25px; + background-position: 4px 4px; + background-repeat: no-repeat; +} + +a.phabricator-nav-app-item-selected { + background-color: #f3f3f3; + box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.20); + border-color: #b0b0b0; +} + + +a.phabricator-nav-app-button-expand, +a.phabricator-nav-app-button-collapse { + position: fixed; + display: none; + left: 0; + bottom: 0; + padding: 4px; + z-index: 2; + background: #d9d9d9; + line-height: 14px; + border-top: 1px solid #a9a9a9; + text-align: center; + font-size: 11px; + box-shadow: inset -1px -1px 3px rgba(0, 0, 0, 0.1); + color: #696969; + text-decoration: none; +} + +.phabricator-nav-app-button-collapse { + width: 141px; +} + +.phabricator-nav-app-button-expand { + width: 25px; + display: none; +} + +.device-desktop .phabricator-nav-app-button-expand, +.device-desktop .phabricator-nav-app-button-collapse { + display: block; +} + +.device-desktop .phabricator-nav-app-collapsed .phabricator-nav-app { + width: 33px; +} + +.phabricator-nav-app-collapsed .phabricator-nav-app-button-collapse { + display: none; +} + +.device-desktop .phabricator-nav-app-collapsed + .phabricator-nav-app-button-expand { + display: block; +} + +.device-desktop .phabricator-nav-head { + display: none; +} + +.device-tablet .phabricator-nav-col, +.device-phone .phabricator-nav-col { + position: absolute; + top: 0px; +} + +.device-tablet .phabricator-nav-app, +.device-phone .phabricator-nav-app { + left: -450px; +} + +.device-tablet .phabricator-nav-local, +.device-phone .phabricator-nav-local { + left: -300px; +} + +.device-phone .phabricator-nav-head-tablet { + display: none; +} + +.device-tablet .phabricator-nav-head-phone { + display: none; +} + +.device-tablet .phabricator-nav, +.device-phone .phabricator-nav { + overflow-x: hidden; + position: relative; +} + +.device-tablet .phabricator-nav-content, +.device-phone .phabricator-nav-content { + width: 100%; +} + +.device-tablet .phabricator-nav-content, +.device-phone .phabricator-nav-content { + margin-left: 0; + position: relative; +} + +.phabricator-nav-head { + display: block; + position: relative; + height: 43px; + background: #e6e6e6; + overflow: hidden; + border-bottom: 1px solid #9d9d9d; + text-align: center; + box-shadow: inset 0 0px 3px rgba(0, 0, 0, 0.30), + 0px 1px 2px rgba(0, 0, 0, 0.10); + z-index: 3; +} + +.nav-button { + background-color: #f3f3f3; + height: 32px; + width: 40px; + margin: 5px 0px; + display: inline-block; + border: 1px solid #999999; + box-shadow: inset -1px -1px 3px rgba(0, 0, 0, 0.10); + background-repeat: no-repeat; +} + +.nav-button-selected { + background-color: #c9c9c9; + box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.20); +} + +.nav-button + .nav-button { + margin-left: -1px; +} + +.nav-button-w { + border-radius: 6px 0 0 6px; +} + +.nav-button-e { + border-radius: 0 6px 6px 0; +} + +.nav-button-apps { + background-image: url(/rsrc/image/button_apps.png); + background-size: 24px auto; + background-position: center; +} + +.nav-button-menu { + background-image: url(/rsrc/image/button_menu.png); + background-size: 24px auto; + background-position: center; +} + +.nav-button-content { + background-image: url(/rsrc/image/button_content.png); + background-size: 24px auto; + background-position: center; +} diff --git a/webroot/rsrc/image/app/app_applications.png b/webroot/rsrc/image/app/app_applications.png new file mode 100644 index 0000000000..ff13f2866e Binary files /dev/null and b/webroot/rsrc/image/app/app_applications.png differ diff --git a/webroot/rsrc/image/button_apps.png b/webroot/rsrc/image/button_apps.png new file mode 100644 index 0000000000..ff13f2866e Binary files /dev/null and b/webroot/rsrc/image/button_apps.png differ diff --git a/webroot/rsrc/image/button_content.png b/webroot/rsrc/image/button_content.png new file mode 100755 index 0000000000..3ae1bf4fc9 Binary files /dev/null and b/webroot/rsrc/image/button_content.png differ diff --git a/webroot/rsrc/image/button_menu.png b/webroot/rsrc/image/button_menu.png new file mode 100755 index 0000000000..e4d35d52da Binary files /dev/null and b/webroot/rsrc/image/button_menu.png differ diff --git a/webroot/rsrc/js/application/core/behavior-device.js b/webroot/rsrc/js/application/core/behavior-device.js index 920d7569c7..06bd90a4b6 100644 --- a/webroot/rsrc/js/application/core/behavior-device.js +++ b/webroot/rsrc/js/application/core/behavior-device.js @@ -31,6 +31,8 @@ JX.behavior('device', function(config) { JX.DOM.alterClass(e, 'device-phone', (device == 'phone')); JX.DOM.alterClass(e, 'device-tablet', (device == 'tablet')); JX.DOM.alterClass(e, 'device-desktop', (device == 'desktop')); + + JX.Stratcom.invoke('phabricator-device-change', device); } JX.Stratcom.listen('resize', null, onresize); diff --git a/webroot/rsrc/js/application/core/behavior-phabricator-nav.js b/webroot/rsrc/js/application/core/behavior-phabricator-nav.js index c1c182f122..c485660636 100644 --- a/webroot/rsrc/js/application/core/behavior-phabricator-nav.js +++ b/webroot/rsrc/js/application/core/behavior-phabricator-nav.js @@ -5,93 +5,202 @@ * javelin-dom * javelin-magical-init * javelin-vector + * javelin-request + * javelin-util + * javelin-fx * @javelin */ JX.behavior('phabricator-nav', function(config) { - var dragging; - var track; - - var nav = JX.$(config.navID); - var drag = JX.$(config.dragID); + var app = JX.$(config.appID); var content = JX.$(config.contentID); + var local = config.localID ? JX.$(config.localID) : null; - JX.enableDispatch(document.body, 'mousemove'); - JX.DOM.listen(drag, 'mousedown', null, function(e) { - dragging = JX.$V(e); +// - Sliding Menu Animations --------------------------------------------------- - // Show the "col-resize" cursor on the whole document while we're - // dragging, since the mouse will slip off the actual bar fairly often and - // we don't want it to flicker. - JX.DOM.alterClass(document.body, 'jx-drag-col', true); + var animations = []; + function slide_menu(position) { + var app_width = 150; + var local_width = local ? 300 : 0; - track = [ - { - element: nav, - parameter: 'width', - start: JX.Vector.getDim(nav).x, - scale: 1, + var shifts = { + 0: 0, + 1: app_width - 10, + 2: app_width + local_width + }; + var shift = shifts[position]; - width: JX.Vector.getDim(nav).x, - minWidth: 150, - minScale: 1 - }, - { - element: drag, - parameter: 'left', - start: JX.$V(drag).x, - scale: 1 - }, - { - element: content, - parameter: 'marginLeft', - start: parseInt(getComputedStyle(content).marginLeft, 10), - scale: 1, + while (animations.length) { + animations.pop().stop(); + } + animations.push(build_animation(app, -shift)); + local && animations.push(build_animation(local, -shift + app_width)); + animations.push(build_animation(content, -shift + app_width + local_width)); - width: JX.Vector.getDim(content).x, - minWidth: 300, - minScale: -1 + select_button(position); + } + + function build_animation(element, target) { + return new JX.FX(element) + .setDuration(100) + .start({left: [JX.$V(element).x, target]}); + } + + +// - Sliding Menu Buttons ------------------------------------------------------ + + var button_positions = { + 0: [JX.$('phone-menu1'), JX.$('tablet-menu1')], + 1: [JX.$('phone-menu2')], + 2: [JX.$('phone-menu3'), JX.$('tablet-menu2')] + }; + + for (var k in button_positions) { + for (var ii = 0; ii < button_positions[k].length; ii++) { + var onclick = function(p, e) { + e.kill(); + slide_menu(p); + }; + onclick = JX.bind(null, onclick, k); + JX.DOM.listen( + button_positions[k][ii], + ['touchstart', 'mousedown'], + null, + onclick); + } + } + + function select_button(position) { + for (var k in button_positions) { + for (var ii = 0; ii < button_positions[k].length; ii++) { + JX.DOM.alterClass( + button_positions[k][ii], + 'nav-button-selected', + (k == position)); } - ]; + } + } - e.kill(); + +// - Application Menu Collapse/Expand ------------------------------------------ + + function collapse(state, skip_save) { + JX.DOM.alterClass( + JX.$(config.mainID), + 'phabricator-nav-app-collapsed', + state); + + if (!skip_save) { + new JX.Request('/settings/adjust/', JX.bag) + .setData({key: config.collapseKey, value: state ? 1 : ''}) + .send(); + } + } + + JX.DOM.listen(JX.$(config.collapseID), 'click', null, function(e) { + collapse(true); }); - JX.Stratcom.listen('mousemove', null, function(e) { - if (!dragging) { - return; - } - - var dx = JX.$V(e).x - dragging.x; - var panel; - - for (var k = 0; k < track.length; k++) { - panel = track[k]; - if (!panel.minWidth) { - continue; - } - var new_width = panel.width + (dx * panel.minScale); - if (new_width < panel.minWidth) { - dx = (panel.minWidth - panel.width) * panel.minScale; - } - } - - for (var k = 0; k < track.length; k++) { - panel = track[k]; - var v = (panel.start + (dx * panel.scale)); - panel.element.style[panel.parameter] = v + 'px'; - } + JX.DOM.listen(JX.$(config.expandID), 'click', null, function(e) { + collapse(false); }); - JX.Stratcom.listen('mouseup', null, function(e) { - if (!dragging) { - return; - } - JX.DOM.alterClass(document.body, 'jx-drag-col', false); - dragging = false; +// - Flexible Navigation Column ------------------------------------------------ + + if (config.dragID) { + var dragging; + var track; + + var drag = JX.$(config.dragID); + JX.enableDispatch(document.body, 'mousemove'); + + JX.DOM.listen(drag, 'mousedown', null, function(e) { + dragging = JX.$V(e); + + // Show the "col-resize" cursor on the whole document while we're + // dragging, since the mouse will slip off the actual bar fairly often and + // we don't want it to flicker. + JX.DOM.alterClass(document.body, 'jx-drag-col', true); + + track = [ + { + element: local, + parameter: 'width', + start: JX.Vector.getDim(local).x, + scale: 1, + + width: JX.Vector.getDim(local).x, + minWidth: 150, + minScale: 1 + }, + { + element: drag, + parameter: 'left', + start: JX.$V(drag).x, + scale: 1 + }, + { + element: content, + parameter: 'marginLeft', + start: parseInt(getComputedStyle(content).marginLeft, 10), + scale: 1, + + width: JX.Vector.getDim(content).x, + minWidth: 300, + minScale: -1 + } + ]; + + e.kill(); + }); + + JX.Stratcom.listen('mousemove', null, function(e) { + if (!dragging) { + return; + } + + var dx = JX.$V(e).x - dragging.x; + var panel; + + for (var k = 0; k < track.length; k++) { + panel = track[k]; + if (!panel.minWidth) { + continue; + } + var new_width = panel.width + (dx * panel.minScale); + if (new_width < panel.minWidth) { + dx = (panel.minWidth - panel.width) * panel.minScale; + } + } + + for (var k = 0; k < track.length; k++) { + panel = track[k]; + var v = (panel.start + (dx * panel.scale)); + panel.element.style[panel.parameter] = v + 'px'; + } + }); + + JX.Stratcom.listen('mouseup', null, function(e) { + if (!dragging) { + return; + } + JX.DOM.alterClass(document.body, 'jx-drag-col', false); + dragging = false; + }); + } + + +// - Navigation Reset ---------------------------------------------------------- + + JX.Stratcom.listen('phabricator-device-change', null, function(device) { + app.style.left = ''; + local && (local.style.left = ''); + content.style.left = ''; + + select_button(2); }); }); diff --git a/webroot/rsrc/js/application/core/behavior-toggle-class.js b/webroot/rsrc/js/application/core/behavior-toggle-class.js index 1e10038338..ce55deb3dc 100644 --- a/webroot/rsrc/js/application/core/behavior-toggle-class.js +++ b/webroot/rsrc/js/application/core/behavior-toggle-class.js @@ -18,7 +18,7 @@ */ JX.behavior('toggle-class', function() { JX.Stratcom.listen( - 'click', + ['touchstart', 'mousedown'], 'jx-toggle-class', function(e) { e.kill();