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

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;
});
});

Related

Any way to prevent losing focus when clicking an input text out of tinymce container?

I've made this tinymce fiddle to show what I say.
Highlight text in the editor, then click on the input text, highlight in tinyMCE is lost (obviously).
Now, I know it's not easy since both, the inline editor and the input text are in the same document, thus, the focus is only one. But is there any tinymce way to get like an "unfocused" highlight (gray color) whenever I click in an input text?
I'm saying this because I have a customized color picker, this color picker has an input where you can type in the HEX value, when clicking OK it would execCommand a color change on the selected text, but it looks ugly because the highlight is lost.
I don't want to use an iframe, I know that by using the non-inline editor (iframe) is one of the solutions, but for a few reasons, i can't use an iframe text editor.
Any suggestion here? Thanks.
P.S: Out of topic, does any of you guys know why I can't access to tinymce object in the tinyMCE Fiddle ? looks like the tinyMCE global var was overwritten by the tinymce select dom element of the page itself. I can't execute a tinyMCE command lol.
Another solution:
http://fiddle.tinymce.com/sBeaab/5
P.S: Out of topic, does any of you guys know why I can't access to
tinymce object in the tinyMCE Fiddle ? looks like the tinyMCE global
var was overwritten by the tinymce select dom element of the page
itself. I can't execute a tinyMCE command lol.
Well, you can access the tinyMCE variable and even execute commands.
this line is wrong
var colorHex = document.getElementById("colorHex")
colorHex contains input element, not value.
var colorHex = document.getElementById("colorHex").value
now it works ( neolist couldn't load, so I removed it )
http://fiddle.tinymce.com/DBeaab/1
I had to do something similar recently.
First off, you can't really have two different elements "selected" simultaneously. So in order to accomplish this you're going to need to mimic the browser's built-in 'selected text highlight'. To do this, you're going to have to insert spans into the text to simulate highlighting, and then capture the mousedown and mouseup events.
Here's a fiddle from StackOverflow user "fullpipe" which illustrates the technique I used.
http://jsfiddle.net/fullpipe/DpP7w/light/
$(document).ready(function() {
var keylist = "abcdefghijklmnopqrstuvwxyz123456789";
function randWord(length) {
var temp = '';
for (var i=0; i < length; i++)
temp += keylist.charAt(Math.floor(Math.random()*keylist.length));
return temp;
}
for(var i = 0; i < 500; i++) {
var len = Math.round(Math.random() * 5 + 3);
document.body.innerHTML += '<span id="'+ i +'">' + randWord(len) + '</span> ';
}
var start = null;
var end = null;
$('body').on('mousedown', function(event) {
start = null;
end = null;
$('span.s').removeClass('s');
start = $(event.target);
start.addClass('s');
});
$('body').on('mouseup', function(event) {
end = $(event.target);
end.addClass('s');
if(start && end) {
var between = getAllBetween(start,end);
for(var i=0, len=between.length; i<len;i++)
between[i].addClass('s');
alert('You select ' + (len) + ' words');
}
});
});
function getAllBetween(firstEl,lastEl) {
var firstIdx = $('span').index($(firstEl));
var lastIdx = $('span').index($(lastEl));
if(lastIdx == firstIdx)
return [$(firstEl)];
if(lastIdx > firstIdx) {
var firstElement = $(firstEl);
var lastElement = $(lastEl);
} else {
var lastElement = $(firstEl);
var firstElement = $(lastEl);
}
var collection = new Array();
collection.push(firstElement);
firstElement.nextAll().each(function(){
var siblingID = $(this).attr("id");
if (siblingID != $(lastElement).attr("id")) {
collection.push($(this));
} else {
return false;
}
});
collection.push(lastElement);
return collection;
}
As you can see in the fiddle, the gibberish text in the right pane stays highlighted regardless of focus elsewhere on the page.
At that point, you're going to have to apply your color changes to all matching spans.

REGEX - Highlight part over 19 chars

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);
}

disable textarea by height not characters

