diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 6cf8ea88c8..6c39f42915 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -135,7 +135,7 @@ celerity_register_resource_map(array( ), 'aphront-table-view-css' => array( - 'uri' => '/res/f4f39a2e/rsrc/css/aphront/table-view.css', + 'uri' => '/res/3ff30c4f/rsrc/css/aphront/table-view.css', 'type' => 'css', 'requires' => array( @@ -1962,7 +1962,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - '78e8854e' => + '05e42357' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -1987,7 +1987,7 @@ celerity_register_resource_map(array( 17 => 'aphront-pager-view-css', 18 => 'phabricator-transaction-view-css', ), - 'uri' => '/res/pkg/78e8854e/core.pkg.css', + 'uri' => '/res/pkg/05e42357/core.pkg.css', 'type' => 'css', ), '21d01ed8' => @@ -2134,17 +2134,17 @@ celerity_register_resource_map(array( 'reverse' => array( 'aphront-attached-file-view-css' => '31583232', - 'aphront-crumbs-view-css' => '78e8854e', - 'aphront-dialog-view-css' => '78e8854e', - 'aphront-form-view-css' => '78e8854e', + 'aphront-crumbs-view-css' => '05e42357', + 'aphront-dialog-view-css' => '05e42357', + 'aphront-form-view-css' => '05e42357', 'aphront-headsup-action-list-view-css' => '551249fc', - 'aphront-list-filter-view-css' => '78e8854e', - 'aphront-pager-view-css' => '78e8854e', - 'aphront-panel-view-css' => '78e8854e', - 'aphront-side-nav-view-css' => '78e8854e', - 'aphront-table-view-css' => '78e8854e', - 'aphront-tokenizer-control-css' => '78e8854e', - 'aphront-typeahead-control-css' => '78e8854e', + 'aphront-list-filter-view-css' => '05e42357', + 'aphront-pager-view-css' => '05e42357', + 'aphront-panel-view-css' => '05e42357', + 'aphront-side-nav-view-css' => '05e42357', + 'aphront-table-view-css' => '05e42357', + 'aphront-tokenizer-control-css' => '05e42357', + 'aphront-typeahead-control-css' => '05e42357', 'differential-changeset-view-css' => '551249fc', 'differential-core-view-css' => '551249fc', 'differential-inline-comment-editor' => '0d08d81b', @@ -2202,23 +2202,23 @@ celerity_register_resource_map(array( 'maniphest-task-detail-css' => '31583232', 'maniphest-task-summary-css' => '31583232', 'maniphest-transaction-detail-css' => '31583232', - 'phabricator-app-buttons-css' => '78e8854e', + 'phabricator-app-buttons-css' => '05e42357', 'phabricator-content-source-view-css' => '551249fc', - 'phabricator-core-buttons-css' => '78e8854e', - 'phabricator-core-css' => '78e8854e', - 'phabricator-directory-css' => '78e8854e', + 'phabricator-core-buttons-css' => '05e42357', + 'phabricator-core-css' => '05e42357', + 'phabricator-directory-css' => '05e42357', 'phabricator-drag-and-drop-file-upload' => '0d08d81b', 'phabricator-dropdown-menu' => '21d01ed8', - 'phabricator-jump-nav' => '78e8854e', + 'phabricator-jump-nav' => '05e42357', 'phabricator-keyboard-shortcut' => '21d01ed8', 'phabricator-keyboard-shortcut-manager' => '21d01ed8', 'phabricator-menu-item' => '21d01ed8', 'phabricator-object-selector-css' => '551249fc', 'phabricator-paste-file-upload' => '21d01ed8', - 'phabricator-remarkup-css' => '78e8854e', + 'phabricator-remarkup-css' => '05e42357', 'phabricator-shaped-request' => '0d08d81b', - 'phabricator-standard-page-view' => '78e8854e', - 'phabricator-transaction-view-css' => '78e8854e', - 'syntax-highlighting-css' => '78e8854e', + 'phabricator-standard-page-view' => '05e42357', + 'phabricator-transaction-view-css' => '05e42357', + 'syntax-highlighting-css' => '05e42357', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6fff0395ea..a56b3c3a3d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -807,6 +807,7 @@ phutil_register_library_map(array( 'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/option', 'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/poll', 'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/poll', + 'PhabricatorSortTableExample' => 'applications/uiexample/examples/sorttable', 'PhabricatorStandardPageView' => 'view/page/standard', 'PhabricatorStatusController' => 'applications/status/base', 'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/xhpastsymbolname', @@ -1562,6 +1563,7 @@ phutil_register_library_map(array( 'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvotePoll' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController', + 'PhabricatorSortTableExample' => 'PhabricatorUIExample', 'PhabricatorStandardPageView' => 'AphrontPageView', 'PhabricatorStatusController' => 'PhabricatorController', 'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook', diff --git a/src/applications/uiexample/examples/sorttable/PhabricatorSortTableExample.php b/src/applications/uiexample/examples/sorttable/PhabricatorSortTableExample.php new file mode 100644 index 0000000000..d60d7abc45 --- /dev/null +++ b/src/applications/uiexample/examples/sorttable/PhabricatorSortTableExample.php @@ -0,0 +1,112 @@ + 'Honda', + 'model' => 'Civic', + 'year' => 2004, + 'price' => 3199, + 'color' => 'Blue', + ), + array( + 'make' => 'Ford', + 'model' => 'Focus', + 'year' => 2001, + 'price' => 2549, + 'color' => 'Red', + ), + array( + 'make' => 'Toyota', + 'model' => 'Camry', + 'year' => 2009, + 'price' => 4299, + 'color' => 'Black', + ), + array( + 'make' => 'NASA', + 'model' => 'Shuttle', + 'year' => 1998, + 'price' => 1000000000, + 'color' => 'White', + ), + ); + + $request = $this->getRequest(); + + $orders = array( + 'make', + 'model', + 'year', + 'price', + ); + + $sort = $request->getStr('sort'); + list($sort, $reverse) = AphrontTableView::parseSort($sort); + if (!in_array($sort, $orders)) { + $sort = 'make'; + } + + $rows = isort($rows, $sort); + if ($reverse) { + $rows = array_reverse($rows); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'Make', + 'Model', + 'Year', + 'Price', + 'Color', + )); + $table->setColumnClasses( + array( + '', + 'wide', + 'n', + 'n', + '', + )); + $table->makeSortable( + $request->getRequestURI(), + 'sort', + $sort, + $reverse, + $orders); + + $panel = new AphrontPanelView(); + $panel->setHeader('Sortable Table of Vehicles'); + $panel->appendChild($table); + + return $panel; + } +} diff --git a/src/applications/uiexample/examples/sorttable/__init__.php b/src/applications/uiexample/examples/sorttable/__init__.php new file mode 100644 index 0000000000..4fe215cb95 --- /dev/null +++ b/src/applications/uiexample/examples/sorttable/__init__.php @@ -0,0 +1,16 @@ +data = $data; } @@ -66,6 +72,34 @@ final class AphrontTableView extends AphrontView { return $this; } + /** + * Parse a sorting parameter: + * + * list($sort, $reverse) = AphrontTableView::parseSortParam($sort_param); + * + * @param string Sort request parameter. + * @return pair Sort value, sort direction. + */ + public static function parseSort($sort) { + return array(ltrim($sort, '-'), preg_match('/^-/', $sort)); + } + + public function makeSortable( + PhutilURI $base_uri, + $param, + $selected, + $reverse, + array $sort_values) { + + $this->sortURI = $base_uri; + $this->sortParam = $param; + $this->sortSelected = $selected; + $this->sortReverse = $reverse; + $this->sortValues = array_values($sort_values); + + return $this; + } + public function render() { require_celerity_resource('aphront-table-view-css'); @@ -80,7 +114,7 @@ final class AphrontTableView extends AphrontView { $col_classes = array(); foreach ($this->columnClasses as $key => $class) { if (strlen($class)) { - $col_classes[] = ' class="'.$class.'"'; + $col_classes[] = $class; } else { $col_classes[] = null; } @@ -88,21 +122,79 @@ final class AphrontTableView extends AphrontView { $visibility = array_values($this->columnVisibility); $headers = $this->headers; + $sort_values = $this->sortValues; if ($headers) { while (count($headers) > count($visibility)) { $visibility[] = true; } + while (count($headers) > count($sort_values)) { + $sort_values[] = null; + } $table[] = ''; foreach ($headers as $col_num => $header) { if (!$visibility[$col_num]) { continue; } - $class = idx($col_classes, $col_num); + + $classes = array(); + + if (!empty($col_classes[$col_num])) { + $classes[] = $col_classes[$col_num]; + } + + if ($sort_values[$col_num] !== null) { + $classes[] = 'aphront-table-view-sortable'; + + $sort_value = $sort_values[$col_num]; + $sort_glyph = "\xE2\x86\x93"; + if ($sort_value == $this->sortSelected) { + if ($this->sortReverse) { + $sort_glyph = "\xE2\x86\x91"; + } else if (!$this->sortReverse) { + $sort_value = '-'.$sort_value; + } + $classes[] = 'aphront-table-view-sortable-selected'; + } + + $sort_glyph = phutil_render_tag( + 'span', + array( + 'class' => 'aphront-table-view-sort-glyph', + ), + $sort_glyph); + + $header = phutil_render_tag( + 'a', + array( + 'href' => $this->sortURI->alter($this->sortParam, $sort_value), + 'class' => 'aphront-table-view-sort-link', + ), + $sort_glyph.' '.$header); + } + + if ($classes) { + $class = ' class="'.implode(' ', $classes).'"'; + } else { + $class = null; + } + $table[] = ''.$header.''; } $table[] = ''; } + foreach ($col_classes as $key => $value) { + + if (($sort_values[$key] !== null) && + ($sort_values[$key] == $this->sortSelected)) { + $value = trim($value.' sorted-column'); + } + + if ($value !== null) { + $col_classes[$key] = ' class="'.$value.'"'; + } + } + $data = $this->data; if ($data) { $row_num = 0; diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index d428a8af22..c014f05f48 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -22,6 +22,19 @@ white-space: nowrap; } +.aphront-table-view th a, +.aphront-table-view th a:hover, +.aphront-table-view th a:link { + padding: 4px 8px; + color: white; + display: block; + text-decoration: none; +} + +.aphront-table-view th a:hover { + background: #3366aa; +} + .aphront-table-view td.header { padding: 4px 8px; background: #3b5998; @@ -37,6 +50,14 @@ white-space: nowrap; } +.aphront-table-view td.sorted-column { + background: #f6f6fd; +} + +.aphront-table-view tr.alt td.sorted-column { + background: #e0e0ef; +} + .aphront-table-view td.action { padding-top: 1px; padding-bottom: 1px; @@ -106,3 +127,21 @@ span.single-display-line-content { max-height: 64px; } +.aphront-table-view th.aphront-table-view-sortable { + padding: 0; +} + +.aphront-table-view-sort-glyph { + float: right; + padding-left: 8px; + color: #6677bb; +} + +th a:hover .aphront-table-view-sort-glyph { + color: #ffffff; +} + +.aphront-table-view-sortable-selected .aphront-table-view-sort-glyph { + display: block; + color: white; +}