Got it, finally:
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.emphasized {
text-decoration: underline;
font-weight: bold;
font-style: italic;
}
</style>
</head>
<body>
<button type="button" onclick="applyTagwClass(this);" data-tag="span" data-tagClass="emphasized">Bold</button>
<div contenteditable="true" class="textEditor">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In malesuada quis lorem non consequat. Proin diam magna, molestie nec leo non, sodales eleifend nibh. Suspendisse a tellus facilisis, adipiscing dui vitae, rutrum mi. Curabitur aliquet
lorem quis augue laoreet feugiat. Nulla at volutpat enim, et facilisis velit. Nulla feugiat quis augue nec sodales. Nulla nunc elit, viverra nec cursus non, gravida ac leo. Proin vehicula tincidunt euismod.</p>
<p>Suspendisse non consectetur arcu, ut ultricies nulla. Sed vel sem quis lacus faucibus interdum in sed quam. Nulla ullamcorper bibendum ornare. Proin placerat volutpat dignissim. Ut sit amet tellus enim. Nulla ut convallis quam. Morbi et
sollicitudin nibh. Maecenas justo lectus, porta non felis eu, condimentum dictum nisi. Nulla eu nisi neque. Phasellus id sem congue, consequat lorem nec, tincidunt libero.</p>
<p>Integer eu elit eu massa placerat venenatis nec in elit. Ut ullamcorper nec mauris et volutpat. Phasellus ullamcorper tristique quam. In pellentesque nisl eget arcu fermentum ornare. Aenean nisl augue, mollis nec tristique a, dapibus quis urna.
Vivamus volutpat ullamcorper lectus, et malesuada risus adipiscing nec. Ut nec ligula orci. Morbi sollicitudin nunc tempus, vestibulum arcu nec, feugiat velit. Aenean scelerisque, ligula sed molestie iaculis, massa risus ultrices nisl, et placerat
augue libero vitae est. Pellentesque ornare adipiscing massa eleifend fermentum. In fringilla accumsan lectus sit amet aliquam.</p>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
function applyTagwClass(self) {
var selection = window.getSelection();
if (selection.rangeCount) {
var text = selection.toString();
var range = selection.getRangeAt(0);
var parent = $(range.startContainer.parentNode);
if (range.startOffset > 0 && parent.hasClass(self.attributes['data-tagClass'].value)) {
var prefix = '<' + self.attributes['data-tag'].value + ' class="' + self.attributes['data-tagClass'].value + '">' + parent.html().substr(0,selection.anchorOffset) + '</' + self.attributes['data-tag'].value + '>';
var suffix = '<' + self.attributes['data-tag'].value + ' class="' + self.attributes['data-tagClass'].value + '">' + parent.html().substr(selection.focusOffset) + '</' + self.attributes['data-tag'].value + '>';
parent.replaceWith(prefix + text + suffix);
} else {
range.deleteContents();
range.insertNode($('<' + self.attributes['data-tag'].value + ' class="' + self.attributes['data-tagClass'].value + '">' + text + '</' + self.attributes['data-tag'].value + '>')[0]);
//Remove all empty elements (deleteContents leaves the HTML in place)
$(self.attributes['data-tag'].value + '.' + self.attributes['data-tagClass'].value + ':empty').remove();
}
}
}
</script>
</body>
</html>
You'll notice that I extended the button to have a couple data-
attributes. They should be rather self-explanatory.
This will also de-apply to subsections of the selected text which are within the currently-targeted element (everything goes by class name).
As you can see, I'm using a class which is a combination of things so this gives you more versatility.