Why can't I insertAdjecentHTML into a textarea? - javascript

I can't successfully use
document.querySelector('textarea').insertAdjacentHTML('beforeend', '<span style="color:red">Danger</span>')
<textarea>TEXT</textarea>
in my code. Chrome inserts the text escaped, FF doesn't show the node. Is there any documentation about this behavior?
Update: Beside the type on the function name it doesn't work (which is expected) because <textarea> only supports text content as specified in the HTML5 Standard (see "Content model: Text").
The method insertAdjacentHTML is inherited from HTMLElement -> Element.
Sidenote: Chrome and Firefox behave different if your first create an element and then append it (append but don't show) instead of showing the html as text.

The child nodes of a textarea:
Can only be text nodes. Element descendants are forbidden.
Represent the default value of the textarea, not the current value. Manipulating it is not guaranteed to update the current value.
If you want to change the value of a textarea, use the value property.
document.querySelector('textarea').value += '<span style="color:red">Danger</span>';
If you want a formatted and editable control, then don't use a textarea. See making content editable.

Related

How to avoid breaking block level HTML elements from partial selection?

The problem in a nutshell is: given a wysiwyg editor (CKEditor) you want to make a plugin doing text transformation - select a piece of text and manipulate the text in it (eg uppercase). Example:
this is paragraph one
this is paragraph two
If bold represents your selection the result would be
this is paragraph ONE
THIS is paragraph two
This issue here is the selection will be a complete HTML fragment, even when a selection is no containing the full tag. The selected HTML is:
<p>one</p> <p>this</p>
Notice the first and last <p> tags. When you do your dom traverse in the selection html, apply the text transformation and replace the html it will use those partial tags, so your result become:
this is paragraph
ONE
THIS
is paragraph two
I checked if it's possible to "merge" the first and last partial tags with their dom parents, however the selection object is isolated, it doesn't have siblings or parents from it's original context.
Also tried to find an option to retrieve the selection without these auto-fixed tags, but no luck.
On the CKEditor documentation they mention a walker object - however that automatically expands from the selection to the full enclosing tag, which means the selection is only used as a minimum boundary.
Also because the selection object is isolated, it's not possible to just change the dom node text values there - the original dom fragment needs to be replaced (at least in case of CKEditor).
I tried not to stick with the CKEditor api as much as possible, however at this point I don't see any alternatives either. Is this is really a hard problem or I'm missing something?
One solution is to use the browser engine to mark the selected area with a tag (afaik that's a native operation). This is the same as you make your selection bold or italic - however here it's gonna be a temporary wrapper. Then you walk the DOM and replace the content in the temporary tags - and finally remove the tag (keeping the content).
This makes sure you can apply your transformation on the exact selection and by removing the tag you won't break the original DOM. Steps in a nutshell:
apply tag on selection (use the browser or wysiwyg api)
find all temp tags:
recursively walk the children of the tag
if tag is text node then apply transformation
otherwise recursive walk
collect tag's left sibling text node + tag's html + right sibling text node
replace tag's parent html with the previous compilation (=remove temp tag)
Not too elegant however works. This is inspired by #Andrew_Mast's idea about wrapping in a span.
I would loop through all of the word(s) and for each set of words inside a different tag, surround it with <span style="text-transform: uppercase;"> and </span>

Is there a flexible way to modify the contents of an editable element?

Subject:
I am creating a Google Chrome extension that interacts with web pages via a content script and an event page.
I have context menu options that appear if a user clicks on an element that is categorized as editable by the chrome.contextMenus API.
The options act like shortcuts for commonly entered text. When the user clicks an option, some text is placed inside the element at the position of the cursor. If the user has text highlighted, it is deleted.
Problem:
Not all editable elements can be modified the same way.
If the element is a simple textarea the desired result can be achieved by implementing this solution:
Replacing selected text in the textarea
However, I can not assume that I am interacting with a normal textarea.
Possible nuances include:
The text area being hidden inside an Iframe, which complicates the process of finding the element to interact with (document.activeElement may return the Iframe, rather than the element that actually contains the text).
The <textarea> not being a <textarea>/<input> at all, but rather a contentEditable <div>. The .value approach will not work in this case.
So I am searching for a flexible way to do this that can handle all edge cases elegantly.
Some solutions I have tried:
option 1 :
I originally planned on storing the value in the system clipboard. Then, I could conceivably just use document.execCommand('paste') to modify the element. However, after trying it, this approach seems to have the same drawbacks as my initial approach. (See this question) Additionally, this approach would delete whatever was in the clipboard before the operation. This is undesirable and any solution that utilizes this approach must provide a work around.
option 2 :
Another option that I have considered is dispatching keyboard events for each character in the string. However, with this solution, you still run into the Iframe problem and it doesn't allow you do use special Unicode characters. ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻
Your problem consists of two subproblems:
Identify the target element of the contextmenu action.
Insert a custom text fragment at the caret (remove any selection if it is present).
Subproblem 1: Identifying the target element
If crbug.com/39507 is resolved, then getting the element is easy. This feature request is almost 5 year old without any progress, so don't get your hopes high on this one.
Alternative methods require you to break problem 1 in two more subproblems: Identifying the target frame, and then select the target DOM element.
There are several APIs that help with identifying the frame (use a combination of them, choose whichever combination fits best in your situation):
The contextMenus.onClicked event provides the tab's ID (tab.id) as a property in an instance of tabs.Tab and the frame's URL (frameUrl) in a separate object.
The chrome.tabs.executeScript method can directly run a script in a frame.
(currently only the top-level frame or all frames, targetting a specific frame is work in progress - crbug.com/63979, planned for Chrome 42).
Until targetting a specific frame is implemented, you could insert the content script in every frame and compare the URL with frameUrl (or use a combination of the next methods).
Assuming that you have already inserted a content script with a chrome.runtime.onMessage listener, use chrome.tabs.sendMessage to send a message to a specific frame identified by frameId (since Chrome 41).
Use the chrome.webNavigation.getAllFrames method to get a list of all frames in a tab for a given tabId, then get the frameId of the target frame by filtering the list of frames by a known frameUrl.
(in the future (Chrome 42?), contextMenus.onClicked will get the frameId).
Okay, assuming that you have the correct frame, you can simply use document.activeElement to get the target element, because input elements get focused upon click.
Subproblem 2: Inserting a text fragment at the caret
If the target element is a <textarea> or <input>, then you can simply use
// Assume: elem is an input or textarea element.
var YOURSTRING = 'whatever';
var start = elem.selectionStart;
var end = elem.selectionEnd;
elem.value = elem.value.slice(0, start) + YOURSTRING + elem.value.substr(end);
// Set cursor after selected text
elem.selectionStart = start + YOURSTRING.length;
elem.selectionEnd = elem.selectionStart;
Otherwise, you need to know whether there is a content editable element, and if so, remove any selection if existent, and finally put your desired text over there.
var elem = document.activeElement;
if (elem && elem.isContentEditable) {
// YOURSTRING as defined before
var newNode = document.createTextNode(YOURSTRING);
var sel = window.getSelection();
// Remove previous selection, if any.
sel.deleteFromDocument();
// If there is no range in the selection, add a new one, with the
// caret set to the end of the input element to avoid the next error:
//"Failed to execute 'getRangeAt' on 'Selection': 0 is not a valid index."
if (sel.rangeCount === 0) {
sel.addRange(document.createRange());
sel.getRangeAt(0).collapse(elem, 1);
}
// Insert new text
var range = sel.getRangeAt(0);
range.insertNode(newNode);
// Now, set the cursor to the end of your text node
sel.collapse(newNode, 1);
}
Relevant documentation for the web platform APIs used in the last example:
Selection
Selection.deleteFromDocument()
Selection.collapse
Range
Range.insertNode

Why don't select and textarea value changes get updated upon cloning as with input elements?

In Firefox at least, when using cloneNode(true) on a dynamically or user-altered textarea or select element, the value property is not preserved (nor is the DOM altered to reflect dynamic changes), whereas with input elements, a change to the value property or by the user gets reflected in the DOM (so it is preserved upon the call to cloneNode). Why is there this distinction?
UPDATE:
I meant to ask: Is this behavior prescribed in a spec somewhere? (or detailed in a bug report?)
A sample is at: http://jsfiddle.net/9RSNt/1/
I suspect the difference arises because textarea and selects' values are determined by their node content. Modifying the control's value modifies their DOM properties, but not their node content, so when you clone them, the clone has the value of the original element.
You can get around this by updating their node content on the change event:
// textarea
$("textarea").change(function() { $(this).text($(this).val()); });
// select
$("select").change(function() {
var sel = $(this).children(":selected");
$(this.children).not(sel).removeAttr("selected");
sel.attr("selected", "selected");
});
http://jsfiddle.net/emr2w/8/
EDIT:
There are a number of Mozilla bug cases on this (some resolved and some not), but any mention of the actual specs are few and far between. It seems that the the behavior of the value property after a cloneNode() may be a gray area that is not clearly defined in the spec. After all, the purpose of cloneNode() is to clone a DOM node, and not necessarily its object state.
https://bugzilla.mozilla.org/show_bug.cgi?id=197294
https://bugzilla.mozilla.org/show_bug.cgi?id=230307
https://bugzilla.mozilla.org/show_bug.cgi?id=237783

Equivalent of selectionStart, selectionEnd, and focus() for contenteditable spans and divs

I am trying to set a contenteditable span so that it will on the onkeyup event refill the span with formatted text.
However, when I try this the cursor disappears and I can't type anymore. This is because the formatting program, instead of modifying the text already in the span, erases it all and then writes the formatted text in its place.
It is important (I think it is) to note that the element does not appear to be losing focus. Also, because I would like for this project to remain fairly "secret" until it's release, I would rather not give away the source code right now.
This is probably due to you using the innerHTML to set the formatted text.
Instead use childNodes collection to access the content and replace textNodes with formatted html element. This avoids setting innerHTML and avoids loosing focus.

can any text on a webpage in a <p> tag or <span> tag be selected programmatically?

can any text on a webpage in a tag or tag be selected programmatically? the input element and textarea element can do that easily, but how about text in other cases?
You can find an example of getting inner text from a <p> tag here. Same thing with a <span>.
To set it you just need to assign the InnerText property.
If your javascript snippet is out of scope (in a function etc) use the GetElementById method of the document global object to retrieve your <p> or <span> or whatever - in fact you can do this with any element provided you assign an id to the element.
JohnIdol's answer will work in all modern browsers except Firefox. See here for a browser compatibility chart on innerText. As the link also shows, you can use textContent to get the same thing in FF.
The suggestion to use getElementById is an excellent one, but it's good to note that you can still access the text of an element if it doesn't have an id attached to it, which is frequently the case with <p> and <span> tags. For example if you know the 4th <p> tag has the text you want you can do the following:
var p = document.getElementsByTagName('p')[3];
var text = p.innerText || p.textContent;

Categories