@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.

Allow users to create inline comments by directly selecting text directly

Summary: Ref T13513. Support direct text selection for inlines. This is currently just an alternate way to get to the same place as using line numbers, but can preserve offset/range information in the future.

Test Plan:
- Selected some text, hit "c", clicked "New Inline Comment", got sensible comments on both sides of a diff in Safari, Chrome, and (with limitations) Firefox.
- Caveats: no unified support, doesn't work across lines in Firefox.

Maniphest Tasks: T13513

Differential Revision: https://secure.phabricator.com/D21248

+396 -44
+24 -24
resources/celerity/map.php
··· 10 10 'conpherence.pkg.css' => '0e3cf785', 11 11 'conpherence.pkg.js' => '020aebcf', 12 12 'core.pkg.css' => 'a560707d', 13 - 'core.pkg.js' => '1e667bcb', 13 + 'core.pkg.js' => '0efaf0ac', 14 14 'dark-console.pkg.js' => '187792c2', 15 15 'differential.pkg.css' => 'd71d4531', 16 - 'differential.pkg.js' => 'b3e29cb8', 16 + 'differential.pkg.js' => '06a7949c', 17 17 'diffusion.pkg.css' => '42c75c37', 18 18 'diffusion.pkg.js' => 'a98c0bf7', 19 19 'maniphest.pkg.css' => '35995d6d', ··· 380 380 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9', 381 381 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8', 382 382 'rsrc/js/application/diff/DiffChangeset.js' => '20715b98', 383 - 'rsrc/js/application/diff/DiffChangesetList.js' => '40d6c41c', 383 + 'rsrc/js/application/diff/DiffChangesetList.js' => '4a3639a1', 384 384 'rsrc/js/application/diff/DiffInline.js' => '6227a0e3', 385 385 'rsrc/js/application/diff/DiffPathView.js' => '8207abf9', 386 386 'rsrc/js/application/diff/DiffTreeView.js' => '5d83623b', ··· 488 488 'rsrc/js/core/behavior-linked-container.js' => '74446546', 489 489 'rsrc/js/core/behavior-more.js' => '506aa3f4', 490 490 'rsrc/js/core/behavior-object-selector.js' => '98ef467f', 491 - 'rsrc/js/core/behavior-oncopy.js' => 'ff7b3f22', 491 + 'rsrc/js/core/behavior-oncopy.js' => 'b475aae5', 492 492 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '54262396', 493 493 'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f', 494 494 'rsrc/js/core/behavior-redirect.js' => '407ee861', ··· 523 523 'rsrc/js/phuix/PHUIXActionView.js' => 'a8f573a9', 524 524 'rsrc/js/phuix/PHUIXAutocomplete.js' => '2fbe234d', 525 525 'rsrc/js/phuix/PHUIXButtonView.js' => '55a24e84', 526 - 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '7acfd98b', 526 + 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'b557770a', 527 527 'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7', 528 528 'rsrc/js/phuix/PHUIXFormControl.js' => '38c1f3fb', 529 529 'rsrc/js/phuix/PHUIXFormationColumnView.js' => '4bcc1f78', ··· 648 648 'javelin-behavior-phabricator-line-linker' => '590e6527', 649 649 'javelin-behavior-phabricator-notification-example' => '29819b75', 650 650 'javelin-behavior-phabricator-object-selector' => '98ef467f', 651 - 'javelin-behavior-phabricator-oncopy' => 'ff7b3f22', 651 + 'javelin-behavior-phabricator-oncopy' => 'b475aae5', 652 652 'javelin-behavior-phabricator-remarkup-assist' => '54262396', 653 653 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6', 654 654 'javelin-behavior-phabricator-search-typeahead' => '1cb7d027', ··· 775 775 'phabricator-darkmessage' => '26cd4b73', 776 776 'phabricator-dashboard-css' => '5a205b9d', 777 777 'phabricator-diff-changeset' => '20715b98', 778 - 'phabricator-diff-changeset-list' => '40d6c41c', 778 + 'phabricator-diff-changeset-list' => '4a3639a1', 779 779 'phabricator-diff-inline' => '6227a0e3', 780 780 'phabricator-diff-path-view' => '8207abf9', 781 781 'phabricator-diff-tree-view' => '5d83623b', ··· 886 886 'phuix-action-view' => 'a8f573a9', 887 887 'phuix-autocomplete' => '2fbe234d', 888 888 'phuix-button-view' => '55a24e84', 889 - 'phuix-dropdown-menu' => '7acfd98b', 889 + 'phuix-dropdown-menu' => 'b557770a', 890 890 'phuix-form-control-view' => '38c1f3fb', 891 891 'phuix-formation-column-view' => '4bcc1f78', 892 892 'phuix-formation-flank-view' => '6648270a', ··· 1259 1259 'javelin-behavior', 1260 1260 'javelin-uri', 1261 1261 ), 1262 - '40d6c41c' => array( 1263 - 'javelin-install', 1264 - 'phuix-button-view', 1265 - 'phabricator-diff-tree-view', 1266 - ), 1267 1262 '42c44e8b' => array( 1268 1263 'javelin-behavior', 1269 1264 'javelin-workflow', ··· 1332 1327 '490e2e2e' => array( 1333 1328 'phui-oi-list-view-css', 1334 1329 ), 1330 + '4a3639a1' => array( 1331 + 'javelin-install', 1332 + 'phuix-button-view', 1333 + 'phabricator-diff-tree-view', 1334 + ), 1335 1335 '4a7fb02b' => array( 1336 1336 'javelin-behavior', 1337 1337 'javelin-dom', ··· 1613 1613 '7930776a' => array( 1614 1614 'javelin-install', 1615 1615 'javelin-dom', 1616 - ), 1617 - '7acfd98b' => array( 1618 - 'javelin-install', 1619 - 'javelin-util', 1620 - 'javelin-dom', 1621 - 'javelin-vector', 1622 - 'javelin-stratcom', 1623 1616 ), 1624 1617 '7ad020a5' => array( 1625 1618 'javelin-behavior', ··· 1975 1968 'javelin-workboard-card-template', 1976 1969 'javelin-workboard-order-template', 1977 1970 ), 1971 + 'b475aae5' => array( 1972 + 'javelin-behavior', 1973 + 'javelin-dom', 1974 + ), 1978 1975 'b49fd60c' => array( 1979 1976 'multirow-row-manager', 1980 1977 'trigger-rule', 1981 1978 ), 1982 1979 'b517bfa0' => array( 1983 1980 'phui-oi-list-view-css', 1981 + ), 1982 + 'b557770a' => array( 1983 + 'javelin-install', 1984 + 'javelin-util', 1985 + 'javelin-dom', 1986 + 'javelin-vector', 1987 + 'javelin-stratcom', 1984 1988 ), 1985 1989 'b58d1a2a' => array( 1986 1990 'javelin-behavior', ··· 2202 2206 'ff688a7a' => array( 2203 2207 'owners-path-editor', 2204 2208 'javelin-behavior', 2205 - ), 2206 - 'ff7b3f22' => array( 2207 - 'javelin-behavior', 2208 - 'javelin-dom', 2209 2209 ), 2210 2210 ), 2211 2211 'packages' => array(
+8
src/applications/differential/view/DifferentialChangesetListView.php
··· 365 365 pht('Jump to the comment area.'), 366 366 367 367 'Show Changeset' => pht('Show Changeset'), 368 + 369 + 'You must select source text to create a new inline comment.' => 370 + pht('You must select source text to create a new inline comment.'), 371 + 372 + 'New Inline Comment' => pht('New Inline Comment'), 373 + 374 + 'Add new inline comment on selected source text.' => 375 + pht('Add new inline comment on selected source text.'), 368 376 ), 369 377 )); 370 378
+297 -1
webroot/rsrc/js/application/diff/DiffChangesetList.js
··· 59 59 null, 60 60 onrangeup); 61 61 62 + var onrange = JX.bind(this, this._ifawake, this._onSelectRange); 63 + JX.enableDispatch(window, 'selectionchange'); 64 + JX.Stratcom.listen('selectionchange', null, onrange); 65 + 62 66 this._setupInlineCommentListeners(); 63 67 }, 64 68 ··· 193 197 label = pht('Reply and quote selected inline comment.'); 194 198 this._installKey('R', 'inline', label, 195 199 JX.bind(this, this._onkeyreply, true)); 200 + 201 + label = pht('Add new inline comment on selected source text.'); 202 + this._installKey('c', 'inline', label, 203 + JX.bind(this, this._onKeyCreate)); 196 204 197 205 label = pht('Edit selected inline comment.'); 198 206 this._installKey('e', 'inline', label, this._onkeyedit); ··· 415 423 416 424 var pht = this.getTranslations(); 417 425 this._warnUser(pht('You must select a comment to edit.')); 426 + }, 427 + 428 + _onKeyCreate: function() { 429 + var start = this._sourceSelectionStart; 430 + var end = this._sourceSelectionEnd; 431 + 432 + if (!this._sourceSelectionStart) { 433 + var pht = this.getTranslations(); 434 + this._warnUser( 435 + pht( 436 + 'You must select source text to create a new inline comment.')); 437 + return; 438 + } 439 + 440 + this._setSourceSelection(null, null); 441 + 442 + var changeset = start.changeset; 443 + changeset.newInlineForRange(start.targetNode, end.targetNode); 418 444 }, 419 445 420 446 _onkeydone: function() { ··· 2197 2223 inline.activateMenu(node, e); 2198 2224 break; 2199 2225 } 2200 - } 2226 + }, 2227 + 2228 + _onSelectRange: function(e) { 2229 + this._updateSourceSelection(); 2230 + }, 2231 + 2232 + _updateSourceSelection: function() { 2233 + var ranges = this._getSelectedRanges(); 2234 + 2235 + // If we have zero or more than one range, don't do anything. 2236 + if (ranges.length === 1) { 2237 + for (var ii = 0; ii < ranges.length; ii++) { 2238 + var range = ranges[ii]; 2239 + 2240 + var head = range.startContainer; 2241 + var last = range.endContainer; 2201 2242 2243 + var head_loc = this._getFragmentLocation(head); 2244 + var last_loc = this._getFragmentLocation(last); 2245 + 2246 + if (head_loc === null || last_loc === null) { 2247 + break; 2248 + } 2249 + 2250 + if (head_loc.changesetID !== last_loc.changesetID) { 2251 + break; 2252 + } 2253 + 2254 + head_loc.offset += range.startOffset; 2255 + last_loc.offset += range.endOffset; 2256 + 2257 + this._setSourceSelection(head_loc, last_loc); 2258 + return; 2259 + } 2260 + } 2261 + 2262 + this._setSourceSelection(null, null); 2263 + }, 2264 + 2265 + _setSourceSelection: function(start, end) { 2266 + var start_updated = 2267 + !this._isSameSourceSelection(this._sourceSelectionStart, start); 2268 + 2269 + var end_updated = 2270 + !this._isSameSourceSelection(this._sourceSelectionEnd, end); 2271 + 2272 + if (!start_updated && !end_updated) { 2273 + return; 2274 + } 2275 + 2276 + this._sourceSelectionStart = start; 2277 + this._sourceSelectionEnd = end; 2278 + 2279 + if (!start) { 2280 + this._closeSourceSelectionMenu(); 2281 + return; 2282 + } 2283 + 2284 + var menu; 2285 + if (this._sourceSelectionMenu) { 2286 + menu = this._sourceSelectionMenu; 2287 + } else { 2288 + menu = this._newSourceSelectionMenu(); 2289 + this._sourceSelectionMenu = menu; 2290 + } 2291 + 2292 + var pos = JX.$V(start.node) 2293 + .add(0, -menu.getMenuNodeDimensions().y) 2294 + .add(0, -24); 2295 + 2296 + menu.setPosition(pos); 2297 + menu.open(); 2298 + }, 2299 + 2300 + _newSourceSelectionMenu: function() { 2301 + var pht = this.getTranslations(); 2302 + 2303 + var menu = new JX.PHUIXDropdownMenu(null) 2304 + .setWidth(240); 2305 + 2306 + // We need to disable autofocus for this menu, since it operates on the 2307 + // text selection in the document. If we leave this enabled, opening the 2308 + // menu immediately discards the selection. 2309 + menu.setDisableAutofocus(true); 2310 + 2311 + var list = new JX.PHUIXActionListView(); 2312 + menu.setContent(list.getNode()); 2313 + 2314 + var oncreate = JX.bind(this, this._onSourceSelectionMenuAction, 'create'); 2315 + 2316 + var comment_item = new JX.PHUIXActionView() 2317 + .setIcon('fa-comment-o') 2318 + .setName(pht('New Inline Comment')) 2319 + .setKeyCommand('c') 2320 + .setHandler(oncreate); 2321 + 2322 + list.addItem(comment_item); 2323 + 2324 + return menu; 2325 + }, 2326 + 2327 + _onSourceSelectionMenuAction: function(action, e) { 2328 + e.kill(); 2329 + this._closeSourceSelectionMenu(); 2330 + 2331 + switch (action) { 2332 + case 'create': 2333 + this._onKeyCreate(); 2334 + break; 2335 + } 2336 + }, 2337 + 2338 + _closeSourceSelectionMenu: function() { 2339 + if (this._sourceSelectionMenu) { 2340 + this._sourceSelectionMenu.close(); 2341 + } 2342 + }, 2343 + 2344 + _isSameSourceSelection: function(u, v) { 2345 + if (u === null && v === null) { 2346 + return true; 2347 + } 2348 + 2349 + if (u === null && v !== null) { 2350 + return false; 2351 + } 2352 + 2353 + if (u !== null && v === null) { 2354 + return false; 2355 + } 2356 + 2357 + return ( 2358 + (u.changesetID === v.changesetID) && 2359 + (u.line === v.line) && 2360 + (u.displayColumn === v.displayColumn) && 2361 + (u.offset === v.offset) 2362 + ); 2363 + }, 2364 + 2365 + _getFragmentLocation: function(fragment) { 2366 + // Find the changeset containing the fragment. 2367 + var changeset = null; 2368 + try { 2369 + var node = JX.DOM.findAbove( 2370 + fragment, 2371 + 'div', 2372 + 'differential-changeset'); 2373 + 2374 + changeset = this.getChangesetForNode(node); 2375 + if (!changeset) { 2376 + return null; 2377 + } 2378 + } catch (ex) { 2379 + return null; 2380 + } 2381 + 2382 + // Find the line number and display column for the fragment. 2383 + var line = null; 2384 + var column_count = -1; 2385 + var offset = null; 2386 + var target_node = null; 2387 + var td; 2388 + try { 2389 + 2390 + // NOTE: In Safari, you can carefully select an entire line and then 2391 + // move your mouse down slightly, causing selection of an empty 2392 + // document fragment which is an immediate child of the next "<tr />". 2393 + 2394 + // If the fragment is a direct child of a "<tr />" parent, assume the 2395 + // user has done this and select the last child of the previous row 2396 + // instead. It's possible there are other ways to do this, so this may 2397 + // not always be the right rule. 2398 + 2399 + // Otherwise, select the containing "<td />". 2400 + 2401 + var is_end; 2402 + if (JX.DOM.isType(fragment.parentNode, 'tr')) { 2403 + // Assume this is Safari, and that the user has carefully selected a 2404 + // row and then moved their mouse down a few pixels to select the 2405 + // invisible fragment at the beginning of the next row. 2406 + var cells = fragment.parentNode.previousSibling.childNodes; 2407 + td = cells[cells.length - 1]; 2408 + is_end = true; 2409 + } else { 2410 + td = JX.DOM.findAbove(fragment, 'td'); 2411 + is_end = false; 2412 + } 2413 + 2414 + var cursor = td; 2415 + while (cursor) { 2416 + if (cursor.getAttribute('data-copy-mode')) { 2417 + column_count++; 2418 + } 2419 + 2420 + var n = parseInt(cursor.getAttribute('data-n')); 2421 + 2422 + if (n) { 2423 + if (line === null) { 2424 + target_node = cursor; 2425 + line = n; 2426 + } 2427 + } 2428 + 2429 + cursor = cursor.previousSibling; 2430 + } 2431 + 2432 + if (!line) { 2433 + return null; 2434 + } 2435 + 2436 + if (column_count < 0) { 2437 + return null; 2438 + } 2439 + 2440 + var seen = 0; 2441 + for (var ii = 0; ii < td.childNodes.length; ii++) { 2442 + var child = td.childNodes[ii]; 2443 + if (child === fragment) { 2444 + offset = seen; 2445 + break; 2446 + } 2447 + seen += child.textContent.length; 2448 + } 2449 + 2450 + if (offset === null) { 2451 + if (is_end) { 2452 + offset = seen; 2453 + } else { 2454 + offset = 0; 2455 + } 2456 + } 2457 + } catch (ex) { 2458 + return null; 2459 + } 2460 + 2461 + var changeset_id; 2462 + if (column_count > 0) { 2463 + changeset_id = changeset.getRightChangesetID(); 2464 + } else { 2465 + changeset_id = changeset.getLeftChangesetID(); 2466 + } 2467 + 2468 + return { 2469 + node: td, 2470 + changeset: changeset, 2471 + changesetID: changeset_id, 2472 + line: line, 2473 + displayColumn: column_count, 2474 + offset: offset, 2475 + targetNode: target_node 2476 + }; 2477 + }, 2478 + 2479 + _getSelectedRanges: function() { 2480 + var ranges = []; 2481 + 2482 + if (!window.getSelection) { 2483 + return ranges; 2484 + } 2485 + 2486 + var selection = window.getSelection(); 2487 + for (var ii = 0; ii < selection.rangeCount; ii++) { 2488 + var range = selection.getRangeAt(ii); 2489 + if (range.collapsed) { 2490 + continue; 2491 + } 2492 + 2493 + ranges.push(range); 2494 + } 2495 + 2496 + return ranges; 2497 + } 2202 2498 } 2203 2499 2204 2500 });
+1 -1
webroot/rsrc/js/core/behavior-oncopy.js
··· 119 119 120 120 // When the selection range changes, apply CSS classes if the selection is 121 121 // nonempty. We don't want to make visual changes to the document immediately 122 - // when the user press the mouse button, since we aren't yet sure that 122 + // when the user presses the mouse button, since we aren't yet sure that 123 123 // they are starting a selection: instead, wait for them to actually select 124 124 // something. 125 125 function onchangeselect() {
+66 -18
webroot/rsrc/js/phuix/PHUIXDropdownMenu.js
··· 21 21 construct : function(node) { 22 22 this._node = node; 23 23 24 - JX.DOM.listen( 25 - this._node, 26 - 'click', 27 - null, 28 - JX.bind(this, this._onclick)); 24 + if (node) { 25 + JX.DOM.listen( 26 + this._node, 27 + 'click', 28 + null, 29 + JX.bind(this, this._onclick)); 30 + } 29 31 30 32 JX.Stratcom.listen( 31 33 'mousedown', ··· 54 56 width: null, 55 57 align: 'right', 56 58 offsetX: 0, 57 - offsetY: 0 59 + offsetY: 0, 60 + disableAutofocus: false 58 61 }, 59 62 60 63 members: { ··· 62 65 _menu: null, 63 66 _open: false, 64 67 _content: null, 68 + _position: null, 69 + _visible: false, 65 70 66 71 setContent: function(content) { 67 72 JX.DOM.setContent(this._getMenuNode(), content); ··· 94 99 return this; 95 100 }, 96 101 102 + setPosition: function(pos) { 103 + this._position = pos; 104 + this._setMenuNodePosition(pos); 105 + return this; 106 + }, 107 + 97 108 _getMenuNode: function() { 98 109 if (!this._menu) { 99 110 var attrs = { ··· 161 172 }, 162 173 163 174 _show : function() { 164 - document.body.appendChild(this._menu); 175 + if (!this._visible) { 176 + this._visible = true; 177 + document.body.appendChild(this._menu); 178 + } 165 179 166 180 if (this.getWidth()) { 167 181 new JX.Vector(this.getWidth(), null).setDim(this._menu); ··· 169 183 170 184 this._adjustposition(); 171 185 172 - JX.DOM.alterClass(this._node, 'phuix-dropdown-open', true); 173 - 174 - this._node.setAttribute('aria-expanded', 'true'); 186 + if (this._node) { 187 + JX.DOM.alterClass(this._node, 'phuix-dropdown-open', true); 188 + this._node.setAttribute('aria-expanded', 'true'); 189 + } 175 190 176 191 // Try to highlight the first link in the menu for assistive technologies. 177 - var links = JX.DOM.scry(this._menu, 'a'); 178 - if (links[0]) { 179 - JX.DOM.focus(links[0]); 192 + if (!this.getDisableAutofocus()) { 193 + var links = JX.DOM.scry(this._menu, 'a'); 194 + if (links[0]) { 195 + JX.DOM.focus(links[0]); 196 + } 180 197 } 181 198 }, 182 199 183 200 _hide : function() { 201 + this._visible = false; 184 202 JX.DOM.remove(this._menu); 185 203 186 - JX.DOM.alterClass(this._node, 'phuix-dropdown-open', false); 187 - 188 - this._node.setAttribute('aria-expanded', 'false'); 204 + if (this._node) { 205 + JX.DOM.alterClass(this._node, 'phuix-dropdown-open', false); 206 + this._node.setAttribute('aria-expanded', 'false'); 207 + } 189 208 }, 190 209 191 210 _adjustposition : function() { 192 211 if (!this._open) { 212 + return; 213 + } 214 + 215 + if (this._position) { 216 + this._setMenuNodePosition(this._position); 217 + return; 218 + } 219 + 220 + if (!this._node) { 193 221 return; 194 222 } 195 223 ··· 236 264 break; 237 265 } 238 266 239 - v = v.add(this.getOffsetX(), this.getOffsetY()); 267 + this._setMenuNodePosition(v); 268 + }, 240 269 270 + _setMenuNodePosition: function(v) { 271 + v = v.add(this.getOffsetX(), this.getOffsetY()); 241 272 v.setPos(this._menu); 242 273 }, 243 274 275 + getMenuNodeDimensions: function() { 276 + if (!this._visible) { 277 + document.body.appendChild(this._menu); 278 + } 279 + 280 + var dim = JX.Vector.getDim(this._menu); 281 + 282 + if (!this._visible) { 283 + JX.DOM.remove(this._menu); 284 + } 285 + 286 + return dim; 287 + }, 288 + 244 289 _onkey: function(e) { 245 290 // When the user presses escape with a menu open, close the menu and 246 291 // refocus the button which activates the menu. In particular, this makes ··· 255 300 } 256 301 257 302 this.close(); 258 - JX.DOM.focus(this._node); 303 + 304 + if (this._node) { 305 + JX.DOM.focus(this._node); 306 + } 259 307 260 308 e.prevent(); 261 309 }