Remove whitespace from window selection in js - javascript

I'm trying to remove leading or trailing (sometimes both) whitespace from the user selected text. Implemented according to this answer. This works for simple cases, however, when the selected text contains tags or , it fails.
Example: in the fiddle try to highlight -- hi this is bob. from right to left, including the space at the end, then press Trim.
This results in:
Uncaught IndexSizeError: Failed to execute 'setEnd' on 'Range': The offset 24 is larger than or equal to the node's length (5).
I guess this can be caught with
if (method == range.setEnd && range.startOffset + ind >= range.endContainer.length)
but I'm not sure how to handle it.
I've also tried to replace the hard spaces using
e2 = document.getElementById('e2');
e2.innerHTML = e2.innerHTML.replace(/ /gi, ' ');
However, this makes the selection empty.
Code:
function removeWsFromSelection(fromStart) {
selection = window.getSelection();
range = selection.getRangeAt(0);
if (fromStart) {
regex = /[^\s]/;
container = range.startContainer;
method = range.setStart;
} else {
regex = /\s+$/;
container = range.endContainer;
method = range.setEnd;
}
match = regex.exec(selection.toString());
if (match) {
ind = match.index;
if (ind > 0) {
// ind is the first non-ws char from the start or first ws char from the end,
// hence (startOffset + ind)
method.call(range, container, range.startOffset + ind);
rng = range.cloneRange();
selection.removeAllRanges();
selection.addRange(rng);
}
}
}
BTW, unfortunately, Selection.modify didn't work for me, and besides it's considered non-standard.

If you are fine with range modifying, you're able to check the length of trimmed symbols from the start and the end, and then modify startOffset and endOffset of the Range. But sure this is not a silver bullet for a cases where startContainer and endContainer are not the same node. At least it works for a certain of cases.
const sel = window.getSelection();
const text = sel.toString();
const range = sel.getRangeAt(0);
const startOffset = text.length - text.trimStart().length;
const endOffset = text.length - text.trimEnd().length;
if (startOffset) {
range.setStart(range.startContainer, range.startOffset + startOffset);
}
if (endOffset) {
range.setEnd(range.endContainer, range.endOffset - endOffset);
}

This is very ugly and doesn't handle the general case, but seems to work:
function removeWsFromSelection(fromStart) {
selection = window.getSelection();
range = selection.getRangeAt(0);
if (fromStart) {
regex = /[^\s]/;
container = range.startContainer;
method = range.setStart;
}
else {
regex = /\s+$/;
container = range.endContainer;
method = range.setEnd;
}
match = regex.exec(selection.toString());
if (match) {
ind = match.index;
if (ind > 0) {
// ind is the first non-ws char from the start or first ws char from the end,
// hence (startOffset + ind)
if (method == range.setEnd && range.startOffset + ind >= range.endContainer.length) {
match = regex.exec(range.endContainer.textContent);
if (match) {
range.setEnd(range.endContainer, match.index);
}
}
else {
method.call(range, container, range.startOffset + ind);
}
rng = range.cloneRange();
selection.removeAllRanges();
selection.addRange(rng);
}
}
}

