YUI Editor (RTE): Insert HTML element and place cursor inside - javascript

I have a problem. I've been trying to tackle it for a while now and I'm ready to explode. Here's my requirement:
I have an external toolbar (not part of YUI) above the editor that I want to use to insert HTML tags. The user should be able to click a link on the toolbar after which a few things may happen:
If there's any selected text, this text gets wrapped into an HTML tag
If there's no selected text, an empty HTML tag is inserted in the editor
Regardless of the scenario, cursor MUST be placed inside the new element, so that when the user enters more text, it resides in the new element
The functionality is very similar to that of pressing "B" or "U" buttons on the editor's toolbar (now that I'm using this editor, it also does it well :-)). It preserves everything nicely. So far I'm able to do 1 or 2, but not 3. Step 3 is VERY important, because without it, user experience greatly suffers. I really need your assistance to implement it. Below is a simplified version of the method that performs the insertion (just inserting DIV for the sake of simplicity). this._oEditor - local instance of YUI Editor:
this._insertElement = function() {
var sSelection = this._oEditor._getSelection(); // Attempt to get selected text from the editor
if (sSelection == '') sSelection = ' '; // If nothing was selected, insert a non-breaking space
var sNewElt = '<div>' + sSelection + '</div>';
this._oEditor.execCommand('inserthtml', sNewElt);
this._oEditor.focus(); // This gives the editor focus, but places cursor in the beginning!
}
What is it that I must do to place the cursor in the right position? Is it even possible? Also, if there's a better way of implementing this, I'm all for it. Thank you!

Here's complete the solution:
this._insertElement = function() {
var sSelection = this._oEditor._getSelection();
if (sSelection == '') sSelection = ' ';
var sNewElt = '<div>' + sSelection + '</div>';
this._oEditor.execCommand('inserthtml', sNewElt);
var pos = 1000; //pos determines where to place the cursor. if greater than the length of characters, it will place at the end.
if(this._oEditor.createTextRange) { //IE Specific code
var range = this._oEditor.createTextRange();
range.move("character", pos);
range.select();
} else if(this._oEditor.selectionStart) { //Works with Firefox and other browsers
this._oEditor.focus();
this._oEditor.setSelectionRange(pos, pos);
}
this._oEditor.focus();
}

Placing the cursor requires different methods for different browsers. With IE you'll want to create a TextRange object, in Mozilla you can make use of window.find() or a Selection object, webkit/safari/chrome require yet another method.

Related

Trying to create new custom HTML elements which perform action in JQuery

