diff --git a/src/infrastructure/celerity/CelerityResourceMap.php b/src/infrastructure/celerity/CelerityResourceMap.php index c9beffc2d3..030fd3233d 100644 --- a/src/infrastructure/celerity/CelerityResourceMap.php +++ b/src/infrastructure/celerity/CelerityResourceMap.php @@ -1,243 +1,243 @@ resources = $resources; $map = $resources->loadMap(); $this->symbolMap = idx($map, 'symbols', array()); $this->requiresMap = idx($map, 'requires', array()); $this->packageMap = idx($map, 'packages', array()); $this->nameMap = idx($map, 'names', array()); // We derive these reverse maps at runtime. $this->hashMap = array_flip($this->nameMap); $this->componentMap = array(); foreach ($this->packageMap as $package_name => $symbols) { foreach ($symbols as $symbol) { $this->componentMap[$symbol] = $package_name; } } } public static function getNamedInstance($name) { if (empty(self::$instances[$name])) { $resources_list = CelerityPhysicalResources::getAll(); if (empty($resources_list[$name])) { throw new Exception( pht( 'No resource source exists with name "%s"!', $name)); } $instance = new CelerityResourceMap($resources_list[$name]); self::$instances[$name] = $instance; } return self::$instances[$name]; } public function getPackagedNamesForSymbols(array $symbols) { $resolved = $this->resolveResources($symbols); return $this->packageResources($resolved); } private function resolveResources(array $symbols) { $map = array(); foreach ($symbols as $symbol) { if (!empty($map[$symbol])) { continue; } $this->resolveResource($map, $symbol); } return $map; } private function resolveResource(array &$map, $symbol) { if (empty($this->symbolMap[$symbol])) { throw new Exception( pht( 'Attempting to resolve unknown resource, "%s".', $symbol)); } $hash = $this->symbolMap[$symbol]; $map[$symbol] = $hash; if (isset($this->requiresMap[$hash])) { $requires = $this->requiresMap[$hash]; } else { $requires = array(); } foreach ($requires as $required_symbol) { if (!empty($map[$required_symbol])) { continue; } $this->resolveResource($map, $required_symbol); } } private function packageResources(array $resolved_map) { $packaged = array(); $handled = array(); foreach ($resolved_map as $symbol => $hash) { if (isset($handled[$symbol])) { continue; } if (empty($this->componentMap[$symbol])) { $packaged[] = $this->hashMap[$hash]; } else { $package_name = $this->componentMap[$symbol]; $packaged[] = $package_name; $package_symbols = $this->packageMap[$package_name]; foreach ($package_symbols as $package_symbol) { $handled[$package_symbol] = true; } } } return $packaged; } public function getResourceDataForName($resource_name) { return $this->resources->getResourceData($resource_name); } public function getResourceNamesForPackageName($package_name) { $package_symbols = idx($this->packageMap, $package_name); if (!$package_symbols) { return null; } $resource_names = array(); foreach ($package_symbols as $symbol) { $resource_names[] = $this->hashMap[$this->symbolMap[$symbol]]; } return $resource_names; } /** * Get the epoch timestamp of the last modification time of a symbol. * * @param string Resource symbol to lookup. * @return int Epoch timestamp of last resource modification. */ public function getModifiedTimeForName($name) { if ($this->isPackageResource($name)) { $names = array(); foreach ($this->packageMap[$name] as $symbol) { $names[] = $this->getResourceNameForSymbol($symbol); } } else { $names = array($name); } $mtime = 0; foreach ($names as $name) { $mtime = max($mtime, $this->resources->getResourceModifiedTime($name)); } return $mtime; } /** * Return the absolute URI for the resource associated with a symbol. This * method is fairly low-level and ignores packaging. * * @param string Resource symbol to lookup. * @return string|null Resource URI, or null if the symbol is unknown. */ public function getURIForSymbol($symbol) { $hash = idx($this->symbolMap, $symbol); return $this->getURIForHash($hash); } /** * Return the absolute URI for the resource associated with a resource name. * This method is fairly low-level and ignores packaging. * * @param string Resource name to lookup. * @return string|null Resource URI, or null if the name is unknown. */ public function getURIForName($name) { $hash = idx($this->nameMap, $name); return $this->getURIForHash($hash); } /** * Return the absolute URI for a resource, identified by hash. * This method is fairly low-level and ignores packaging. * * @param string Resource hash to lookup. * @return string|null Resource URI, or null if the hash is unknown. */ private function getURIForHash($hash) { if ($hash === null) { return null; } return $this->resources->getResourceURI($hash, $this->hashMap[$hash]); } /** * Return the resource symbols required by a named resource. * * @param string Resource name to lookup. * @return list|null List of required symbols, or null if the name * is unknown. */ public function getRequiredSymbolsForName($name) { - $hash = idx($this->symbolMap, $name); + $hash = idx($this->nameMap, $name); if ($hash === null) { return null; } return idx($this->requiresMap, $hash, array()); } /** * Return the resource name for a given symbol. * * @param string Resource symbol to lookup. * @return string|null Resource name, or null if the symbol is unknown. */ public function getResourceNameForSymbol($symbol) { $hash = idx($this->symbolMap, $symbol); return idx($this->hashMap, $hash); } public function isPackageResource($name) { return isset($this->packageMap[$name]); } public function getResourceTypeForName($name) { return $this->resources->getResourceType($name); } } diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php index b979c9e87b..4c73e10d8e 100644 --- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php +++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php @@ -1,265 +1,265 @@ symbolsBinary === null) { list($err, $stdout) = exec_manual('which javelinsymbols'); $this->symbolsBinary = ($err ? false : rtrim($stdout)); } return $this->symbolsBinary; } public function willLintPaths(array $paths) { if (!$this->getBinaryPath()) { return; } $root = dirname(phutil_get_library_root('phabricator')); require_once $root.'/scripts/__init_script__.php'; $futures = array(); foreach ($paths as $path) { if ($this->shouldIgnorePath($path)) { continue; } $future = $this->newSymbolsFuture($path); $futures[$path] = $future; } foreach (Futures($futures)->limit(8) as $path => $future) { $this->symbols[$path] = $future->resolvex(); } } public function getLinterName() { return 'JAVELIN'; } public function getLintSeverityMap() { return array( self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING, ); } public function getLintNameMap() { return array( self::LINT_PRIVATE_ACCESS => 'Private Method/Member Access', self::LINT_MISSING_DEPENDENCY => 'Missing Javelin Dependency', self::LINT_UNNECESSARY_DEPENDENCY => 'Unnecessary Javelin Dependency', self::LINT_UNKNOWN_DEPENDENCY => 'Unknown Javelin Dependency', self::LINT_MISSING_BINARY => '`javelinsymbols` Not In Path', ); } public function getCacheGranularity() { return ArcanistLinter::GRANULARITY_REPOSITORY; } public function getCacheVersion() { $version = '0'; $binary_path = $this->getBinaryPath(); if ($binary_path) { $version .= '-'.md5_file($binary_path); } return $version; } private function shouldIgnorePath($path) { return preg_match('@/__tests__/|externals/javelin/docs/@', $path); } public function lintPath($path) { if ($this->shouldIgnorePath($path)) { return; } if (!$this->symbolsBinary) { if (!$this->haveWarnedAboutBinary) { $this->haveWarnedAboutBinary = true; // TODO: Write build documentation for the Javelin binaries and point // the user at it. $this->raiseLintAtLine( 1, 0, self::LINT_MISSING_BINARY, "The 'javelinsymbols' binary in the Javelin project is not ". "available in \$PATH, so the Javelin linter can't run. This ". "isn't a big concern, but means some Javelin problems can't be ". "automatically detected."); } return; } list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path); foreach ($uses as $symbol => $line) { $parts = explode('.', $symbol); foreach ($parts as $part) { if ($part[0] == '_' && $part[1] != '_') { $base = implode('.', array_slice($parts, 0, 2)); if (!array_key_exists($base, $installs)) { $this->raiseLintAtLine( $line, 0, self::LINT_PRIVATE_ACCESS, "This file accesses private symbol '{$symbol}' across file ". "boundaries. You may only access private members and methods ". "from the file where they are defined."); } break; } } } if ($this->getEngine()->getCommitHookMode()) { // Don't do the dependency checks in commit-hook mode because we won't // have an available working copy. return; } $external_classes = array(); foreach ($uses as $symbol => $line) { $parts = explode('.', $symbol); $class = implode('.', array_slice($parts, 0, 2)); if (!array_key_exists($class, $external_classes) && !array_key_exists($class, $installs)) { $external_classes[$class] = $line; } } $celerity = CelerityResourceMap::getNamedInstance('phabricator'); $path = preg_replace( '@^externals/javelinjs/src/@', 'webroot/rsrc/js/javelin/', $path); $need = $external_classes; - $resource_name = substr($path, strlen('webroot')); + $resource_name = substr($path, strlen('webroot/')); $requires = $celerity->getRequiredSymbolsForName($resource_name); if (!$requires) { $requires = array(); } foreach ($requires as $key => $requires_symbol) { $requires_name = $celerity->getResourceNameForSymbol($requires_symbol); if ($requires_name === null) { $this->raiseLintAtLine( 0, 0, self::LINT_UNKNOWN_DEPENDENCY, "This file @requires component '{$requires_symbol}', but it does ". "not exist. You may need to rebuild the Celerity map."); unset($requires[$key]); continue; } if (preg_match('/\\.css$/', $requires_name)) { // If JS requires CSS, just assume everything is fine. unset($requires[$key]); } else { - $symbol_path = 'webroot'.$requires_name; + $symbol_path = 'webroot/'.$requires_name; list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath( $symbol_path); if (array_intersect_key($req_install, $external_classes)) { $need = array_diff_key($need, $req_install); unset($requires[$key]); } } } foreach ($need as $class => $line) { $this->raiseLintAtLine( $line, 0, self::LINT_MISSING_DEPENDENCY, "This file uses '{$class}' but does not @requires the component ". "which installs it. You may need to rebuild the Celerity map."); } foreach ($requires as $component) { $this->raiseLintAtLine( 0, 0, self::LINT_UNNECESSARY_DEPENDENCY, "This file @requires component '{$component}' but does not use ". "anything it provides."); } } private function loadSymbols($path) { if (empty($this->symbols[$path])) { $this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex(); } return $this->symbols[$path]; } private function newSymbolsFuture($path) { $future = new ExecFuture('javelinsymbols # %s', $path); $future->write($this->getData($path)); return $future; } private function getUsedAndInstalledSymbolsForPath($path) { list($symbols) = $this->loadSymbols($path); $symbols = trim($symbols); $uses = array(); $installs = array(); if (empty($symbols)) { // This file has no symbols. return array($uses, $installs); } $symbols = explode("\n", trim($symbols)); foreach ($symbols as $line) { $matches = null; if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) { throw new Exception( "Received malformed output from `javelinsymbols`."); } $type = $matches[1]; $symbol = $matches[2]; $line = $matches[3]; switch ($type) { case '?': $uses[$symbol] = $line; break; case '+': $installs['JX.'.$symbol] = $line; break; } } $contents = $this->getData($path); $matches = null; $count = preg_match_all( '/@javelin-installs\W+(\S+)/', $contents, $matches, PREG_PATTERN_ORDER); if ($count) { foreach ($matches[1] as $symbol) { $installs[$symbol] = 0; } } return array($uses, $installs); } } diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 87037f1df7..56be0790ce 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -1,299 +1,308 @@ /** * @provides phabricator-draggable-list * @requires javelin-install * javelin-dom * javelin-stratcom * javelin-util * javelin-vector * javelin-magical-init * @javelin */ JX.install('DraggableList', { construct : function(sigil, root) { this._sigil = sigil; this._root = root || document.body; // NOTE: Javelin does not dispatch mousemove by default. JX.enableDispatch(document.body, 'mousemove'); JX.DOM.listen(this._root, 'mousedown', sigil, JX.bind(this, this._ondrag)); JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove)); JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop)); }, events : [ 'didLock', 'didUnlock', 'shouldBeginDrag', 'didBeginDrag', 'didCancelDrag', 'didEndDrag', 'didDrop'], properties : { findItemsHandler : null }, members : { _root : null, _dragging : null, _locked : 0, _origin : null, _target : null, _targets : null, _dimensions : null, _ghostHandler : null, _ghostNode : null, setGhostHandler : function(handler) { this._ghostHandler = handler; return this; }, getGhostHandler : function() { return this._ghostHandler || JX.bind(this, this._defaultGhostHandler); }, getGhostNode : function() { if (!this._ghostNode) { this._ghostNode = JX.$N('li', {className: 'drag-ghost'}); } return this._ghostNode; }, setGhostNode : function(node) { this._ghostNode = node; return this; }, _defaultGhostHandler : function(ghost, target) { var parent = this._dragging.parentNode; if (target && target.nextSibling) { parent.insertBefore(ghost, target.nextSibling); } else if (!target && parent.firstChild) { parent.insertBefore(ghost, parent.firstChild); } else { parent.appendChild(ghost); } }, findItems : function() { var handler = this.getFindItemsHandler(); if (__DEV__) { if (!handler) { JX.$E('JX.Draggable.findItems(): No findItemsHandler set!'); } } return handler(); }, _ondrag : function(e) { if (this._dragging) { // Don't start dragging if we're already dragging something. return; } if (this._locked) { // Don't start drag operations while locked. return; } if (!e.isNormalMouseEvent()) { // Don't start dragging for shift click, right click, etc. return; } if (this.invoke('shouldBeginDrag', e).getPrevented()) { return; } e.kill(); this._dragging = e.getNode(this._sigil); this._origin = JX.$V(e); this._dimensions = JX.$V(this._dragging); var targets = []; var items = this.findItems(); for (var ii = 0; ii < items.length; ii++) { targets.push({ item: items[ii], y: JX.$V(items[ii]).y + (JX.Vector.getDim(items[ii]).y / 2) }); } targets.sort(function(u, v) { return v.y - u.y; }); this._targets = targets; this._target = false; if (!this.invoke('didBeginDrag', this._dragging).getPrevented()) { var ghost = this.getGhostNode(); ghost.style.height = JX.Vector.getDim(this._dragging).y + 'px'; JX.DOM.alterClass(this._dragging, 'drag-dragging', true); } }, _onmove : function(e) { if (!this._dragging) { return; } var ghost = this.getGhostNode(); var target = this._target; var targets = this._targets; var dragging = this._dragging; var origin = this._origin; var p = JX.$V(e); // Compute the size and position of the drop target indicator, because we // need to update our static position computations to account for it. var adjust_h = JX.Vector.getDim(ghost).y; var adjust_y = JX.$V(ghost).y; // Find the node we're dragging the object underneath. This is the first // node in the list that's above the cursor. If that node is the node // we're dragging or its predecessor, don't select a target, because the // operation would be a no-op. // NOTE: When we're dragging into the first position in the list, we // use the target `null`. When we don't have a valid target, we use // the target `false`. Spooky! Magic! Anyway, `null` and `false` mean // completely different things. var cur_target = null; var trigger; for (var ii = 0; ii < targets.length; ii++) { // If the drop target indicator is above the target, we need to adjust // the target's trigger height down accordingly. This makes dragging // items down the list smoother, because the target doesn't jump to the // next item while the cursor is over it. trigger = targets[ii].y; if (adjust_y <= trigger) { trigger += adjust_h; } // If the cursor is above this target, we aren't dropping underneath it. if (trigger >= p.y) { continue; } // Don't choose the dragged row or its predecessor as targets. cur_target = targets[ii].item; if (cur_target == dragging) { cur_target = false; } if (targets[ii - 1] && targets[ii - 1].item == dragging) { cur_target = false; } break; } + // If the dragged row is the first row, don't allow it to be dragged + // into the first position, since this operation doesn't make sense. + if (cur_target === null) { + var first_item = targets[targets.length - 1].item; + if (dragging === first_item) { + cur_target = false; + } + } + // If we've selected a new target, update the UI to show where we're // going to drop the row. - if (cur_target != target) { + if (cur_target !== target) { - if (target) { + if (target !== false) { JX.DOM.remove(ghost); } if (cur_target !== false) { var ok = this.getGhostHandler()(ghost, cur_target); // If the handler returns explicit `false`, prevent the drag. if (ok === false) { cur_target = false; } } target = cur_target; if (target !== false) { // If we've changed where the ghost node is, update the adjustments // so we accurately reflect document state when we tweak things below. // This avoids a flash of bad state as the mouse is dragged upward // across the document. adjust_h = JX.Vector.getDim(ghost).y; adjust_y = JX.$V(ghost).y; } } // If the drop target indicator is above the cursor in the document, // adjust the cursor position for the change in node document position. // Do this before choosing a new target to avoid a flash of nonsense. if (target !== false) { if (adjust_y <= origin.y) { p.y -= adjust_h; } } p.x = 0; p.y -= origin.y; p.setPos(dragging); this._target = target; e.kill(); }, _ondrop : function(e) { if (!this._dragging) { return; } var target = this._target; var dragging = this._dragging; var ghost = this.getGhostNode(); this._dragging = null; JX.$V(0, 0).setPos(dragging); if (target !== false) { JX.DOM.remove(dragging); JX.DOM.replace(ghost, dragging); this.invoke('didDrop', dragging, target); } else { this.invoke('didCancelDrag', dragging); } if (!this.invoke('didEndDrag', dragging).getPrevented()) { JX.DOM.alterClass(dragging, 'drag-dragging', false); } e.kill(); }, lock : function() { this._locked++; if (this._locked === 1) { this.invoke('didLock'); } return this; }, unlock : function() { if (__DEV__) { if (!this._locked) { JX.$E("JX.Draggable.unlock(): Draggable is not locked!"); } } this._locked--; if (!this._locked) { this.invoke('didUnlock'); } return this; } } });