I've modified Micheal's answer to work with multiple ranges and ranges that cross node boundaries. This seems to trim selections consistently in all my tests, but there's always room for edge cases I'm sure.
TypeScript, but easy enough to adapt to vanilla JS.
export function trimRanges(selection: Selection) {
for (let i = 0, range = selection.getRangeAt(0); i < selection.rangeCount; range = selection.getRangeAt(i++)) {
const text = selection.toString();
const startOffset = text.length - text.trimStart().length;
const endOffset = text.length - text.trimEnd().length;
if (startOffset) {
const offset = range.startOffset + startOffset;
if (offset < 0) {
// If the range will underflow the current element, then it belongs in the previous element
const start = range.startContainer.parentElement.previousSibling;
range.setStart(start, start.textContent.length + offset);
} else if (offset > range.startContainer.textContent.length) {
// If the range will overflow the current element, then it belongs in the next element
const start = range.startContainer.parentElement.nextSibling;
range.setStart(start, offset - range.startContainer.textContent.length);
} else {
range.setStart(range.startContainer, offset);
}
}
if (endOffset) {
const offset = range.endOffset - endOffset;
if (offset < 0) {
// If the range will underflow the current element, then it belongs in the previous element
const end = range.endContainer.parentElement.previousSibling;
range.setEnd(end, end.textContent.length + offset);
} else if (offset > range.endContainer.textContent.length) {
// If the range will overflow the current element, then it belongs in the next element
const end = range.endContainer.parentElement.nextSibling;
range.setEnd(end, offset - range.endContainer.textContent.length);
} else {
range.setEnd(range.endContainer, offset);
}
}
}
}
Note that this does modify the ranges attached to the selection. If you need atomicity, you could hypothetically clone each range before editing it, remove all ranges, then add the cloned ranges back to the selection, but I haven't found any issues with the approach as it stands.

Related

character position of a multi-line selection within a content editable div using javascript

Please take a look at this fiddle:
https://jsfiddle.net/darrengates/jwx41Lkz/
In this example, I have an editable div with 3 lines. I'm attempting to get the start character position, and end character position, of a selection.
This appears to work for the first line only. If I highlight more than 1 line, the values are incorrect (see the comments for an explanation).
How do I get the start and end character position of a selection when it spans multiple lines? (i.e., the character position starting at the first letter of the entire div, so for example the "s" in "second line" would be approximately character position 11).
Here's what I have so far (runnable in the fiddle):
<div id="test" contenteditable="true" class="selecttest">
first line<br />
second line<br />
third line
</div>
.selecttest {
width: 400px;
height: 200px;
border: 1px solid black;
padding: 5px;
font-size: 20px;
}
function getSelection() {
const sel = document.getSelection() || window.getSelection();
// note: if I highlight the 2nd and 3rd lines
// start and end values are 1, 11
// they should be about 11 and 22 (or so)
// on account of the existence of the first line
let start = sel.baseOffset
let end = sel.extentOffset
// if selected from right to left, reverse...
if ( start > end ) {
const temp = start;
start = end;
end = temp;
}
console.log('start, end', start, end)
}
$("#test").on('mouseup', function() {
getSelection();
})
It seems that there are many S.O. posts relating to getting selected text, but answers relating to getting the selected character positions all fail when selecting multiple lines.
Since you want to measure position in a range, you should be using a range object rather than the selection object.
A proposal on this code pen > https://codepen.io/aSH-uncover/pen/ExEdXwy
The main trick is to address the two scenarios where the range is inside the same container or not :)
function getSelection() {
const sel = document.getSelection() || window.getSelection();
const range = sel.getRangeAt(0)
let rangeContainer = range.commonAncestorContainer
let same = false
if (rangeContainer === range.startContainer) {
same = true
rangeContainer = range.startContainer.parentElement
}
let start = -1
let end = -1
const list = rangeContainer.childNodes
let offset = 0
for (let i = 0 ; i < list.length ; i++) {
const current = list[i]
if (current === range.startContainer) {
start = offset + range.startOffset
if (same) {
end = offset + range.endOffset
break
}
} else if (current === range.endContainer) {
end = offset + range.endOffset
break
}
if (current.nodeName === '#text') {
offset += current.length
}
}
console.log('start, end', start, end)
}
Edit : if you want to support more complex text (like a div with spans) you will have to improve this function and include some recursive stuff :)

How to set caret position of a contenteditable div containing combination of text and element nodes

