diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -339,11 +339,29 @@ border-top-color: {$thinblueborder}; border-radius: 0; + box-shadow: none; + -webkit-box-shadow: none; + + /* Set line height explicitly so the metrics and the real textarea + are forced to the same value. */ + line-height: 1.25em; + /* Prevent Safari and Chrome users from dragging the textarea any wider, because the top bar won't resize along with it. */ resize: vertical; } +var.remarkup-assist-textarea { + /* This is an invisible element used to measure the size of text in the + textarea so we can float typeaheads over the cursor position. */ + display: block; + border-color: orange; + box-sizing: border-box; + padding: 4px 6px; + white-space: pre-wrap; + visibility: hidden; +} + .remarkup-assist-textarea:focus { border: 1px solid rgba(82, 168, 236, 0.8); } @@ -424,11 +442,6 @@ opacity: 1.0; } -.remarkup-assist-textarea { - box-shadow: none; - -webkit-box-shadow: none; -} - .remarkup-control-fullscreen-mode { position: fixed; top: -1px; diff --git a/webroot/rsrc/js/core/TextAreaUtils.js b/webroot/rsrc/js/core/TextAreaUtils.js --- a/webroot/rsrc/js/core/TextAreaUtils.js +++ b/webroot/rsrc/js/core/TextAreaUtils.js @@ -1,5 +1,7 @@ /** * @requires javelin-install + * javelin-dom + * javelin-vector * @provides phabricator-textareautils * @javelin */ @@ -44,6 +46,62 @@ area.value = v; JX.TextAreaUtils.setSelectionRange(area, r.start, r.start + text.length); + }, + + /** + * Get the document pixel positions of the beginning and end of a character + * range in a textarea. + */ + getPixelDimensions: function(area, start, end) { + var v = area.value; + + // We're using zero-width spaces to make sure the spans get some + // height even if there's no text in the metrics tag. + + var head = v.substring(0, start); + var before = JX.$N('span', {}, '\u200b'); + var body = v.substring(start, end); + var after = JX.$N('span', {}, '\u200b'); + + // Create a similar shadow element which we can measure. + var metrics = JX.$N( + 'var', + { + className: area.className, + }, + [head, before, body, after]); + + // If the textarea has a scrollbar, force a scrollbar on the shadow + // element too. + if (area.scrollHeight > area.clientHeight) { + metrics.style.overflowY = 'scroll'; + } + + area.parentNode.appendChild(metrics); + + // Adjust the positions we read out of the document to account for the + // current scroll position of the textarea. + var metrics_pos = JX.Vector.getPos(metrics); + metrics_pos.x += area.scrollLeft; + metrics_pos.y += area.scrollTop; + + var area_pos = JX.Vector.getPos(area); + var before_pos = JX.Vector.getPos(before); + var after_pos = JX.Vector.getPos(after); + + JX.DOM.remove(metrics); + + return { + start: { + x: area_pos.x + (before_pos.x - metrics_pos.x), + y: area_pos.y + (before_pos.y - metrics_pos.y) + }, + end: { + x: area_pos.x + (after_pos.x - metrics_pos.x), + y: area_pos.y + (after_pos.y - metrics_pos.y) + } + }; } + } });