@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+1233
+28
resources/celerity/map.php
··· 155 155 'rsrc/css/phui/phui-fontkit.css' => '1ec937e5', 156 156 'rsrc/css/phui/phui-form-view.css' => '01b796c0', 157 157 'rsrc/css/phui/phui-form.css' => '1f177cb7', 158 + 'rsrc/css/phui/phui-formation-view.css' => 'aec68a01', 158 159 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', 159 160 'rsrc/css/phui/phui-header-view.css' => '36c86a58', 160 161 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', ··· 519 520 'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9', 520 521 'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b', 521 522 'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4', 523 + 'rsrc/js/phui/behavior-phuix-formation-view.js' => '1a12beef', 522 524 'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f', 523 525 'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b', 524 526 'rsrc/js/phuix/PHUIXAutocomplete.js' => '2fbe234d', ··· 526 528 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '7acfd98b', 527 529 'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7', 528 530 'rsrc/js/phuix/PHUIXFormControl.js' => '38c1f3fb', 531 + 'rsrc/js/phuix/PHUIXFormationColumnView.js' => '08fc09e9', 532 + 'rsrc/js/phuix/PHUIXFormationFlankView.js' => '6648270a', 533 + 'rsrc/js/phuix/PHUIXFormationView.js' => '0113c54c', 529 534 'rsrc/js/phuix/PHUIXIconView.js' => 'a5257c4e', 530 535 ), 531 536 'symbols' => array( ··· 667 672 'javelin-behavior-phui-tab-group' => '242aa08b', 668 673 'javelin-behavior-phui-timer-control' => 'f84bcbf4', 669 674 'javelin-behavior-phuix-example' => 'c2c500a7', 675 + 'javelin-behavior-phuix-formation-view' => '1a12beef', 670 676 'javelin-behavior-policy-control' => '0eaa33a9', 671 677 'javelin-behavior-policy-rule-editor' => '9347f172', 672 678 'javelin-behavior-project-boards' => '58cb6a88', ··· 844 850 'phui-fontkit-css' => '1ec937e5', 845 851 'phui-form-css' => '1f177cb7', 846 852 'phui-form-view-css' => '01b796c0', 853 + 'phui-formation-view-css' => 'aec68a01', 847 854 'phui-head-thing-view-css' => 'd7f293df', 848 855 'phui-header-view-css' => '36c86a58', 849 856 'phui-hovercard' => '074f0783', ··· 886 893 'phuix-button-view' => '55a24e84', 887 894 'phuix-dropdown-menu' => '7acfd98b', 888 895 'phuix-form-control-view' => '38c1f3fb', 896 + 'phuix-formation-column-view' => '08fc09e9', 897 + 'phuix-formation-flank-view' => '6648270a', 898 + 'phuix-formation-view' => '0113c54c', 889 899 'phuix-icon-view' => 'a5257c4e', 890 900 'policy-css' => 'ceb56a08', 891 901 'policy-edit-css' => '8794e2ed', ··· 912 922 'unhandled-exception-css' => '9ecfc00d', 913 923 ), 914 924 'requires' => array( 925 + '0113c54c' => array( 926 + 'javelin-install', 927 + 'javelin-dom', 928 + ), 915 929 '0116d3e8' => array( 916 930 'javelin-behavior', 917 931 'javelin-dom', ··· 984 998 'javelin-util', 985 999 'javelin-magical-init', 986 1000 ), 1001 + '08fc09e9' => array( 1002 + 'javelin-install', 1003 + 'javelin-dom', 1004 + ), 987 1005 '0922e81d' => array( 988 1006 'herald-rule-editor', 989 1007 'javelin-behavior', ··· 1035 1053 ), 1036 1054 '16e97ebc' => array( 1037 1055 'javelin-dom', 1056 + ), 1057 + '1a12beef' => array( 1058 + 'javelin-behavior', 1059 + 'phuix-formation-view', 1060 + 'phuix-formation-column-view', 1061 + 'phuix-formation-flank-view', 1038 1062 ), 1039 1063 '1a844c06' => array( 1040 1064 'javelin-install', ··· 1518 1542 '66365ee2' => array( 1519 1543 'javelin-behavior', 1520 1544 'javelin-stratcom', 1545 + 'javelin-dom', 1546 + ), 1547 + '6648270a' => array( 1548 + 'javelin-install', 1521 1549 'javelin-dom', 1522 1550 ), 1523 1551 '6a1583a8' => array(
+18
src/__phutil_library_map__.php
··· 179 179 'AphrontAccessDeniedQueryException' => 'infrastructure/storage/exception/AphrontAccessDeniedQueryException.php', 180 180 'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php', 181 181 'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php', 182 + 'AphrontAutoIDView' => 'view/AphrontAutoIDView.php', 182 183 'AphrontBarView' => 'view/widget/bars/AphrontBarView.php', 183 184 'AphrontBaseMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php', 184 185 'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php', ··· 2035 2036 'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php', 2036 2037 'PHUIFormNumberControl' => 'view/form/control/PHUIFormNumberControl.php', 2037 2038 'PHUIFormTimerControl' => 'view/form/control/PHUIFormTimerControl.php', 2039 + 'PHUIFormationColumnDynamicView' => 'view/formation/PHUIFormationColumnDynamicView.php', 2040 + 'PHUIFormationColumnItem' => 'view/formation/PHUIFormationColumnItem.php', 2041 + 'PHUIFormationColumnView' => 'view/formation/PHUIFormationColumnView.php', 2042 + 'PHUIFormationContentView' => 'view/formation/PHUIFormationContentView.php', 2043 + 'PHUIFormationExpanderView' => 'view/formation/PHUIFormationExpanderView.php', 2044 + 'PHUIFormationFlankView' => 'view/formation/PHUIFormationFlankView.php', 2045 + 'PHUIFormationResizerView' => 'view/formation/PHUIFormationResizerView.php', 2046 + 'PHUIFormationView' => 'view/formation/PHUIFormationView.php', 2038 2047 'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php', 2039 2048 'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php', 2040 2049 'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php', ··· 6193 6202 'AphrontAccessDeniedQueryException' => 'AphrontQueryException', 6194 6203 'AphrontAjaxResponse' => 'AphrontResponse', 6195 6204 'AphrontApplicationConfiguration' => 'Phobject', 6205 + 'AphrontAutoIDView' => 'AphrontView', 6196 6206 'AphrontBarView' => 'AphrontView', 6197 6207 'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 6198 6208 'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType', ··· 8315 8325 'PHUIFormLayoutView' => 'AphrontView', 8316 8326 'PHUIFormNumberControl' => 'AphrontFormControl', 8317 8327 'PHUIFormTimerControl' => 'AphrontFormControl', 8328 + 'PHUIFormationColumnDynamicView' => 'PHUIFormationColumnView', 8329 + 'PHUIFormationColumnItem' => 'Phobject', 8330 + 'PHUIFormationColumnView' => 'AphrontAutoIDView', 8331 + 'PHUIFormationContentView' => 'PHUIFormationColumnView', 8332 + 'PHUIFormationExpanderView' => 'AphrontAutoIDView', 8333 + 'PHUIFormationFlankView' => 'PHUIFormationColumnDynamicView', 8334 + 'PHUIFormationResizerView' => 'PHUIFormationColumnView', 8335 + 'PHUIFormationView' => 'AphrontView', 8318 8336 'PHUIHandleListView' => 'AphrontTagView', 8319 8337 'PHUIHandleTagListView' => 'AphrontTagView', 8320 8338 'PHUIHandleView' => 'AphrontView',
+15
src/view/AphrontAutoIDView.php
··· 1 + <?php 2 + 3 + abstract class AphrontAutoIDView 4 + extends AphrontView { 5 + 6 + private $id; 7 + 8 + final public function getID() { 9 + if (!$this->id) { 10 + $this->id = celerity_generate_unique_node_id(); 11 + } 12 + return $this->id; 13 + } 14 + 15 + }
+37
src/view/formation/PHUIFormationColumnDynamicView.php
··· 1 + <?php 2 + 3 + abstract class PHUIFormationColumnDynamicView 4 + extends PHUIFormationColumnView { 5 + 6 + private $isVisible = true; 7 + private $isResizable; 8 + private $width; 9 + 10 + public function setIsVisible($is_visible) { 11 + $this->isVisible = $is_visible; 12 + return $this; 13 + } 14 + 15 + public function getIsVisible() { 16 + return $this->isVisible; 17 + } 18 + 19 + public function setIsResizable($is_resizable) { 20 + $this->isResizable = $is_resizable; 21 + return $this; 22 + } 23 + 24 + public function getIsResizable() { 25 + return $this->isResizable; 26 + } 27 + 28 + public function setWidth($width) { 29 + $this->width = $width; 30 + return $this; 31 + } 32 + 33 + public function getWidth() { 34 + return $this->width; 35 + } 36 + 37 + }
+116
src/view/formation/PHUIFormationColumnItem.php
··· 1 + <?php 2 + 3 + final class PHUIFormationColumnItem 4 + extends Phobject { 5 + 6 + private $id; 7 + private $column; 8 + private $controlItem; 9 + private $resizerItem; 10 + private $isRightAligned; 11 + private $expander; 12 + private $expanders = array(); 13 + 14 + public function getID() { 15 + if (!$this->id) { 16 + $this->id = celerity_generate_unique_node_id(); 17 + } 18 + return $this->id; 19 + } 20 + 21 + public function setColumn(PHUIFormationColumnView $column) { 22 + $this->column = $column; 23 + return $this; 24 + } 25 + 26 + public function getColumn() { 27 + return $this->column; 28 + } 29 + 30 + public function setControlItem(PHUIFormationColumnItem $control_item) { 31 + $this->controlItem = $control_item; 32 + return $this; 33 + } 34 + 35 + public function getControlItem() { 36 + return $this->controlItem; 37 + } 38 + 39 + public function setIsRightAligned($is_right_aligned) { 40 + $this->isRightAligned = $is_right_aligned; 41 + return $this; 42 + } 43 + 44 + public function getIsRightAligned() { 45 + return $this->isRightAligned; 46 + } 47 + 48 + public function setResizerItem(PHUIFormationColumnItem $resizer_item) { 49 + $this->resizerItem = $resizer_item; 50 + return $this; 51 + } 52 + 53 + public function getResizerItem() { 54 + return $this->resizerItem; 55 + } 56 + 57 + public function setExpander(PHUIFormationExpanderView $expander) { 58 + $this->expander = $expander; 59 + return $this; 60 + } 61 + 62 + public function getExpander() { 63 + return $this->expander; 64 + } 65 + 66 + public function appendExpander(PHUIFormationExpanderView $expander) { 67 + $this->expanders[] = $expander; 68 + return $this; 69 + } 70 + 71 + public function getExpanders() { 72 + return $this->expanders; 73 + } 74 + 75 + public function newClientProperties() { 76 + $expander_id = null; 77 + 78 + $expander = $this->getExpander(); 79 + if ($expander) { 80 + $expander_id = $expander->getID(); 81 + } 82 + 83 + 84 + $resizer_details = null; 85 + $resizer_item = $this->getResizerItem(); 86 + if ($resizer_item) { 87 + $resizer_details = array( 88 + 'itemID' => $resizer_item->getID(), 89 + 'controlID' => $resizer_item->getColumn()->getID(), 90 + ); 91 + } 92 + 93 + $column = $this->getColumn(); 94 + 95 + $width = $column->getWidth(); 96 + if ($width !== null) { 97 + $width = (int)$width; 98 + } 99 + 100 + $is_visible = (bool)$column->getIsVisible(); 101 + $is_right_aligned = $this->getIsRightAligned(); 102 + 103 + $column_details = $column->newClientProperties(); 104 + 105 + return array( 106 + 'itemID' => $this->getID(), 107 + 'width' => $width, 108 + 'isVisible' => $is_visible, 109 + 'isRightAligned' => $is_right_aligned, 110 + 'expanderID' => $expander_id, 111 + 'resizer' => $resizer_details, 112 + 'column' => $column_details, 113 + ); 114 + } 115 + 116 + }
+37
src/view/formation/PHUIFormationColumnView.php
··· 1 + <?php 2 + 3 + abstract class PHUIFormationColumnView 4 + extends AphrontAutoIDView { 5 + 6 + private $item; 7 + 8 + final public function setColumnItem(PHUIFormationColumnItem $item) { 9 + $this->item = $item; 10 + return $this; 11 + } 12 + 13 + final public function getColumnItem() { 14 + return $this->item; 15 + } 16 + 17 + public function getWidth() { 18 + return null; 19 + } 20 + 21 + public function getIsResizable() { 22 + return false; 23 + } 24 + 25 + public function getIsVisible() { 26 + return true; 27 + } 28 + 29 + public function getIsControlColumn() { 30 + return false; 31 + } 32 + 33 + public function newClientProperties() { 34 + return null; 35 + } 36 + 37 + }
+21
src/view/formation/PHUIFormationContentView.php
··· 1 + <?php 2 + 3 + final class PHUIFormationContentView 4 + extends PHUIFormationColumnView { 5 + 6 + public function getIsControlColumn() { 7 + return true; 8 + } 9 + 10 + public function render() { 11 + require_celerity_resource('phui-formation-view-css'); 12 + 13 + return phutil_tag( 14 + 'div', 15 + array( 16 + 'class' => 'phui-formation-view-content', 17 + ), 18 + $this->renderChildren()); 19 + } 20 + 21 + }
+64
src/view/formation/PHUIFormationExpanderView.php
··· 1 + <?php 2 + 3 + final class PHUIFormationExpanderView 4 + extends AphrontAutoIDView { 5 + 6 + private $tooltip; 7 + private $columnItem; 8 + 9 + public function setTooltip($tooltip) { 10 + $this->tooltip = $tooltip; 11 + return $this; 12 + } 13 + 14 + public function getTooltip() { 15 + return $this->tooltip; 16 + } 17 + 18 + public function setColumnItem($column_item) { 19 + $this->columnItem = $column_item; 20 + return $this; 21 + } 22 + 23 + public function getColumnItem() { 24 + return $this->columnItem; 25 + } 26 + 27 + public function render() { 28 + $classes = array(); 29 + $classes[] = 'phui-formation-view-expander'; 30 + 31 + $is_right = $this->getColumnItem()->getIsRightAligned(); 32 + if ($is_right) { 33 + $icon = id(new PHUIIconView()) 34 + ->setIcon('fa-chevron-left grey'); 35 + $classes[] = 'phui-formation-view-expander-right'; 36 + } else { 37 + $icon = id(new PHUIIconView()) 38 + ->setIcon('fa-chevron-right grey'); 39 + $classes[] = 'phui-formation-view-expander-left'; 40 + } 41 + 42 + $icon_view = phutil_tag( 43 + 'div', 44 + array( 45 + 'class' => 'phui-formation-view-expander-icon', 46 + ), 47 + $icon); 48 + 49 + return javelin_tag( 50 + 'div', 51 + array( 52 + 'id' => $this->getID(), 53 + 'class' => implode(' ', $classes), 54 + 'sigil' => 'has-tooltip', 55 + 'style' => 'display: none', 56 + 'meta' => array( 57 + 'tip' => $this->getTooltip(), 58 + 'align' => 'E', 59 + ), 60 + ), 61 + $icon_view); 62 + } 63 + 64 + }
+177
src/view/formation/PHUIFormationFlankView.php
··· 1 + <?php 2 + 3 + final class PHUIFormationFlankView 4 + extends PHUIFormationColumnDynamicView { 5 + 6 + private $isFixed; 7 + 8 + private $head; 9 + private $body; 10 + private $tail; 11 + 12 + private $headID; 13 + private $bodyID; 14 + private $tailID; 15 + 16 + private $headerText; 17 + 18 + public function setIsFixed($fixed) { 19 + $this->isFixed = $fixed; 20 + return $this; 21 + } 22 + 23 + public function getIsFixed() { 24 + return $this->isFixed; 25 + } 26 + 27 + public function setHead($head) { 28 + $this->head = $head; 29 + return $this; 30 + } 31 + 32 + public function setBody($body) { 33 + $this->body = $body; 34 + return $this; 35 + } 36 + 37 + public function setTail($tail) { 38 + $this->tail = $tail; 39 + return $this; 40 + } 41 + 42 + public function getHeadID() { 43 + if (!$this->headID) { 44 + $this->headID = celerity_generate_unique_node_id(); 45 + } 46 + return $this->headID; 47 + } 48 + 49 + public function getBodyID() { 50 + if (!$this->bodyID) { 51 + $this->bodyID = celerity_generate_unique_node_id(); 52 + } 53 + return $this->bodyID; 54 + } 55 + 56 + public function getTailID() { 57 + if (!$this->tailID) { 58 + $this->tailID = celerity_generate_unique_node_id(); 59 + } 60 + return $this->tailID; 61 + } 62 + 63 + public function setHeaderText($header_text) { 64 + $this->headerText = $header_text; 65 + return $this; 66 + } 67 + 68 + public function getHeaderText() { 69 + return $this->headerText; 70 + } 71 + 72 + public function newClientProperties() { 73 + return array( 74 + 'type' => 'flank', 75 + 'nodeID' => $this->getID(), 76 + 'isFixed' => (bool)$this->getIsFixed(), 77 + 'headID' => $this->getHeadID(), 78 + 'bodyID' => $this->getBodyID(), 79 + 'tailID' => $this->getTailID(), 80 + ); 81 + } 82 + 83 + public function render() { 84 + require_celerity_resource('phui-formation-view-css'); 85 + 86 + $width = $this->getWidth(); 87 + 88 + $style = array(); 89 + $style[] = sprintf('width: %dpx;', $width); 90 + 91 + $classes = array(); 92 + $classes[] = 'phui-flank-view'; 93 + 94 + if ($this->getIsFixed()) { 95 + $classes[] = 'phui-flank-view-fixed'; 96 + } 97 + 98 + $head_id = $this->getHeadID(); 99 + $body_id = $this->getBodyID(); 100 + $tail_id = $this->getTailID(); 101 + 102 + $head_content = phutil_tag( 103 + 'div', 104 + array( 105 + 'class' => 'phui-flank-header', 106 + ), 107 + array( 108 + phutil_tag( 109 + 'div', 110 + array( 111 + 'class' => 'phui-flank-header-text', 112 + ), 113 + $this->getHeaderText()), 114 + $this->newHideButton(), 115 + )); 116 + 117 + $content = phutil_tag( 118 + 'div', 119 + array( 120 + 'id' => $this->getID(), 121 + 'class' => implode(' ', $classes), 122 + 'style' => implode(' ', $style), 123 + ), 124 + array( 125 + phutil_tag( 126 + 'div', 127 + array( 128 + 'id' => $head_id, 129 + 'class' => 'phui-flank-view-head', 130 + ), 131 + $head_content), 132 + phutil_tag( 133 + 'div', 134 + array( 135 + 'id' => $body_id, 136 + 'class' => 'phui-flank-view-body', 137 + ), 138 + $this->getBody()), 139 + phutil_tag( 140 + 'div', 141 + array( 142 + 'id' => $tail_id, 143 + 'class' => 'phui-flank-view-tail', 144 + ), 145 + $this->getTail()), 146 + )); 147 + 148 + return $content; 149 + } 150 + 151 + private function newHideButton() { 152 + $item = $this->getColumnItem(); 153 + $is_right = $item->getIsRightAligned(); 154 + 155 + $hide_classes = array(); 156 + $hide_classes[] = 'phui-flank-header-hide'; 157 + 158 + if ($is_right) { 159 + $hide_icon = id(new PHUIIconView()) 160 + ->setIcon('fa-chevron-right grey'); 161 + $hide_classes[] = 'phui-flank-header-hide-right'; 162 + } else { 163 + $hide_icon = id(new PHUIIconView()) 164 + ->setIcon('fa-chevron-left grey'); 165 + $hide_classes[] = 'phui-flank-header-hide-left'; 166 + } 167 + 168 + return javelin_tag( 169 + 'div', 170 + array( 171 + 'sigil' => 'phui-flank-header-hide', 172 + 'class' => implode(' ', $hide_classes), 173 + ), 174 + $hide_icon); 175 + } 176 + 177 + }
+34
src/view/formation/PHUIFormationResizerView.php
··· 1 + <?php 2 + 3 + final class PHUIFormationResizerView 4 + extends PHUIFormationColumnView { 5 + 6 + private $isVisible; 7 + 8 + public function setIsVisible($is_visible) { 9 + $this->isVisible = $is_visible; 10 + return $this; 11 + } 12 + 13 + public function getIsVisible() { 14 + return $this->isVisible; 15 + } 16 + 17 + public function getWidth() { 18 + return 8; 19 + } 20 + 21 + public function render() { 22 + $width = $this->getWidth(); 23 + $style = sprintf('width: %dpx;', $width); 24 + 25 + return phutil_tag( 26 + 'div', 27 + array( 28 + 'id' => $this->getID(), 29 + 'class' => 'phui-formation-resizer', 30 + 'style' => $style, 31 + )); 32 + } 33 + 34 + }
+188
src/view/formation/PHUIFormationView.php
··· 1 + <?php 2 + 3 + final class PHUIFormationView 4 + extends AphrontView { 5 + 6 + private $items = array(); 7 + 8 + public function newFlankColumn() { 9 + $item = $this->newItem(new PHUIFormationFlankView()); 10 + return $item->getColumn(); 11 + } 12 + 13 + public function newContentColumn() { 14 + $item = $this->newItem(new PHUIFormationContentView()); 15 + return $item->getColumn(); 16 + } 17 + 18 + private function newItem(PHUIFormationColumnView $column) { 19 + $item = id(new PHUIFormationColumnItem()) 20 + ->setColumn($column); 21 + 22 + $column->setColumnItem($item); 23 + 24 + $this->items[] = $item; 25 + 26 + return $item; 27 + } 28 + 29 + public function render() { 30 + require_celerity_resource('phui-formation-view-css'); 31 + 32 + $items = $this->items; 33 + 34 + $items = $this->generateControlBindings($items); 35 + $items = $this->generateExpanders($items); 36 + $items = $this->generateResizers($items); 37 + 38 + $cells = array(); 39 + foreach ($items as $item) { 40 + $style = array(); 41 + 42 + $column = $item->getColumn(); 43 + 44 + $width = $column->getWidth(); 45 + if ($width !== null) { 46 + $style[] = sprintf('width: %dpx;', $width); 47 + } 48 + 49 + if (!$column->getIsVisible()) { 50 + $style[] = 'display: none;'; 51 + } 52 + 53 + $cells[] = phutil_tag( 54 + 'td', 55 + array( 56 + 'id' => $item->getID(), 57 + 'style' => implode(' ', $style), 58 + ), 59 + array( 60 + $column, 61 + $item->getExpanders(), 62 + )); 63 + } 64 + 65 + $formation_id = celerity_generate_unique_node_id(); 66 + 67 + $table_row = phutil_tag('tr', array(), $cells); 68 + $table_body = phutil_tag('tbody', array(), $table_row); 69 + $table = phutil_tag( 70 + 'table', 71 + array( 72 + 'class' => 'phui-formation-view', 73 + 'id' => $formation_id, 74 + ), 75 + $table_body); 76 + 77 + $phuix_columns = array(); 78 + foreach ($items as $item) { 79 + $phuix_columns[] = $item->newClientProperties(); 80 + } 81 + 82 + Javelin::initBehavior( 83 + 'phuix-formation-view', 84 + array( 85 + 'nodeID' => $formation_id, 86 + 'columns' => $phuix_columns, 87 + )); 88 + 89 + return $table; 90 + } 91 + 92 + private function newColumnExpanderView() { 93 + return new PHUIFormationExpanderView(); 94 + } 95 + 96 + private function newResizerItem() { 97 + return $this->newItem(new PHUIFormationResizerView()); 98 + } 99 + 100 + private function generateControlBindings(array $items) { 101 + $count = count($items); 102 + 103 + if (!$count) { 104 + return $items; 105 + } 106 + 107 + $last_control = null; 108 + 109 + for ($ii = 0; $ii < $count; $ii++) { 110 + $item = $items[$ii]; 111 + $column = $item->getColumn(); 112 + 113 + $is_control = $column->getIsControlColumn(); 114 + if ($is_control) { 115 + $last_control = $ii; 116 + } 117 + } 118 + 119 + if ($last_control === null) { 120 + return $items; 121 + } 122 + 123 + for ($ii = ($count - 1); $ii >= 0; $ii--) { 124 + $item = $items[$ii]; 125 + $column = $item->getColumn(); 126 + 127 + $is_control = $column->getIsControlColumn(); 128 + if ($is_control) { 129 + $last_control = $ii; 130 + continue; 131 + } 132 + 133 + $is_right = ($last_control < $ii); 134 + 135 + $item 136 + ->setControlItem($items[$last_control]) 137 + ->setIsRightAligned($is_right); 138 + } 139 + 140 + return $items; 141 + } 142 + 143 + private function generateResizers(array $items) { 144 + $result = array(); 145 + foreach ($items as $item) { 146 + $column = $item->getColumn(); 147 + 148 + $resizer_item = null; 149 + if ($column->getIsResizable()) { 150 + $resizer_item = $this->newResizerItem(); 151 + $item->setResizerItem($resizer_item); 152 + 153 + $resizer_item 154 + ->getColumn() 155 + ->setIsVisible($column->getIsVisible()); 156 + } 157 + 158 + if (!$resizer_item) { 159 + $result[] = $item; 160 + } else if ($item->getIsRightAligned()) { 161 + $result[] = $resizer_item; 162 + $result[] = $item; 163 + } else { 164 + $result[] = $item; 165 + $result[] = $resizer_item; 166 + } 167 + } 168 + 169 + return $result; 170 + } 171 + 172 + private function generateExpanders(array $items) { 173 + foreach ($items as $item) { 174 + $control_item = $item->getControlItem(); 175 + if ($control_item) { 176 + $expander = $this->newColumnExpanderView(); 177 + 178 + $expander->setColumnItem($item); 179 + $item->setExpander($expander); 180 + 181 + $control_item->appendExpander($expander); 182 + } 183 + } 184 + 185 + return $items; 186 + } 187 + 188 + }
+145
webroot/rsrc/css/phui/phui-formation-view.css
··· 1 + /** 2 + * @provides phui-formation-view-css 3 + */ 4 + 5 + .phui-formation-view { 6 + table-layout: fixed; 7 + width: 100%; 8 + } 9 + 10 + .phui-formation-view-expander { 11 + position: fixed; 12 + width: 24px; 13 + height: 36px; 14 + top: 64px; 15 + border-style: solid; 16 + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); 17 + border-color: {$lightgreyborder}; 18 + background: {$lightgreybackground}; 19 + z-index: 4; 20 + } 21 + 22 + .phui-formation-view-expander-left { 23 + border-radius: 0 12px 12px 0; 24 + border-width: 1px 1px 1px 0; 25 + cursor: e-resize; 26 + } 27 + 28 + .phui-formation-view-expander-right { 29 + border-radius: 12px 0 0 12px; 30 + border-width: 1px 0 1px 1px; 31 + cursor: w-resize; 32 + } 33 + 34 + .phui-formation-view-expander-icon { 35 + position: absolute; 36 + width: 18px; 37 + height: 18px; 38 + top: 9px; 39 + left: 3px; 40 + text-align: center; 41 + } 42 + 43 + .device-desktop .phui-formation-view-expander:hover { 44 + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1); 45 + background: {$darkgreybackground}; 46 + transition: 0.1s; 47 + } 48 + 49 + .device-desktop .phui-formation-view-expander:hover 50 + .phui-icon-view { 51 + color: {$bluetext}; 52 + transition: 0.1s; 53 + } 54 + 55 + .phui-flank-header { 56 + padding: 8px; 57 + background: {$greybackground}; 58 + border-bottom: 1px solid {$lightgreyborder}; 59 + } 60 + 61 + .phui-flank-header-text { 62 + color: {$darkgreytext}; 63 + font-weight: bold; 64 + } 65 + 66 + .phui-flank-header-hide { 67 + font-size: {$normalfontsize}; 68 + position: absolute; 69 + display: inline-block; 70 + top: 6px; 71 + right: 6px; 72 + width: 20px; 73 + height: 20px; 74 + text-align: center; 75 + border: 1px solid {$lightgreyborder}; 76 + border-radius: 4px; 77 + line-height: 20px; 78 + } 79 + 80 + .phui-flank-header-hide-left { 81 + cursor: w-resize; 82 + } 83 + 84 + 85 + .device-desktop .phui-flank-header-hide:hover { 86 + box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.05); 87 + background: {$darkgreybackground}; 88 + transition: 0.1s; 89 + } 90 + 91 + .device-desktop .phui-flank-header-hide:hover 92 + .phui-icon-view { 93 + color: {$bluetext}; 94 + transition: 0.1s; 95 + } 96 + 97 + .phui-formation-resizer { 98 + position: fixed; 99 + top: 0; 100 + bottom: 0; 101 + 102 + cursor: col-resize; 103 + background: #f5f5f5; 104 + border-style: solid; 105 + border-width: 0 1px 0 1px; 106 + border-color: #fff #999c9e #fff #999c9e; 107 + box-sizing: border-box; 108 + 109 + box-shadow: inset -1px 0px 1px rgba({$alphablack}, 0.15); 110 + 111 + background-image: url(/rsrc/image/divot.png); 112 + background-position: center; 113 + background-repeat: no-repeat; 114 + } 115 + 116 + .phui-flank-view-fixed { 117 + position: fixed; 118 + top: {$menu.main.height}; 119 + bottom: 0; 120 + overflow: hidden; 121 + } 122 + 123 + .phui-flank-view-fixed .phui-flank-view-body { 124 + overflow: hidden auto; 125 + } 126 + 127 + .device-desktop .phui-flank-view-fixed 128 + .phui-flank-view-body::-webkit-scrollbar { 129 + height: 6px; 130 + width: 6px; 131 + background: rgba(0, 0, 0, 0.1); 132 + border-radius: 4px; 133 + } 134 + 135 + .device-desktop .phui-flank-view-fixed 136 + .phui-flank-view-body::-webkit-scrollbar-thumb { 137 + background: rgba(0, 0, 0, 0.25); 138 + border-radius: 4px; 139 + } 140 + 141 + .phui-flank-view-fixed .phui-flank-view-tail { 142 + position: absolute; 143 + bottom: 0; 144 + width: 100%; 145 + }
+54
webroot/rsrc/js/phui/behavior-phuix-formation-view.js
··· 1 + /** 2 + * @provides javelin-behavior-phuix-formation-view 3 + * @requires javelin-behavior 4 + * phuix-formation-view 5 + * phuix-formation-column-view 6 + * phuix-formation-flank-view 7 + */ 8 + 9 + JX.behavior('phuix-formation-view', function(config) { 10 + 11 + var formation_node = JX.$(config.nodeID); 12 + var formation = new JX.PHUIXFormationView(formation_node); 13 + 14 + var count = config.columns.length; 15 + for (var ii = 0; ii < count; ii++) { 16 + var spec = config.columns[ii]; 17 + var node = JX.$(spec.itemID); 18 + 19 + var column = new JX.PHUIXFormationColumnView(node) 20 + .setIsRightAligned(spec.isRightAligned) 21 + .setWidth(spec.width) 22 + .setIsVisible(spec.isVisible); 23 + 24 + if (spec.expanderID) { 25 + column.setExpanderNode(JX.$(spec.expanderID)); 26 + } 27 + 28 + if (spec.resizer) { 29 + column 30 + .setResizerItem(JX.$(spec.resizer.itemID)) 31 + .setResizerControl(JX.$(spec.resizer.controlID)); 32 + } 33 + 34 + var colspec = spec.column; 35 + if (colspec) { 36 + if (colspec.type === 'flank') { 37 + var flank_node = JX.$(colspec.nodeID); 38 + 39 + var head = JX.$(colspec.headID); 40 + var body = JX.$(colspec.bodyID); 41 + var tail = JX.$(colspec.tailID); 42 + 43 + var flank = new JX.PHUIXFormationFlankView(flank_node, head, body, tail) 44 + .setIsFixed(colspec.isFixed); 45 + 46 + column.setFlank(flank); 47 + } 48 + } 49 + 50 + formation.addColumn(column); 51 + } 52 + 53 + formation.start(); 54 + });
+174
webroot/rsrc/js/phuix/PHUIXFormationColumnView.js
··· 1 + /** 2 + * @provides phuix-formation-column-view 3 + * @requires javelin-install 4 + * javelin-dom 5 + */ 6 + 7 + JX.install('PHUIXFormationColumnView', { 8 + 9 + construct: function(node) { 10 + this._node = node; 11 + }, 12 + 13 + properties: { 14 + isRightAligned: false, 15 + isVisible: true, 16 + expanderNode: null, 17 + resizerItem: null, 18 + resizerControl: null, 19 + width: null, 20 + flank: null 21 + }, 22 + 23 + members: { 24 + _node: null, 25 + _resizingWidth: null, 26 + _resizingBarPosition: null, 27 + _dragging: null, 28 + 29 + start: function() { 30 + var onshow = JX.bind(this, this._setVisibility, true); 31 + var onhide = JX.bind(this, this._setVisibility, false); 32 + 33 + JX.DOM.listen(this._node, 'click', 'phui-flank-header-hide', onhide); 34 + 35 + var expander = this.getExpanderNode(); 36 + if (expander) { 37 + JX.DOM.listen(expander, 'click', null, onshow); 38 + } 39 + 40 + var resizer = this.getResizerItem(); 41 + if (resizer) { 42 + var ondown = JX.bind(this, this._onresizestart); 43 + JX.DOM.listen(resizer, 'mousedown', null, ondown); 44 + 45 + var onmove = JX.bind(this, this._onresizemove); 46 + JX.Stratcom.listen('mousemove', null, onmove); 47 + 48 + var onup = JX.bind(this, this._onresizeend); 49 + JX.Stratcom.listen('mouseup', null, onup); 50 + } 51 + 52 + this.repaint(); 53 + }, 54 + 55 + _onresizestart: function(e) { 56 + if (!e.isNormalMouseEvent()) { 57 + return; 58 + } 59 + 60 + this._dragging = JX.$V(e); 61 + this._resizingWidth = this.getWidth(); 62 + this._resizingBarPosition = JX.$V(this.getResizerControl()); 63 + 64 + // Show the "col-resize" cursor on the whole document while we're 65 + // dragging, since the mouse will slip off the actual bar fairly often 66 + // and we don't want it to flicker. 67 + JX.DOM.alterClass(document.body, 'jx-drag-col', true); 68 + 69 + e.kill(); 70 + }, 71 + 72 + _onresizemove: function(e) { 73 + if (!this._dragging) { 74 + return; 75 + } 76 + 77 + var dx = (JX.$V(e).x - this._dragging.x); 78 + 79 + var width; 80 + if (this.getIsRightAligned()) { 81 + width = this.getWidth() - dx; 82 + } else { 83 + width = this.getWidth() + dx; 84 + } 85 + 86 + // TODO: Make these configurable? 87 + width = Math.max(width, 150); 88 + width = Math.min(width, 512); 89 + 90 + this._resizingWidth = width; 91 + 92 + this._node.style.width = this._resizingWidth + 'px'; 93 + 94 + var adjust_x = (this._resizingWidth - this.getWidth()); 95 + if (this.getIsRightAligned()) { 96 + adjust_x = -adjust_x; 97 + } 98 + 99 + this.getResizerControl().style.left = 100 + (this._resizingBarPosition.x + adjust_x) + 'px'; 101 + 102 + var flank = this.getFlank(); 103 + if (flank) { 104 + flank 105 + .setWidth(this._resizingWidth) 106 + .repaint(); 107 + } 108 + }, 109 + 110 + _onresizeend: function(e) { 111 + if (!this._dragging) { 112 + return; 113 + } 114 + 115 + this.setWidth(this._resizingWidth); 116 + 117 + JX.log('new width is ' + this.getWidth()); 118 + 119 + JX.DOM.alterClass(document.body, 'jx-drag-col', false); 120 + this._dragging = null; 121 + 122 + // TODO: Save new width setting. 123 + 124 + // new JX.Request('/settings/adjust/', JX.bag) 125 + // .setData( 126 + // { 127 + // key: 'filetree.width', 128 + // value: get_width() 129 + // }) 130 + // .send(); 131 + 132 + }, 133 + 134 + _setVisibility: function(visible, e) { 135 + e.kill(); 136 + 137 + // TODO: Save the visibility setting. 138 + 139 + this.setIsVisible(visible); 140 + this.repaint(); 141 + }, 142 + 143 + repaint: function() { 144 + var resizer = this.getResizerItem(); 145 + var expander = this.getExpanderNode(); 146 + 147 + if (this.getIsVisible()) { 148 + JX.DOM.show(this._node); 149 + if (resizer) { 150 + JX.DOM.show(resizer); 151 + } 152 + if (expander) { 153 + JX.DOM.hide(expander); 154 + } 155 + } else { 156 + JX.DOM.hide(this._node); 157 + if (resizer) { 158 + JX.DOM.hide(resizer); 159 + } 160 + if (expander) { 161 + JX.DOM.show(expander); 162 + } 163 + } 164 + 165 + if (this.getFlank()) { 166 + this.getFlank().repaint(); 167 + } 168 + 169 + }, 170 + 171 + 172 + } 173 + 174 + });
+57
webroot/rsrc/js/phuix/PHUIXFormationFlankView.js
··· 1 + /** 2 + * @provides phuix-formation-flank-view 3 + * @requires javelin-install 4 + * javelin-dom 5 + */ 6 + 7 + JX.install('PHUIXFormationFlankView', { 8 + 9 + construct: function(node, head, body, tail) { 10 + this._node = node; 11 + 12 + this._headNode = head; 13 + this._bodyNode = body; 14 + this._tailNode = tail; 15 + }, 16 + 17 + properties: { 18 + isFixed: false, 19 + bannerHeight: null, 20 + width: null 21 + }, 22 + 23 + members: { 24 + _node: null, 25 + _headNode: null, 26 + _bodyNode: null, 27 + _tailNode: null, 28 + 29 + getBodyNode: function() { 30 + return this._bodyNode; 31 + }, 32 + 33 + getTailNode: function() { 34 + return this._tailNode; 35 + }, 36 + 37 + repaint: function() { 38 + if (!this.getIsFixed()) { 39 + return; 40 + } 41 + 42 + this._node.style.top = this.getBannerHeight() + 'px'; 43 + this._node.style.width = this.getWidth() + 'px'; 44 + 45 + var body = this.getBodyNode(); 46 + var body_pos = JX.$V(body); 47 + 48 + var tail = this.getTailNode(); 49 + var tail_pos = JX.$V(tail); 50 + 51 + var max_height = (tail_pos.y - body_pos.y); 52 + 53 + body.style.maxHeight = max_height + 'px'; 54 + } 55 + } 56 + 57 + });
+68
webroot/rsrc/js/phuix/PHUIXFormationView.js
··· 1 + /** 2 + * @provides phuix-formation-view 3 + * @requires javelin-install 4 + * javelin-dom 5 + */ 6 + 7 + JX.install('PHUIXFormationView', { 8 + 9 + construct: function() { 10 + this._columns = []; 11 + }, 12 + 13 + members: { 14 + _columns: null, 15 + 16 + addColumn: function(column) { 17 + this._columns.push(column); 18 + }, 19 + 20 + start: function() { 21 + JX.enableDispatch(document.body, 'mousemove'); 22 + 23 + for (var ii = 0; ii < this._columns.length; ii++) { 24 + this._columns[ii].start(); 25 + } 26 + 27 + var repaint = JX.bind(this, this.repaint); 28 + JX.Stratcom.listen(['scroll', 'resize'], null, repaint); 29 + 30 + this.repaint(); 31 + }, 32 + 33 + repaint: function(e) { 34 + // Unless we've scrolled past it, the page has a 44px main menu banner. 35 + var menu_height = (44 - JX.Vector.getScroll().y); 36 + 37 + // When the buoyant header is visible, move the menu down below it. This 38 + // is a bit of a hack. 39 + var banner_height = 0; 40 + try { 41 + var banner = JX.$('diff-banner'); 42 + banner_height = JX.Vector.getDim(banner).y; 43 + } catch (error) { 44 + // Ignore if there's no banner on the page. 45 + } 46 + 47 + var header_height = Math.max(0, menu_height, banner_height); 48 + 49 + var column; 50 + var flank; 51 + for (var ii = 0; ii < this._columns.length; ii++) { 52 + column = this._columns[ii]; 53 + 54 + flank = column.getFlank(); 55 + if (!flank) { 56 + continue; 57 + } 58 + 59 + flank 60 + .setBannerHeight(header_height) 61 + .setWidth(column.getWidth()) 62 + .repaint(); 63 + } 64 + } 65 + 66 + } 67 + 68 + });