My HTML:
<div id="text" contenteditable="true">abcd<img src="icon.gif"/>efgh</div>
My caret = 5; so I want to set the caret poisition to be immediately after the image as the image is treated as 1 character.
So I wrote thise code:
var node = document.querySelector("div");
node.focus();
var textNode = node.firstChild;
var caret = 5;
var range = document.createRange();
range.setStart(textNode, caret);
range.setEnd(textNode, caret);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
But I get this error:
Uncaught DOMException: Failed to execute 'setStart' on 'Range': The offset 5 is larger than or equal to the node's length (4).
Please suggest how I might achieve this ? I might have several images before the caret position I desire and each image would be treated as 1 character.
Your textNode has 3 children (1 text, 1 element, 1 text) and therefore you can't just use firstChild.
You need to iterate over the childNodes of the <div> and track the character count where the nodeType of the childNode equals Node.TEXT_NODE (see here on MDN). Where the character count is less than the value of caret you can deduct that from caret and move onto the next text node.
Per your condition that:
I desire and each image would be treated as 1 character
The code will deduct 1 from caret where nodeType == 1 i.e. Node.ELEMENT_NODE
Here is a code example with multiple icons:
var node = document.querySelector("div");
node.focus();
var caret = 24;
var child;
var childNodeIndex = 0;
for(var i=0; i<node.childNodes.length; i++) {
child = node.childNodes[i];
// Node.ELEMENT_NODE == 1
// Node.TEXT_NODE == 3
if(child.nodeType == Node.TEXT_NODE) {
// keep track of caret across text childNodes
if(child.length <= caret) {
caret -= child.length;
} else {
break;
}
} else if (child.nodeType == Node.ELEMENT_NODE) {
// condition that 'each image would be treated as 1 character'
if(caret > 0) {
caret -= 1;
} else {
break;
}
};
childNodeIndex += 1;
}
var textNode = node.childNodes[childNodeIndex];
// your original code continues here...
var range = document.createRange();
range.setStart(textNode, caret);
range.setEnd(textNode, caret);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
<div id="text" contenteditable="true">a<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/><img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/><img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/><img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>b<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>cdefghijkl<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>mnopq<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>rst<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>uvw<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>xyz<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/></div>

Single-click only works once

