diff --git a/src/applications/celerity/CelerityStaticResourceResponse.php b/src/applications/celerity/CelerityStaticResourceResponse.php index f81b8d5957..7db6c2741b 100644 --- a/src/applications/celerity/CelerityStaticResourceResponse.php +++ b/src/applications/celerity/CelerityStaticResourceResponse.php @@ -1,337 +1,347 @@ metadataBlock = (int)$_REQUEST['__metablock__']; } } public function addMetadata($metadata) { + if ($this->metadataLocked) { + throw new Exception( + pht( + 'Attempting to add more metadata after metadata has been '. + 'locked.')); + } + $id = count($this->metadata); $this->metadata[$id] = $metadata; return $this->metadataBlock.'_'.$id; } public function getMetadataBlock() { return $this->metadataBlock; } public function setPostprocessorKey($postprocessor_key) { $this->postprocessorKey = $postprocessor_key; return $this; } public function getPostprocessorKey() { return $this->postprocessorKey; } /** * Register a behavior for initialization. * * NOTE: If `$config` is empty, a behavior will execute only once even if it * is initialized multiple times. If `$config` is nonempty, the behavior will * be invoked once for each configuration. */ public function initBehavior( $behavior, array $config = array(), $source_name = null) { $this->requireResource('javelin-behavior-'.$behavior, $source_name); if (empty($this->behaviors[$behavior])) { $this->behaviors[$behavior] = array(); } if ($config) { $this->behaviors[$behavior][] = $config; } return $this; } public function requireResource($symbol, $source_name) { if (isset($this->symbols[$source_name][$symbol])) { return $this; } // Verify that the resource exists. $map = CelerityResourceMap::getNamedInstance($source_name); $name = $map->getResourceNameForSymbol($symbol); if ($name === null) { throw new Exception( pht( 'No resource with symbol "%s" exists in source "%s"!', $symbol, $source_name)); } $this->symbols[$source_name][$symbol] = true; $this->needsResolve = true; return $this; } private function resolveResources() { if ($this->needsResolve) { $this->packaged = array(); foreach ($this->symbols as $source_name => $symbols_map) { $symbols = array_keys($symbols_map); $map = CelerityResourceMap::getNamedInstance($source_name); $packaged = $map->getPackagedNamesForSymbols($symbols); $this->packaged[$source_name] = $packaged; } $this->needsResolve = false; } return $this; } public function renderSingleResource($symbol, $source_name) { $map = CelerityResourceMap::getNamedInstance($source_name); $packaged = $map->getPackagedNamesForSymbols(array($symbol)); return $this->renderPackagedResources($map, $packaged); } public function renderResourcesOfType($type) { $this->resolveResources(); $result = array(); foreach ($this->packaged as $source_name => $resource_names) { $map = CelerityResourceMap::getNamedInstance($source_name); $resources_of_type = array(); foreach ($resource_names as $resource_name) { $resource_type = $map->getResourceTypeForName($resource_name); if ($resource_type == $type) { $resources_of_type[] = $resource_name; } } $result[] = $this->renderPackagedResources($map, $resources_of_type); } return phutil_implode_html('', $result); } private function renderPackagedResources( CelerityResourceMap $map, array $resources) { $output = array(); foreach ($resources as $name) { if (isset($this->hasRendered[$name])) { continue; } $this->hasRendered[$name] = true; $output[] = $this->renderResource($map, $name); } return $output; } private function renderResource( CelerityResourceMap $map, $name) { $uri = $this->getURI($map, $name); $type = $map->getResourceTypeForName($name); $multimeter = MultimeterControl::getInstance(); if ($multimeter) { $event_type = MultimeterEvent::TYPE_STATIC_RESOURCE; $multimeter->newEvent($event_type, 'rsrc.'.$name, 1); } switch ($type) { case 'css': return phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => $uri, )); case 'js': return phutil_tag( 'script', array( 'type' => 'text/javascript', 'src' => $uri, ), ''); } throw new Exception( pht( 'Unable to render resource "%s", which has unknown type "%s".', $name, $type)); } public function renderHTMLFooter() { + $this->metadataLocked = true; + $data = array(); if ($this->metadata) { $json_metadata = AphrontResponse::encodeJSONForHTTPResponse( $this->metadata); $this->metadata = array(); } else { $json_metadata = '{}'; } // Even if there is no metadata on the page, Javelin uses the mergeData() // call to start dispatching the event queue. $data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '. $json_metadata.');'; $onload = array(); if ($this->behaviors) { $behaviors = $this->behaviors; $this->behaviors = array(); $higher_priority_names = array( 'refresh-csrf', 'aphront-basic-tokenizer', 'dark-console', 'history-install', ); $higher_priority_behaviors = array_select_keys( $behaviors, $higher_priority_names); foreach ($higher_priority_names as $name) { unset($behaviors[$name]); } $behavior_groups = array( $higher_priority_behaviors, $behaviors, ); foreach ($behavior_groups as $group) { if (!$group) { continue; } $group_json = AphrontResponse::encodeJSONForHTTPResponse( $group); $onload[] = 'JX.initBehaviors('.$group_json.')'; } } if ($onload) { foreach ($onload as $func) { $data[] = 'JX.onload(function(){'.$func.'});'; } } if ($data) { $data = implode("\n", $data); return self::renderInlineScript($data); } else { return ''; } } public static function renderInlineScript($data) { if (stripos($data, '') !== false) { throw new Exception( pht( 'Literal %s is not allowed inside inline script.', '')); } if (strpos($data, ' because it is ignored by HTML parsers. We // would need to send the document with XHTML content type. return phutil_tag( 'script', array('type' => 'text/javascript'), phutil_safe_html($data)); } public function buildAjaxResponse($payload, $error = null) { $response = array( 'error' => $error, 'payload' => $payload, ); if ($this->metadata) { $response['javelin_metadata'] = $this->metadata; $this->metadata = array(); } if ($this->behaviors) { $response['javelin_behaviors'] = $this->behaviors; $this->behaviors = array(); } $this->resolveResources(); $resources = array(); foreach ($this->packaged as $source_name => $resource_names) { $map = CelerityResourceMap::getNamedInstance($source_name); foreach ($resource_names as $resource_name) { $resources[] = $this->getURI($map, $resource_name); } } if ($resources) { $response['javelin_resources'] = $resources; } return $response; } public function getURI( CelerityResourceMap $map, $name, $use_primary_domain = false) { $uri = $map->getURIForName($name); // If we have a postprocessor selected, add it to the URI. $postprocessor_key = $this->getPostprocessorKey(); if ($postprocessor_key) { $uri = preg_replace('@^/res/@', '/res/'.$postprocessor_key.'X/', $uri); } // In developer mode, we dump file modification times into the URI. When a // page is reloaded in the browser, any resources brought in by Ajax calls // do not trigger revalidation, so without this it's very difficult to get // changes to Ajaxed-in CSS to work (you must clear your cache or rerun // the map script). In production, we can assume the map script gets run // after changes, and safely skip this. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $mtime = $map->getModifiedTimeForName($name); $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri); } if ($use_primary_domain) { return PhabricatorEnv::getURI($uri); } else { return PhabricatorEnv::getCDNURI($uri); } } } diff --git a/src/view/layout/PhabricatorActionListView.php b/src/view/layout/PhabricatorActionListView.php index 4965f02793..5c3facbf42 100644 --- a/src/view/layout/PhabricatorActionListView.php +++ b/src/view/layout/PhabricatorActionListView.php @@ -1,57 +1,63 @@ object = $object; return $this; } public function addAction(PhabricatorActionView $view) { $this->actions[] = $view; return $this; } public function setID($id) { $this->id = $id; return $this; } public function render() { $viewer = $this->getViewer(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS, array( 'object' => $this->object, 'actions' => $this->actions, )); $event->setUser($viewer); PhutilEventEngine::dispatchEvent($event); $actions = $event->getValue('actions'); if (!$actions) { return null; } foreach ($actions as $action) { $action->setViewer($viewer); } require_celerity_resource('phabricator-action-list-view-css'); return phutil_tag( 'ul', array( 'class' => 'phabricator-action-list-view', 'id' => $this->id, ), $actions); } + public function getDropdownMenuMetadata() { + return array( + 'items' => (string)hsprintf('%s', $this), + ); + } + } diff --git a/src/view/phui/PHUIButtonView.php b/src/view/phui/PHUIButtonView.php index 9b9d4f0293..f9266a170a 100644 --- a/src/view/phui/PHUIButtonView.php +++ b/src/view/phui/PHUIButtonView.php @@ -1,195 +1,192 @@ name = $name; return $this; } public function getName() { return $this->name; } public function setText($text) { $this->text = $text; return $this; } public function setHref($href) { $this->href = $href; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function setSubtext($subtext) { $this->subtext = $subtext; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function setTag($tag) { $this->tag = $tag; return $this; } public function setSize($size) { $this->size = $size; return $this; } public function setDropdown($dd) { $this->dropdown = $dd; return $this; } public function setTooltip($text) { $this->tooltip = $text; return $this; } public function setIcon($icon, $first = true) { if (!($icon instanceof PHUIIconView)) { $icon = id(new PHUIIconView()) ->setIcon($icon); } $this->icon = $icon; $this->iconFirst = $first; return $this; } protected function getTagName() { return $this->tag; } public function setDropdownMenu(PhabricatorActionListView $actions) { Javelin::initBehavior('phui-dropdown-menu'); $this->addSigil('phui-dropdown-menu'); - $this->setMetadata( - array( - 'items' => $actions, - )); + $this->setMetadata($actions->getDropdownMenuMetadata()); return $this; } protected function getTagAttributes() { require_celerity_resource('phui-button-css'); $classes = array(); $classes[] = 'button'; if ($this->color) { $classes[] = $this->color; } if ($this->size) { $classes[] = $this->size; } if ($this->dropdown) { $classes[] = 'dropdown'; } if ($this->icon) { $classes[] = 'has-icon'; } if ($this->iconFirst == false) { $classes[] = 'icon-last'; } if ($this->disabled) { $classes[] = 'disabled'; } $sigil = null; $meta = null; if ($this->tooltip) { Javelin::initBehavior('phabricator-tooltips'); require_celerity_resource('aphront-tooltip-css'); $sigil = 'has-tooltip'; $meta = array( 'tip' => $this->tooltip, ); } return array( 'class' => $classes, 'href' => $this->href, 'name' => $this->name, 'title' => $this->title, 'sigil' => $sigil, 'meta' => $meta, ); } protected function getTagContent() { $icon = null; $text = $this->text; if ($this->icon) { $icon = $this->icon; $subtext = null; if ($this->subtext) { $subtext = phutil_tag( 'div', array('class' => 'phui-button-subtext'), $this->subtext); } $text = phutil_tag( 'div', array('class' => 'phui-button-text'), array($text, $subtext)); } $caret = null; if ($this->dropdown) { $caret = phutil_tag('span', array('class' => 'caret'), ''); } if ($this->iconFirst == true) { return array($icon, $text, $caret); } else { return array($text, $icon); } } } diff --git a/src/view/phui/PHUIListItemView.php b/src/view/phui/PHUIListItemView.php index 33c511c9c0..4af02e572e 100644 --- a/src/view/phui/PHUIListItemView.php +++ b/src/view/phui/PHUIListItemView.php @@ -1,310 +1,307 @@ hideInApplicationMenu = $hide; return $this; } public function getHideInApplicationMenu() { return $this->hideInApplicationMenu; } public function setDropdownMenu(PhabricatorActionListView $actions) { Javelin::initBehavior('phui-dropdown-menu'); $this->addSigil('phui-dropdown-menu'); - $this->setMetadata( - array( - 'items' => $actions, - )); + $this->setMetadata($actions->getDropdownMenuMetadata()); return $this; } public function setAural($aural) { $this->aural = $aural; return $this; } public function getAural() { return $this->aural; } public function setOrder($order) { $this->order = $order; return $this; } public function getOrder() { return $this->order; } public function setRenderNameAsTooltip($render_name_as_tooltip) { $this->renderNameAsTooltip = $render_name_as_tooltip; return $this; } public function getRenderNameAsTooltip() { return $this->renderNameAsTooltip; } public function setSelected($selected) { $this->selected = $selected; return $this; } public function getSelected() { return $this->selected; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setProfileImage($image) { $this->profileImage = $image; return $this; } public function getIcon() { return $this->icon; } public function setIndented($indented) { $this->indented = $indented; return $this; } public function getIndented() { return $this->indented; } public function setKey($key) { $this->key = (string)$key; return $this; } public function getKey() { return $this->key; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setHref($href) { $this->href = $href; return $this; } public function getHref() { return $this->href; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setIsExternal($is_external) { $this->isExternal = $is_external; return $this; } public function getIsExternal() { return $this->isExternal; } public function setStatusColor($color) { $this->statusColor = $color; return $this; } public function addIcon($icon) { $this->icons[] = $icon; return $this; } public function getIcons() { return $this->icons; } protected function getTagName() { return 'li'; } protected function getTagAttributes() { $classes = array(); $classes[] = 'phui-list-item-view'; $classes[] = 'phui-list-item-'.$this->type; if ($this->icon) { $classes[] = 'phui-list-item-has-icon'; } if ($this->selected) { $classes[] = 'phui-list-item-selected'; } if ($this->disabled) { $classes[] = 'phui-list-item-disabled'; } if ($this->statusColor) { $classes[] = $this->statusColor; } return array( 'class' => $classes, ); } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } protected function getTagContent() { $name = null; $icon = null; $meta = null; $sigil = null; if ($this->name) { if ($this->getRenderNameAsTooltip()) { Javelin::initBehavior('phabricator-tooltips'); $sigil = 'has-tooltip'; $meta = array( 'tip' => $this->name, 'align' => 'E', ); } else { $external = null; if ($this->isExternal) { $external = " \xE2\x86\x97"; } // If this element has an aural representation, make any name visual // only. This is primarily dealing with the links in the main menu like // "Profile" and "Logout". If we don't hide the name, the mobile // version of these elements will have two redundant names. $classes = array(); $classes[] = 'phui-list-item-name'; if ($this->aural !== null) { $classes[] = 'visual-only'; } $name = phutil_tag( 'span', array( 'class' => implode(' ', $classes), ), array( $this->name, $external, )); } } $aural = null; if ($this->aural !== null) { $aural = javelin_tag( 'span', array( 'aural' => true, ), $this->aural); } if ($this->icon) { $icon_name = $this->icon; if ($this->getDisabled()) { $icon_name .= ' grey'; } $icon = id(new PHUIIconView()) ->addClass('phui-list-item-icon') ->setIcon($icon_name); } if ($this->profileImage) { $icon = id(new PHUIIconView()) ->setHeadSize(PHUIIconView::HEAD_SMALL) ->addClass('phui-list-item-icon') ->setImage($this->profileImage); } $classes = array(); if ($this->href) { $classes[] = 'phui-list-item-href'; } if ($this->indented) { $classes[] = 'phui-list-item-indented'; } $icons = $this->getIcons(); return javelin_tag( $this->href ? 'a' : 'div', array( 'href' => $this->href, 'class' => implode(' ', $classes), 'meta' => $meta, 'sigil' => $sigil, ), array( $aural, $icon, $icons, $this->renderChildren(), $name, )); } }