diff --git a/src/applications/uiexample/examples/PhabricatorPropertyListExample.php b/src/applications/uiexample/examples/PhabricatorPropertyListExample.php index b5ee7c2d88..68e1449aa6 100644 --- a/src/applications/uiexample/examples/PhabricatorPropertyListExample.php +++ b/src/applications/uiexample/examples/PhabricatorPropertyListExample.php @@ -35,6 +35,90 @@ final class PhabricatorPropertyListExample extends PhabricatorUIExample { 'viverra. Nunc tempus tempor quam id iaculis. Maecenas lectus '. 'velit, aliquam et consequat quis, tincidunt id dolor.'); - return $view; + + $view->addSectionHeader('Colors of the Rainbow'); + + $view->addProperty('R', 'Red'); + $view->addProperty('O', 'Orange'); + $view->addProperty('Y', 'Yellow'); + $view->addProperty('G', 'Green'); + $view->addProperty('B', 'Blue'); + $view->addProperty('I', 'Indigo'); + $view->addProperty('V', 'Violet'); + + $view->addSectionHeader('Haiku About Pasta'); + + $view->addTextContent( + 'this is a pasta
'. + 'haiku. it is very bad.
'. + 'what did you expect?'); + + $edge_cases_header = id(new PhabricatorHeaderView()) + ->setHeader(pht('Edge Cases')); + + $edge_cases_view = new PhabricatorPropertyListView(); + + $edge_cases_view->addProperty( + pht('Description'), + pht('These layouts test UI edge cases in the element. This block '. + 'tests wrapping and overflow behavior.')); + + $edge_cases_view->addProperty( + pht('A Very Very Very Very Very Very Very Very Very Long Property Label'), + pht('This property label and property value are quite long. They '. + 'demonstrate the wrapping behavior of the element, or lack thereof '. + 'if something terrible has happened.')); + + $edge_cases_view->addProperty( + pht('AVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongUnbrokenPropertyLabel'), + pht('Thispropertylabelandpropertyvaluearequitelongandhave'. + 'nospacestheydemonstratetheoverflowbehavioroftheelement'. + 'orlackthereof.')); + + + $edge_cases_view->addProperty( + pht('Description'), + pht('The next section is an empty text block.')); + + $edge_cases_view->addTextContent(''); + + $edge_cases_view->addProperty( + pht('Description'), + pht('This section should have multiple properties with the same name.')); + + $edge_cases_view->addProperty( + pht('Joe'), + pht('Smith')); + $edge_cases_view->addProperty( + pht('Joe'), + pht('Smith')); + $edge_cases_view->addProperty( + pht('Joe'), + pht('Smith')); + + $edge_cases_view->addProperty( + pht('Description'), + pht('The next section shows adjacent section headers.')); + + $edge_cases_view->addSectionHeader('Several'); + $edge_cases_view->addSectionHeader('Adjacent'); + $edge_cases_view->addSectionHeader('Section'); + $edge_cases_view->addSectionHeader('Headers'); + + $edge_cases_view->addProperty( + pht('Description'), + pht('The next section is several adjacent text blocks.')); + + $edge_cases_view->addTextContent('Lorem'); + $edge_cases_view->addTextContent('ipsum'); + $edge_cases_view->addTextContent('dolor'); + $edge_cases_view->addTextContent('sit'); + $edge_cases_view->addTextContent('amet...'); + + return array( + $view, + $edge_cases_header, + $edge_cases_view, + ); } } diff --git a/src/view/AphrontView.php b/src/view/AphrontView.php index d0377bc12d..b1ac91f7f0 100644 --- a/src/view/AphrontView.php +++ b/src/view/AphrontView.php @@ -4,7 +4,16 @@ abstract class AphrontView { protected $children = array(); + protected function canAppendChild() { + return true; + } + final public function appendChild($child) { + if (!$this->canAppendChild()) { + $class = get_class($this); + throw new Exception( + "View '{$class}' does not support children."); + } $this->children[] = $child; return $this; } diff --git a/src/view/layout/PhabricatorPropertyListView.php b/src/view/layout/PhabricatorPropertyListView.php index 8e6d765870..ba3191e5ad 100644 --- a/src/view/layout/PhabricatorPropertyListView.php +++ b/src/view/layout/PhabricatorPropertyListView.php @@ -2,56 +2,69 @@ final class PhabricatorPropertyListView extends AphrontView { - private $properties = array(); + private $parts = array(); + + protected function canAppendChild() { + return false; + } public function addProperty($key, $value) { - $this->properties[$key] = $value; + $current = array_pop($this->parts); + + if (!$current || $current['type'] != 'property') { + if ($current) { + $this->parts[] = $current; + } + $current = array( + 'type' => 'property', + 'list' => array(), + ); + } + + $current['list'][] = array( + 'key' => $key, + 'value' => $value, + ); + + $this->parts[] = $current; + return $this; + } + + public function addSectionHeader($name) { + $this->parts[] = array( + 'type' => 'section', + 'name' => $name, + ); return $this; } public function addTextContent($content) { - return $this->appendChild( - phutil_render_tag( - 'div', - array( - 'class' => 'phabricator-property-list-text-content', - ), - $content)); + $this->parts[] = array( + 'type' => 'text', + 'content' => $content, + ); + return $this; } public function render() { require_celerity_resource('phabricator-property-list-view-css'); $items = array(); - foreach ($this->properties as $key => $value) { - $items[] = phutil_render_tag( - 'dt', - array( - 'class' => 'phabricator-property-key', - ), - phutil_escape_html($key)); - $items[] = phutil_render_tag( - 'dd', - array( - 'class' => 'phabricator-property-value', - ), - $this->renderSingleView($value)); - } - - $list = phutil_render_tag( - 'dl', - array( - ), - $this->renderSingleView($items)); - - $content = $this->renderChildren(); - if (strlen($content)) { - $content = phutil_render_tag( - 'div', - array( - 'class' => 'phabricator-property-list-content', - ), - $content); + foreach ($this->parts as $part) { + $type = $part['type']; + switch ($type) { + case 'property': + $items[] = $this->renderPropertyPart($part); + break; + case 'section': + $items[] = $this->renderSectionPart($part); + break; + case 'text': + $items[] = $this->renderTextPart($part); + break; + default: + throw new Exception("Unknown part type '{$type}'!"); + } } return phutil_render_tag( @@ -59,14 +72,61 @@ final class PhabricatorPropertyListView extends AphrontView { array( 'class' => 'phabricator-property-list-view', ), - $list. - // NOTE: We need this (which is basically a "clear: both;" div) to make - // sure the property list is taller than the action list for objects with - // few properties but many actions. Otherwise, the action list may - // obscure the document content. - '
'). - $content; + $this->renderSingleView($items)); } + private function renderPropertyPart(array $part) { + $items = array(); + foreach ($part['list'] as $spec) { + $key = $spec['key']; + $value = $spec['value']; + + $items[] = phutil_render_tag( + 'dt', + array( + 'class' => 'phabricator-property-list-key', + ), + phutil_escape_html($key)); + $items[] = phutil_render_tag( + 'dd', + array( + 'class' => 'phabricator-property-list-value', + ), + $this->renderSingleView($value)); + } + + $list = phutil_render_tag( + 'dl', + array( + 'class' => 'phabricator-property-list-properties', + ), + $this->renderSingleView($items)); + + return phutil_render_tag( + 'div', + array( + 'class' => 'phabricator-property-list-container', + ), + $list. + '
'); + } + + private function renderSectionPart(array $part) { + return phutil_render_tag( + 'div', + array( + 'class' => 'phabricator-property-list-section-header', + ), + phutil_escape_html($part['name'])); + } + + private function renderTextPart(array $part) { + return phutil_render_tag( + 'div', + array( + 'class' => 'phabricator-property-list-text-content', + ), + $part['content']); + } } diff --git a/webroot/rsrc/css/layout/phabricator-property-list-view.css b/webroot/rsrc/css/layout/phabricator-property-list-view.css index dec6656600..afacf7d6cf 100644 --- a/webroot/rsrc/css/layout/phabricator-property-list-view.css +++ b/webroot/rsrc/css/layout/phabricator-property-list-view.css @@ -5,30 +5,34 @@ .phabricator-property-list-view { border-color: #dbdbdb; border-style: solid; - border-width: 1px 0; + border-width: 1px 0 0 0; +} + +.phabricator-property-list-container { + border-color: #dbdbdb; + border-style: solid; + border-width: 0 0 1px; background-color: #f9f9f9; } -.phabriator-property-list-view-end { - clear: both; +.device-desktop .phabricator-property-list-container { + padding: 1em 0 0.5em; } -.device-desktop .phabricator-property-list-view { - padding: 1em 0 0.75em; +.device-tablet .phabricator-property-list-container, +.device-phone .phabricator-property-list-container { + padding: .5em 0; } -.device-tablet .phabricator-property-list-view, -.device-phone .phabricator-property-list-view { - padding: .5em; -} - -.phabricator-property-key { +.phabricator-property-list-key { color: #333333; font-weight: bold; + overflow: hidden; + white-space: nowrap; } -.device-desktop .phabricator-property-key { - width: 12%; +.device-desktop .phabricator-property-list-key { + width: 15%; margin-left: 1%; text-align: right; float: left; @@ -36,35 +40,44 @@ margin-bottom: .5em; } -.device-tablet .phabricator-property-key, -.device-phone .phabricator-property-key { +.device-tablet .phabricator-property-list-key, +.device-phone .phabricator-property-list-key { padding-left: .5em; } -.phabricator-property-value { +.phabricator-property-list-value { color: #333333; + overflow: hidden; } -.device-desktop .phabricator-property-value { - width: 53%; +.device-desktop .phabricator-property-list-value { + width: 50%; margin-left: 1%; float: left; margin-bottom: .5em; } -.device-tablet .phabricator-property-value, -.device-phone .phabricator-property-value { +.device-tablet .phabricator-property-list-value, +.device-phone .phabricator-property-list-value { padding-left: 1.5em; margin-bottom: .5em; } -.phabricator-property-list-content { - background: #fdfdfd; +.phabriator-property-list-view-end { + clear: both; +} + +.phabricator-property-list-section-header { + background: #f0f0f0; + color: #666666; + padding: 4px 18px; border-bottom: 1px solid #dbdbdb; } .phabricator-property-list-text-content { padding: 12px 18px; + background: #fdfdfd; + border-bottom: 1px solid #dbdbdb; } /* When we follow an action list view on the Desktop, move down 30px so the