I want to allow the user to remove words in a div by a single mouse click. It work fine, see jsFiddle.
The only problem is that the single-click feature only works on the first click. After that, you need to double click.
I can't get my head around why it is behaving like this. Maybe you can? Could be problem with jQuery(document).ready()...
jQuery:
// highlight a word/term quicker and smarter: so.com/a/35103840/1185126
jQuery(document).ready(function(e){
(function(els){
// variable declaration for previous range info
// and function for finding the sibling
var prevRangeInfo = {},
findSibling = function(thisNode, direction){
// get the child node list of the parent node
var childNodeList = thisNode.parentNode.childNodes,
children = [];
// convert the child node list to an array
for(var i=0, l=childNodeList.length; i<l; i++) children.push(childNodeList[i]);
return children[children.indexOf(thisNode) + direction];
};
for(var i=0;i<els.length;i++){
var el = els[i];
el.addEventListener('mouseup',function(evt){
if (document.createRange) { // Works on all browsers, including IE 9+
var selected = window.getSelection();
// Removing the following line from comments will make the function drag-only
/* if(selected.toString().length){ */
var d = document,
nA = selected.anchorNode,
oA = selected.anchorOffset,
nF = selected.focusNode,
oF = selected.focusOffset,
range = d.createRange(),
rangeLength = 0;
range.setStart(nA,oA);
range.setEnd(nF,oF);
// Check if direction of selection is right to left
if(range.startContainer !== nA || (nA === nF && oF < oA)){
range.setStart(nF,oF);
range.setEnd(nA,oA);
}
// Extend range to the next space or end of node
while(range.endOffset < range.endContainer.textContent.length && !/\s$/.test(range.toString())){
range.setEnd(range.endContainer, range.endOffset + 1);
}
// Extend range to the previous space or start of node
while(range.startOffset > 0 && !/^\s/.test(range.toString())){
range.setStart(range.startContainer, range.startOffset - 1);
}
// Remove spaces
if(/\s$/.test(range.toString()) && range.endOffset > 0)
range.setEnd(range.endContainer, range.endOffset - 1);
if(/^\s/.test(range.toString()))
range.setStart(range.startContainer, range.startOffset + 1);
// Store the length of the range
rangeLength = range.toString().length;
// Check if another range was previously selected
if(prevRangeInfo.startContainer && nA === nF && oA === oF){
var rangeTryContain = d.createRange(),
rangeTryLeft = d.createRange(),
rangeTryRight = d.createRange(),
nAp = prevRangeInfo.startContainer;
oAp = prevRangeInfo.startOffset;
nFp = prevRangeInfo.endContainer;
oFp = prevRangeInfo.endOffset;
rangeTryContain.setStart(nAp, oAp);
rangeTryContain.setEnd(nFp, oFp);
rangeTryLeft.setStart(nFp, oFp-1);
rangeTryLeft.setEnd(range.endContainer, range.endOffset);
rangeTryRight.setStart(range.startContainer, range.startOffset);
rangeTryRight.setEnd(nAp, oAp+1);
// Store range boundary comparisons
// & inner nodes close to the range boundary --> stores null if none
var compareStartPoints = range.compareBoundaryPoints(0, rangeTryContain) === 0,
compareEndPoints = range.compareBoundaryPoints(2, rangeTryContain) === 0,
leftInnerNode = range.endContainer.previousSibling,
rightInnerNode = range.startContainer.nextSibling;
// Do nothing if clicked on the right end of a word
if(range.toString().length < 1){
range.setStart(nAp,oAp);
range.setEnd(nFp,oFp);
}
// Collapse the range if clicked on last highlighted word
else if(compareStartPoints && compareEndPoints)
range.collapse();
// Remove a highlighted word from left side if clicked on
// This part is quite tricky!
else if(compareStartPoints){
range.setEnd(nFp,oFp);
if(range.startOffset + rangeLength + 1 >= range.startContainer.length){
if(rightInnerNode)
// there is a right inner node, set its start point as range start
range.setStart(rightInnerNode.firstChild, 0);
else {
// there is no right inner node
// there must be a text node on the right side of the clicked word
// set start of the next text node as start point of the range
var rightTextNode = findSibling(range.startContainer.parentNode, 1),
rightTextContent = rightTextNode.textContent,
level=1;
// if beginning of paragraph, find the first child of the paragraph
if(/^(?:\r\n|[\r\n])|\s{2,}$/.test(rightTextContent)){
rightTextNode = findSibling(rightTextNode, 1).firstChild;
level--;
}
range.setStart(rightTextNode, level);
}
}
else
range.setStart(range.startContainer, range.startOffset + rangeLength + 1);
}
// Remove a hightlighted word from right side if clicked on
// This part is also tricky!
else if (compareEndPoints){
range.setStart(nAp,oAp);
if(range.endOffset - rangeLength - 1 <= 0){
if(leftInnerNode)
// there is a right inner node, set its start point as range start
range.setEnd(leftInnerNode.lastChild, leftInnerNode.lastChild.textContent.length);
else {
// there is no left inner node
// there must be a text node on the left side of the clicked word
// set start of the previous text node as start point of the range
var leftTextNode = findSibling(range.endContainer.parentNode, -1),
leftTextContent = leftTextNode.textContent,
level = 1;
// if end of paragraph, find the last child of the paragraph
if(/^(?:\r\n|[\r\n])|\s{2,}$/.test(leftTextContent)){
leftTextNode = findSibling(leftTextNode, -1).lastChild;
level--;
}
range.setEnd(leftTextNode, leftTextNode.length - level);
}
}
else
range.setEnd(range.endContainer, range.endOffset - rangeLength - 1);
}
// Add previously selected range if adjacent
// Upgraded to include previous/next word even in a different paragraph
else if(/^[^\s]*((?:\r\n|[\r\n])|\s{1,})[^\s]*$/.test(rangeTryLeft.toString()))
range.setStart(nAp,oAp);
else if(/^[^\s]*((?:\r\n|[\r\n])|\s{1,})[^\s]*$/.test(rangeTryRight.toString()))
range.setEnd(nFp,oFp);
// Detach the range objects we are done with, clear memory
rangeTryContain.detach();
rangeTryRight.detach();
rangeTryLeft.detach();
}
// Save the current range --> not the whole Range object but what is neccessary
prevRangeInfo = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
// Clear the saved range info if clicked on last highlighted word
if(compareStartPoints && compareEndPoints)
prevRangeInfo = {};
// Remove all ranges from selection --> necessary due to potential removals
selected.removeAllRanges();
// Assign the current range as selection
selected.addRange(range);
// Detach the range object we are done with, clear memory
range.detach();
el.style.MozUserSelect = '-moz-none';
// Removing the following line from comments will make the function drag-only
/* } */
} else {
// Fallback for Internet Explorer 8 and earlier
// (if you think it still is worth the effort of course)
}
});
/* This part is necessary to eliminate a FF specific dragging behavior */
el.addEventListener('mousedown',function(e){
if (window.getSelection) { // Works on all browsers, including IE 9+
var selection = window.getSelection ();
selection.collapse (selection.anchorNode, selection.anchorOffset);
} else {
// Fallback for Internet Explorer 8 and earlier
// (if you think it still is worth the effort of course)
}
el.style.MozUserSelect = 'text';
});
}
})(document.getElementsByClassName('taggable'));
});
// remove selected text
jQuery(document).ready(function() {
jQuery('.taggable').bind("mouseup", function() {
var text1;
if (window.getSelection().toString() != "") {
selectedText = window.getSelection().toString()
text1 = jQuery(".taggable").text().split("")
pointStart = window.getSelection().anchorOffset
pointEnd = window.getSelection().focusOffset
if (pointEnd < pointStart) {
pointStart = pointEnd
}
text1.splice(pointStart, selectedText.length);
text1 = text1.join("")
} else {
selectedText = jQuery(".taggable").text()
text1 = selectedText;
}
jQuery(".taggable").text(text1);
});
});
This might not solve your issue, but it's another approach.
This code wrap every word inside a span and create an event listener for each.
HTML
<p>This is an example text</p>
Javascript
jQuery(document).ready(function($) {
var text = $('p').text();
var arr = text.split(' ');
$('p').html('');
for (var i = 0; i < arr.length; i++) {
$('<span />').html(arr[i] + ' ').appendTo('p');
$('p').on('click', 'span:nth-of-type(' + (i + 1) + ')', function() {
$(this).remove();
});
}
});
I've figured out that the main problem is because if the anchorOffset is equal to focusOffset, it doesn't work, so, the possible solution is to add +1 when it's equal and then the code will work as desired since it finds at least a letter when substring. Change the code:
from
var selected = window.getSelection();
// Removing the following line from comments will make the function drag-only
/* if(selected.toString().length){ */
var d = document,
nA = selected.anchorNode,
oA = selected.anchorOffset,
nF = selected.focusNode,
oF = selected.focusOffset,
range = d.createRange(),
rangeLength = 0;
to
var selected = window.getSelection();
var offset = selected.focusOffset;
if(selected.anchorOffset == selected.focusOffset)
offset++;
// Removing the following line from comments will make the function drag-only
/* if(selected.toString().length){ */
var d = document,
nA = selected.anchorNode,
oA = selected.anchorOffset,
nF = selected.focusNode,
oF = offset,
range = d.createRange(),
rangeLength = 0;
I've tested many times here on JFiddle and it worked OK, but
I'm still afraid that this can result in some other problems.
If you get some problem, please notify me and i'll help.
EDIT:
jsFiddle

