From fef2cdabfe43b0be0ae5348f0d3595b5b7992f0d Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Apr 2020 06:51:27 -0700 Subject: [PATCH] Add a "FormationView" to support dynamic flank panels Summary: Ref T13516. Currently, the "File Tree" element is a semi-dynamic side panel that's implemented as a special mode of a side nav panel. This implementation is fairly clunky, and arose from organic growth out of the side nav. As such, it has some weird behaviors, doesn't have builtin support for show/hide, and can't generalize easily. Introduce a "FormationView" which supports loading a page up with piles of side panels in various modes. Test Plan: No callers and no user-visible impact. Maniphest Tasks: T13516 Differential Revision: https://secure.phabricator.com/D21150 --- resources/celerity/map.php | 28 +++ src/__phutil_library_map__.php | 18 ++ src/view/AphrontAutoIDView.php | 15 ++ .../PHUIFormationColumnDynamicView.php | 37 ++++ .../formation/PHUIFormationColumnItem.php | 116 +++++++++++ .../formation/PHUIFormationColumnView.php | 37 ++++ .../formation/PHUIFormationContentView.php | 21 ++ .../formation/PHUIFormationExpanderView.php | 64 ++++++ src/view/formation/PHUIFormationFlankView.php | 177 +++++++++++++++++ .../formation/PHUIFormationResizerView.php | 34 ++++ src/view/formation/PHUIFormationView.php | 188 ++++++++++++++++++ webroot/rsrc/css/phui/phui-formation-view.css | 145 ++++++++++++++ .../js/phui/behavior-phuix-formation-view.js | 54 +++++ .../rsrc/js/phuix/PHUIXFormationColumnView.js | 174 ++++++++++++++++ .../rsrc/js/phuix/PHUIXFormationFlankView.js | 57 ++++++ webroot/rsrc/js/phuix/PHUIXFormationView.js | 68 +++++++ 16 files changed, 1233 insertions(+) create mode 100644 src/view/AphrontAutoIDView.php create mode 100644 src/view/formation/PHUIFormationColumnDynamicView.php create mode 100644 src/view/formation/PHUIFormationColumnItem.php create mode 100644 src/view/formation/PHUIFormationColumnView.php create mode 100644 src/view/formation/PHUIFormationContentView.php create mode 100644 src/view/formation/PHUIFormationExpanderView.php create mode 100644 src/view/formation/PHUIFormationFlankView.php create mode 100644 src/view/formation/PHUIFormationResizerView.php create mode 100644 src/view/formation/PHUIFormationView.php create mode 100644 webroot/rsrc/css/phui/phui-formation-view.css create mode 100644 webroot/rsrc/js/phui/behavior-phuix-formation-view.js create mode 100644 webroot/rsrc/js/phuix/PHUIXFormationColumnView.js create mode 100644 webroot/rsrc/js/phuix/PHUIXFormationFlankView.js create mode 100644 webroot/rsrc/js/phuix/PHUIXFormationView.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6d8f0e18cc..debb915232 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -155,6 +155,7 @@ return array( 'rsrc/css/phui/phui-fontkit.css' => '1ec937e5', 'rsrc/css/phui/phui-form-view.css' => '01b796c0', 'rsrc/css/phui/phui-form.css' => '1f177cb7', + 'rsrc/css/phui/phui-formation-view.css' => 'aec68a01', 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', 'rsrc/css/phui/phui-header-view.css' => '36c86a58', 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', @@ -519,6 +520,7 @@ return array( 'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9', 'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b', 'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4', + 'rsrc/js/phui/behavior-phuix-formation-view.js' => '1a12beef', 'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f', 'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '2fbe234d', @@ -526,6 +528,9 @@ return array( 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '7acfd98b', 'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7', 'rsrc/js/phuix/PHUIXFormControl.js' => '38c1f3fb', + 'rsrc/js/phuix/PHUIXFormationColumnView.js' => '08fc09e9', + 'rsrc/js/phuix/PHUIXFormationFlankView.js' => '6648270a', + 'rsrc/js/phuix/PHUIXFormationView.js' => '0113c54c', 'rsrc/js/phuix/PHUIXIconView.js' => 'a5257c4e', ), 'symbols' => array( @@ -667,6 +672,7 @@ return array( 'javelin-behavior-phui-tab-group' => '242aa08b', 'javelin-behavior-phui-timer-control' => 'f84bcbf4', 'javelin-behavior-phuix-example' => 'c2c500a7', + 'javelin-behavior-phuix-formation-view' => '1a12beef', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', 'javelin-behavior-project-boards' => '58cb6a88', @@ -844,6 +850,7 @@ return array( 'phui-fontkit-css' => '1ec937e5', 'phui-form-css' => '1f177cb7', 'phui-form-view-css' => '01b796c0', + 'phui-formation-view-css' => 'aec68a01', 'phui-head-thing-view-css' => 'd7f293df', 'phui-header-view-css' => '36c86a58', 'phui-hovercard' => '074f0783', @@ -886,6 +893,9 @@ return array( 'phuix-button-view' => '55a24e84', 'phuix-dropdown-menu' => '7acfd98b', 'phuix-form-control-view' => '38c1f3fb', + 'phuix-formation-column-view' => '08fc09e9', + 'phuix-formation-flank-view' => '6648270a', + 'phuix-formation-view' => '0113c54c', 'phuix-icon-view' => 'a5257c4e', 'policy-css' => 'ceb56a08', 'policy-edit-css' => '8794e2ed', @@ -912,6 +922,10 @@ return array( 'unhandled-exception-css' => '9ecfc00d', ), 'requires' => array( + '0113c54c' => array( + 'javelin-install', + 'javelin-dom', + ), '0116d3e8' => array( 'javelin-behavior', 'javelin-dom', @@ -984,6 +998,10 @@ return array( 'javelin-util', 'javelin-magical-init', ), + '08fc09e9' => array( + 'javelin-install', + 'javelin-dom', + ), '0922e81d' => array( 'herald-rule-editor', 'javelin-behavior', @@ -1036,6 +1054,12 @@ return array( '16e97ebc' => array( 'javelin-dom', ), + '1a12beef' => array( + 'javelin-behavior', + 'phuix-formation-view', + 'phuix-formation-column-view', + 'phuix-formation-flank-view', + ), '1a844c06' => array( 'javelin-install', 'javelin-util', @@ -1520,6 +1544,10 @@ return array( 'javelin-stratcom', 'javelin-dom', ), + '6648270a' => array( + 'javelin-install', + 'javelin-dom', + ), '6a1583a8' => array( 'javelin-behavior', 'javelin-history', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3e80fbe7ba..ecf0010678 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -179,6 +179,7 @@ phutil_register_library_map(array( 'AphrontAccessDeniedQueryException' => 'infrastructure/storage/exception/AphrontAccessDeniedQueryException.php', 'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php', 'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php', + 'AphrontAutoIDView' => 'view/AphrontAutoIDView.php', 'AphrontBarView' => 'view/widget/bars/AphrontBarView.php', 'AphrontBaseMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php', 'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php', @@ -2035,6 +2036,14 @@ phutil_register_library_map(array( 'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php', 'PHUIFormNumberControl' => 'view/form/control/PHUIFormNumberControl.php', 'PHUIFormTimerControl' => 'view/form/control/PHUIFormTimerControl.php', + 'PHUIFormationColumnDynamicView' => 'view/formation/PHUIFormationColumnDynamicView.php', + 'PHUIFormationColumnItem' => 'view/formation/PHUIFormationColumnItem.php', + 'PHUIFormationColumnView' => 'view/formation/PHUIFormationColumnView.php', + 'PHUIFormationContentView' => 'view/formation/PHUIFormationContentView.php', + 'PHUIFormationExpanderView' => 'view/formation/PHUIFormationExpanderView.php', + 'PHUIFormationFlankView' => 'view/formation/PHUIFormationFlankView.php', + 'PHUIFormationResizerView' => 'view/formation/PHUIFormationResizerView.php', + 'PHUIFormationView' => 'view/formation/PHUIFormationView.php', 'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php', 'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php', 'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php', @@ -6193,6 +6202,7 @@ phutil_register_library_map(array( 'AphrontAccessDeniedQueryException' => 'AphrontQueryException', 'AphrontAjaxResponse' => 'AphrontResponse', 'AphrontApplicationConfiguration' => 'Phobject', + 'AphrontAutoIDView' => 'AphrontView', 'AphrontBarView' => 'AphrontView', 'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType', @@ -8315,6 +8325,14 @@ phutil_register_library_map(array( 'PHUIFormLayoutView' => 'AphrontView', 'PHUIFormNumberControl' => 'AphrontFormControl', 'PHUIFormTimerControl' => 'AphrontFormControl', + 'PHUIFormationColumnDynamicView' => 'PHUIFormationColumnView', + 'PHUIFormationColumnItem' => 'Phobject', + 'PHUIFormationColumnView' => 'AphrontAutoIDView', + 'PHUIFormationContentView' => 'PHUIFormationColumnView', + 'PHUIFormationExpanderView' => 'AphrontAutoIDView', + 'PHUIFormationFlankView' => 'PHUIFormationColumnDynamicView', + 'PHUIFormationResizerView' => 'PHUIFormationColumnView', + 'PHUIFormationView' => 'AphrontView', 'PHUIHandleListView' => 'AphrontTagView', 'PHUIHandleTagListView' => 'AphrontTagView', 'PHUIHandleView' => 'AphrontView', diff --git a/src/view/AphrontAutoIDView.php b/src/view/AphrontAutoIDView.php new file mode 100644 index 0000000000..22ccea96fd --- /dev/null +++ b/src/view/AphrontAutoIDView.php @@ -0,0 +1,15 @@ +id) { + $this->id = celerity_generate_unique_node_id(); + } + return $this->id; + } + +} diff --git a/src/view/formation/PHUIFormationColumnDynamicView.php b/src/view/formation/PHUIFormationColumnDynamicView.php new file mode 100644 index 0000000000..1c0853fd2b --- /dev/null +++ b/src/view/formation/PHUIFormationColumnDynamicView.php @@ -0,0 +1,37 @@ +isVisible = $is_visible; + return $this; + } + + public function getIsVisible() { + return $this->isVisible; + } + + public function setIsResizable($is_resizable) { + $this->isResizable = $is_resizable; + return $this; + } + + public function getIsResizable() { + return $this->isResizable; + } + + public function setWidth($width) { + $this->width = $width; + return $this; + } + + public function getWidth() { + return $this->width; + } + +} diff --git a/src/view/formation/PHUIFormationColumnItem.php b/src/view/formation/PHUIFormationColumnItem.php new file mode 100644 index 0000000000..0897afd4e9 --- /dev/null +++ b/src/view/formation/PHUIFormationColumnItem.php @@ -0,0 +1,116 @@ +id) { + $this->id = celerity_generate_unique_node_id(); + } + return $this->id; + } + + public function setColumn(PHUIFormationColumnView $column) { + $this->column = $column; + return $this; + } + + public function getColumn() { + return $this->column; + } + + public function setControlItem(PHUIFormationColumnItem $control_item) { + $this->controlItem = $control_item; + return $this; + } + + public function getControlItem() { + return $this->controlItem; + } + + public function setIsRightAligned($is_right_aligned) { + $this->isRightAligned = $is_right_aligned; + return $this; + } + + public function getIsRightAligned() { + return $this->isRightAligned; + } + + public function setResizerItem(PHUIFormationColumnItem $resizer_item) { + $this->resizerItem = $resizer_item; + return $this; + } + + public function getResizerItem() { + return $this->resizerItem; + } + + public function setExpander(PHUIFormationExpanderView $expander) { + $this->expander = $expander; + return $this; + } + + public function getExpander() { + return $this->expander; + } + + public function appendExpander(PHUIFormationExpanderView $expander) { + $this->expanders[] = $expander; + return $this; + } + + public function getExpanders() { + return $this->expanders; + } + + public function newClientProperties() { + $expander_id = null; + + $expander = $this->getExpander(); + if ($expander) { + $expander_id = $expander->getID(); + } + + + $resizer_details = null; + $resizer_item = $this->getResizerItem(); + if ($resizer_item) { + $resizer_details = array( + 'itemID' => $resizer_item->getID(), + 'controlID' => $resizer_item->getColumn()->getID(), + ); + } + + $column = $this->getColumn(); + + $width = $column->getWidth(); + if ($width !== null) { + $width = (int)$width; + } + + $is_visible = (bool)$column->getIsVisible(); + $is_right_aligned = $this->getIsRightAligned(); + + $column_details = $column->newClientProperties(); + + return array( + 'itemID' => $this->getID(), + 'width' => $width, + 'isVisible' => $is_visible, + 'isRightAligned' => $is_right_aligned, + 'expanderID' => $expander_id, + 'resizer' => $resizer_details, + 'column' => $column_details, + ); + } + +} diff --git a/src/view/formation/PHUIFormationColumnView.php b/src/view/formation/PHUIFormationColumnView.php new file mode 100644 index 0000000000..d15da9cc56 --- /dev/null +++ b/src/view/formation/PHUIFormationColumnView.php @@ -0,0 +1,37 @@ +item = $item; + return $this; + } + + final public function getColumnItem() { + return $this->item; + } + + public function getWidth() { + return null; + } + + public function getIsResizable() { + return false; + } + + public function getIsVisible() { + return true; + } + + public function getIsControlColumn() { + return false; + } + + public function newClientProperties() { + return null; + } + +} diff --git a/src/view/formation/PHUIFormationContentView.php b/src/view/formation/PHUIFormationContentView.php new file mode 100644 index 0000000000..3f42e1536a --- /dev/null +++ b/src/view/formation/PHUIFormationContentView.php @@ -0,0 +1,21 @@ + 'phui-formation-view-content', + ), + $this->renderChildren()); + } + +} diff --git a/src/view/formation/PHUIFormationExpanderView.php b/src/view/formation/PHUIFormationExpanderView.php new file mode 100644 index 0000000000..7cada3c0d2 --- /dev/null +++ b/src/view/formation/PHUIFormationExpanderView.php @@ -0,0 +1,64 @@ +tooltip = $tooltip; + return $this; + } + + public function getTooltip() { + return $this->tooltip; + } + + public function setColumnItem($column_item) { + $this->columnItem = $column_item; + return $this; + } + + public function getColumnItem() { + return $this->columnItem; + } + + public function render() { + $classes = array(); + $classes[] = 'phui-formation-view-expander'; + + $is_right = $this->getColumnItem()->getIsRightAligned(); + if ($is_right) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-chevron-left grey'); + $classes[] = 'phui-formation-view-expander-right'; + } else { + $icon = id(new PHUIIconView()) + ->setIcon('fa-chevron-right grey'); + $classes[] = 'phui-formation-view-expander-left'; + } + + $icon_view = phutil_tag( + 'div', + array( + 'class' => 'phui-formation-view-expander-icon', + ), + $icon); + + return javelin_tag( + 'div', + array( + 'id' => $this->getID(), + 'class' => implode(' ', $classes), + 'sigil' => 'has-tooltip', + 'style' => 'display: none', + 'meta' => array( + 'tip' => $this->getTooltip(), + 'align' => 'E', + ), + ), + $icon_view); + } + +} diff --git a/src/view/formation/PHUIFormationFlankView.php b/src/view/formation/PHUIFormationFlankView.php new file mode 100644 index 0000000000..35eca5fbc7 --- /dev/null +++ b/src/view/formation/PHUIFormationFlankView.php @@ -0,0 +1,177 @@ +isFixed = $fixed; + return $this; + } + + public function getIsFixed() { + return $this->isFixed; + } + + public function setHead($head) { + $this->head = $head; + return $this; + } + + public function setBody($body) { + $this->body = $body; + return $this; + } + + public function setTail($tail) { + $this->tail = $tail; + return $this; + } + + public function getHeadID() { + if (!$this->headID) { + $this->headID = celerity_generate_unique_node_id(); + } + return $this->headID; + } + + public function getBodyID() { + if (!$this->bodyID) { + $this->bodyID = celerity_generate_unique_node_id(); + } + return $this->bodyID; + } + + public function getTailID() { + if (!$this->tailID) { + $this->tailID = celerity_generate_unique_node_id(); + } + return $this->tailID; + } + + public function setHeaderText($header_text) { + $this->headerText = $header_text; + return $this; + } + + public function getHeaderText() { + return $this->headerText; + } + + public function newClientProperties() { + return array( + 'type' => 'flank', + 'nodeID' => $this->getID(), + 'isFixed' => (bool)$this->getIsFixed(), + 'headID' => $this->getHeadID(), + 'bodyID' => $this->getBodyID(), + 'tailID' => $this->getTailID(), + ); + } + + public function render() { + require_celerity_resource('phui-formation-view-css'); + + $width = $this->getWidth(); + + $style = array(); + $style[] = sprintf('width: %dpx;', $width); + + $classes = array(); + $classes[] = 'phui-flank-view'; + + if ($this->getIsFixed()) { + $classes[] = 'phui-flank-view-fixed'; + } + + $head_id = $this->getHeadID(); + $body_id = $this->getBodyID(); + $tail_id = $this->getTailID(); + + $head_content = phutil_tag( + 'div', + array( + 'class' => 'phui-flank-header', + ), + array( + phutil_tag( + 'div', + array( + 'class' => 'phui-flank-header-text', + ), + $this->getHeaderText()), + $this->newHideButton(), + )); + + $content = phutil_tag( + 'div', + array( + 'id' => $this->getID(), + 'class' => implode(' ', $classes), + 'style' => implode(' ', $style), + ), + array( + phutil_tag( + 'div', + array( + 'id' => $head_id, + 'class' => 'phui-flank-view-head', + ), + $head_content), + phutil_tag( + 'div', + array( + 'id' => $body_id, + 'class' => 'phui-flank-view-body', + ), + $this->getBody()), + phutil_tag( + 'div', + array( + 'id' => $tail_id, + 'class' => 'phui-flank-view-tail', + ), + $this->getTail()), + )); + + return $content; + } + + private function newHideButton() { + $item = $this->getColumnItem(); + $is_right = $item->getIsRightAligned(); + + $hide_classes = array(); + $hide_classes[] = 'phui-flank-header-hide'; + + if ($is_right) { + $hide_icon = id(new PHUIIconView()) + ->setIcon('fa-chevron-right grey'); + $hide_classes[] = 'phui-flank-header-hide-right'; + } else { + $hide_icon = id(new PHUIIconView()) + ->setIcon('fa-chevron-left grey'); + $hide_classes[] = 'phui-flank-header-hide-left'; + } + + return javelin_tag( + 'div', + array( + 'sigil' => 'phui-flank-header-hide', + 'class' => implode(' ', $hide_classes), + ), + $hide_icon); + } + +} diff --git a/src/view/formation/PHUIFormationResizerView.php b/src/view/formation/PHUIFormationResizerView.php new file mode 100644 index 0000000000..e0f66a1d7b --- /dev/null +++ b/src/view/formation/PHUIFormationResizerView.php @@ -0,0 +1,34 @@ +isVisible = $is_visible; + return $this; + } + + public function getIsVisible() { + return $this->isVisible; + } + + public function getWidth() { + return 8; + } + + public function render() { + $width = $this->getWidth(); + $style = sprintf('width: %dpx;', $width); + + return phutil_tag( + 'div', + array( + 'id' => $this->getID(), + 'class' => 'phui-formation-resizer', + 'style' => $style, + )); + } + +} diff --git a/src/view/formation/PHUIFormationView.php b/src/view/formation/PHUIFormationView.php new file mode 100644 index 0000000000..6dbb23fac0 --- /dev/null +++ b/src/view/formation/PHUIFormationView.php @@ -0,0 +1,188 @@ +newItem(new PHUIFormationFlankView()); + return $item->getColumn(); + } + + public function newContentColumn() { + $item = $this->newItem(new PHUIFormationContentView()); + return $item->getColumn(); + } + + private function newItem(PHUIFormationColumnView $column) { + $item = id(new PHUIFormationColumnItem()) + ->setColumn($column); + + $column->setColumnItem($item); + + $this->items[] = $item; + + return $item; + } + + public function render() { + require_celerity_resource('phui-formation-view-css'); + + $items = $this->items; + + $items = $this->generateControlBindings($items); + $items = $this->generateExpanders($items); + $items = $this->generateResizers($items); + + $cells = array(); + foreach ($items as $item) { + $style = array(); + + $column = $item->getColumn(); + + $width = $column->getWidth(); + if ($width !== null) { + $style[] = sprintf('width: %dpx;', $width); + } + + if (!$column->getIsVisible()) { + $style[] = 'display: none;'; + } + + $cells[] = phutil_tag( + 'td', + array( + 'id' => $item->getID(), + 'style' => implode(' ', $style), + ), + array( + $column, + $item->getExpanders(), + )); + } + + $formation_id = celerity_generate_unique_node_id(); + + $table_row = phutil_tag('tr', array(), $cells); + $table_body = phutil_tag('tbody', array(), $table_row); + $table = phutil_tag( + 'table', + array( + 'class' => 'phui-formation-view', + 'id' => $formation_id, + ), + $table_body); + + $phuix_columns = array(); + foreach ($items as $item) { + $phuix_columns[] = $item->newClientProperties(); + } + + Javelin::initBehavior( + 'phuix-formation-view', + array( + 'nodeID' => $formation_id, + 'columns' => $phuix_columns, + )); + + return $table; + } + + private function newColumnExpanderView() { + return new PHUIFormationExpanderView(); + } + + private function newResizerItem() { + return $this->newItem(new PHUIFormationResizerView()); + } + + private function generateControlBindings(array $items) { + $count = count($items); + + if (!$count) { + return $items; + } + + $last_control = null; + + for ($ii = 0; $ii < $count; $ii++) { + $item = $items[$ii]; + $column = $item->getColumn(); + + $is_control = $column->getIsControlColumn(); + if ($is_control) { + $last_control = $ii; + } + } + + if ($last_control === null) { + return $items; + } + + for ($ii = ($count - 1); $ii >= 0; $ii--) { + $item = $items[$ii]; + $column = $item->getColumn(); + + $is_control = $column->getIsControlColumn(); + if ($is_control) { + $last_control = $ii; + continue; + } + + $is_right = ($last_control < $ii); + + $item + ->setControlItem($items[$last_control]) + ->setIsRightAligned($is_right); + } + + return $items; + } + + private function generateResizers(array $items) { + $result = array(); + foreach ($items as $item) { + $column = $item->getColumn(); + + $resizer_item = null; + if ($column->getIsResizable()) { + $resizer_item = $this->newResizerItem(); + $item->setResizerItem($resizer_item); + + $resizer_item + ->getColumn() + ->setIsVisible($column->getIsVisible()); + } + + if (!$resizer_item) { + $result[] = $item; + } else if ($item->getIsRightAligned()) { + $result[] = $resizer_item; + $result[] = $item; + } else { + $result[] = $item; + $result[] = $resizer_item; + } + } + + return $result; + } + + private function generateExpanders(array $items) { + foreach ($items as $item) { + $control_item = $item->getControlItem(); + if ($control_item) { + $expander = $this->newColumnExpanderView(); + + $expander->setColumnItem($item); + $item->setExpander($expander); + + $control_item->appendExpander($expander); + } + } + + return $items; + } + +} diff --git a/webroot/rsrc/css/phui/phui-formation-view.css b/webroot/rsrc/css/phui/phui-formation-view.css new file mode 100644 index 0000000000..60c6317c18 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-formation-view.css @@ -0,0 +1,145 @@ +/** + * @provides phui-formation-view-css + */ + +.phui-formation-view { + table-layout: fixed; + width: 100%; +} + +.phui-formation-view-expander { + position: fixed; + width: 24px; + height: 36px; + top: 64px; + border-style: solid; + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); + border-color: {$lightgreyborder}; + background: {$lightgreybackground}; + z-index: 4; +} + +.phui-formation-view-expander-left { + border-radius: 0 12px 12px 0; + border-width: 1px 1px 1px 0; + cursor: e-resize; +} + +.phui-formation-view-expander-right { + border-radius: 12px 0 0 12px; + border-width: 1px 0 1px 1px; + cursor: w-resize; +} + +.phui-formation-view-expander-icon { + position: absolute; + width: 18px; + height: 18px; + top: 9px; + left: 3px; + text-align: center; +} + +.device-desktop .phui-formation-view-expander:hover { + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1); + background: {$darkgreybackground}; + transition: 0.1s; +} + +.device-desktop .phui-formation-view-expander:hover + .phui-icon-view { + color: {$bluetext}; + transition: 0.1s; +} + +.phui-flank-header { + padding: 8px; + background: {$greybackground}; + border-bottom: 1px solid {$lightgreyborder}; +} + +.phui-flank-header-text { + color: {$darkgreytext}; + font-weight: bold; +} + +.phui-flank-header-hide { + font-size: {$normalfontsize}; + position: absolute; + display: inline-block; + top: 6px; + right: 6px; + width: 20px; + height: 20px; + text-align: center; + border: 1px solid {$lightgreyborder}; + border-radius: 4px; + line-height: 20px; +} + +.phui-flank-header-hide-left { + cursor: w-resize; +} + + +.device-desktop .phui-flank-header-hide:hover { + box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.05); + background: {$darkgreybackground}; + transition: 0.1s; +} + +.device-desktop .phui-flank-header-hide:hover + .phui-icon-view { + color: {$bluetext}; + transition: 0.1s; +} + +.phui-formation-resizer { + position: fixed; + top: 0; + bottom: 0; + + cursor: col-resize; + background: #f5f5f5; + border-style: solid; + border-width: 0 1px 0 1px; + border-color: #fff #999c9e #fff #999c9e; + box-sizing: border-box; + + box-shadow: inset -1px 0px 1px rgba({$alphablack}, 0.15); + + background-image: url(/rsrc/image/divot.png); + background-position: center; + background-repeat: no-repeat; +} + +.phui-flank-view-fixed { + position: fixed; + top: {$menu.main.height}; + bottom: 0; + overflow: hidden; +} + +.phui-flank-view-fixed .phui-flank-view-body { + overflow: hidden auto; +} + +.device-desktop .phui-flank-view-fixed + .phui-flank-view-body::-webkit-scrollbar { + height: 6px; + width: 6px; + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +.device-desktop .phui-flank-view-fixed + .phui-flank-view-body::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.25); + border-radius: 4px; +} + +.phui-flank-view-fixed .phui-flank-view-tail { + position: absolute; + bottom: 0; + width: 100%; +} diff --git a/webroot/rsrc/js/phui/behavior-phuix-formation-view.js b/webroot/rsrc/js/phui/behavior-phuix-formation-view.js new file mode 100644 index 0000000000..0e2d12f2ef --- /dev/null +++ b/webroot/rsrc/js/phui/behavior-phuix-formation-view.js @@ -0,0 +1,54 @@ +/** + * @provides javelin-behavior-phuix-formation-view + * @requires javelin-behavior + * phuix-formation-view + * phuix-formation-column-view + * phuix-formation-flank-view + */ + +JX.behavior('phuix-formation-view', function(config) { + + var formation_node = JX.$(config.nodeID); + var formation = new JX.PHUIXFormationView(formation_node); + + var count = config.columns.length; + for (var ii = 0; ii < count; ii++) { + var spec = config.columns[ii]; + var node = JX.$(spec.itemID); + + var column = new JX.PHUIXFormationColumnView(node) + .setIsRightAligned(spec.isRightAligned) + .setWidth(spec.width) + .setIsVisible(spec.isVisible); + + if (spec.expanderID) { + column.setExpanderNode(JX.$(spec.expanderID)); + } + + if (spec.resizer) { + column + .setResizerItem(JX.$(spec.resizer.itemID)) + .setResizerControl(JX.$(spec.resizer.controlID)); + } + + var colspec = spec.column; + if (colspec) { + if (colspec.type === 'flank') { + var flank_node = JX.$(colspec.nodeID); + + var head = JX.$(colspec.headID); + var body = JX.$(colspec.bodyID); + var tail = JX.$(colspec.tailID); + + var flank = new JX.PHUIXFormationFlankView(flank_node, head, body, tail) + .setIsFixed(colspec.isFixed); + + column.setFlank(flank); + } + } + + formation.addColumn(column); + } + + formation.start(); +}); diff --git a/webroot/rsrc/js/phuix/PHUIXFormationColumnView.js b/webroot/rsrc/js/phuix/PHUIXFormationColumnView.js new file mode 100644 index 0000000000..993fe7b016 --- /dev/null +++ b/webroot/rsrc/js/phuix/PHUIXFormationColumnView.js @@ -0,0 +1,174 @@ +/** + * @provides phuix-formation-column-view + * @requires javelin-install + * javelin-dom + */ + +JX.install('PHUIXFormationColumnView', { + + construct: function(node) { + this._node = node; + }, + + properties: { + isRightAligned: false, + isVisible: true, + expanderNode: null, + resizerItem: null, + resizerControl: null, + width: null, + flank: null + }, + + members: { + _node: null, + _resizingWidth: null, + _resizingBarPosition: null, + _dragging: null, + + start: function() { + var onshow = JX.bind(this, this._setVisibility, true); + var onhide = JX.bind(this, this._setVisibility, false); + + JX.DOM.listen(this._node, 'click', 'phui-flank-header-hide', onhide); + + var expander = this.getExpanderNode(); + if (expander) { + JX.DOM.listen(expander, 'click', null, onshow); + } + + var resizer = this.getResizerItem(); + if (resizer) { + var ondown = JX.bind(this, this._onresizestart); + JX.DOM.listen(resizer, 'mousedown', null, ondown); + + var onmove = JX.bind(this, this._onresizemove); + JX.Stratcom.listen('mousemove', null, onmove); + + var onup = JX.bind(this, this._onresizeend); + JX.Stratcom.listen('mouseup', null, onup); + } + + this.repaint(); + }, + + _onresizestart: function(e) { + if (!e.isNormalMouseEvent()) { + return; + } + + this._dragging = JX.$V(e); + this._resizingWidth = this.getWidth(); + this._resizingBarPosition = JX.$V(this.getResizerControl()); + + // 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); + + e.kill(); + }, + + _onresizemove: function(e) { + if (!this._dragging) { + return; + } + + var dx = (JX.$V(e).x - this._dragging.x); + + var width; + if (this.getIsRightAligned()) { + width = this.getWidth() - dx; + } else { + width = this.getWidth() + dx; + } + + // TODO: Make these configurable? + width = Math.max(width, 150); + width = Math.min(width, 512); + + this._resizingWidth = width; + + this._node.style.width = this._resizingWidth + 'px'; + + var adjust_x = (this._resizingWidth - this.getWidth()); + if (this.getIsRightAligned()) { + adjust_x = -adjust_x; + } + + this.getResizerControl().style.left = + (this._resizingBarPosition.x + adjust_x) + 'px'; + + var flank = this.getFlank(); + if (flank) { + flank + .setWidth(this._resizingWidth) + .repaint(); + } + }, + + _onresizeend: function(e) { + if (!this._dragging) { + return; + } + + this.setWidth(this._resizingWidth); + + JX.log('new width is ' + this.getWidth()); + + JX.DOM.alterClass(document.body, 'jx-drag-col', false); + this._dragging = null; + + // TODO: Save new width setting. + + // new JX.Request('/settings/adjust/', JX.bag) + // .setData( + // { + // key: 'filetree.width', + // value: get_width() + // }) + // .send(); + + }, + + _setVisibility: function(visible, e) { + e.kill(); + + // TODO: Save the visibility setting. + + this.setIsVisible(visible); + this.repaint(); + }, + + repaint: function() { + var resizer = this.getResizerItem(); + var expander = this.getExpanderNode(); + + if (this.getIsVisible()) { + JX.DOM.show(this._node); + if (resizer) { + JX.DOM.show(resizer); + } + if (expander) { + JX.DOM.hide(expander); + } + } else { + JX.DOM.hide(this._node); + if (resizer) { + JX.DOM.hide(resizer); + } + if (expander) { + JX.DOM.show(expander); + } + } + + if (this.getFlank()) { + this.getFlank().repaint(); + } + + }, + + + } + +}); diff --git a/webroot/rsrc/js/phuix/PHUIXFormationFlankView.js b/webroot/rsrc/js/phuix/PHUIXFormationFlankView.js new file mode 100644 index 0000000000..28b5d80703 --- /dev/null +++ b/webroot/rsrc/js/phuix/PHUIXFormationFlankView.js @@ -0,0 +1,57 @@ +/** + * @provides phuix-formation-flank-view + * @requires javelin-install + * javelin-dom + */ + +JX.install('PHUIXFormationFlankView', { + + construct: function(node, head, body, tail) { + this._node = node; + + this._headNode = head; + this._bodyNode = body; + this._tailNode = tail; + }, + + properties: { + isFixed: false, + bannerHeight: null, + width: null + }, + + members: { + _node: null, + _headNode: null, + _bodyNode: null, + _tailNode: null, + + getBodyNode: function() { + return this._bodyNode; + }, + + getTailNode: function() { + return this._tailNode; + }, + + repaint: function() { + if (!this.getIsFixed()) { + return; + } + + this._node.style.top = this.getBannerHeight() + 'px'; + this._node.style.width = this.getWidth() + 'px'; + + var body = this.getBodyNode(); + var body_pos = JX.$V(body); + + var tail = this.getTailNode(); + var tail_pos = JX.$V(tail); + + var max_height = (tail_pos.y - body_pos.y); + + body.style.maxHeight = max_height + 'px'; + } + } + +}); diff --git a/webroot/rsrc/js/phuix/PHUIXFormationView.js b/webroot/rsrc/js/phuix/PHUIXFormationView.js new file mode 100644 index 0000000000..9ab9ad2019 --- /dev/null +++ b/webroot/rsrc/js/phuix/PHUIXFormationView.js @@ -0,0 +1,68 @@ +/** + * @provides phuix-formation-view + * @requires javelin-install + * javelin-dom + */ + +JX.install('PHUIXFormationView', { + + construct: function() { + this._columns = []; + }, + + members: { + _columns: null, + + addColumn: function(column) { + this._columns.push(column); + }, + + start: function() { + JX.enableDispatch(document.body, 'mousemove'); + + for (var ii = 0; ii < this._columns.length; ii++) { + this._columns[ii].start(); + } + + var repaint = JX.bind(this, this.repaint); + JX.Stratcom.listen(['scroll', 'resize'], null, repaint); + + this.repaint(); + }, + + repaint: function(e) { + // Unless we've scrolled past it, the page has a 44px main menu banner. + var menu_height = (44 - JX.Vector.getScroll().y); + + // When the buoyant header is visible, move the menu down below it. This + // is a bit of a hack. + var banner_height = 0; + try { + var banner = JX.$('diff-banner'); + banner_height = JX.Vector.getDim(banner).y; + } catch (error) { + // Ignore if there's no banner on the page. + } + + var header_height = Math.max(0, menu_height, banner_height); + + var column; + var flank; + for (var ii = 0; ii < this._columns.length; ii++) { + column = this._columns[ii]; + + flank = column.getFlank(); + if (!flank) { + continue; + } + + flank + .setBannerHeight(header_height) + .setWidth(column.getWidth()) + .repaint(); + } + } + + } + +});