Skip to content

Instantly share code, notes, and snippets.

@bwrrp
Last active August 29, 2015 14:02
Show Gist options
  • Save bwrrp/4bb35f7317baa6d6124d to your computer and use it in GitHub Desktop.
Save bwrrp/4bb35f7317baa6d6124d to your computer and use it in GitHub Desktop.
Proof of concept: stretchable inline elements by abusing the browser selection

Stretchable inlines

This is a proof of concept of abusing the browser selection to make inline elements stretchable by the user. Inline handles (span elements) are added to each stretchable inline. We then hook up a set of mousedown / move / up event handlers to catch drag operations starting on these handles and reset the selection to cover the entire element, with the focus at the side of the clicked handle. It looks like Chrome and Firefox are quite forgiving and integrate the resulting selection into the one being created by the mouse drag!

IE is, as always, a lot more trouble:

  • It doesn't seem to want to continue selecting after the selection is modified programmatically.
  • It messes up most ways of styling the handles as anything vaguely block-like becomes selectable in IE's "control selection" mode, complete with being draggable and having resize handles, with no way to disable this behavior. Of course this prevents any text selections being created when starting a drag on these elements.
  • It seems to be the only browser lacking a way to programmatically set a backward selection (focus before anchor in DOM order)

Even if a way can be found around the first two issues, the lack of a way to control the direction of a selection probably prevents this from ever being usable in IE. Perhaps using the selection as a delta would work, but the visual appearance of this is confusing.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Stretchable elements proof of concept</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="editor" contenteditable>
<p><strong class="stretch">Bacon</strong> ipsum dolor sit filet mignon venison incididunt, laboris frankfurter sirloin duis swine consequat drumstick. Tenderloin sunt ham hock sausage, consectetur salami tri-tip leberkas aute ex velit. Eiusmod boudin <em class="stretch">beef jowl turkey</em> occaecat strip steak pork pig voluptate est sunt in culpa elit. T-bone veniam sunt reprehenderit ea id. Drumstick meatloaf leberkas ex beef ribs reprehenderit salami.</p>
<p>Velit doner biltong tail cillum in pariatur laborum sunt swine fugiat pig. Labore fatback laboris aliqua. Ex incididunt exercitation culpa consectetur, in commodo fugiat ullamco officia cillum. Excepteur aute t-bone, jowl ut id capicola.</p>
<p>Fugiat boudin aliquip quis, laboris bresaola fatback ut beef ex filet mignon sint labore tongue mollit. Short ribs quis occaecat corned beef rump ground round ut. Short ribs dolore strip steak porchetta. Id nostrud consectetur qui.</p>
<p>Eiusmod sausage brisket, kevin do ut sint bacon <s class="stretch">turducken pariatur fatback</s> qui t-bone. T-bone andouille boudin excepteur, shankle frankfurter nisi prosciutto bacon incididunt officia spare ribs. Ut bresaola t-bone, sint non aliqua sunt minim laborum ham et ball tip. Shank occaecat kielbasa minim et.</p>
<p>Commodo turducken ea drumstick laboris qui, nisi pariatur proident. Ad ground round ham ea. Tenderloin labore ball tip shoulder, filet mignon minim ribeye turducken kielbasa id nostrud salami in doner. Shoulder do ham cillum swine flank tail sed in filet mignon drumstick. Pork loin pastrami sausage, anim doner tongue consectetur mollit pork chop landjaeger in dolore biltong.</p>
</div>
<script src="stretchables.js"></script>
</body>
</html>
(function() {
[].forEach.call(document.querySelectorAll('.stretch'), function(el) {
el.insertBefore(document.createElement('span'), el.firstChild);
el.insertBefore(document.createElement('span'), null);
});
document.addEventListener('dragstart', function(event) {
event.preventDefault();
}, true);
function isStretchHandle(node) {
return node.parentNode.nodeType == 1 && node.parentNode.classList.contains('stretch') && node.nodeName == 'SPAN';
}
var stretchTarget = null,
stretchingFromEnd = false,
mouseDownX = 0,
mouseDownY = 0,
stretching = false;
function setBackwardSelection(selection, range) {
if (!selection.extend) {
// IE...
selection.addRange(range);
return;
}
var endRange = range.cloneRange();
endRange.collapse(false);
selection.addRange(endRange);
selection.extend(range.startContainer, range.startOffset);
}
function collapseAndPreserveRange(wrapper, range) {
// Grab static copy of range properties
var startContainer = range.startContainer,
startOffset = range.startOffset,
endContainer = range.endContainer,
endOffset = range.endOffset;
for (var child = wrapper.firstChild, offset = 0; child; child = wrapper.firstChild, ++offset) {
wrapper.parentNode.insertBefore(child, wrapper);
if (child === startContainer) {
range.setStart(child, startOffset);
} else if (wrapper === startContainer && offset === startOffset) {
range.setStartBefore(wrapper);
}
if (child === endContainer) {
range.setEnd(child, endOffset);
} else if (wrapper === endContainer && offset === endOffset) {
range.setEndBefore(wrapper);
}
}
wrapper.parentNode.removeChild(wrapper);
}
function isValidRangeForRewrap(range, wrapper) {
var startContainer = range.startContainer;
if (wrapper.contains(startContainer)) {
startContainer = wrapper.parentNode;
}
if (startContainer.nodeType !== 1) {
startContainer = startContainer.parentNode;
}
var endContainer = range.endContainer;
if (wrapper.contains(endContainer)) {
endContainer = wrapper.parentNode;
}
if (endContainer.nodeType !== 1) {
endContainer = endContainer.parentNode;
}
return startContainer === endContainer;
}
function rewrap(wrapper, range) {
if (!isValidRangeForRewrap(range, wrapper)) {
console.warn('Final range not valid for rewrap');
return;
}
var handles = [wrapper.firstChild, wrapper.lastChild];
collapseAndPreserveRange(wrapper, range);
range.surroundContents(wrapper);
wrapper.insertBefore(handles[0], wrapper.firstChild);
wrapper.appendChild(handles[1]);
}
document.addEventListener('mousedown', function(event) {
if (!isStretchHandle(event.target)) {
return;
}
stretchTarget = event.target.parentNode;
stretchingFromEnd = !event.target.nextSibling;
mouseDownX = event.clientX;
mouseDownY = event.clientY;
document.body.classList.add('stretching');
}, true);
document.addEventListener('mousemove', function(event) {
if (!stretchTarget) {
return;
}
var selection = window.getSelection();
// Wait for enough movement
if (!stretching) {
var dx = event.clientX - mouseDownX,
dy = event.clientY - mouseDownY;
if (dx * dx + dy * dy < 100) {
return;
}
stretching = true;
// Select the stretchable element
var range = selection.rangeCount ? selection.getRangeAt(0) : document.createRange();
range.selectNode(stretchTarget);
selection.removeAllRanges();
if (stretchingFromEnd) {
selection.addRange(range);
} else {
setBackwardSelection(selection, range);
}
}
document.body.classList.toggle('stretching-invalid', !selection.rangeCount || !isValidRangeForRewrap(selection.getRangeAt(0), stretchTarget));
}, true);
document.addEventListener('mouseup', function(event) {
if (!stretchTarget) {
return;
}
document.body.classList.remove('stretching');
document.body.classList.remove('stretching-invalid');
var wrapper = stretchTarget;
stretchTarget = null;
if (!stretching) {
return;
}
stretching = false;
var selection = window.getSelection();
if (!selection.rangeCount) {
console.warn('expected to find a range in the selection');
return;
}
var range = selection.getRangeAt(0);
rewrap(wrapper, range);
range.detach();
window.getSelection().removeAllRanges();
}, true);
})();
html {
background: #ddd;
}
body {
max-width: 900px;
margin: 50px auto;
background: #fff;
color: #321;
}
.editor {
padding: 50px;
outline: none;
}
.stretch {
box-shadow: 0 0 1px 1px rgba(255, 127, 0, 0.6);
position: relative;
padding: 0 3px;
}
/* handles */
.stretch > span {
padding: 1px 4px;
background: rgba(255, 127, 0, 0.7);
position: absolute;
right: 100%;
top: -1px;
bottom: -1px;
visibility: hidden;
border-radius: 100% 0 0 100%;
cursor: e-resize;
}
.stretch > span ~ span {
left: 100%;
right: auto;
top: -1px;
bottom: -1px;
border-radius: 0 100% 100% 0;
}
.stretching {
cursor: e-resize;
}
.stretching-invalid {
cursor: not-allowed;
}
/* hover */
.stretch:hover {
box-shadow: 0 0 1px 1px rgba(255, 127, 0, 0.7);
}
.stretch:hover > span {
visibility: visible;
}
.stretching .stretch:hover > span {
visibility: hidden;
}
/* selection */
::selection {
background: rgba(0, 127, 255, 0.8);
}
.stretch::selection{
background: rgba(255, 127, 0, 0.8);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment