Nothing changed when I try to insert an html element using JavaScript - javascript

I'm creating a simple text editor in html and I have a function which allow user to insert website link into a text. Here is the code:
function setUrl() {
window.url = document.getElementById('txtFormatUrl').value;
window.sText = document.getSelection();
var urlanchor = document.createElement("A");
var urlatt = document.createAttribute("href");
urlatt.value = window.url;
urlanchor.setAttributeNode(urlatt);
window.sText = urlanchor;
}
How it works is that there will be a place to edit text and a box to enter URL. The user first highlighted the text then enter the URL, after that. The user presses the insert button which will called the setUrl() function.
But when I try, the URL didn't get inserted into the text, when open the F12 console, I saw that the element don't get insert. So what's wrong with my code?

Supposing that the element with ID txtFormatUrl is an <input> with a valid URL, the code is the following:
function setUrl() {
var url = document.getElementById('txtFormatUrl').value; // Get value from input tag
var selection = document.getSelection(); // Get info from text selected
if(selection.rangeCount) {
var textSelected = selection.toString(); // Get text selected
var element = document.createElement('a'); // Create a new anchor DOM node
element.innerText = textSelected; // Set de text selected to the new anchor DOM node
element.setAttribute('href', url); // Set a href value
var range = selection.getRangeAt(0); // Get selection range
range.deleteContents(); // Delete the current text
range.insertNode(element); // Replace the text
}
}
You can get more info here about replacing a selected text form another. Also, take a look at the DOM Range W3 specification or DOM Range MDN docs.

Related

Programmatically change input value in Facebook's editable div input area

