How to check if the selected text is editable? - javascript

I'm working on a Chrome extension that deals with text selection. I want to detect whether the selected text is editable. Given a Selection object returned by getSelection(), is it possible to check whether the selected text is editable? How?

A selection can contain text from multiple elements. Some elements may be editable and some may not.
However, if you are only interested in the element where the selection starts, you can use
Selection.prototype.anchorNode, which returns the Node in which the selection begins.
Node.prototype.parentNode, which should be the Element in which the selection begins. Latest browsers also support Node.prototype.parentElement.
HTMLElement.prototype.isContentEditable, a read-only property returns a Boolean that is true if the contents of the element are editable; otherwise it returns false.
That is:
getSelection().anchorNode.parentNode.isContentEditable
However, that won't work for input and textarea elements because:
getSelection() won't return selections in them, because their value doesn't exist as a text node. You can use document.activeElement instead.
isContentEditable won't apply because you edit their value instead of their content. Instead, you should check if they are disabled or readOnly (disabled ones doesn't seem to be selectable, but you can check just in case).
function isEditable() {
var el = document.activeElement; // focused element
if(el && ~['input','textarea'].indexOf(el.tagName.toLowerCase())
return !el.readOnly && !el.disabled;
el = getSelection().anchorNode; // selected node
if(!el) return undefined; // no selected node
el = el.parentNode; // selected element
return el.isContentEditable;
}
var el;
function isEditable() {
if(el) el.className = '';
el = document.activeElement; // focused element
if(el && ~['input','textarea'].indexOf(el.tagName.toLowerCase())) {
el.className = 'selected';
return !el.readOnly && !el.disabled;
}
el = getSelection().anchorNode; // selected node
if(!el) return undefined; // no selected node
el = el.parentNode; // selected element
el.className = 'selected';
return el.isContentEditable;
}
var res = document.getElementById('result');
setInterval(function() {
res.textContent = isEditable();
}, 200);
#result {
font-size: 200%;
font-weight: bold;
}
.selected {
outline: 3px solid red;
}
<div>Non-editable div</div>
<div contentEditable="true">Editable div</div>
<input value="Editable input" />
<input value="Read-only input" readonly />
<input value="Disabled input" disabled />
<textarea>Editable textarea</textarea>
<textarea readonly>Read-only textarea</textarea>
<textarea disabled>Disabled textarea</textarea>
<hr />
<p>Is editable: <span id="result"></span></p>

Input tags have the attribute readonly to tell if the input can be edited.
You can try:
if(yourInput.readonly == true) { // where yourInput is the input tag
// can not be edited
} else {
// can be edited
}
You can tinker with this to fit what you are using.

Related

JS: if innerhtml is empty, change innerhtml to something. HOW?

i'm new to js
so i have a simple html element that has contenteditable="true" which i'm using as an input box.
i want the innerhtml to change to "CAN'T BE EMPTY" when the user has typed nothing in the input box ( " " )
and apparently it doesn't work, any tips on how i can do it?
this is my code (which is not working):
HTML:
<p contenteditable="true" id="myparagraph">Input</p>
JS:
if(document.getElementById("myparagraph").innerHTML == ""){
document.getElementById("myparagraph").innerHTML = "CAN'T BE EMPTY";}
i've also tried using the LENGTH property, but didn't work either:
var plength = document.getElementById("myparagraph").innerHTML;
var plength2 = plength.length;
if(plength2 == 0){
document.getElementById("myparagraph").innerHTML = "CAN'T BE EMPTY";}
It's not empty. It's got a value of Input according to your HTML.
If you want to change the value when the user edits, you need to attach an event listener:
document.getElementById('myparagraph').addEventListener('input', (e) => {
if (e.target.textContent.trim() === '') {
e.target.textContent = 'Cannot be empty';
}
})
<p contenteditable="true" id="myparagraph">Input</p>
Note that I changed the logic from using innerHTML to using textContent because otherwise remaining empty HTML tags can prevent the warning from triggering. (Firefox, for example inserts a <br> when there is no content.)
It would be better to display the warning anywhere than the content of the element you're trying to check. Add an event listener to the paragraph element. In the handler get the textContent from the element, and then show/hide the warning depending on whether that text is empty.
const para = document.querySelector('#para');
const warning = document.querySelector('#warning');
para.addEventListener('input', handleInput, false);
function handleInput() {
const text = this.textContent;
warning.textContent = text ? '' : 'Cannot be empty';
}
#para { border: 1px solid #565656; padding: 0.5em; }
#warning { color: red; }
<p contenteditable="true" id="para">Input</p>
<p id="warning"></p>

