What is this?
- This is a user interface mockup aimed at a very specific problem: when I hold backspace on iOS, text deletion would eventually accelerate. I find this behavior stressful. I'd like a warning for when backspace starts to accelerate.
- Backspace Mirror highlights in red text that would get deleted faster, and highlights in yellow text that would get deleted slower.
- The name Backspace Mirror is a wordplay on “backspace” and “rear-view mirror”. Backspace Mirror allows you to see rearward when you hold backspace.
- Also, an absurd exercise in solving a problem that shouldn't exist, playing with statistics, exploring text manipulation on the web, and unearthing an iOS 6 keyboard made with CSS.
Below is the in-depth case study.
Implementation
Holding backspace
Detecting whether the user is holding backspace on a mobile browser is not trivial.
On a physical keyboard, a key has a travel. Pressing a key can be described by at least two movements: one movement down, and one movement up. Web browsers listen to two corresponding events: keydown and keyup. Therefore, after a keydown was fired, and as long as a keyup isn't, we can assume that the user is holding down the key.
On mobile, events fired by backspace behave differently. Mobile Safari fires a pair of keydown and keyup events for each backspace call, even if the user don't lift their finger. When the user is holding backspace, the machine steadily fires pairs of keydown and keyup events. If we only monitor keydown and keyup events, holding backspace and repeatedly hitting backspace are equivalent.
Measuring time-lapses
In order to determine whether the user is holding backspace or not, I measure the time-lapse between each backspace call. If it’s less than a given threshold (arbitrarily set to 150ms), I decide that the user is holding backspace.
When I first wrote this experiment in 2014, I was testing it on an iPad 3. The iPad would sometimes stutter while I'm holding backspace, which makes the time-lapses spike above the threshold. This performance issue would break the algorithm.
I needed some form of fault tolerance. A single irregularity in a series of otherwise steady time-lapses should be ignored. I searched for how we can quantify the regularity of a series of measures in order to ignore the faulty spikes. Such an operation is called outlier filtering and it's in the domain of statistical measurement.
Outlier filtering
I read about different statistical measures: variance, standard deviation, interquartile range, etc. I tried several of them, and settled on standard deviation. Standard deviation is a measure of the evenness of a data collection. The bigger the deviation, the more scattered are the data points. The smaller the deviation, the more alike are the data points.
I could push successive time-lapses into an array and compute their standard deviation. If the standard deviation remains below a threshold, any outlier should be filtered and I can assume that the user is still holding backspace.
Here's an experiment that plots time-lapses between key presses and their standard deviation:
Hold backspace on the textarea, then quickly lift your finger and hold it back again to simulate a stutter. If you do it fast enough, you can get a result like this:
After a little while, we can see that even though some time lapses spike above the threshold, the standard deviation remains green, which means that we could use the standard deviation to ignore the occasional stutter!
However, there is one major drawback: computing the standard deviation of the last 10 time lapses adds a “drag” to the system. That is why the first couple of standard deviations are all red. The user has to hold a key long enough without hiccups before we can use the standard deviation to filter out the outliers.
The current implementation of Backspace Mirror ditches this processing altogether. The discovery of e.repeat
makes the algorithm reliable on recent iOS devices. Older devices fall back to simple time-lapse measurement.
Nonetheless, experimenting with statistical measurement was interesting. There are other observations we can make from the experiment above:
- The standard deviation does not depend on the absolute value of the timelapses. If we hit a key repeatedly and regularly but slowly, the standard deviation will stabilize under the threshold, no matter the threshold we set for the timelapses.
- Whenever there is a change of pace in the user input, the standard deviation spikes, then decreases, then stabilizes.
At this point, I got caught into philosophical considerations about statistical tools and temporal pattern detection. But it was only a way to delay the second part of the implementation of Backspace Mirror.
Text styling
The hardest part of this mockup was, by far, actually highlighting the text.
Here's what Backspace Mirror needs to do:
-
Once the user holds backspace:
- Highlight in yellow the n last characters before the caret
- Highlight in red the remaining characters before the yellow range
- Trim the highlighted ranges as the text is erased.
- When the user releases backspace, remove the highlighting.
We need a toolset to select text from position a to position b and apply a styling to that selection. Apparently, this is complicated.
In theory, the Selection API, the Range API and contentEditable are the tools we need to manipulate text programmatically on a web browser.
The Selection API handles the user selection. It gives us information about what is being selected, like where the caret is positioned in an editable area. For example window.getSelection().anchorNode
tells you where the selection is starting on the current document (any selection has a beginning and an end).
The Range API allows us to set selections programmatically. For example, given a node of type text, range.setStart(text_node, offset)
begins a selection range at the given offset after the start of the node.
contentEditable makes any DOM element editable by the user. For example, a div element with contentEditable becomes an area where the user can write rich text. Once we have a selection, we can apply document.execCommand("HiliteColor", false, "red")
to highlight the selection in red. Under the hood, the styled selection is a text node wrapped inside an element with inline CSS.
For Backspace Mirror, we need to highlight a specific range of text before the caret. So we need to get the current caret position with the Selection API, then start the selection n characters before the caret with range.setStart(node, offset)
, and end the selection where the caret is with range.setEnd(node, offset)
. But we have to determine in which node the text we’re targeting is wrapped in.
The issue is that even though text may appear visually contiguous, it might not be contiguous from the DOM perspective. If the user hits Enter in a div
with contentEditable, the browser might insert a br
element or a new paragraph. Moreover, any existing styling inside the div
means that there are wrapper elements that we need to locate and traverse. For example, the expression red text yellow text has contiguous text, but because of the styling, it actually spans at least two different elements:
<mark class="red">red text </mark><mark class="yellow">yellow text</mark>
This DOM structure makes selecting text hilariously over complicated. I tried using the Range API to target the right nodes, and execCommand to apply and remove styling, but after many frustrating attempts, I gave up.
Highlight div
The solution I settled for is a hack based on a plugin made by Will Boyd called Highlight Within Textarea. Here’s how Will describes the hack:
The basic idea is to carefully position adiv
behind thetextarea
. JavaScript will be used to copy any text entered into thetextarea
to thediv
. A bit more JavaScript will make that both elements scroll as one. With everything perfectly aligned, we can add markup inside thediv
to highlight text, which will show through thetextarea
, completing the illusion.
The benefit of this hack is that it uses textarea instead of a div with contentEditable. Textareas provide a simple JavaScript API to select text from offset a to offset b:
var textarea = document.querySelector('#my_textarea');
textarea.selectionStart = 1;
textarea.selectionEnd = 10;
There are no nodes or hidden elements to fiddle with, because a textarea cannot contain any markup. Once we clone the value of textarea into the highlight div, we are free to style the latter however we want, as its content is entirely overwritten whenever the value of textarea is changed.
The downside is that this solution prevents Backspace Mirror from becoming a standalone JavaScript plugin that we could just drop on any div with contentEditable. The highlight div hack requires careful positioning that depends on the styling of the original textarea, and a textarea does not provide the rich text formating of contentEditable (paragraphs, underlines, bold, italic, etc).
Android keyCode bug
Both Chrome and Firefox on Android have a strange bug: keyboard keyCodes are not captured properly. On desktop and iOS, hitting backspace reliably returns a value of 8 on e.keyCode
:
document.addEventListener('keydown', function(e){
console.log(e.keyCode) // should return 8 if you hit backspace
}, false);
But Android browsers return 0 or 229 for many different keys, which prevents from detecting backspace reliably, and therefore activate Backspace Mirror. However, this mockup was originally meant for iOS. Modern Android systems do not need this, as backspace fires steadily without accelerating.
Conclusion
This whole experiment is lipstick on a pig. Text editing on touch devices remains perfectible. However, diving into a specific technical problem is a good exercise. While I was implementing Backspace Mirror, the opportunity to play with statistical measurement was enlightening, and battling with text manipulation on the web was humbling. My sympathy goes out to developers who make web-based text editors.
Trivia
- I found that iOS accelerates after 21 characters. Samsung keyboard on Android 7 (Galaxy A7) accelerates after 11 characters. Gboard on stock Android 7 does not accelerate.
- When I hold backspace on Mobile Safari, the browser fires a pair of keydown and keyup events every ~100ms.
- When I hold backspace on a desktop browser, a keydown event is fired every ~25ms.
Goodies
The very first version of Backspace Mirror dates back to 2012, complete with a responsive iOS 6 keyboard made in CSS, Marker Felt typeface (I loved that typeface. I immediately knew I was in note-taking mode) and linen background! Click the image to visit:
Links
- MDN, KeyboardEvent.repeat.
- Math is Fun, Standard Deviation and Variance.
- Larry Battle, Javascript Standard Deviation, Variance, Average functions (Internet Archive), 2011.
- Wikipedia, Hysteresis.
- W3C, Overview of the issues of editing text in a browser, 2017.
- Sten Hougaard, execCommands examples, 2017.
- Nick Santos, Why ContentEditable is Terrible, 2014.
- Will Boyd, Highlight Within Textarea, 2015. Making of.
- Chromium Bugs, Android keyboard event bug, an open thread since 2012.