I'm trying to write a Chrome Extension that needs to be able to insert a character at the cursor location in an input field.
It's very easy when the input is an actual HTMLInputElement (insertAtCaretInput borrowed from another stack answer):
function insertAtCaretInput(text) {
text = text || '';
if (document.selection) {
// IE
this.focus();
var sel = document.selection.createRange();
sel.text = text;
} else if (this.selectionStart || this.selectionStart === 0) {
// Others
var startPos = this.selectionStart;
var endPos = this.selectionEnd;
this.value = this.value.substring(0, startPos) + text + this.value.substring(endPos, this.value.length);
this.selectionStart = startPos + text.length;
this.selectionEnd = startPos + text.length;
} else {
this.value += text;
}
}
HTMLInputElement.prototype.insertAtCaret = insertAtCaretInput;
onKeyDown(e){
...
targetElement = e.target;
target.insertAtCaret(charToInsert);
...
}
But the moment an input is actually represented differently in the HTML structure (e.g. Facebook having a <div> with <span> elements showing up and consolidating at weird times) I can't figure out how to do it reliably. The new character disappears or changes position or the cursor jumps to unpredictable places the moment I start interacting with the input.
Example HTML structure for Facebook's (Chrome desktop page, new post or message input fields) editable <div> containing string Test :
<div data-offset-key="87o4u-0-0" class="_1mf _1mj">
<span>
<span data-offset-key="87o4u-0-0">
<span data-text="true">Test
</span>
</span>
</span>
<span data-offset-key="87o4u-1-0">
<span data-text="true">
</span>
</span>
</div>
Here's my most successful attempt so far. I extend the span element like so (insertTextAtCursor also borrowed from another answer):
function insertTextAtCursor(text) {
let selection = window.getSelection();
let range = selection.getRangeAt(0);
range.deleteContents();
let node = document.createTextNode(text);
range.insertNode(node);
for (let position = 0; position != text.length; position++) {
selection.modify('move', 'right', 'character');
}
}
HTMLSpanElement.prototype.insertAtCaret = insertTextAtCursor;
And since the element triggering key press events is a <div> that then holds <span> elements which then hold the text nodes with the actual input, I find the deepest <span> element and perform insertAtCaret on that element:
function findDeepestChild(parent) {
var result = { depth: 0, element: parent };
[].slice.call(parent.childNodes).forEach(function (child) {
var childResult = findDeepestChild(child);
if (childResult.depth + 1 > result.depth) {
result = {
depth: 1 + childResult.depth,
element: childResult.element,
parent: childResult.element.parentNode,
};
}
});
return result;
}
onKeyDown(e){
...
targetElement = findDeepestChild(e.target).parent; // deepest child is a text node
target.insertAtCaret(charToInsert);
...
}
The code above can successfully insert the character but then strange things happen when Facebook's behind-the-scenes framework tries to process the new value. I tried all kinds of tricks with repositioning the cursors and inserting <span> elements similar to what seems to be happening when Facebook manipulates the dom on inserts but in the end, all of it fails one way or another. I imagine it's because the state of the input area is held somewhere and is not synchronized with my modifications.
Do you think it's possible to do this reliably and if so, how? Ideally, the answer wouldn't be specific to Facebook but would also work on other pages that use other elements instead of HTMLInputElement as input fields but I understand that it might not be possible.
I had a similar problem with Whatsapp web
try to dispatch an input event
target.dispatchEvent(new InputEvent('input', {bubbles: true}));
I've made a Firefox extension that is able to paste a remembered note from context menu into input fields. However Facebook Messenger fields are heavly scripted divs and spans - not input fields. I've struggled to make them work and dispatching an event as suggested by #user9977151 helped me!
However it needs to be dispatched from a specific element and also you need to check if your Facebook Messenger input field is empty or not.
Empty field will look like that:
<div class="" data-block="true" data-editor="e1m9r" data-offset-key="6hbkl-0-0">
<div data-offset-key="6hbkl-0-0" class="_1mf _1mj">
<span data-offset-key="6hbkl-0-0">
<br data-text="true">
</span>
</div>
</div>
And not empty like that
<div class="" data-block="true" data-editor="e1m9r" data-offset-key="6hbkl-0-0">
<div data-offset-key="6hbkl-0-0" class="_1mf _1mj">
<span data-offset-key="6hbkl-0-0">
<span data-text="true">
Some input
</span>
</span>
</div>
</div>
The event needs to be dispatched from
<span data-offset-key="6hbkl-0-0">
It's simple when you add something to not empty field - you just change the innerText and dispatch the event.
It's more tricky for an empty field. Normally when the user writes something <br data-text="true"> changes into <span data-text="true"> with the user's input.
I've tried doing it programically (adding a span with innerText, removing the br) but it broke the Messenger input. What worked for me was to add a span, dispatch the event and then remove it! After that Facebook removed br like it normally does and added span with my input.
Facebook seems to somehow store user keypresses in it's memory and then input them itself.
My code was
if(document.body.parentElement.id == "facebook"){
var dc = getDeepestChild(actEl);
var elementToDispatchEventFrom = dc.parentElement;
let newEl;
if(dc.nodeName.toLowerCase() == "br"){
// attempt to paste into empty messenger field
// by creating new element and setting it's value
newEl = document.createElement("span");
newEl.setAttribute("data-text", "true");
dc.parentElement.appendChild(newEl);
newEl.innerText = message.content;
}else{
// attempt to paste into not empty messenger field
// by changing existing content
let sel = document.getSelection();
selStart = sel.anchorOffset;
selStartCopy = selStart;
selEnd = sel.focusOffset;
intendedValue = dc.textContent.slice(0,selStart) + message.content + dc.textContent.slice(selEnd);
dc.textContent = intendedValue;
elementToDispatchEventFrom = elementToDispatchEventFrom.parentElement;
}
// simulate user's input
elementToDispatchEventFrom.dispatchEvent(new InputEvent('input', {bubbles: true}));
// remove new element if it exists
// otherwise there will be two of them after
// Facebook adds it itself!
if (newEl) newEl.remove();
}else ...
where
function getDeepestChild(element){
if(element.lastChild){
return getDeepestChild(element.lastChild)
}else{
return element;
}
}
and message.content was a string that I wanted to be pasted into Messenger field.
This solution can change the content of Messenger field but will move the cursor to the beginning of the field - and I'm not sure if is possible to keep the cursor's position unchanged (as there's no selectionStart and selectionEnd that could be changed).
The answer of #raandremsil and #ATP mostly worked for me.
However, I had a case when it was not working.
My extension displays a list of texts that will be inserted in the input when the user clicks on an item. It was not working properly until I focused on the field before modifying the content and dispatching the event.
const myNewText = 'whatever text I get a click on my list';
elementToDispatchEventFrom.focus(); // <- I had to add this line, focus the field before editing (`elementToDispatchEventFrom` is from raandremsil's answer)
deepestChild.contextText = myNewText; // Edit the text
elementToDispatchEventFrom.dispatchEvent(new InputEvent('input', {bubbles: true})); // Dispatch event to simulate user input
UPDATE 27/02/2022:
This stopped working for some reason. Here is was I am doing now to replace a text in the input, assuming it is just before the cursor position.
const selection = window.getSelection()!;
const cursorPosition = selection.focusOffset;
const focusNode = selection.focusNode!;
const doc = focusNode.ownerDocument!;
const range = new Range();
range.setStart(focusNode, cursorPosition - textToReplace.length);
range.setEnd(focusNode, cursorPosition);
selection.removeAllRanges();
selection.addRange(range);
doc.execCommand('insertText', false, myNewText); // Careful, for some reason, this does not work if `myNewText` ends with the space.

Wrap an html tag after clicking a toolbar button using js or jquery

I want to do something similar to what this website and wordpress does. When a user highlights text on the screen, then clicks a button on the toolbar it will wrap an html tag around the text. In jquery I would probably use the .wrap class but how would I detect if the user highlighted something.
For example, when the user writes Hello World then clicks on the bold button it will say <b>Hello World</b>
This mainly requires (1) accessing the selectionStart and selectionEnd properties of the input/textarea element and (2) replacing the substring of the value property across that range with the same text, but wrapped in the desired start and end tags. Also, I think it makes sense to reselect the replaced text, which requires a couple of calls to select() and setSelectionRange(). Also, if there's no selection (meaning start equals end) it's probably a good idea to do nothing at all.
window.selWrapBold = function(id) { selWrap(id,'<b>','</b>'); };
window.selWrapItalic = function(id) { selWrap(id,'<i>','</i>'); };
window.selWrap = function(id,startTag,endTag) {
let elem = document.getElementById(id);
let start = elem.selectionStart;
let end = elem.selectionEnd;
let sel = elem.value.substring(start,end);
if (sel==='') return;
let replace = startTag+sel+endTag;
elem.value = elem.value.substring(0,start)+replace+elem.value.substring(end);
elem.select();
elem.setSelectionRange(start,start+replace.length);
} // end selWrap()
<input type="button" value="bold" onclick="selWrapBold('ta1');"/>
<input type="button" value="italic" onclick="selWrapItalic('ta1');"/>
<br/>
<textarea id="ta1"></textarea>
Get the text of the html element which is wrapping the text, then add as html the text embedded in the <b> tag.
See jQuery DOM Manipulation for tutorials.
I used this question to get the selected text. And this question to
get the element with selected text in it. I combined them in a single function.
function updateHighlightedText() {
var text = "";
if (window.getSelection) {
text = window.getSelection().toString();
} else if (document.selection && document.selection.type != "Control") {
text = document.selection.createRange().text;
}
var node = $(window.getSelection().anchorNode.parentNode); //Get the selected node
node.html(node.text().replace(text, "<b>"+text+"</b>")); //Update the node
}

javascript: deleting tags of a selected element in a text

I put a tag to a selected element in my textarea and every tag take an id. And I store the element an a table.
But, I want to delete some tags(I have to type of tag "ref" et "point"). And I try this function but, it doesn't work:
function deleteTag(textField, tag)
{
var scrollTop = textField.scrollTop; //For the scroll of the text
var start = textField.selectionStart; //beginning of the selected text
var end = textField.selectionEnd; //End of the selected text
var modif=textField.value.substring(start,end); //The text to modify
// Remove the tag
if(modif.match('[^<]'+tag+'id="(\\d+)">') && modif.match('</'+tag+'[>$]'))
{
var regex;
if(tag=="ref")
{
regex=new RegExp('<'+tag+' id="(\\d+)">');
var opt=modif.match(regex)[1];
document.getElementById("refList").remove(opt-1);
}
regex=new RegExp('<'+tag+'[^>]+>');
modif=modif.replace(regex, "");
modif=modif.replace('</'+tag+'>', "");
}
textField.value=textSup+modif+textInf; //We modify the text inside the text area
textField.scrollTop=scrollTop;
}
And the button have this code:
<button onclick="deleteTag(textAreaId, 'ref')"> Effacer
By the way I'm beginner in javascript
if(modif.match('[^<]'+tag+'id="(\\d+)">') && modif.match('</'+tag+'[>$]'))
maybe you need an extra space in the regex:
if(modif.match('[^<]'+tag+' id="(\\d+)">') && modif.match('</'+tag+'[>$]'))

Add tags around selected text in an element

How can I add <span> tags around selected text within an element?
For example, if somebody highlights "John", I would like to add span tags around it.
HTML
<p>My name is Jimmy John, and I hate sandwiches. My name is still Jimmy John.</p>
JS
function getSelectedText() {
t = (document.all) ? document.selection.createRange().text : document.getSelection();
return t;
}
$('p').mouseup(function(){
var selection = getSelectedText();
var selection_text = selection.toString();
console.log(selection);
console.log(selection_text);
// How do I add a span around the selected text?
});
http://jsfiddle.net/2w35p/
There is a identical question here: jQuery select text and add span to it in an paragraph, but it uses outdated jquery methods (e.g. live), and the accepted answer has a bug.
I have a solution. Get the Range of the selecion and deleteContent of it, then insert a new span in it .
$('body').mouseup(function(){
var selection = getSelectedText();
var selection_text = selection.toString();
// How do I add a span around the selected text?
var span = document.createElement('SPAN');
span.textContent = selection_text;
var range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(span);
});
You can see the DEMO here
UPDATE
Absolutly, the selection will be delete at the same time. So you can add the selection range with js code if you want.
You can simply do like this.
$('body').mouseup(function(){
var span = document.createElement("span");
if (window.getSelection) {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0).cloneRange();
range.surroundContents(span);
sel.removeAllRanges();
sel.addRange(range);
}
}
});
Fiddle
Reference Wrapping a selected text node with span
You can try this:
$('body').mouseup(function(){
var selection = getSelectedText();
var innerHTML = $('p').html();
var selectionWithSpan = '<span>'+selection+'</span>';
innerHTML = innerHTML.replace(selection,selectionWithSpan);
$('p').html(innerHTML);
});
and In your fiddle you are again opening a new <p> instead of a closing </p>. Update that please.
THIS WORKS (mostly*)!! (technically, it does what you want, but it needs HALP!)
JSFiddle
This adds <span ...> and </span> correctly, even if there are multiple instances of the selection in your element and you only care about the instance that's selected!
It works perfectly the first time if you include my commented line. It's after that when things get funky.
I can add the span tags, but I'm having a hard time replacing the plaintext with html. Maybe you can figure it out? We're almost there!! This uses nodes from getSelection. Nodes can be hard to work with though.
document.getElementById('d').addEventListener('mouseup',function(e){
var s = window.getSelection();
var n = s.anchorNode; //DOM node
var o = s.anchorOffset; //index of start selection in the node
var f = s.focusOffset; //index of end selection in the node
n.textContent = n.textContent.substring(0,o)+'<span style="color:red;">'
+n.textContent.substring(o,f)+'</span>'
+n.textContent.substring(f,n.textContent.length);
//adds the span tag
// document.getElementById('d').innerHTML = n.textContent;
// this line messes stuff up because of the difference
// between a node's textContent and it's innerHTML.
});

how do i highlight a selected text and store in db and when the page reloads highlight back the selected text?

I am able to store the highlighted text .how do i highlight the text back when the page loads back ?
here is the sample code :
$(document).mouseup(function(event){
highlighterOn = true;
if (highlighterOn){
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var sel = range.extractContents();
var span = document.createElement("span");
span.style.backgroundColor = "yellow";
span.appendChild(sel);
range.insertNode(span);
}
});
I'm not sure what you're trying to do with the code above, but I would suggest (since you have already stored the highlighted text in a database of some kind) is just to select the innerHTML of the div and do something like .replace('the text I want to highlight','<span class="highlighted">the text I want to highlight</span>') and then style the .highlighted class with CSS.

Categories