Uncaught TypeError: Cannot read properties of null (reading 'checked')

I try to get the value of Checkbox through the following function:
function sickness_selection() {
var element = event.target.id;
// console.log( event.target.checked);
element = document.getElementById(element);
if (element.checked === true) {
element.parentElement.classList.add("asanism-selected-radio");
} else {
element.parentElement.classList.remove("asanism-selected-radio");
}
}
the function works but receives the following error in the console:
Uncaught TypeError: Cannot read properties of null (reading 'checked')
at sickness_selection (js_cEGLc76e0OdunEJf9-WqG_aDsYoNtkkBvpRxiXeaSmg.js:1701:17)
at HTMLDivElement.onclick (create:1:1)
because the value does not render in the HTML file while changing the input:
<div class="js-form-item js-form-type-checkbox checkbox form-check js-form-item-sickness7 form-item-sickness7">
<label class="form-check-label">
<input data-drupal-selector="edit-sickness7" type="checkbox" id="edit-sickness7--iViLtzwqW-A" name="sickness7" value="1" class="form-checkbox form-check-input" required="">
<label for="edit-sickness7--iViLtzwqW-A" class="option">somthing</label>
</label>
</div>
is there any solutions for error?
The error is telling you that element is null, because getElementById returned null. The only reason getElementById returns null is that no element with that id exists (see this question's answers).
But just that isn't the root of the problem. You're using a click handler on a div element (from the error message), then using event.target.id to find the input element using getElementById. There are two problems there:
If event.target.id were the correct ID, you'd already have the element: event.target.
It's entirely possible to click somewhere in the div that isn't the input element. event.target is the element that was clicked (in your case, that might be the div, the label, or the input itself). I suspect that's what's happening, you're clicking an element (a label, for instance) that doesn't have an id, so you're passing undefined into getElemntById, so it's looking for id="undefined" and not finding it.
Instead, given the structure of your HTML, you should look for the input within the div using querySelector:
const div = event.currentTarget;
const checkbox = div.querySelector("input[type=checkbox]");
Also note that if (element.checked === true) is more idiomatically written as simply if (element.checked).
So doing all of the above:
function sickness_selection() {
const div = event.currentTarget;
const checkbox = div.querySelector("input[type=checkbox]");
// assert(checkbox !== null);
const label = checkbox.parentElement;
if (checkbox.checked) {
label.classList.add("asanism-selected-radio");
} else {
label.classList.remove("asanism-selected-radio");
}
}
If you don't need to support Internet Explorer, you could use the second argument to classList.toggle instead:
function sickness_selection() {
const div = event.currentTarget;
const checkbox = div.querySelector("input[type=checkbox]");
// assert(checkbox !== null);
const label = checkbox.parentElement;
label.classList.toggle("asanism-selected-radio", checkbox.checked);
}
The above assumes that the handler function is hooked on the div that's immediately around the checkbox (again, this is an asumption I'm making from the error message). But in a comment you've asked how to handle multiple checkboxes, which suggests to me it may be on a div deeper in the document that contains multiple copies of the structure you've shown.
If so, the code above won't work (it'll always find the first input[type=checkbox] in that outer div). Instead, starting from the target, find that inner div to start with (see the *** line):
function sickness_selection() {
const div = event.target.closest("div"); // ***
const checkbox = div.querySelector("input[type=checkbox]");
// assert(checkbox !== null);
const label = checkbox.parentElement;
label.classList.toggle("asanism-selected-radio", checkbox.checked);
}
Internet Explorer doesn't have that closest method. You can probably find a polyfill for it. Or just replace it with a loop:
// If you really need IE support
function sickness_selection() {
let div = event.target;
while (div && !div.matches("div")) {
div = div.parentElement;
}
// assert(div !== null);
const checkbox = div.querySelector("input[type=checkbox]");
// assert(checkbox !== null);
const label = checkbox.parentElement;
if (checkbox.checked) {
label.classList.add("asanism-selected-radio");
} else {
label.classList.remove("asanism-selected-radio");
}
}
Finally: It looks like your HTML has a label within a label. That's invalid HTML:
label
...
Content model:
Phrasing content, but with no descendant labelable elements unless it is the element's labeled control, and no descendant label elements.
(my emphasis)

