From fe042def42b0788e16c9f0f5292b566b3fc2a0b1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Aug 2014 13:15:40 -0700 Subject: [PATCH] Add a Javascript method to find the pixel position of a range in a textarea Summary: Ref T3725. This might eventually allow us to do `@username` typeaheads in textareas. Javascript!!! Test Plan: Dumped this into console and got a "<<<" at the caret position in Safari, Firefox and Chrome. ``` setInterval(function() { var area = JX.$('comment-content'); var r = JX.TextAreaUtils.getSelectionRange(area); var d = JX.TextAreaUtils.getPixelDimensions(area, r.start, r.end); JX.log(d); try { JX.DOM.remove(JX.$('ptr')); } catch (_) {} document.body.appendChild( JX.$N( 'div', {id: "ptr", style: { position: 'absolute', left: d.start.x + 'px', top: d.start.y + 'px', zIndex: 9999, border: '2px solid red' }}, '<<<')); }, 1000); ``` Reviewers: chad, btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T3725 Differential Revision: https://secure.phabricator.com/D10280 --- webroot/rsrc/css/core/remarkup.css | 23 ++++++++--- webroot/rsrc/js/core/TextAreaUtils.js | 58 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index 06fefd9cbb..c6ae372c04 100644 --- 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 index 9beac8b1d0..b96099e0e5 100644 --- 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 @@ JX.install('TextAreaUtils', { 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) + } + }; } + } });