I am trying to create my own custom HTML elements where a user can interact with the text within that element. For Example, I created an element where anything between those tags will have a pointer as a mouse cursor and when double clicked, something happens. EG:
<objdc>Double click me!</objdc>
However, this is my code and it is not working:
$(document).ready(function() {
var ObjDblClk = $('objdc');
ObjDblClk.css({ cursor: 'pointer' });
ObjDblClk.dblclick(function(e) {
var range = window.getSelection() || document.getSelection() || document.selection.createRange();
var word = $.trim(range.toString());
if(word != '') {
//Do Something
}
range.collapse();
e.stopPropagation();
});
});
}
Any suggestions?
The problem you have is related with the fact you are not using the collapse method right. It expects a node as parameter and an offset.
So... to fix that exact behavior you posted you would need to do something like:
ObjDblClk.dblclick(function(e) {
var range = window.getSelection() || document.getSelection() || document.selection.createRange();
var word = $.trim(range.toString());
if(word != '') {
//Do Something
}
range.collapse(ObjDblClk[0], 0);
e.stopPropagation();
});
BUT (and this is important): That will do absolutely nothing for your custom selection (especially since is on double click witch affects selection). So you can just remove that line completely and try another solution.
Also: You should read the comments. The guys are right. Unless you are working on some reall strange inhouse thing there may be better aproaches.
Fiddle here (added an alert so you see the function is called - don't forget to select something before double clicking): https://jsfiddle.net/713ndkm0/1/
To create a custom tag like that, you have to be aware of certain things:
Not all browsers will understand your custom tag as a DOM object. IE is a notable example.
Your new custom tag should have a hyphen in it, like obj-dc (more info).
If you want to use it in IE, you have to declare it up-front, as:
document.createElement('obj-dc');
Here is a link to creating new HTML tags for Chrome, in the new way, and here is a link for the older API. As you can see, even the same browser cannot operate with custom tags consistently.

How to get cursor HTML offset in contenteditable? [duplicate]

I have a contenteditable div as follow (| = cursor position):
<div id="mydiv" contenteditable="true">lorem ipsum <spanclass="highlight">indol|or sit</span> amet consectetur <span class='tag'>adipiscing</span> elit</div>
I would like to get the current cursor position including html tags. My code :
var offset = document.getSelection().focusOffset;
Offset is returning 5 (full text from the last tag) but i need it to handle html tags. The expected return value is 40. The code has to work with all recents browsers.
(i also checked this : window.getSelection() offset with HTML tags? but it doesn't answer my question).
Any ideas ?
Another way to do it is by adding a temporary marker in the DOM and calculating the offset from this marker. The algorithm looks for the HTML serialization of the marker (its outerHTML) within the inner serialization (the innerHTML) of the div of interest. Repeated text is not a problem with this solution.
For this to work, the marker's serialization must be unique within its div. You cannot control what users type into a field but you can control what you put into the DOM so this should not be difficult to achieve. In my example, the marker is made unique statically: by choosing a class name unlikely to cause a clash ahead of time. It would also be possible to do it dynamically, by checking the DOM and changing the class until it is unique.
I have a fiddle for it (derived from Alvaro Montoro's own fiddle). The main part is:
function getOffset() {
if ($("." + unique).length)
throw new Error("marker present in document; or the unique class is not unique");
// We could also use rangy.getSelection() but there's no reason here to do this.
var sel = document.getSelection();
if (!sel.rangeCount)
return; // No ranges.
if (!sel.isCollapsed)
return; // We work only with collapsed selections.
if (sel.rangeCount > 1)
throw new Error("can't handle multiple ranges");
var range = sel.getRangeAt(0);
var saved = rangy.serializeSelection();
// See comment below.
$mydiv[0].normalize();
range.insertNode($marker[0]);
var offset = $mydiv.html().indexOf($marker[0].outerHTML);
$marker.remove();
// Normalizing before and after ensures that the DOM is in the same shape before
// and after the insertion and removal of the marker.
$mydiv[0].normalize();
rangy.deserializeSelection(saved);
return offset;
}
As you can see, the code has to compensate for the addition and removal of the marker into the DOM because this causes the current selection to get lost:
Rangy is used to save the selection and restore it afterwards. Note that the save and restore could be done with something lighter than Rangy but I did not want to load the answer with minutia. If you decide to use Rangy for this task, please read the documentation because it is possible to optimize the serialization and deserialization.
For Rangy to work, the DOM must be in exactly the same state before and after the save. This is why normalize() is called before we add the marker and after we remove it. What this does is merge immediately adjacent text nodes into a single text node. The issue is that adding a marker to the DOM can cause a text node to be broken into two new text nodes. This causes the selection to be lost and, if not undone with a normalization, would cause Rangy to be unable to restore the selection. Again, something lighter than calling normalize could do the trick but I did not want to load the answer with minutia.
EDIT: This is an old answer that doesn't work for OP's requirement of having nodes with the same text. But it's cleaner and lighter if you don't have that requirement.
Here is one option that you can use and that works in all major browsers:
Get the offset of the caret within its node (document.getSelection().anchorOffset)
Get the text of the node in which the caret is located (document.getSelection().anchorNode.data)
Get the offset of that text within #mydiv by using indexOf()
Add the values obtained in 1 and 3, to get the offset of the caret within the div.
The code would look like this for your particular case:
var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = $("#mydiv").html().indexOf( text );
offsetCaret = textOffset + offset;
You can see a working demo on this JSFiddle (view the console to see the results).
And a more generic version of the function (that allows to pass the div as a parameter, so it can be used with different contenteditable) on this other JSFiddle:
function getCaretHTMLOffset(obj) {
var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = obj.innerHTML.indexOf( text );
return textOffset + offset;
}
About this answer
It will work in all recent browsers as requested (tested on Chrome 42, Firefox 37, and Explorer 11).
It is short and light, and doesn't require any external library (not even jQuery)
Issue: If you have different nodes with the same text, it may return the offset of the first occurrence instead of the real position of the caret.
NOTE: This solution works even in nodes with repeated text, but it detects html entities (e.g.: ) as only one character.
I came up with a completely different solution based on processing the nodes. It is not as clean as the old answer (see other answer), but it works fine even when there are nodes with the same text (OP's requirement).
This is a description of how it works:
Create a stack with all the parent elements of the node in which the caret is located.
While the stack is not empty, traverse the nodes of the containing element (initially the content editable div).
If the node is not the same one at the top of the stack, add its size to the offset.
If the node is the same as the one at the top of the stack: pop it from the stack, go to step 2.
The code is like this:
function getCaretOffset(contentEditableDiv) {
// read the node in which the caret is and store it in a stack
var aux = document.getSelection().anchorNode;
var stack = [ aux ];
// add the parents to the stack until we get to the content editable div
while ($(aux).parent()[0] != contentEditableDiv) { aux = $(aux).parent()[0]; stack.push(aux); }
// traverse the contents of the editable div until we reach the one with the caret
var offset = 0;
var currObj = contentEditableDiv;
var children = $(currObj).contents();
while (stack.length) {
// add the lengths of the previous "siblings" to the offset
for (var x = 0; x < children.length; x++) {
if (children[x] == stack[stack.length-1]) {
// if the node is not a text node, then add the size of the opening tag
if (children[x].nodeType != 3) { offset += $(children[x])[0].outerHTML.indexOf(">") + 1; }
break;
} else {
if (children[x].nodeType == 3) {
// if it's a text node, add it's size to the offset
offset += children[x].length;
} else {
// if it's a tag node, add it's size + the size of the tags
offset += $(children[x])[0].outerHTML.length;
}
}
}
// move to a more inner container
currObj = stack.pop();
children = $(currObj).contents();
}
// finally add the offset within the last node
offset += document.getSelection().anchorOffset;
return offset;
}
You can see a working demo on this JSFiddle.
About this answer:
It works in all major browsers.
It is light and doesn't require external libraries (apart from jQuery)
It has an issue: html entities like are counted as one character only.

javascript : focusOffset with html tags

I have a contenteditable div as follow (| = cursor position):
<div id="mydiv" contenteditable="true">lorem ipsum <spanclass="highlight">indol|or sit</span> amet consectetur <span class='tag'>adipiscing</span> elit</div>
I would like to get the current cursor position including html tags. My code :
var offset = document.getSelection().focusOffset;
Offset is returning 5 (full text from the last tag) but i need it to handle html tags. The expected return value is 40. The code has to work with all recents browsers.
(i also checked this : window.getSelection() offset with HTML tags? but it doesn't answer my question).
Any ideas ?
Another way to do it is by adding a temporary marker in the DOM and calculating the offset from this marker. The algorithm looks for the HTML serialization of the marker (its outerHTML) within the inner serialization (the innerHTML) of the div of interest. Repeated text is not a problem with this solution.
For this to work, the marker's serialization must be unique within its div. You cannot control what users type into a field but you can control what you put into the DOM so this should not be difficult to achieve. In my example, the marker is made unique statically: by choosing a class name unlikely to cause a clash ahead of time. It would also be possible to do it dynamically, by checking the DOM and changing the class until it is unique.
I have a fiddle for it (derived from Alvaro Montoro's own fiddle). The main part is:
function getOffset() {
if ($("." + unique).length)
throw new Error("marker present in document; or the unique class is not unique");
// We could also use rangy.getSelection() but there's no reason here to do this.
var sel = document.getSelection();
if (!sel.rangeCount)
return; // No ranges.
if (!sel.isCollapsed)
return; // We work only with collapsed selections.
if (sel.rangeCount > 1)
throw new Error("can't handle multiple ranges");
var range = sel.getRangeAt(0);
var saved = rangy.serializeSelection();
// See comment below.
$mydiv[0].normalize();
range.insertNode($marker[0]);
var offset = $mydiv.html().indexOf($marker[0].outerHTML);
$marker.remove();
// Normalizing before and after ensures that the DOM is in the same shape before
// and after the insertion and removal of the marker.
$mydiv[0].normalize();
rangy.deserializeSelection(saved);
return offset;
}
As you can see, the code has to compensate for the addition and removal of the marker into the DOM because this causes the current selection to get lost:
Rangy is used to save the selection and restore it afterwards. Note that the save and restore could be done with something lighter than Rangy but I did not want to load the answer with minutia. If you decide to use Rangy for this task, please read the documentation because it is possible to optimize the serialization and deserialization.
For Rangy to work, the DOM must be in exactly the same state before and after the save. This is why normalize() is called before we add the marker and after we remove it. What this does is merge immediately adjacent text nodes into a single text node. The issue is that adding a marker to the DOM can cause a text node to be broken into two new text nodes. This causes the selection to be lost and, if not undone with a normalization, would cause Rangy to be unable to restore the selection. Again, something lighter than calling normalize could do the trick but I did not want to load the answer with minutia.
EDIT: This is an old answer that doesn't work for OP's requirement of having nodes with the same text. But it's cleaner and lighter if you don't have that requirement.
Here is one option that you can use and that works in all major browsers:
Get the offset of the caret within its node (document.getSelection().anchorOffset)
Get the text of the node in which the caret is located (document.getSelection().anchorNode.data)
Get the offset of that text within #mydiv by using indexOf()
Add the values obtained in 1 and 3, to get the offset of the caret within the div.
The code would look like this for your particular case:
var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = $("#mydiv").html().indexOf( text );
offsetCaret = textOffset + offset;
You can see a working demo on this JSFiddle (view the console to see the results).
And a more generic version of the function (that allows to pass the div as a parameter, so it can be used with different contenteditable) on this other JSFiddle:
function getCaretHTMLOffset(obj) {
var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = obj.innerHTML.indexOf( text );
return textOffset + offset;
}
About this answer
It will work in all recent browsers as requested (tested on Chrome 42, Firefox 37, and Explorer 11).
It is short and light, and doesn't require any external library (not even jQuery)
Issue: If you have different nodes with the same text, it may return the offset of the first occurrence instead of the real position of the caret.
NOTE: This solution works even in nodes with repeated text, but it detects html entities (e.g.: ) as only one character.
I came up with a completely different solution based on processing the nodes. It is not as clean as the old answer (see other answer), but it works fine even when there are nodes with the same text (OP's requirement).
This is a description of how it works:
Create a stack with all the parent elements of the node in which the caret is located.
While the stack is not empty, traverse the nodes of the containing element (initially the content editable div).
If the node is not the same one at the top of the stack, add its size to the offset.
If the node is the same as the one at the top of the stack: pop it from the stack, go to step 2.
The code is like this:
function getCaretOffset(contentEditableDiv) {
// read the node in which the caret is and store it in a stack
var aux = document.getSelection().anchorNode;
var stack = [ aux ];
// add the parents to the stack until we get to the content editable div
while ($(aux).parent()[0] != contentEditableDiv) { aux = $(aux).parent()[0]; stack.push(aux); }
// traverse the contents of the editable div until we reach the one with the caret
var offset = 0;
var currObj = contentEditableDiv;
var children = $(currObj).contents();
while (stack.length) {
// add the lengths of the previous "siblings" to the offset
for (var x = 0; x < children.length; x++) {
if (children[x] == stack[stack.length-1]) {
// if the node is not a text node, then add the size of the opening tag
if (children[x].nodeType != 3) { offset += $(children[x])[0].outerHTML.indexOf(">") + 1; }
break;
} else {
if (children[x].nodeType == 3) {
// if it's a text node, add it's size to the offset
offset += children[x].length;
} else {
// if it's a tag node, add it's size + the size of the tags
offset += $(children[x])[0].outerHTML.length;
}
}
}
// move to a more inner container
currObj = stack.pop();
children = $(currObj).contents();
}
// finally add the offset within the last node
offset += document.getSelection().anchorOffset;
return offset;
}
You can see a working demo on this JSFiddle.
About this answer:
It works in all major browsers.
It is light and doesn't require external libraries (apart from jQuery)
It has an issue: html entities like are counted as one character only.

Targeting specific row/line of textarea and appending that row

I want to be able to click on a specific element, and have it send a value to a textarea. However, I want it to append to a specific row/line of the textarea.
What I am trying to build is very similar to what happens when you click the notes of the fret board on this site: http://www.guitartabcreator.com/version2/ In fact, i want it almost exactly the same as this.
But right now I am really just trying to see how I can target the specific row, as it seems doable based on this website.
Currently I am using javascript to send a value based on clicking a specific element.
Here is the js:
<script type="text/javascript">
function addNote0(text,element_id) {
document.getElementById(element_id).value += text;
}
</script>
This is the HTML that represents the clickable element:
<td> x </td>
This is the textarea:
<textarea rows="6" cols="24" id="tabText" name="text">-
-
-
-
-
-</textarea>
This works fine for sending the value. But it obviously just goes to the next available space. I am a total newb when it comes to javascript, so I am just not sure where to begin with trying to target a specific line.
What I have currently can be viewed here: http://aldentec.com/tab/
Working code:
After some help, here is the final code that made this work:
<script>
function addNote0(text,element_id) {
document.getElementById(element_id).value += text;
var tabTextRows = ['','','','','',''];
$('td').click(function(){
var fret = $(this).index() - 1;
var line = $(this).parent().index() -1;
updateNote(fret, line);
});
function updateNote(fret, line){
var i;
for(i=0;i<tabTextRows.length;i++){
if(i == line) tabTextRows[i]+='-'+fret+'-';
else tabTextRows[i]+='---';
$('#tabText').val(tabTextRows.join('\n'));
}
}}
window.onload = function() {
addNote0('', 'tabText');
};
</script>
Tried to solve this only in JS.
What I did here is use an array to model each row of the textfield (note the array length is 6).
Then I used a jQuery selector to trigger any time a <td> element is clicked which calculates the fret and string that was clicked relative to the HTML tree then calls the updateNote function. (If you change the table, the solution will probably break).
In the update note function, I iterate through the tabTextRows array, adding the appropriate note. Finally, I set the value of the <textarea> to the array joined by '\n' (newline char).
Works for me on the site you linked.
This solution is dependant on jQuery however, so make sure that's included.
Also you should consider using a monospaced font so the spacing doesn't get messed up.
var tabTextRows = ['','','','','',''];
$('td').click(function(){
var fret = $(this).index() - 1;
var line = $(this).parent().index() -1;
updateNote(fret, line);
});
function updateNote(fret, line){
var i;
for(i=0;i<tabTextRows.length;i++){
if(i == line) tabTextRows[i]+='-'+fret+'-';
else tabTextRows[i]+='---';
$('#tabText').val(tabTextRows.join('\n'));
}
}
I wrote the guitartabcreator website. Jacob Mattison is correct - I am using the text area for display purposes. Managing the data occurs in the backend. After seeing your site, it looks like you've got the basics of my idea down.

Programmatically select text (like as done with a mouse) that matches string in a textarea element

Consider the following problem:
Have a textarea like so:
<textarea id="body" name="body"></textarea>
Also have some simple JavaScript (jQuery) that inserts some new text into the textarea so a user can embed an image:
$('textarea').val($('textarea').val() + '[img]path to image file[/img]');
The trick is to automatically highlight the text in between the [img][/img] tags after that text is inserted so the user can just copy and paste their image URL in quickly, instead of manually selecting, then copy and pasting.
I've racked my brain and gone all over the internet trying to figure this out, and the best I could do was this popular StackOverflow question Selecting text in an element (akin to highlighting with your mouse) which only addresses the problem of selecting the text inside an ENTIRE element, which is not what is desired here. The problem is to select text that matches a certain string, in this case path to image file, so the user can just copy/paste. (not sure if this is the best way to do it, but that's what I thought of...).
Is this possible? I'm guessing we're going to need getSelection() and createRange() but other than that I have no idea where to go... any JavaScript wizards figured this one out already? I feel like this could be a popular question. Using jQuery is fine, as I'm already using it on the rest of the document.
You could use my jQuery plug-in. It works around browser differences in textarea selection manipulation and has some convenience methods:
https://code.google.com/p/rangyinputs/
For your example, the code would be
var $textarea = $("#body");
var text = "path to image file"
$textarea.replaceSelectedText(text, "select");
$textarea.surroundSelectedText("[img]", "[/img]");
Demo: http://jsfiddle.net/P8Jrh/1/
I actually figured this one out myself... I used the Rangy library https://code.google.com/p/rangy/ and code like this:
// Add text to the reply area at the very end, and move the cursor to the very end.
function insertText(textarea, text) {
textarea = $(textarea);
textarea.focus();
textarea.val(textarea.val() + text);
textarea.focus();
// Trigger the textarea's keyup to emulate typing.
textarea.trigger("keyup");
}
// Add text to the reply area, with the options of wrapping it around a selection and selecting a part of it when it's inserted.
function wrapText(textarea, tagStart, tagEnd, selectArgument, defaultArgumentValue) {
textarea = $(textarea);
// Save the scroll position of the textarea.
var scrollTop = textarea.scrollTop();
// Work out what text is currently selected.
var selectionInfo = textarea.getSelection();
if (textarea.val().substring(selectionInfo.start, selectionInfo.start + 1).match(/ /)) selectionInfo.start++;
if (textarea.val().substring(selectionInfo.end - 1, selectionInfo.end).match(/ /)) selectionInfo.end--;
var selection = textarea.val().substring(selectionInfo.start, selectionInfo.end);
// Work out the text to insert over the selection.
selection = selection ? selection : (defaultArgumentValue ? defaultArgumentValue : "");
var text = tagStart + selection + (typeof tagEnd != "undefined" ? tagEnd : tagStart);
// Replace the textarea's value.
textarea.val(textarea.val().substr(0, selectionInfo.start) + text + textarea.val().substr(selectionInfo.end));
// Scroll back down and refocus on the textarea.
textarea.focus();
// If a selectArgument was passed, work out where it is and select it. Otherwise, select the text that was selected
// before this function was called.
if (selectArgument) {
var newStart = selectionInfo.start + tagStart.indexOf(selectArgument);
var newEnd = newStart + selectArgument.length;
} else {
var newStart = selectionInfo.start + tagStart.length;
var newEnd = newStart + selection.length;
}
textarea.selectRange(newStart, newEnd);
// Trigger the textarea's keyup to emulate typing.
textarea.trigger("keyup");
}
var bbcode = {
bold: function(id) {wrapText($("textarea"), "[b]", "[/b]", "", "bolded text");},
};
Example usage:
bbcode.bold();
Full code (in a larger project I did): https://github.com/wnajar/textarea

Categories

Resources