Is it possible to make something like the autocorrect feature in iOS for contenteditable fields? As the user types, spelling would be checked (I already have this part), but then I need a way to correct the word. I could replace the whole html of the contenteditable, but then cursor position is lost, and there is a possibility that the user would try to type before html is rewritten. Ideally, this would just replace x characters before the user's cursor. Is this possible?
the core replace-in-node-being-typed in feature can be powered by this function:
function runRep(from, to) {
var sel = document.getSelection(),
nd = sel.anchorNode,
text = nd.textContent.slice(0, sel.focusOffset),
newText = text.replace(from, to),
wholeNew = nd.textContent.replace(text, newText),
range = document.createRange();
nd.textContent = wholeNew;
range.setStart(nd, newText.length);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
nd.parentNode.focus()
}
usage: runRep("helo", "hello");, which will replace a mis-spelled "hello" to the left of the cursor in the current node. you need to be careful about sub-string matches. Upgrading the replace() to use a RegExp would likely allow more precise targeting (like only whole-words, ignoring cAsE, etc), but this will work as-is and the RegExp upgrade doesn't change the rest of the code.
not shown is how one actually detects the mis-spelled words, which is another topic...
obligatory fiddle: http://jsfiddle.net/p0dxdnub/
Related
I created a function that wraps the selected text in the h1 tag, and also deletes the tags when it is called again. The function also deletes tags and creates again if you select an area larger than the previous selection.
However, if after creating the heading from the selection, select another text, and then select the word again, then the tags do not include in the selection (although the selection completely coincides with the created heading). I use the Selection and Range methods.
It turns out that with repeated selection, the tags do not fall into Selection.
I also tried to do auto-replace using a regular expression, but this method is not good, because a phrase or a word can occur several times in the text.
Please tell me, is there any way in Javascript that allows us to find a node that matches this selection?
Maybe I didn’t explain well, and to better show, I created a snippet that demonstrates that. what's happening:
https://jsfiddle.net/narzantaria/2ex6bnq3/2/
Also here is the same code. The function is called by clicking the button:
document.getElementById('h1-trigger').addEventListener('click', function () {
headerFormatter();
});
function headerFormatter() {
let newTag = document.createElement('h1');
if (window.getSelection) {
let sel = window.getSelection();
if (sel.rangeCount) {
let range = sel.getRangeAt(0).cloneRange();
// create an object from range for querying tags
let rangeProxy = sel.getRangeAt(0).cloneContents();
if (rangeProxy.querySelector('h1')) {
let tagContent = rangeProxy.querySelector('h1').innerHTML;
// compare selection length with queried tag length
if (range.startOffset == 1) {
tagContent = tagContent.replace(/(<([^>]+)>)/ig, "");
range.deleteContents();
range.insertNode(document.createTextNode(tagContent));
sel.removeAllRanges();
sel.addRange(range);
return;
}
else {
let rangeToString = range.toString().replace(/(<([^>]+)>)/ig, "");
range.deleteContents();
range.insertNode(document.createTextNode(rangeToString));
sel.removeAllRanges();
sel.addRange(range);
return;
}
} else {
range.surroundContents(newTag);
sel.removeAllRanges();
sel.addRange(range);
}
}
}
}
Regards.
You'll have to do a few things to achieve the type of functionality you desire:
When you insert an H1 tag, add an identifier to it. Store this identifier along with the selected range values. Also, use a boolean flag variable to show the text is being highlighted.
Next time when you click the button, check whether the flag variable is true. If it is, using the stored range values from the previous selection, remove the h1 highlighting.
Once done, you can now add the highlighting to the new range and repeat step 1.
This might be a lengthier approach, but it may be more effective than your current approach.
I hope this helps. Thanks!
A flowchart to explain the steps above
You can get tag where the selection starts/ends using getSelection().anchorNode and getSelection().focusNode.
So I can append text to a textarea using this method
document.getElementById('myArea').value += msg;
This tacks the new input onto the end of the current input.
Suppose the textarea already contains text. Suppose also that using "=" instead of "+=" and inputting the values textarea already had along with the new ones is not a possible solution in this context
How would one input new text to this textarea on the correct line and in the correct position with respect to the text that is already in place?
Here is a YouTube video demonstrating the problem
https://www.youtube.com/watch?v=GpwEuI3_73I&feature=youtu.be
UPDATE:
Instead of sending one letter at a time, I sent the whole textarea each time a key is pressed. Obviously more computationally taxing, but that's the only solution I have right now. I am still interested in hearing any better solutions if you have one!
I'm assuming you send only the last character typed (as in your original approach), and it is stored in a variable named "newChar".
Take this as pseudo-code, although I hope it does not require many changes to actually work:
// deserialize the text of the target textearea
var txt = targetTextarea.text;
var txtAsArray = txt.split(/\r?\n/);
var txtLine = txtAsArray[cursorRowNum];
// write the new character in the right position (but in memory)
txtLine = txtLine.substr(0, cursorColNum) + newChar + txtLine.substr(cursorColNum);
// now serialize the text back and update the target textarea
txtAsArray[cursorRowNum] = txtLine;
txt = txtAsArray.join("\n");
targetTextarea.text = txt;
A reference used was: How in node to split string by newline ('\n')?
Regarding performance, there is no additional network activity here, and we are accessing the DOM only twice (first and last line). Remember than accessing the DOM is around 100 times slower than plain variables in memory as shown by http://www.phpied.com/dom-access-optimization/ .
That "txt = txtAsArray.join("\n");" might need to be "txt = txtAsArray.join("\r\n");" on Windows. Detecting if you are in one or the other is explained at How to find the operating system version using JavaScript as pointed by Angel Joseph Piscola.
Hi this will add text to existing text in textarea
i have try that
var msg = "Hi How are you ?";
document.getElementById('myArea').value += msg;
I'm working on a Chrome extension that will extract a plain text url from a selection,
but the selection will always be a fraction of the url, so I need to scan left and right.
I basically need to extract 500 characters around the selection from innerText, since
I don't want to parse html elements, and also don't want to re-assemble textContent's.
Here's what I have so far, it's working quite well, but I feel there's a better way
that doesn't modify the document object and then restore it back to original state ...
// create a random 8 digit number - or bigger since we don't want duplicates
var randId = Math.floor(Math.random()*(90000000)+10000000);
var selection = document.getSelection();
var selectionRange = selection.getRangeAt(0);
// backup content, and then clone selection the user has made
var selectionContents = selectionRange.cloneContents();
var selectionContentsTxt = selectionContents.textContent;
// add random number to previous selection, this will expand selection
selectionContents.textContent = randId;
selectionRange.insertNode(selectionContents);
// find the random number in the document (in innerText in document.body)
var everything = document.body.innerText;
var offset = everything.indexOf(randId);
// restore backed up content to original state,- replace the selection
selectionContents = selectionRange.extractContents();
selectionContents.textContent = selectionContentsTxt;
selectionRange.insertNode(selectionContents);
// now we have the selection location offset in document.body.innerText
everything = document.body.innerText;
var extract = everything.substring(offset-250, offset+250);
extract holds the extracted text, with line breaks, etc, evaluated by the browser.
This will by the way be Chrome only so I don't need cross compatibility at all.
Now I can parse the selection - from middle to the edges to find the url,
note that starting the parse from middle is important so I didn't just extract
an innerText from a parentNode of my getSelection() without knowing the offsets ...
Is there a better way of doing this? maybe copying the entire document to a DOMParser,
but then how do I apply the getSelection from the original document to it ?
------ UPDATE ------
I've simplified the code above (forgot to mention I'm working with mouse clicks so I detect event
click, after a double click to select some text) and now it finds the caretOffset in
document.body.innerText but it still modifies the document and then restores it,
so still thinking of a better way to do this.
I'm wondering if insertNode and deleteContents is bad in some way ?
var randId = Math.floor(Math.random()*(90000000)+10000000);
var range = document.caretRangeFromPoint(event.clientX, event.clientY);
range.insertNode(document.createTextNode(randId));
var everything = document.body.innerText;
var offset = everything.indexOf(randId);
range.deleteContents();
var everything = document.body.innerText;
var extract = everything.substring(offset-250, offset+250);
document.getSelection().anchorNode.parentNode.normalize();
Any thoughts?
after skimming all possible questions and answers, i'll try it this way.
I'm programming an RTE, but didn't manage to successfully extract text in a contenteditable element.
The reason for this is, that each browser handles nodes and keypress (#13) events in a slightly different way (as ex.g. one creates 'br', the other 'div', 'p', etc.)
To keep this all consistent, I'm writing a cross-browser editor which kills all default action with e.preventDefault();
Following scenario:
1) User hits the #13 key (check)
2) Caret position detected (check)
3) create new p(aragraph) after the element the caret's in (check)
4) if text(node(s)) between caret and the element's end, extract it (? ? ?)
5) put text(node(s)) to newly created p(aragraph) (check)
As you can imagine, everything works except point 4.
There's a similar functionality in the well-known js rangy library, where a specific selection is being extracted.
What i need is following: Get and extract all text (with tags of course, so splitBoundaries) from the caret on to the end of the contenteditable paragraph (p, h1, h2, ...) element.
Any clues, hints or snippets are welcome!
Thanks in advance.
Kind regards,
p
You can do this by creating a Range object that encompasses the content from the caret to the end of the containing block element and calling its extractContents() method:
function getBlockContainer(node) {
while (node) {
// Example block elements below, you may want to add more
if (node.nodeType == 1 && /^(P|H[1-6]|DIV)$/i.test(node.nodeName)) {
return node;
}
node = node.parentNode;
}
}
function extractBlockContentsFromCaret() {
var sel = window.getSelection();
if (sel.rangeCount) {
var selRange = sel.getRangeAt(0);
var blockEl = getBlockContainer(selRange.endContainer);
if (blockEl) {
var range = selRange.cloneRange();
range.selectNodeContents(blockEl);
range.setStart(selRange.endContainer, selRange.endOffset);
return range.extractContents();
}
}
}
This won't work in IE <= 8, which doesn't support Range or the same selection object as other browsers. You can use Rangy (which you mentioned) to provide IE support. Just replace window.getSelection() with rangy.getSelection().
jsFiddle: http://jsfiddle.net/LwXvk/3/
Why would the below eliminate the whitespace around matched keyword text when replacing it with an anchor link? Note, this error only occurs in Chrome, and not firefox.
For complete context, the file is located at: http://seox.org/lbp/lb-core.js
To view the code in action (no errors found yet), the demo page is at http://seox.org/test.html. Copy/Pasting the first paragraph into a rich text editor (ie: dreamweaver, or gmail with rich text editor turned on) will reveal the problem, with words bunched together. Pasting it into a plain text editor will not.
// Find page text (not in links) -> doxdesk.com
function findPlainTextExceptInLinks(element, substring, callback) {
for (var childi= element.childNodes.length; childi-->0;) {
var child= element.childNodes[childi];
if (child.nodeType===1) {
if (child.tagName.toLowerCase()!=='a')
findPlainTextExceptInLinks(child, substring, callback);
} else if (child.nodeType===3) {
var index= child.data.length;
while (true) {
index= child.data.lastIndexOf(substring, index);
if (index===-1 || limit.indexOf(substring.toLowerCase()) !== -1)
break;
// don't match an alphanumeric char
var dontMatch =/\w/;
if(child.nodeValue.charAt(index - 1).match(dontMatch) || child.nodeValue.charAt(index+keyword.length).match(dontMatch))
break;
// alert(child.nodeValue.charAt(index+keyword.length + 1));
callback.call(window, child, index)
}
}
}
}
// Linkup function, call with various type cases (below)
function linkup(node, index) {
node.splitText(index+keyword.length);
var a= document.createElement('a');
a.href= linkUrl;
a.appendChild(node.splitText(index));
node.parentNode.insertBefore(a, node.nextSibling);
limit.push(keyword.toLowerCase()); // Add the keyword to memory
urlMemory.push(linkUrl); // Add the url to memory
}
// lower case (already applied)
findPlainTextExceptInLinks(lbp.vrs.holder, keyword, linkup);
Thanks in advance for your help. I'm nearly ready to launch the script, and will gladly comment in kudos to you for your assistance.
It's not anything to do with the linking functionality; it happens to copied links that are already on the page too, and the credit content, even if the processSel() call is commented out.
It seems to be a weird bug in Chrome's rich text copy function. The content in the holder is fine; if you cloneContents the selected range and alert its innerHTML at the end, the whitespaces are clearly there. But whitespaces just before, just after, and at the inner edges of any inline element (not just links!) don't show up in rich text.
Even if you add new text nodes to the DOM containing spaces next to a link, Chrome swallows them. I was able to make it look right by inserting non-breaking spaces:
var links= lbp.vrs.holder.getElementsByTagName('a');
for (var i= links.length; i-->0;) {
links[i].parentNode.insertBefore(document.createTextNode('\xA0 '), links[i]);
links[i].parentNode.insertBefore(document.createTextNode(' \xA0), links[i].nextSibling);
}
but that's pretty ugly, should be unnecessary, and doesn't fix up other inline elements. Bad Chrome!
var keyword = links[i].innerHTML.toLowerCase();
It's unwise to rely on innerHTML to get text from an element, as the browser may escape or not-escape characters in it. Most notably &, but there's no guarantee over what characters the browser's innerHTML property will output.
As you seem to be using jQuery already, grab the content with text() instead.
var isDomain = new RegExp(document.domain, 'g');
if (isDomain.test(linkUrl)) { ...
That'll fail every second time, because global regexps remember their previous state (lastIndex): when used with methods like test, you're supposed to keep calling repeatedly until they return no match.
You don't seem to need g (multiple matches) here... but then you don't seem to need regexp here either as a simple String indexOf would be more reliable. (In a regexp, each . in the domain would match any character in the link.)
Better still, use the URL decomposition properties on Location to do a direct comparison of hostnames, rather than crude string-matching over the whole URL:
if (location.hostname===links[i].hostname) { ...
// don't match an alphanumeric char
var dontMatch =/\w/;
if(child.nodeValue.charAt(index - 1).match(dontMatch) || child.nodeValue.charAt(index+keyword.length).match(dontMatch))
break;
If you want to match words on word boundaries, and case insensitively, I think you'd be better off using a regex rather than plain substring matching. That'd also save doing four calls to findText for each keyword as it is at the moment. You can grab the inner bit (in if (child.nodeType==3) { ...) of the function in this answer and use that instead of the current string matching.
The annoying thing about making regexps from string is adding a load of backslashes to the punctuation, so you'll want a function for that:
// Backslash-escape string for literal use in a RegExp
//
function RegExp_escape(s) {
return s.replace(/([/\\^$*+?.()|[\]{}])/g, '\\$1')
};
var keywordre= new RegExp('\\b'+RegExp_escape(keyword)+'\\b', 'gi');
You could even do all the keyword replacements in one go for efficiency:
var keywords= [];
var hrefs= [];
for (var i=0; i<links.length; i++) {
...
var text= $(links[i]).text();
keywords.push('(\\b'+RegExp_escape(text)+'\\b)');
hrefs.push[text]= links[i].href;
}
var keywordre= new RegExp(keywords.join('|'), 'gi');
and then for each match in linkup, check which match group has non-zero length and link with the hrefs[ of the same number.
I'd like to help you more, but it's hard to guess without being able to test it, but I suppose you can get around it by adding space-like characters around your links, eg. .
By the way, this feature of yours that adds helpful links on copying is really interesting.