diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -155,6 +155,7 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ 'unhandled-exception-css' => '9ecfc00d', ), 'requires' => array( + '0113c54c' => array( + 'javelin-install', + 'javelin-dom', + ), '0116d3e8' => array( 'javelin-behavior', 'javelin-dom', @@ -984,6 +998,10 @@ 'javelin-util', 'javelin-magical-init', ), + '08fc09e9' => array( + 'javelin-install', + 'javelin-dom', + ), '0922e81d' => array( 'herald-rule-editor', 'javelin-behavior', @@ -1036,6 +1054,12 @@ '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 @@ '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 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -179,6 +179,7 @@ '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 @@ '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 @@ 'AphrontAccessDeniedQueryException' => 'AphrontQueryException', 'AphrontAjaxResponse' => 'AphrontResponse', 'AphrontApplicationConfiguration' => 'Phobject', + 'AphrontAutoIDView' => 'AphrontView', 'AphrontBarView' => 'AphrontView', 'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType', @@ -8315,6 +8325,14 @@ '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 --- /dev/null +++ b/src/view/AphrontAutoIDView.php @@ -0,0 +1,15 @@ +<?php + +abstract class AphrontAutoIDView + extends AphrontView { + + private $id; + + final public function getID() { + if (!$this->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 --- /dev/null +++ b/src/view/formation/PHUIFormationColumnDynamicView.php @@ -0,0 +1,37 @@ +<?php + +abstract class PHUIFormationColumnDynamicView + extends PHUIFormationColumnView { + + private $isVisible = true; + private $isResizable; + private $width; + + public function setIsVisible($is_visible) { + $this->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 --- /dev/null +++ b/src/view/formation/PHUIFormationColumnItem.php @@ -0,0 +1,116 @@ +<?php + +final class PHUIFormationColumnItem + extends Phobject { + + private $id; + private $column; + private $controlItem; + private $resizerItem; + private $isRightAligned; + private $expander; + private $expanders = array(); + + public function getID() { + if (!$this->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 --- /dev/null +++ b/src/view/formation/PHUIFormationColumnView.php @@ -0,0 +1,37 @@ +<?php + +abstract class PHUIFormationColumnView + extends AphrontAutoIDView { + + private $item; + + final public function setColumnItem(PHUIFormationColumnItem $item) { + $this->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 --- /dev/null +++ b/src/view/formation/PHUIFormationContentView.php @@ -0,0 +1,21 @@ +<?php + +final class PHUIFormationContentView + extends PHUIFormationColumnView { + + public function getIsControlColumn() { + return true; + } + + public function render() { + require_celerity_resource('phui-formation-view-css'); + + return phutil_tag( + 'div', + array( + 'class' => 'phui-formation-view-content', + ), + $this->renderChildren()); + } + +} diff --git a/src/view/formation/PHUIFormationExpanderView.php b/src/view/formation/PHUIFormationExpanderView.php new file mode 100644 --- /dev/null +++ b/src/view/formation/PHUIFormationExpanderView.php @@ -0,0 +1,64 @@ +<?php + +final class PHUIFormationExpanderView + extends AphrontAutoIDView { + + private $tooltip; + private $columnItem; + + public function setTooltip($tooltip) { + $this->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 --- /dev/null +++ b/src/view/formation/PHUIFormationFlankView.php @@ -0,0 +1,177 @@ +<?php + +final class PHUIFormationFlankView + extends PHUIFormationColumnDynamicView { + + private $isFixed; + + private $head; + private $body; + private $tail; + + private $headID; + private $bodyID; + private $tailID; + + private $headerText; + + public function setIsFixed($fixed) { + $this->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 --- /dev/null +++ b/src/view/formation/PHUIFormationResizerView.php @@ -0,0 +1,34 @@ +<?php + +final class PHUIFormationResizerView + extends PHUIFormationColumnView { + + private $isVisible; + + public function setIsVisible($is_visible) { + $this->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 --- /dev/null +++ b/src/view/formation/PHUIFormationView.php @@ -0,0 +1,188 @@ +<?php + +final class PHUIFormationView + extends AphrontView { + + private $items = array(); + + public function newFlankColumn() { + $item = $this->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 --- /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 --- /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 --- /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 --- /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 --- /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(); + } + } + + } + +});