REGEX - Highlight part over 19 chars - javascript

Hi,
I have some text inside div[contenteditable="true"] and I should highlight (span.tooLong) part which goes over the 19 character limit. Content in div may have HTML tags or entities and those should be ignored when counting to 19.
Twitter has similar way to highlight too long tweet:
Examples:
This is text ⇨ This is text
This is just too long text ⇨ This is just too lo<span class="tooLong">ng text</span>
This <b>text</b> has been <i>formatted</i> with HTML ⇨ This <b>text</b> has been <span class="tooLong"><i>formatted</i> with HTML</span>
How can I implement this in JavaScript?
(I want to use regular expressions as much as possible)

Okay... here's some code that I think will work for you, or at least get your started.
Basically, the regex you need to find everything over 19 characters is this:
var extra = content.match(/.{19}(.*)/)[1];
So, I put together a sample document of how you might use this.
Take a look at the DEMO.
Here's the Javascript I'm using (I'm using jQuery for the locators here, but this can easily be modified to use straight Javascript... I just prefer jQuery for stuff like this)...
$(document).ready(function() {
$('#myDiv').keyup(function() {
var content = $('#myDiv').html();
var extra = content.match(/.{19}(.*)/)[1];
$('#extra').html(extra);
var newContent = content.replace(extra, "<span class='highlight'>" + extra + "</span>");
$('#sample').html(newContent);
});
});
Basically, I have three DIVs setup. One for you to enter your text. One to show what characters are over the 19 character limit. And one to show how you might highlight the extra characters.
My code sample does not check for html tags, as there are too many to try and handle... but should give you a great starting point as to how this might work.
NOTE: you can view the complete code I wrote using this link: http://jsbin.com/OnAxULu/1/edit

Here's an answer that uses my Rangy library. It uses the Class Applier and TextRange modules to apply styling on character ranges within the editable content while preserving the selection. It also uses a configurable debounce interval to prevent sluggishness in editor performance. Also, it should work on old IE.
Demo: http://jsfiddle.net/timdown/G4jn7/2/
Sample code:
var characterLimit = 40;
var debounceInterval = 200;
function highlightExcessCharacters() {
// Bookmark selection so we can restore it later
var sel = rangy.getSelection();
var savedSel = sel.saveCharacterRanges(editor);
// Remove previous highlight
var range = rangy.createRange();
range.selectNodeContents(editor);
classApplier.undoToRange(range);
// Store the total number of characters
var editorCharCount = range.text().length;
// Create a range selecting the excess characters
range.selectCharacters(editor, characterLimit, editorCharCount);
// Highlight the excess
classApplier.applyToRange(range);
// Restore the selection
sel.restoreCharacterRanges(editor, savedSel);
}
var handleEditorChangeEvent = (function() {
var timer;
function debouncer() {
if (timer) {
timer = null;
}
highlightExcessCharacters();
}
return function() {
if (timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(debouncer, debounceInterval);
};
})();
function listen(target, eventName, listener) {
if (target.addEventListener) {
target.addEventListener(eventName, listener, false);
} else if (target.attachEvent) {
target.attachEvent("on" + eventName, listener);
}
}
rangy.init();
var editor = document.getElementById("editor");
var classApplier = rangy.createClassApplier("overrun");
// Set up debounced event handlers
var editEvents = ["input", "keydown", "keypress", "keyup",
"cut", "copy", "paste"];
for (var i = 0, eventName; eventName = editEvents[i++]; ) {
listen(editor, eventName, handleEditorChangeEvent);
}

Related

Split div content at cursor position

I need to split an element after user clicks on it and the attr 'contenteditable' becomes 'true'. This fiddle works for the first paragraph but not second because the latter is in a p tag. Similary in this fiddle you will see that when the element has html tags in it, the counter loses accuracy and hence the text before and after the cursor is not what you'd expect.
The assumption here is that the users will split the data in a way that the help tags will stay intact. As pointed out by dandavis here, e.g. the div has <i>Hello</i> <b>Wo*rld</b>, the user will only need to split the div into two divs, first will have <i>Hello</i> and the second div will have <b>Wo*rld</b> in it.
Html:
<div><mark>{DATE}</mark><i>via email: </i><mark><i>{EMAIL- BROKER OR TENANT}</i></mark></div>
JS:
var $splitbut = $('<p class="split-but">Split</p>');
$(this).attr('contenteditable', 'true').addClass('editing').append($splitbut);
var userSelection;
if (window.getSelection) {
userSelection = window.getSelection();
}
var start = userSelection.anchorOffset;
var end = userSelection.focusOffset;
var before = $(this).html().substr(0, start);
var after = $(this).html().substr(start, $(this).html().length);
The "Split" button is not working as generating the html is not an issue once I get proper "after" and "before" text. Any ideas as to what I am doing wrong here?
Something like this could work for the specific case you describe
$('div, textarea').on('click', function(e) {
var userSelection;
if (window.getSelection) {
userSelection = window.getSelection();
}
var start = userSelection.anchorOffset,
end = userSelection.focusOffset,
node = userSelection.anchorNode,
allText = $(this).text(),
nodeText = $(node).text();
// before and after inside node
var nodeBefore = nodeText.substr(0, start);
var nodeAfter = nodeText.substr(start, nodeText.length);
// before and after for whole of text
var allExceptNode = allText.split(nodeText),
before = allExceptNode[0] + nodeBefore,
after = nodeAfter + allExceptNode[1];
console.log('Before: ', before);
console.log('------');
console.log('After: ', after);
});
Updated demo at https://jsfiddle.net/gaby/vaLz55fv/10/
It might exhibit issues if there are tags whose content is repeated in the whole text. (problem due to splitting)

Change text direction (ltr and rtl) of wysiwyg on fly

I use nicEdit as wysiwig editor on a website in Persian language.
The text of the contents are sometimes mix of Persian and English words,
Persian is an right to left language, so the default direction of the editor is right to left. Then when one types an English word in the middle of the text the rtl directionality applies on it and causes misreading
for example
C# will be written as #C
a solution for it is to surround the word (for example C#) with an span tag which its direction be ltr C#
but how can i achieve it?
I thought in the editor keypress event, check if the entered key is English replace it with the span mentioned above, but I dont know how to do it
in other words you can think of this problem as changing the background color of the typed text on fly
in the following I wrote what I am trying however it doesnt work yet
$('body').on('keypress','.nicEdit-main', function(e) {
var c= String.fromCharCode(e.which);
var isWordcharacter = c.match(/\w/);
if (isWordcharacter && !en)
{
en = true;
nicEditor.nicCommand('insertHTML', '<span dir="ltr" style="direction:ltr; background-color:#eee">');
}
else if (!isWordcharacter)
{
en = false;
// need to close or escape the current span or create a new one with the opposite direction but nor works
nicEditor.nicCommand('insertHTML', '<span dir="rtl" style="direction:rtl;>');
}
});
the problem is in the else I should escape the current span
I prepared small fiddle: http://jsfiddle.net/br8qt320/
This is pretty rough example that shows different background for letters and numbers. You need to fine-tune it a little bit but it should work quite well. Note that I do not know NicEditor and maybe there is a better approach for this library.
Code from fiddle:
$(document).ready(function() {
nicEditors.allTextAreas();
var ltrSpan = $('<span style="background-color:#00ff00">');
var rtlSpan = $('<span style="background-color:#ff0000">');
var currentSpan = null;
var isLtr = true;
$('body').on('keydown','.nicEdit-main', function(e) {
var editorArea = $(this);
var currentChar = String.fromCharCode(e.keyCode || e.which);
var isNumeric = currentChar.match(/\d/);
if (isNumeric) {
if (!isLtr) {
isLtr = true;
currentSpan = ltrSpan.clone();
currentSpan.appendTo(editorArea);
}
} else {
if (isLtr) {
isLtr = false;
currentSpan = rtlSpan.clone();
currentSpan.appendTo(editorArea);
}
}
currentSpan.append(currentChar);
return false;
});
});

Replacing contenteditable characters on the fly with (or without) rangy

I'm working on a little experimental editor where I would like to visualize the time between typed characters. Therefore I'm using javascript and a contenteditable div to wrap every character with a SPAN and a timestamp attribute. I build a little function with the help of rangy:
function insertAtCursor(char, timestamp) {
var sel = rangy.getSelection();
var range = sel.rangeCount ? sel.getRangeAt(0) : null;
if (range) {
var el = document.createElement("span");
$(el).attr('time', timestamp);
el.appendChild(document.createTextNode(char));
range.insertNode(el);
range.setStartAfter(el);
rangy.getSelection().setSingleRange(range);
}
}
Now I'm facing two problems with this concept where I would appreciate some help:
a. With the above function the output ends in nested span's like seen here:
<span time="12345">a
<span time="12345">b
<span time="12345">c</span>
</span>
</span>
b. Even if I could get the above function running, a copy&paste or drag&drop action would possibly also end in some nested span's ... and I wonder if there is a way to avoid that at all?
Thanks,
Andreas
I'm not convinced this a good idea overall, particularly if the text could get large. A couple of improvements:
time should probably be data-time to validate as HTML5
you need to handle the case where some content is selected (adding range.deleteContents() would do).
However, if you are going to do this, I would suggest checking if the cursor is at the end of a text node inside an existing <span> and appending the new <span> after the text node's parent. Something like this:
Live demo: http://jsfiddle.net/kWL82/1/
Code:
function insertAtCursor(char, timestamp) {
var sel = rangy.getSelection();
var range = sel.rangeCount ? sel.getRangeAt(0) : null;
var parent;
if (range) {
var el = document.createElement("span");
$(el).attr('data-time', timestamp);
el.appendChild(document.createTextNode(char));
// Check if the cursor is at the end of the text in an existing span
if (range.endContainer.nodeType == 3
&& (parent = range.endContainer.parentNode)
&& (parent.tagName == "SPAN")) {
range.setStartAfter(parent);
}
range.insertNode(el);
range.setStartAfter(el);
rangy.getSelection().setSingleRange(range);
}
}

Tag-like autocompletion and caret/cursor movement in contenteditable elements

I'm working on a jQuery plugin that will allow you to do #username style tags, like Facebook does in their status update input box.
My problem is, that even after hours of researching and experimenting, it seems REALLY hard to simply move the caret. I've managed to inject the <a> tag with someone's name, but placing the caret after it seems like rocket science, specially if it's supposed work in all browsers.
And I haven't even looked into replacing the typed #username text with the tag yet, rather than just injecting it as I'm doing right now... lol
There's a ton of questions about working with contenteditable here on Stack Overflow, and I think I've read all of them, but they don't really cover properly what I need. So any more information anyone can provide would be great :)
You could use my Rangy library, which attempts with some success to normalize browser range and selection implementations. If you've managed to insert the <a> as you say and you've got it in a variable called aElement, you can do the following:
var range = rangy.createRange();
range.setStartAfter(aElement);
range.collapse(true);
var sel = rangy.getSelection();
sel.removeAllRanges();
sel.addRange(range);
I got interested in this, so I've written the starting point for a full solution. The following uses my Rangy library with its selection save/restore module to save and restore the selection and normalize cross browser issues. It surrounds all matching text (#whatever in this case) with a link element and positions the selection where it had been previously. This is triggered after there has been no keyboard activity for one second. It should be quite reusable.
function createLink(matchedTextNode) {
var el = document.createElement("a");
el.style.backgroundColor = "yellow";
el.style.padding = "2px";
el.contentEditable = false;
var matchedName = matchedTextNode.data.slice(1); // Remove the leading #
el.href = "http://www.example.com/?name=" + matchedName;
matchedTextNode.data = matchedName;
el.appendChild(matchedTextNode);
return el;
}
function shouldLinkifyContents(el) {
return el.tagName != "A";
}
function surroundInElement(el, regex, surrounderCreateFunc, shouldSurroundFunc) {
var child = el.lastChild;
while (child) {
if (child.nodeType == 1 && shouldSurroundFunc(el)) {
surroundInElement(child, regex, surrounderCreateFunc, shouldSurroundFunc);
} else if (child.nodeType == 3) {
surroundMatchingText(child, regex, surrounderCreateFunc);
}
child = child.previousSibling;
}
}
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
var parent = textNode.parentNode;
var result, surroundingNode, matchedTextNode, matchLength, matchedText;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchedText = result[0];
matchLength = matchedText.length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
parent.insertBefore(surroundingNode, matchedTextNode);
parent.removeChild(matchedTextNode);
}
}
function updateLinks() {
var el = document.getElementById("editable");
var savedSelection = rangy.saveSelection();
surroundInElement(el, /#\w+/, createLink, shouldLinkifyContents);
rangy.restoreSelection(savedSelection);
}
var keyTimer = null, keyDelay = 1000;
function keyUpLinkifyHandler() {
if (keyTimer) {
window.clearTimeout(keyTimer);
}
keyTimer = window.setTimeout(function() {
updateLinks();
keyTimer = null;
}, keyDelay);
}
HTML:
<p contenteditable="true" id="editable" onkeyup="keyUpLinkifyHandler()">
Some editable content for #someone or other
</p>
As you say you can already insert an tag at the caret, I'm going to start from there. The first thing to do is to give your tag an id when you insert it. You should then have something like this:
<div contenteditable='true' id='status'>I went shopping with <a href='#' id='atagid'>Jane</a></div>
Here is a function that should place the cursor just after the tag.
function setCursorAfterA()
{
var atag = document.getElementById("atagid");
var parentdiv = document.getElementById("status");
var range,selection;
if(window.getSelection) //FF,Chrome,Opera,Safari,IE9+
{
parentdiv.appendChild(document.createTextNode(""));//FF wont allow cursor to be placed directly between <a> tag and the end of the div, so a space is added at the end (this can be trimmed later)
range = document.createRange();//create range object (like an invisible selection)
range.setEndAfter(atag);//set end of range selection to just after the <a> tag
range.setStartAfter(atag);//set start of range selection to just after the <a> tag
selection = window.getSelection();//get selection object (list of current selections/ranges)
selection.removeAllRanges();//remove any current selections (FF can have more than one)
parentdiv.focus();//Focuses contenteditable div (necessary for opera)
selection.addRange(range);//add our range object to the selection list (make our range visible)
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createRange();//create a "Text Range" object (like an invisible selection)
range.moveToElementText(atag);//select the contents of the a tag (i.e. "Jane")
range.collapse(false);//collapse selection to end of range (between "e" and "</a>").
while(range.parentElement() == atag)//while ranges cursor is still inside <a> tag
{
range.move("character",1);//move cursor 1 character to the right
}
range.move("character",-1);//move cursor 1 character to the left
range.select()//move the actual cursor to the position of the ranges cursor
}
/*OPTIONAL:
atag.id = ""; //remove id from a tag
*/
}
EDIT:
Tested and fixed script. It definitely works in IE6, chrome 8, firefox 4, and opera 11. Don't have other browsers on hand to test, but it doesn't use any functions that have changed recently so it should work in anything that supports contenteditable.
This button is handy for testing:
<input type='button' onclick='setCursorAfterA()' value='Place Cursor After <a/> tag' >
Nico

Keypress in jQuery: Press TAB inside TEXTAREA (when editing an existing text)

I want to insert TAB characters inside a TEXTAREA, like this:
<textarea>{KEYPRESS-INSERTS-TAB-HERE}Hello World</textarea>
I can insert before/after the existing TEXTAREA text - and I can insert / replace all text in the TEXTAREA - but have not yet been able to insert inside the existing TEXTAREA text (by the cursor) in a simple way.
$('textarea:input').live('keypress', function(e) {
if (e.keyCode == 9) {
e.preventDefault();
// Press TAB to append a string (keeps the original TEXTAREA text).
$(this).append("TAB TAB TAB AFTER TEXTAREA TEXT");
// Press TAB to append a string (keeps the original TEXTAREA text).
$(this).focus().prepend("TAB TAB TAB BEFORE TEXTAREA TEXT");
// Press TAB to replace a all text inside TEXTAREA.
$(this).val("INSERT INTO TEXTAREA / REPLACE EXISTING TEXT");
}
});
There is a "tabs in textarea" plug-in for jQuery ("Tabby") - but it's 254 code lines - I was hoping for just a few lines of code.
A few links that I studied: (again, I would prefer fewer code lines).
http://www.dynamicdrive.com/forums/showthread.php?t=34452
http://www.webdeveloper.com/forum/showthread.php?t=32317
http://pallieter.org/Projects/insertTab/
Please advise. Thanks.
I was creating a AJAX powered simple IDE for myself so I can rapidly test out PHP snippets.
I remember stumbling upon the same problem, here's how I solved it:
$('#input').keypress(function (e) {
if (e.keyCode == 9) {
var myValue = "\t";
var startPos = this.selectionStart;
var endPos = this.selectionEnd;
var scrollTop = this.scrollTop;
this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos,this.value.length);
this.focus();
this.selectionStart = startPos + myValue.length;
this.selectionEnd = startPos + myValue.length;
this.scrollTop = scrollTop;
e.preventDefault();
}
});
#input is the ID of the textarea.
The code is not completely mine, I found it on Google somewhere.
I've only tested it on FF 3.5 and IE7. It does not work on IE7 sadly.
Unfortunately, manipulating the text inside textarea elements is not as simple as one might hope. The reason that Tabby is larger than those simple snippets is that it works better. It has better cross-browser compatibility and handles things like tabbing selections.
When minified, it's only about 5k. I'd suggest using it. You'll either have to discover and troubleshoot those same edge cases yourself anyway, or might not even know about them if users don't report them.
Yeah, dealing with input field selections across the different browsers is an annoyance, especially as in IE there are a few methods that look like they should work but actually don't. (Notably, combining using setEndPoint then measuring length, which looks OK until the selection starts or ends in newlines.)
Here's a couple of utility functions I use to deal with input selections. It returns the value of the input split into bits that are before, inside and after the selection (with the selection counting as an empty string at the input focus position if it's not a selection). This makes it fairly simply to replace and insert content at the point you want, whilst taking care of the IE CRLF problem.
(There may be a jQuery that does something like this, but I have yet to meet one.)
// getPartitionedValue: for an input/textarea, return the value text, split into
// an array of [before-selection, selection, after-selection] strings.
//
function getPartitionedValue(input) {
var value= input.value;
var start= input.value.length;
var end= start;
if (input.selectionStart!==undefined) {
start= input.selectionStart;
end= input.selectionEnd;
} else if (document.selection!==undefined) {
value= value.split('\r').join('');
start=end= value.length;
var range= document.selection.createRange();
if (range.parentElement()===input) {
var start= -range.moveStart('character', -10000000);
var end= -range.moveEnd('character', -10000000);
range.moveToElementText(input);
var error= -range.moveStart('character', -10000000);
start-= error;
end-= error;
}
}
return [
value.substring(0, start),
value.substring(start, end),
value.substring(end)
];
}
// setPartitionedValue: set the value text and selected region in an input/
// textarea.
//
function setPartitionedValue(input, value) {
var oldtop= input.scrollTop!==undefined? input.scrollTop : null;
input.value= value.join('');
input.focus();
var start= value[0].length;
var end= value[0].length+value[1].length;
if (input.selectionStart!==undefined) {
input.selectionStart= start;
input.selectionEnd= end;
if (oldtop!==null)
input.scrollTop= oldtop;
}
else if (document.selection!==undefined) {
var range= input.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
range.select();
}
}
btw, see also:
http://aspalliance.com/346_Tabbing_in_the_TextArea

Categories

Resources