how to find the child node element of a div "contenteditable" where the cursor is writing on input event handler

I am trying to create a custom textarea editor by adding contenteditable as attribute on a div element that the text will be as text element and hashtag will be as span element, but the problem is when i want to add a hashtag between text and hashtag, to do that must to find the current index of child element that the cursor is writing, and then i can use inderBefore method to add this element after this index thanks
my html code:
<div class="text">
<div #textarea class="textarea" contenteditable="true"
(input)="onchangeText($event)" (focusin)="toggleFocus()" (focusout)="toggleFocus()" >
that is a post <span class="hashtag">#hashtag</span> try to add hashtag at the
<span class="hashtag">#beginig</span> will be add as last child to the and
</div>
<div class="placeholder" *ngIf="checkPlaceHolder()" (click)="onclickPlaceHolder($event)"
[ngStyle]="{opacity:focus? 0.5 : 1}">
What do you want to share?
</div>
</div>
here is my input event handler:
onchangeText(event) {
// on input event handler
if (this.isAfterHashtag()) {
this.onAddElement(event.data, "text"); //add new element
}
if (this.isHashtag(event.data)) {
// if character is # hashtag
this.onAddElement(event.data, "hash");
}
this.setText();
if (this.checkPlaceHolder()) {// if the length of textare is 0
this.textarea.nativeElement.innerHTML = "";
}
}
here is my addElementMethod:
onAddElement(text, type) {
this.removeLastCharacter();
if (type === "hash") {
// add element for hash tag
const span: HTMLParagraphElement = this.renderer.createElement("span");
span.innerHTML = text;
this.renderer.addClass(span, "hashtag");
this.renderer.appendChild(this.textarea.nativeElement, span);
//must use insertBefore method, but before it must find index of child elemenet where to add it
// this.renderer.insertBefore( this.textarea.nativeElement , p , this.textarea.nativeElement.children[1] );
} else if (type === "text") {
// add element for text
const textNode = this.renderer.createText("");
textNode.textContent = text;
this.renderer.appendChild(this.textarea.nativeElement, textNode);
}
this.setText();
this.setCursorToEnd();
}
here is an example:
custom textarea editor

getRangeAt(0) returns #text node with parentNode document-fragment

I'm trying to get the selected node. Let's say I got all selected "CLICK TO EDIT".
<span style="text-transform: lowercase">CLICK TO EDIT</span>
Attempt to grab the current selection through:
var select = window.getSelection();
var range = select.getRangeAt(0).cloneRange();
var selectedNode = range.cloneContents().childNodes[0];
In some cases, selectedNode is a #text node containing "CLICK TO EDIT" instead of a SPAN element.
What I want is the span containing this text node. But selectedNode.parentNode is a document-fragment, selectedNode.parentElement is null.
How am I supposed to get the span element in this case then?
This code listens for selectionchange events, and gets the parent element of the selected text.
document.addEventListener('selectionchange', e => {
var selection = window.getSelection();
var anchorNode = selection.anchorNode;
if (anchorNode) {
// This is the <span>, when you select the text
var parentElement = anchorNode.parentElement;
console.log(parentElement);
}
});
<span style="text-transform: lowercase">CLICK TO EDIT</span>

