I am making a basic online editing interface for coursework. I would like to save the content of my #textarea div every time there's a keydown event. The code works partially but I can't edit the text without the cursor going to the top of the div.
document.addEventListener('keydown', saveLocally);
const saveLocally = function() {
let areaText = document.getElementById("textArea");
console.log(areaText);
let text = document.getElementById("textArea").innerHTML;
console.log(text);
let localData;
localStorage.setItem('siteData', text);
localData = localStorage.getItem('siteData');
console.log(localData);
console.log(localStorage.getItem('siteData'));
areaText.innerHTML = localData;
}
<div id="inputArea">
<div id="textArea" contenteditable="true"></div>
</div>
The issue is because you immediately update the innerText of the element after someone types. This affects the cursor as the node contents are changed. You don't need to do this anyway, so the line can be removed.
You instead need to perform the logic which retrieves the value from localStorage when the page loads. Also note that you should use the keyup event instead of keydown, otherwise you'll not save the last key which was pressed. Try this:
<div id="inputArea">
<div id="textArea" contenteditable="true"></div>
</div>
let areaText = document.getElementById("textArea");
const saveLocally = function() {
let text = areaText.innerHTML;
localStorage.setItem('siteData', text);
}
const retrieveSavedText = function() {
var text = localStorage.getItem('siteData');
areaText.innerHTML = text;
}
document.addEventListener('keyup', saveLocally);
retrieveSavedText();
Working example
Related
I have a text area and I dynamically add data to it and it works fine. What I wanted to achieve was after the data is appended that data cant be altered (edited) but after the last element of the data user can start typing on the textarea. I was thinking of maybe calculating the length of string data the set read-only to that part. How can I achieve this? Any help is appreciated. Thanks in advance.
For a visual example take a look at the terminal of this website: https://www.online-python.com/
function test() {
x = 'this is a string to be appended to text area'
document.getElementById("textArea").value = x;
}
<textarea id="textArea"></textarea>
<button onclick="test()">Append</button>
You can add a keydown event listener that checks whether the selectionStart is smaller than the length of the textarea's value minus the length of the string appended:
let x = 'this is a string to be appended to text area'
var hasAppended = false;
function test() {
hasAppended = true
document.getElementById("textArea").value = x;
}
textArea.addEventListener('keydown', function(e) {
if (hasAppended) {
if (this.selectionStart > this.value.length - x.length && this.selectionStart != this.value.length) {
e.preventDefault()
e.stopPropagation()
}
}
})
<textarea id="textArea"></textarea><button onclick="test()">Append</button>
It is not possible to selectively mark parts of a <textarea> read-only, however, a similar effect can be achieved with contenteditable elements:
function test() {
const x = 'this is a string to be appended to text area'
const span = document.createElement('span')
span.appendChild(document.createTextNode(x))
span.setAttribute('contenteditable', 'false')
document.getElementById("textArea").appendChild(span);
}
#textArea{
border: 1px solid black;
height: 50px;
}
<div id="textArea" contenteditable="true"></div>
<button onclick="test()">Append</button>
However, this will still allow the user to delete the read-only block, or write before it.
This almost works.
EDIT:
Improved it a bit by changing keydown to keyup.
Now there is no need for space at the end of the read-only text and CTRL+a and then backspace will make the text come back almost instantly.
Maybe you can improve on it.
window.onload = function() {
document.getElementById('textArea').addEventListener('keyup', function (e){
var readOnlyLength = parseInt(document.getElementById("textArea").getAttribute('data-readOnlyLength') );
var currentLength = parseInt(document.getElementById("textArea").value.length );
if (readOnlyLength >= currentLength ) {
document.getElementById("textArea").value = document.getElementById("textArea").getAttribute('data-readonly') ;
}
}, false);
};
function test() {
x = 'this is a string to be appended to text area'
document.getElementById("textArea").value = x;
document.getElementById("textArea").setAttribute('data-readonly' , x);
document.getElementById("textArea").setAttribute('data-readOnlyLength' , x.length);
}
<textarea id="textArea"></textarea>
<button onclick="test()">Append</button>
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.
In a HTML website I have a textarea created like this:
<textarea id = "myTextArea" rows = "30" cols = "80"></textarea>
I would like after something is written in the text area, for that text to be sent to a variable in javascript.
I have tried doing this, but it did not work:
var x = document.getElementById("myTextArea").value;
The console.log(x); gives back nothing, not null, just empty space. However, if I log out console.log(document.getElementById("myTextArea").value) then I get the text that I have written in my textarea.
Why does var x = document.getElementById("myTextArea").value; not work?
My Javascript:
<script>
var x = document.getElementById("myTextArea").value;
const regex = /([a-z]+)/;
const match = regex.exec(x);
var intervalID = window.setInterval(myCallback, 500); <!-- Calls every 5s -->
function myCallback() {
if(match){
const name = match[1];
console.log(name);
}
else{
console.log('no match');
console.log(match);
}
}
To achieve this, you can register an event listener on your textarea:
var textArea = document.getElementById('myTextArea');
textArea.addEventListener('input', function(event){
console.log(event.target.value);
});
The listener listens for any input events on your textara and logs the value of your textarea as the value changes.
Here's a live demo for your quick reference.
You will need to use onkeyup and onchange for this. The onchange will prevent context-menu pasting, and the onkeyup will fire for every keystroke.
See my answer on How to impose maxlength on textArea for a code sample.
In my example, the variable is the text variable. This variable is filled with the text of the text by clicking on the button.
Was it necessary?
var x = document.getElementById("myTextArea");
var button = document.querySelector("button");
var text = "";
button.onclick = function() {
text = x.value;
console.log(text);
}
#myTextArea {
width: 100%;
height: 100px;
}
<textarea id="myTextArea" rows="30" cols="80"></textarea>
<button>Check variable with text</button>
Second example using oninput event.
var x = document.getElementById("myTextArea");
var text = "";
x.oninput = function() {
text = this.value;
console.log(text);
}
#myTextArea {
width: 100%;
height: 100px;
}
<textarea id="myTextArea" rows="30" cols="80"></textarea>
I have this bit of JavaScript that is fired when I press a button on the page. When pressed, it fires an event that runs this code and it turns styled text into html source code:
showHTMLmarkupCode: function(e) {
var text = document.createTextNode($('.storyBookText').html());
var html = $('.storyBookText').html();
$('.storyBookText').empty();
$('.storyBookText').append(text);
}
It works the first time you press the button.
So if I have text like this:
This is bold
When I press the button, it will show this:
<span style="font-weight: bold;">This is bold</span>
Which is what I want.
But if I press it again, to go back to the styled text, it does this:
<span style="font-weight: bold;">This is bold</span>
Is there a way to code it so that when it's pressed a second time, it reverts back to the styled text instead of the ugly code?
Thanks!
Modified function:
showSourceCode: function(e) {
console.log('show source code clicked');
console.log("isHTML: ", isHTML);
if (isHTML) {
$('.storyBookText').textContent = $('.storyBookText').innerHTML;
} else {
$('.storyBookText').innerHTML = $('.storyBookText').textContent;
}
isHTML = !isHTML;
}
You can accomplish the text to HTML conversion by setting the .innerHTML property of the parent to its .textContent:
const $button = document.getElementById('button');
const $element = document.querySelector('.storyBookText');
let isHTML = true;
$button.addEventListener('click', () => {
if (isHTML) {
$element.textContent = $element.innerHTML;
} else {
$element.innerHTML = $element.textContent;
}
isHTML = !isHTML;
});
<div class="storyBookText">
<span style="font-weight: bold;">This is bold</span>
</div>
<button id="button">Change</button>
I have this HTML:
<div id = "options"></div>
I access to this div trough this Javascript:
var test = "hola";
document.getElementById('options').innerHTML=
<p>Test</p> <li>${test}</li>
And I have this button:
function click({
document.getElementById("Click").addEventListener("click", function(){
});
};
click();
I need that when that function called "click" is executed automatically that <li> can clean that value of the variable: "test" and it is seen empty.
How can I do it? Thank you
el.innerHTML is the property which keeps track of HTML text values shown inside the element. You can set that to empty whenever you want to clear it.
Try this sample - https://jsitor.com/J0gXikZmV
<div id = "options"></div>
var test = "hola";
let el = document.getElementById("options");
el.innerHTML = "<p>Test</p> <li>${test}</li>";
el.addEventListener('click', () => {
el.innerHTML = '';
})
Here it clear the html content of div