Skip to content

Commit

Permalink
Support for input type="text" and RTL
Browse files Browse the repository at this point in the history
  • Loading branch information
dandv committed May 2, 2014
1 parent 9cbee62 commit f78a49c
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 49 deletions.
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
# Textarea Caret Position

Get the `top` and `left` coordinates of a caret in a `<textarea>`, in pixels.
Useful for textarea autocompletes like GitHub, Twitter etc.
Get the `top` and `left` coordinates of the caret in a `<textarea>` or
`<input type="text">`, in pixels. Useful for textarea autocompletes like
GitHub or Twitter, or for single-line autocompletes like the name drop-down
in Facebook or the company dropdown on Google Finance.

How it's done: a faux `<div>` is created off-screen and styled exactly like the
textarea. Then, the text of the textarea up to the caret is copied into the div
and a `<span>` is inserted right after it. Then, the text content of the span is
set to the remainder of the text in the textarea, in order to faithfully
reproduce the wrapping in the faux div.
`textarea` or `input`. Then, the text of the element up to the caret is copied
into the `div` and a `<span>` is inserted right after it. Then, the text content
of the span is set to the remainder of the text in the textarea, in order to
faithfully reproduce the wrapping in the faux `div`. The same is done for the
`input` to simplify the code, though it makes no difference.

## Demo