Pure Javascript target label

I have some checkboxes and I want the text of the label to change when the checkbox is selected:
JSFIDDLE
var listener = function() {
document.addEventListener('change', function(e) {
if (e.target.checked) {
e.target.label.className = "option-selected";
}
}
}
}
HTML if you are interested:
<input id="0A" class="individual-checkbox" type="checkbox" value="A">
<label for="0A">A</label>
<br>
<input id="0B" class="individual-checkbox" type="checkbox" value="B">
<label for="0B">B</label>
Obviously, target.label doesn't work. How do I access the label of the target and give it a CSS class (purely in JavaScript)?
To make your jsFiddle work, you have to change the following:
Fix the syntax error (missing a closing paren for the addListener() function call and you have an extra closing brace.
Actually call the listener() function to make it run.
Target the actual label, not the checkbox with your class
There are several different ways to target the label. The simplest would be to enclose the input inside the label and then just use .parentNode to get the label from the checkbox.
HTML:
<label for="0A">
<input id="0A" class="individual-checkbox" type="checkbox" value="A">
A</label>
<br>
<label for="0B">
<input id="0B" class="individual-checkbox" type="checkbox" value="B">
B</label>
code:
var listener = function() {
document.addEventListener('change', function(e) {
if (e.target.checked) {
e.target.parentNode.className = "option-selected";
}
});
}
listener();
Working jsFiddle: http://jsfiddle.net/jfriend00/s2A7W/
If you don't want to change your HTML, then you just need to find the label element that is right after your input element.
You can do that like this:
var listener = function() {
document.addEventListener('change', function(e) {
var label;
if (e.target.checked) {
label = next(e.target, "label");
if (label) {
label.className = "option-selected";
}
}
});
}
listener();
function next(src, tag) {
tag = tag.toUpperCase();
while (src && src.tagName !== tag) {
src = src.nextSibling;
}
return src;
}
Working jsFiddle: http://jsfiddle.net/jfriend00/3wQKa/
FYI, you should probably also restrict the action of your listener to only when a checkbox is actually the target or an element with a particular classname or some other distinguishing feature that makes sure it's a checkbox you want targeted by this code. You are probably safe with e.target.checked, but I don't like the fact that this event listener responds to all propagated change events in the entire page.
Assuming there is only one label element associated with the element:
e.target.labels[0].className = "option-selected";
This is an HTML5 property though, I don't know how well it is supported in older browsers. Source: MDN.
Alternatively, if IE8 support is enough for you, you can explicitly search for it with document.querySelector:
var label = document.querySelector('[for=' + e.target.name + ']');
This only works if you give the input elements name attributes (which you really want to do, otherwise the labels are not properly connected to the input elements).
And finally, if the label always comes after the input, you can traverse the DOM:
var label = e.target.nextSibling;
while (label.nodeName !== 'LABEL') {
label = label.nextSibling;
}
If you'd restructure your HTML so that the input element is a child of the label element:
<label for="0A">
<input id="0A" class="individual-checkbox" type="checkbox" value="A">
A
</label>
then you could simply use e.target.parentNode to get the label. Putting the input element inside the label also connects the label to the input.
this will do the trick:
function addListener(elem) {
// remember: checkboxes never change trair value
elem.addEventListener('click', function () {
var ls = document.getElementsByTagName('label');
for (var l = 0, ll = ls.length, lbl; l < ll; ++l) {
lbl = ls[l];
if ( this.id == lbl.getAttribute("for") )
{
lbl.className = ( this.checked ? "option-selected" : "" );
}
}
});
}
see http://jsfiddle.net/fGSCH/10/
be aware that addListener() might not work in every browser. but since you used it in the example i will use it too.

Categories