Find start position of text range in IE11

I've got the following code to find a piece of text in IE11
var search_position = 0;
function findMyText(find)
{
// create range (selects all text)
var range = document.body.createTextRange();
range.moveToElementText(document.getElementById("editor"));
if(search_position == 0)
{
range.moveStart("word", 0);
range.findText(find);
}
// find the nth word that matches, where n is search_position
for (var i = 0; i < search_position; i++)
{
range.moveStart("word", 1);
range.findText(find);
}
search_position++;
range.select();
var l = range.text.length
if(l < find.length)
{
search_position = 0;
}
}
This finds each successive instance of find and selects it. It works by finding the first instance of a piece of text, marking the search index and then moving to the next word.
What I really want to do is start from a certain cursor position without looping through all the text. As in, if the user places the cursor in the middle of the text, I want to search from that point on. I also want to add a replace function. I can move to a specific character position like this
range.moveStart("character", x);
Where x is the character position. I can fetch this character position from the cursor position using the following code.
function getCaretPosition() {
var ie = (typeof document.selection != "undefined" && document.selection.type != "Control") && true;
var w3 = (typeof window.getSelection != "undefined") && true;
var caretOffset = 0;
if (w3) {
var range = window.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(editor);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
}
else if (ie) {
var textRange = document.selection.createRange();
var preCaretTextRange = document.body.createTextRange();
preCaretTextRange.expand(editor);
preCaretTextRange.setEndPoint("EndToEnd", textRange);
caretOffset = preCaretTextRange.text.length;
}
return caretOffset;
}
What I can't do, and need to, is find the current start position of my range. I need to know this for my replace method to work correctly. When I replace a word, I need to know where I am in the document.
range.startOffset returns undefined. So how do I get the position?
I found a workaround by using Tim Down's excellent Rangy library. The following piece of code
var range = rangy.createRange();
range.setStart(document.getElementById("editor"), 0);
My find function ended up abandoning this approach, but this is how to solve the original problem using Rangy