So many times we want to limit how much a user can write, but here I have a special sized box that it has to fit in, so I want to disable adding more characters if it would surpass a specific height. here is what I did:
var over;
$('textarea').keypress(function(e){
var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;
var t = $(this).val();
jQuery('<div/>', {
style: "visibility:hidden",
text: t,
id: "test"
}).appendTo('body');
var h = $('#test').height();
if(h >= 100){
over = true;
}
else{
over = false;
}
if(over){
//code goes here
}
$('#test').remove();
});
I got the limiting code (what goes where I have the "code goes here" comment) from here and it ALMOST works.
There is only one problem:
if somebody copies and pastes, it can place multiple characters and therefore still go over the limit.
How can I fix this issue?
jsfiddle
Another somewhat hacky solution could be to check scroll on key up. If scroll exists, delete the last character while scroll exists:
function restrictScroll(e){
if(e.target.clientHeight<e.target.scrollHeight){
while(e.target.clientHeight<e.target.scrollHeight){
e.target.value = e.target.value.substring(0, e.target.value.length-1);
}
}
};
document.getElementById("textareaid").addEventListener('keyup', restrictScroll);
This would work as you type and if you paste blocks of text. Large text blocks may take a little longer to loop through though. In which case you may want to split on "\n" and remove lines first, then characters.
jsfiddle
If you want your function to fire whenever the text in your field changes, you can bind it to the input and propertychange events, as per this answer:
https://stackoverflow.com/a/5494697/20578
Like this:
$('#descrip').on('input propertychange', function(e){
This will make sure your code fires when e.g. the user pastes in content using the mouse.
As for stopping them from entering content that would go over the limit, I think you have to keep track of what content they've entered yourself, and then revert their last edit if it infringed your criteria.
Note that e.g. Twitter doesn't stop the user from entering more characters if they've gone over the limit - they just tell the user they're over the limit, and tell them when they're back under. That might be the most usable design.
You may try this:
$('#descrip').bind('paste',function(e) {
var el = $(this);
setTimeout(function() {
//Get text after pasting
var text = $(el).val();
//wath yu want to do
}, 100);
};
Jsfiddle
The solution is taken from here and here. It works by binding to the paste event. Since paste event is fired before the text is pasted, you need to use a setTimeout to catch the text after pasting. There is still some rough edges (i.e. if you select text and press backspace, it does not update).
Still, Spudley comment has some valid points, that you may want to consider.
Edit:
Note on the jsfiddle: It allow you to go over the limit when pasting, but once over the limits, you cannot paste (or type) more text until you go under the limit again.
Must be taken into account that, since you are limiting the text length by the size it ocuppies after rendering (wich have it own issues as pointed by Spudley), and not a defined lenth, you can know if a text fits or not, but not know how much of the text is inside the limits, and how much is out of them.
You may consider reseting textbox value to its previous value if pasted text makes imput go over the limit, like in this one.
However, for cutting down the text after pasting so as non-fitting text is left out, but the rest of the pasted text is allowed, you need an entirely different approach. You may try iterating over the text until you find how much of the new text is enough.
By the way, line feeds and seems to cause your original script to behave weirdly.
I've been able to get the program working:
var over = false;
var num = 0;
var valid_entry = "";
$('#descrip').keyup(function(e){
var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;
var t = $(this).val();
getMaxRow(t,key,this);
});
function getMaxRow(t,key,obj) {
jQuery('<div/>', {
"class": "hide",
text: t,
id: "test"
}).appendTo('body');
var h = $('#test').height();
$('#test').remove();
if(h >= 100){
num += 1;
if(num == 1){
var div = '<div id="message">You have run out of room</div>'
$('body').append(div);
}
over = true;
}
else{
over = false;
valid_entry = t;
if(num >= 1){
$('#message').remove();
num = 0;
}
}
if( over ) {
//Do this for copy and paste event:
while ( over ) {
//Try using a substring here
t = t.substr(0,(t.length-1));
over = getMaxRow(t,key,obj);
}
}
$(obj).val(valid_entry);
return over;
}

How to change the text direction of an element?

On Facebook, for example, if you have chosen the Arabic language for your keyboard, the textbox automatically gets RTL direction.
How can I implement this on my web application? I don't know the method or property used.
You can use the CSS direction property to achieve this:
input{
direction: rtl;
}
Update
To change the direction of the text dynamically based on the user input you can check the first character of input to see if it meets a given criteria, in this case a regular expression.
$('input').keyup(function(){
$this = $(this);
if($this.val().length == 1)
{
var x = new RegExp("[\x00-\x80]+"); // is ascii
//alert(x.test($this.val()));
var isAscii = x.test($this.val());
if(isAscii)
{
$this.css("direction", "ltr");
}
else
{
$this.css("direction", "rtl");
}
}
});
This is a basic example that uses ltr direction for ascii text and rtl for everything else.
Here's a working example.
In HTML5 you can simply do this:
<input type="text" dir="auto" />
this automatically change the direction of input base on first character.
Hope it helps.
NOTE:
Edited: People say that it does not work in IE11.
it's best to detect text direction based on first character of the text. if the first one belongs to RTL language, then direction has to change.
example, a Persian text with English word in it.
به معنای واقعی tired هستم
another example, an English paragraph might contain a Persian word but the whole text has to be in LTR.
this word is used in different situations. the meaning of شیر is...
this function will check the first character typed in. if it belongs to a RTL language, then direction will change. This code supports all RTL languages.
function isRTL(str) {
var letters = [];
allRTL = new RegExp(
"^[\u0590-\u05fe\u0600-۾܀-ݎݐ-ݾހ-\u07be߀-\u07fe\u0800-\u083e\u0840-\u085e\u08a0-\u08fe\u0900-ॾ]|\ud802[\udf60-\udf7e]+$"
);
var cursor = 1;
for (var i = 0; i <= cursor; i++) {
letters[i] = str.substring(i - 1, i);
if(/\s/.test(letters[i])) {
cursor++;
}
if (allRTL.test(letters[i])) {
return true;
}
}
return false;
}
var dir = $("input[type=text]");
dir.keyup(function(e) {
if (isRTL(dir.val())) {
$(this).css("direction", "rtl");
} else {
$(this).css("direction", "ltr");
}
});
for more info visit this codepen.
For input element you can use Unicode intrinsic direction
input {
text-align: start;
unicode-bidi: plaintext;
}
Of course, it works with other elements such as div with contenteditable as true as mentioned by comments.
DISCLAIMER as stated by #bool3max: Many languages utilize Unicode characters and are written
LTR. Your function sets direction: rtl for all non-ASCII characters,
which isn't desired.
I have found also a Pen that does exactly the same without Regex. I have tweaked the code a bit to be compatible with multiple input fields and textareas.
It serves the purpose of using it between; for example; EN and AR languages.
$('input[type="text"], textarea').each(function () {
$(this).on('input keyup keypress', function () {
if (isUnicode($(this).val())) {
$(this).css('direction', 'rtl');
} else {
$(this).css('direction', 'ltr');
}
});
});
function isUnicode(str) {
var letters = [];
for (var i = 0; i <= str.length; i++) {
letters[i] = str.substring((i - 1), i);
if (letters[i].charCodeAt() > 255) { return true; }
}
return false;
}

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

Categories

Resources