**Check out the [JSFiddle](http://jsfiddle.net/dandv/aFPA7/).**

## Features

* supports `<textarea>`s and `<input type="text">' elements
* pixel precision
* RTL (right-to-left) support
* no dependencies whatsoever
* browser compatibility: Chrome, Safari, Firefox (despite [two](https://bugzilla.mozilla.org/show_bug.cgi?id=753662) [bugs](https://bugzilla.mozilla.org/show_bug.cgi?id=984275) it has), Opera, IE9+
* supports any font family and size, as well as text-transforms
* the text area can have arbitrary padding or borders
* not confused by horizontal or vertical scrollbars in the textarea
* supports hard returns, tabs (except on IE) and consecutive spaces in the text
* correct position on lines longer than the columns in the text area
* no ["ghost" position in the empty space](https://github.com/component/textarea-caret-position/blob/06d2197f85f96405b43724e56dc56f220c0092a5/test/position_off_after_wrapping_with_whitespace_before_EOL.gif) at the end of a line when wrapping long words
* [no problem](http://archive.today/F4XCV#13402035) getting the correct position when the input text is scrolled (i.e. the first visible character is no longer the first in the text)
* no ["ghost" position in the empty space](https://github.com/component/textarea-caret-position/blob/06d2197f85f96405b43724e56dc56f220c0092a5/test/position_off_after_wrapping_with_whitespace_before_EOL.gif) at the end of a line when wrapping long words in a `<textarea>`


## API
Expand All @@ -38,15 +44,15 @@ document.querySelector('textarea').addEventListener('input', function () {
})
```

### var coordinates = getCaretCoordinates(textarea, position)
### var coordinates = getCaretCoordinates(element, position)

`position` is a integer of the location of the caret. You basically pass `this.selectionStart` or `this.selectionEnd`. This way, this library isn't opinionated about what the caret is.

`coordinates` is an object of the form `{top: , left: }`.

## Known issues

* Tab characters aren't supported in IE9 (issue #14)
* Tab characters in `<textarea>`s aren't supported in IE9 (issue #14)

## Dependencies

Expand Down Expand Up @@ -81,8 +87,8 @@ Firefox 27

## Contributors

* Jonathan Ong ([jonathanong](https://github.com/jonathanong))
* Dan Dascalescu ([dandv](https://github.com/dandv))
* Jonathan Ong ([jonathanong](https://github.com/jonathanong))


## License
Expand Down
4 changes: 2 additions & 2 deletions component.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "textarea-caret-position",
"repo": "component/textarea-caret-position",
"version": "1.1.2",
"description": "(x, y) coordinates of a textarea's caret",
"version": "2.0.0",
"description": "(x, y) coordinates of the caret in a textarea or input type='text'",
"development": {
"component/assert": "*"
},
Expand Down
24 changes: 16 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// do not concatenate properties, i.e. padding-top, bottom etc. -> padding,
// so we have to do every single property specifically.
var properties = [
'direction', // RTL support
'boxSizing',
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
'height',
Expand All @@ -27,6 +28,7 @@ var properties = [
'fontWeight',
'fontStretch',
'fontSize',
'fontSizeAdjust',
'lineHeight',
'fontFamily',

Expand All @@ -40,45 +42,51 @@ var properties = [
];

var isFirefox = !(window.mozInnerScreenX == null);
module.exports = function (textarea, position, recalculate) {
// module.exports = function (textarea, position, recalculate) {
function getCaretCoordinates(element, position, recalculate) {
// mirrored div
var div = document.createElement('div');
div.id = 'textarea-caret-position-mirror-div';
div.id = 'input-textarea-caret-position-mirror-div';
document.body.appendChild(div);

var style = div.style;
var computed = window.getComputedStyle? getComputedStyle(textarea) : textarea.currentStyle; // currentStyle for IE < 9
var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9

// default textarea styles
style.whiteSpace = 'pre-wrap';
style.wordWrap = 'break-word';
if (element.nodeName !== 'INPUT')
style.wordWrap = 'break-word'; // only for textarea-s

// position off-screen
style.position = 'absolute'; // required to return coordinates properly
style.visibility = 'hidden'; // not 'display: none' because we want rendering

// transfer textarea properties to the div
// transfer the element's properties to the div
properties.forEach(function (prop) {
style[prop] = computed[prop];
});

if (isFirefox) {
style.width = parseInt(computed.width) - 2 + 'px' // Firefox adds 2 pixels to the padding - https://bugzilla.mozilla.org/show_bug.cgi?id=753662
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (textarea.scrollHeight > parseInt(computed.height))
if (element.scrollHeight > parseInt(computed.height))
style.overflowY = 'scroll';
} else {
style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}

div.textContent = textarea.value.substring(0, position);
div.textContent = element.value.substring(0, position);
// the second special handling for input type="text" vs textarea: spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if (element.nodeName === 'INPUT')
div.textContent = div.textContent.replace(/\s/g, "\u00a0");

var span = document.createElement('span');
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
span.textContent = textarea.value.substring(position);
// for inputs, just '.' would be enough, but why bother?
span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
div.appendChild(span);

var coordinates = {
Expand Down
2 changes: 1 addition & 1 deletion test/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
textarea {
input[type="text"], textarea {
font-family: 'Times New Roman'; /* a proportional font makes it more difficult to calculate the position */
font-size: 14px;
line-height: 16px;
Expand Down
64 changes: 36 additions & 28 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>textarea-caret-position testing ground</title>
<title>input and textarea caret-position testing ground</title>
<link rel="stylesheet" type="text/css" href="index.css">
<script src="../build/build.js"></script>
<script src="../index.js"></script>
</head>
<body>
<p>Click anywhere in the text to see a red vertical line &ndash; a 1-pixel-thick
<code>div</code> that should be positioned exactly at the location of the caret.</p>

<input type="text" size="15" maxlength="240" placeholder="Enter text here">

<hr/>

<textarea rows="25" cols="40">
I threw a wish in the well,
Don't ask me, I'll never tell
Expand All @@ -36,33 +41,36 @@
</textarea>

<script>
var textarea = document.querySelector('textarea');
var fontSize = getComputedStyle(textarea).getPropertyValue('font-size');
var getCaretCoordinates = require('textarea-caret-position');

var rect = document.createElement('div');
document.body.appendChild(rect);
rect.style.position = 'absolute';
rect.style.backgroundColor = 'red';
rect.style.height = fontSize;
rect.style.width = '1px';

['keyup', 'click', 'scroll'].forEach(function (event) {
textarea.addEventListener(event, update);
});

function update() {
var coordinates = getCaretCoordinates(textarea, textarea.selectionEnd);
console.log('(top, left) = (%s, %s)', coordinates.top, coordinates.left);
rect.style.top = textarea.offsetTop
- textarea.scrollTop
+ coordinates.top
+ 'px';
rect.style.left = textarea.offsetLeft
- textarea.scrollLeft
+ coordinates.left
+ 'px';
}
if (window.require) // if not in Component, allow the bare HTML demo to work
getCaretCoordinates = require('textarea-caret-position');
['input[type="text"]', 'textarea'].forEach(function (selector) {
var element = document.querySelector(selector);
var fontSize = getComputedStyle(element).getPropertyValue('font-size');

var rect = document.createElement('div');
document.body.appendChild(rect);
rect.style.position = 'absolute';
rect.style.backgroundColor = 'red';
rect.style.height = fontSize;
rect.style.width = '1px';

['keyup', 'click', 'scroll'].forEach(function (event) {
element.addEventListener(event, update);
});

function update() {
var coordinates = getCaretCoordinates(element, element.selectionEnd);
console.log('(top, left) = (%s, %s)', coordinates.top, coordinates.left);
rect.style.top = element.offsetTop
- element.scrollTop
+ coordinates.top
+ 'px';
rect.style.left = element.offsetLeft
- element.scrollLeft
+ coordinates.left
+ 'px';
}
});
</script>
</body>
</html>

3 comments on commit f78a49c

@dandv
Copy link
Member Author

@dandv dandv commented on f78a49c May 2, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonathanong: I deliberated quite a bit on splitting <input type="text"> functionality into a different component, but the changes vs. <textarea> support are so tiny that one library made more sense.

The name is now a little inexact.

@jonathanong
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1!

@jonathanong
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would rename this repo to just caret-position, but that would unnecessarily mess things up

Please sign in to comment.