How to get the start and end points of selection in text area?

I want to get the cursor start and end position of a selected range in a text-field or text-area.
I tried lot of functions in various forums, but when the last character of the selection is a new line character JavaScript ignore it in IE6.
How do I get the start and end points of the selection?
Revised answer, 5 September 2010
To account for trailing line breaks is tricky in IE, and I haven't seen any solution that does this. It is possible, however. The following is a new version of what I previously posted here.
Note that the textarea must have focus for this function to work properly in IE. If in doubt, call the textarea's focus() method first.
function getInputSelection(el) {
var start = 0, end = 0, normalizedValue, range,
textInputRange, len, endRange;
if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
start = el.selectionStart;
end = el.selectionEnd;
} else {
range = document.selection.createRange();
if (range && range.parentElement() == el) {
len = el.value.length;
normalizedValue = el.value.replace(/\r\n/g, "\n");
// Create a working TextRange that lives only in the input
textInputRange = el.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what we want
// in those cases
endRange = el.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
}
}
return {
start: start,
end: end
};
}
Use the Rangy api and all of your problems are gone gone gone gone...
Using it
Read the documentation, or just use the below.
Very simple,
var selection = rangy.getSelection(), // Whole lot of information, supports
// multi-selections too.
start = selection.anchorOffset, // Start position
end = selection.focusOffset; // End position
Hope this api helps you out because it is really helpful in handling cross-browser ranges.
I needed to do something very similar and came up with this:
function getSelection(target) {
var s = {start: 0, end:0};
if (typeof target.selectionStart == "number"
&& typeof target.selectionEnd == "number") {
// Firefox (and others)
s.start = target.selectionStart;
s.end = target.selectionEnd;
} else if (document.selection) {
// IE
var bookmark = document.selection.createRange().getBookmark();
var sel = target.createTextRange();
var bfr = sel.duplicate();
sel.moveToBookmark(bookmark);
bfr.setEndPoint("EndToStart", sel);
s.start = bfr.text.length;
s.end = s.start + sel.text.length;
}
return s;
}
Notes:
The sel range must be created by
target rather than using the range
returned by document.selection,
otherwise bfr.setEndPoint will
complain about an invalid argument.
This "feature" (discovered
in IE8) does not appear to be
documented in the spec.
target must have input focus for this function to work.
Only tested with <textarea>, might work with <input> as well.

Categories

Resources