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.

, and 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.

Update February 2018: Recently, I discovered the existence of KeyboardEvent.repeat. It’s a boolean attached to keyboard events that tells whether or not the user is holding the key. This property wasn’t supported on Mobile Safari when I wrote the first version of this experiment in 2014. KeyboardEvent.repeat requires iOS 10.1 and up. The current mockup uses that property if supported, and falls back to time-lapse measurement if not.

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:

Timelapse Timelapse Standard deviation Standard deviation Reset measures Standard deviation test. The upper canvas shows the time lapse since the last keyboard event. The lower canvas shows the standard deviation of the time lapses between the last 10 keyboard events.



Timelapse threshold = 150ms.

Standard deviation threshold = 150ms.

Red = above the threshold.

Green = below the threshold.

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 a div behind the textarea . JavaScript will be used to copy any text entered into the textarea to the div . A bit more JavaScript will make that both elements scroll as one. With everything perfectly aligned, we can add markup inside the div to highlight text, which will show through the textarea , completing the illusion.

See the Pen Highlight Text Inside a Textarea by Will Boyd (@lonekorean) on CodePen. Toggle perspective to see how the hack works. CodePen by Will Boyd.

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.

Update 12 May 2019: I may have rediscovered the two styles of computer graphics libraries: retained mode, and immediate mode. Retained mode is when a graphic library keeps track of the objects on screen. The DOM is such a library, where a tree of objects persists across time. That provides helpful features, but might also require to “go back up” the tree before being able to move into another state. Immediate mode is when everything is erased and drawn again whenever needed. The highlight div is such a solution, where the previous state is entirely discarded and rebuilt from scratch at each step.

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: