contenteditable - Selecting 2 child paragraphs and writing text over (Firefox Issue) - javascript

I have been searching on Google for over a week now, I've been trying to implement different solutions, but with no success, and it's bugging the hell out of me.
So you have a contenteditable div with several paragraphs(or other child elements of the same kind). Obviously this is the kind of layout you wanna keep. If the user selects two or more paragraphs and types text over it, it removes the paragraphs and sets the caret focus inside the parent div:
body {
font-family: georgia;
}
.editable {
color: red;
}
.editable p {
color: #333;
}
.editable span {
color: limegreen !important;
}
<div class="editable" contenteditable><p>paragraph one</p><p>paragraph two</p></div>
<hr>
<p>How to reproduce the bug:</p>
<ul>
<li>Focus the contenteditable above by placing the cursor somewhere in one of the two paragraphs.</li>
<li>press ctrl-a (in windows or linux) or cmd-a (in osx) to select-all</li>
<li>type some text</li>
<li>red text means that the text went directly inside the contenteditable div, black text means it went inside a paragraph</li>
</ul>
<p>The correct behaviour should be that that select-all and delete (or "type-over") in a contenteditable with only block tags should leave the cursor inside the first block tag.</p>
<p>Webkit gets this right, Firefox gets it wrong.</p>
I did try something like this in Jquery:
$(document).on('blur keyup','div[contenteditable="true"]',function(event){
var sel = window.getSelection();
var activeElement = sel.anchorNode.parentNode;
var tagName = activeElement.tagName.toLowerCase();
if (!(tagName == "p" || tagName == "span")) {
console.log('not on editable area');
event.preventDefault();
//remove window selection
var doselect = window.getSelection();
doselect.removeAllRanges();
}
});
So after blur or keyup event on contenteditable, detect where the caret position and if it's outside accepted editable areas stop the event or something?
I've tried changing the selection range, and a bunch of other stuff but maybe I'm just not seeing it. I'm sure a lot of people have had the same problem, but the only answers I found on Google or here is "content editable sucks", "why don't you just use an open source editor" and that kind of stuff.
Firefox weird behaviour: Inserting BR tags on break line
I have also tried to remove Firefox'es weird behaviour with a function to remove all the <BR> tags firefox automatically inserts. Like this:
function removeBr(txteditor) {
var brs = txteditor.getElementsByTagName("br");
for (var i = 0; i < brs.length; i++) { brs[i].parentNode.removeChild(brs[i]); }
}
So I attached this to a keydown event, and it does exactly what it's expected, however that causes more weird behaviour (like preventing you adding spaces on selected paragraph).
Please vote up the question so we can raise more awareness.
I'm sure a lot of other people have bumped into the same problem, I think it would be good to know if there's a workaround or any "right" way to do it. I think it should really be discussed...

So Firefox injects this abomination - <br></br> - into the contenteditable div when removing the paragraphs.
With a little bit of jQuery we can remove it and replace it with paragraph tags.
Current Limitation - The break tag seems to be injected only when removed with delete or backspace, not when typed over... consider this a concept :-)
Open this example in Firefox to test:
$("button").click(function() {
$("br:not(p br)").replaceWith('<p>Write Me!</p>'); // dont replace when inside p
});
body {
font-family: georgia;
}
.editable {
color: red;
}
.editable p {
color: #333;
}
.editable span {
color: limegreen !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div class="editable" contenteditable>
<p>paragraph one</p>
<p>paragraph two</p>
</div>
<hr>
<p>Remove all text to trigger bug in Firefox and hit "fix problem!"</p>
</ul>
<p>Webkit gets this right, Firefox gets it wrong.</p>
<button class="fix">Fix Problem</button>

Whats happening is this, when you select both groups of text and delete them you are also deleting all tags within the editable element. So your actually deleting the <p> tags from the div and then the only thing to write to is the div itself.
Why do you need two seperate paragraph tags? It would be easier to have the div by itself...
Rather than setting the div as editable, have you tried setting the <p> tags as <p contenteditable="true">

Related

Div innerText loses new lines after setting display to none

While programming a custom WYSIWYG HTML based editor I came to this strange behavior.
jsfiddle
One enters some text containing new lines in a div, which has contentEditable set to true.
At that moment div.innerText contains exactly the entered text including the new lines.
Then one sets div.style.display = none and re-checks the div.innerText: it is the same text, but the new lines are removed.
Why is that? Is there "standard behavior" for this case?
(Tested in both FF Developer Edition 89.0b3 (64-bit) and Chrome Version 90.0.4430.85 (Official Build) (64-bit))
=> Follow up
There is also another similar strange problem:
var d = document.getElementById("divTest")
function setup() {
d.innerText = "1\n2"
}
function log() {
console.log(d.innerText)
logChildren()
}
function logChildren() {
console.log("Child nodes: ");
d.childNodes.forEach(node => {
console.log(node.toString());
});
}
div[contenteditable] {
border: 1px solid red;
}
<div contenteditable="true" id="divTest"></div>
<input type="button" value="setContent" onclick="setup()"/>
<input type="button" value="log" onclick="log()"/>
Click on the setContent button and then on log button. The output is as expected:
1
2
[object Text]
[object HTMLBRElement]
[object Text]
Then click inside the input div. Press enter after the 2 to go to a new line, and press 3. One gets
1
2
3
in the div and one would expect to get the same in the div.innerText, but it is unfortunately:
1
2
3
Child nodes:
[object Text]
[object HTMLBRElement]
[object HTMLDivElement]
[object HTMLDivElement]
Why would 1 be a [object Text] but 2 and 3 [object HTMLDivElement] ? Why would there be empty line between 1 and 2? etc. ...
It does not make any sense to me.
The implementation of innerText depends on whether or not the element is visible. As #Nisala noted in the comments, if you set the display attribute of the div back to block, the innerText contains your newline characters again.
const input = document.querySelector("#input");
function performExperiment() {
console.log(`innerText before style change: ${input.innerText}`);
input.style.display = "none";
console.log(`innerText after first style change: ${input.innerText}`);
input.style.display = "block";
console.log(`innerText after second style change: ${input.innerText}`);
}
#input {
border: 1px solid black;
}
<p>Enter multiple lines of text into the box below, then click the button</p>
<div id="input" contenteditable="true"></div>
<button onclick="performExperiment()">Experiment</button>
If we have a look at the innerText documentation, we see the first step of the behavior for the getter is defined as follows:
If this is not being rendered or if the user agent is a non-CSS user agent, then return this's descendant text content.
Note: This step can produce suprising results, as when the innerText getter is invoked on an element not being rendered, its text contents are returned, but when accessed on an element that is being rendered, all of its children that are not being rendered have their text contents ignored.
So when our div is not being rendered, we should expect that innerText returns the textContent of our div. Indeed, that is what we see.
const input = document.querySelector("#input");
function performExperiment() {
input.style.display = "none";
console.log(`innerText: ${input.innerText}`);
console.log(`textContent: ${input.textContent}`);
}
#input {
border: 1px solid black;
}
<p>Enter multiple lines of text into the box below, then click the button</p>
<div id="input" contenteditable="true"></div>
<button onclick="performExperiment()">Experiment</button>
So why are the newlines present in our innerText when the div is visible? The documentation continues:
Let results be a new empty list
For each child node node of this:
Let current be the list resulting in running the inner text collection steps with node. Each item in results will either be a string or a positive integer (a required line break count).
In this case, innerText is ignoring textContent and is instead operating on the childNodes list. Let's see what the value of that is for our div:
const input = document.querySelector("#input");
function performExperiment() {
input.childNodes.forEach(node => {
console.log(node.toString());
});
}
#input {
border: 1px solid black;
}
<p>Enter multiple lines of text into the box below, then click the button</p>
<div id="input" contenteditable="true"></div>
<button onclick="performExperiment()">Experiment</button>
As you can see, pressing the ENTER key adds a newline to the content of our div by adding a div to the childNodes list of our div. Why this is the case is outside the scope of this question, but would make for a good question on its own.
If you're working on an in-page editor, the HTML spec has a section containing best practices.
To recap:
If the div is visible, the innerText getter uses the textContent property of the div.
If the div is not visible, the inner text collection steps are followed for each node in the childNodes tree and the results are concatenated together.
When computing the value of innerText for our div, the value of the display attribute matters because it determines whether the textContent property or the evaluation of the childNodes tree will be used.
Note: There's a little more information in this answer by #Domino.

Hide div if it only contains <script> tags

Similar to this 6-year old question, but I'm not looking at parent elements and wondering if there is a better solution? hide div (it contains only script tag)
Question:
My website has some blocks for google adsense advertisements. These blocks are just styled divs (background color, padding, margin, border-radius etc.) There is nothing in them except for the adsense script tags (and of course white space/text nodes, etc.). When a user has an ad block extension/plug-in, these divs still display, but they appear totally empty on the screen, which is not desirable. Therefore, I'm looking for a way to hide these divs if no ad is rendered. Using :empty or does not work since it still picks up the <script> tags.
Div element:
<div class="contentBlock"><script>//adsense code</script></div>
Perhaps there is a better way of tackling or conceptualizing this problem? If so, I'm open to alternative solutions. Thanks!
You can remove the script tags, and then check to see if the blocks are empty. In code that runs after the ad code would run if it were allowed to:
var blocks = $(".contentBlock");
blocks.find("script").remove();
blocks.each(function() {
var div = $(this);
if (!div.html().trim()) {
div.remove();
}
});
Or if you like chaining a lot:
$(".contentBlock")
.find("script")
.remove()
.end()
.each(function() {
var div = $(this);
if (!div.html().trim()) {
div.remove();
}
});
If you have to support obsolete browsers, use $.trim() instead of the native String.prototype.trim (or polyfill it).
It seems that you need a feature which is on CSS4 proposed list: https://www.w3.org/TR/selectors-4/#has-pseudo
The relational pseudo-class, :has(), is a functional pseudo-class
taking a relative selector list as an argument. It represents an
element if any of the relative selectors, when absolutized and
evaluated with the element as the :scope elements, would match at
least one element.
...
The following selector matches elements that don’t contain any heading elements:
section:not(:has(h1, h2, h3, h4, h5, h6))
Another workaround is to use jquery to mark divs with content and then remove/hide the others. See fiddle: http://jsfiddle.net/mczv6gys/
$('div.contentBlock').has('p, span, img, div').addClass('markerClass'); // P or any other typical tag
$('div.contentBlock').not('.markerClass').addClass('hideClass'); // eg display:none
You have the ability to either hide the div, or replace it with new content if AdSense hides the content.
If this example isn't working, your going to want to deal with the content dynamically created from Adsense. Such as a check for $("#google_ads_frame1").css('display')=="none"). Instead of checking your container div. Which would work, it was tested.
You wrap your content into a container / div for strictly
advertisements. Then set a timeout function to determined said
issues. If adblock enabled, then show alternative content such as a
video, poster, iframe, etc.
Since adblock hides stuff with css too, you can then edit the css to
ultimately hide everything if enabled instead of replacing. Two
options.
Change snippet timeout to whatever time you want to wait to check
after page loaded, to handle async ads.
// Page has loaded
window.onload = function(){
// Set a timeout function for async google ads
setTimeout(function() {
// We are targeting the first banner ad of AdSense
var ad = document.querySelector(".ads");
// If the ad contains no innerHTML, adblock is enabled
// Or check adsense's generated content
// $("#google_ads_frame1").css('display')=="none")
if (ad && ad.innerHTML.replace(/\s/g, "").length == 0) {
// Adblock hides with css also, or just hide everything.
ad.style.cssText = 'display:block !important; background-color: LightCoral;';
// Replace, if adblock enabled
ad.innerHTML = 'We detected adblock';
}
}, 1); // Change this to 2000 to run after two seconds, to handle async ads
};
.ads {
background-color: lightgreen;
}
<p><b>Example 1:</b> Let us simulate an adblock situation, the <code>script</code> tag did not load, hence adblock</p>
<div class="ads"></div>
<p><b>Example 2:</b> Let us simulate no adblock, <code>script</code> tag loaded</p>
<div class="ads"><script></script>Adblock ran</div>
If you support ES6 features, you can filter .contentBlock which every nodes satisfy condition is "empty" textNode, is Script or is comment:
let emptyBlocks = [...document.querySelectorAll('.contentBlock')].filter(block =>
[...block.childNodes].every(node =>
(node.nodeName === '#text') ?
!node.textContent.trim():
['SCRIPT','#comment'].includes(node.nodeName)
)
);
emptyBlocks.forEach(function(block){
block.classList.add('empty');
});
.contentBlock{
min-height:50px;
width:200px;
outline: solid 2px red;
margin:2px;
}
.contentBlock.empty{
display: none;
}
<div class="contentBlock">
<script>//nothing's happened</script><!--comment-->
</div>
<div class="contentBlock">
<script>document.write('something\'s happened');</script>
</div>
<div class="contentBlock">
<!--comment-->
<script></script>
<!-- empty text nodes with spaces and tabulations -->
</div>
<div class="contentBlock">
<!--//-->
text node
</div>

Copy plain text with no rich styles to clipboard without losing focus?

I looked at this question where it is asked for a way to simply copy text as plain text. I want to do exactly that but with one additional thing - not lose focus on the current element.
I need this for a Chrome extension, so I'm not bothered with cross-browser support. When the user types in an input (or contenteditable), a dropdown with choices appears. If he chooses one of them, it is copied to his clipboard. I don't want the element to lose focus because some sites might have implemented logic to run on the element's blur event.
Here's what I've tried:
Solution 1
Create an <input> element and use its select() method:
function clipWithInput(text) {
var input = document.createElement("input");
document.body.appendChild(input);
input.addEventListener("focus", function (e) {
e.preventDefault();
e.stopPropagation();
});
input.value = text;
input.select();
document.execCommand("copy");
document.body.removeChild(input);
}
document.getElementById("choice").onmousedown = function (e) {
e.preventDefault(); // prevents loss of focus when clicked
clipWithInput("Hello");
};
#main {background: #eee;}
#choice {background: #fac;}
<div id="main" contenteditable="true">Focus this, click the div below and then paste here.</div>
<div id="choice">Click to add "Hello" to clipboard</div>
As you can see, this works. The text is copied. However, when you focus the contenteditable and click on the "choice", the focus is lost. The choice element has preventDefault() on its mousedown event which causes it to not break focus. The dummy <input> element is the problem here, even though it has preventDefault() on its focus event. I guess the problem here is that it's too late - the initial element has already fired its blur, so my dummy input's focus is irrelevant.
Solution 2
Use a dummy text node and the Selection API:
function clipWithSelection(text) {
var node = document.createTextNode(text),
selection = window.getSelection(),
range = document.createRange(),
clone = null;
if (selection.rangeCount > 0) {
clone = selection.getRangeAt(selection.rangeCount - 1).cloneRange();
}
document.body.appendChild(node);
selection.removeAllRanges();
range.selectNodeContents(node);
selection.addRange(range);
document.execCommand("copy");
selection.removeAllRanges();
document.body.removeChild(node);
if (clone !== null) {
selection.addRange(clone);
}
}
document.getElementById("choice").onmousedown = function (e) {
e.preventDefault(); // prevents loss of focus when clicked
clipWithSelection("Hello");
};
#main {background: #eee;}
#choice {background: #fac;}
<div id="main" contenteditable="true">Focus this, click the div below and then paste here.</div>
<div id="choice">Click to add "Hello" to clipboard</div>
This works perfectly at first glance. The text is copied, no focus is lost, the caret stays at the same position. No drama. However, when you paste the text in a contenteditable (like Gmail's email composer), this is the result:
<span style="color: rgb(0, 0, 0); font-family: "Times New Roman"; font-size: medium;">Hello</span>
Not plain text.
I tried appending the element in the <head> where there are no styles - nope. Text isn't selected and nothing is copied.
I tried appending the text node in a <span> and set stuff like style.fontFamily to inherit, as well as fontSize and color. Still doesn't work. I logged the dummy element and it correctly had my inherit styles. However, the pasted text didn't.
Recap
I want to programmatically copy plain text with no styles while preserving focus on the currently active element.
Your solution (especially 2) was okay. When you paste in a contenteditable, it needs to be expected that there are span codes inserted, many use that in insertHTML. You are not to expect plain text programmatically. Some would suggest not using a contenteditable at all (though I understand you're talking about some extension). But your solution is more compatible with mobiles than MDN or such.
So, you programmatically copy plain with no style added (if no contenteditable) while preserving focus on the current element.

jQuery remove() or after() causing whitespace to be removed

I have the following html:
<p>This is some random text in a paragraph with a <span class="blue">blue</span> word.</p>
<p>This is some random text in a paragraph with a <span class="blue">blue</span> <i>word</i>.</p>
<p>This is some random text in a paragraph with a <span class="blue">blue</span> <span class="blue">word</span>.</p>
My CSS is as follows:
.blue{
color:blue;
}
.popup{
background-color:lightblue;
}
And finally my JS:
var popup = false;
$(".blue").click(function(){
if (!popup){
$(this).after("<div class='popup'>This is some popup text</div>");
popup = true;
}
else{
$(".popup").remove();
popup = false;
}
});
Now my problem, when I call the remove function on my popup class it removes whitespace between tags
As explained in some answers below, the after function could also be causing this.
. eg:
<span class="blue">blue</span> <i>word</i>
becomes
<span class="blue">blue</span><i>word</i>
It does not do this when the text following a blue class is not in tags eg:
<span class="blue">blue</span> word
Why does this happen and how can I prevent it?
For further reference here is a fiddle: http://jsfiddle.net/6fqDq/
Edit: It seems this problem is localized to Chrome as it does not occur in FF or IE.
The reason this is happening is because you added a block element(div), the block element breaks into a new line, then when it's removed, it takes away the whitespace with it that's after it, because for HTML a whitespace and a newline is pretty much the same.
You have several solutions, some were mentioned here :
Use &nbsp
Put a space inside the <i> tag instead of between tags, so <i> word</i> would work fine.
Use a <span> tag instead of a div tag on the after.
I don't think the problem is with remove but with after in this case which probably ignore text node and therefore ignoring the white space. If you use append, also it does place the element somewhere else the problem disappear.
http://jsfiddle.net/6fqDq/8/
var popup = false;
$(".blue").click(function() {
if (!popup) {
$(this).append("<div class='popup'>This is some popup text</div>");
popup = true;
} else{
$(".popup").remove();
popup = false;
}
});
Not sure why it does it. But a quick fix would be to put the white space within the <i> tag. Like this:
<p>This is some random text in a paragraph with a <span class="blue">blue</span><i> word</i>.</p>
See http://jsfiddle.net/6fqDq/5/
just add
<p>This is some random text in a paragraph with a <span class="blue">blue</span> <span class="blue">word</span>.</p>
Use .append instead of .after()
if (!popup){
$(this).append("<div class='popup'>This is some popup text</div>");
popup = true;
}
I can't explain why it doesn't work with .after, but i think it could be appending on the whitespace
DEMO
Not sure why it is happening, but maybe replace your spaces with entities? is a space. You could do it with javascript str.replace('> <', '> <' ) or in php str_replace('> <', '> <', str)

Click to show more - maybe JS?

I am not sure what language or how to do this, but I am looking to have a word on a page, and when clicked, it will reveal more underneath. If it is clicked again, that stuff will hide away again? Any ideas?
Basically, you will need to manipulate the display CSS property of the element to be hidden/revealed:
<span id="showHide">Show</span>
<div id="foo" style="display:none">Here is some text</div>
<script>
document.getElementById("showHide").onclick = function() {
var theDiv = document.getElementById("foo");
if(theDiv.style.display == 'none') {
theDiv.style.display = 'block';
this.innerHTML = 'Hide';
} else {
theDiv.style.display = 'none';
this.innerHTML = 'Show';
}
}
</script>
I'd recommend javascript and using jQuery .show() & .hide() methods:
http://api.jquery.com/show/
http://api.jquery.com/hide/
http://www.learningjquery.com/2006/09/slicker-show-and-hide
you could do this with jQuery, here a ready to use example:
http://jsfiddle.net/8sDLg/
$(function() {$('div.more').hide()
$('a.showmemore').click(function(){
$(this).next('div').slideToggle()
})})
Put the stuff in a div with style display:none;. In the onClick handler of the word (can be link or a button), toggle the display style between '' (which means 'default') and 'none'
I created a demo for you here.
You could use jQuery for that and make your life easy:
Show/Hide
<div id="mydiv">Some content</div>
jQuery:
$(function(){
$('#link').click(function(){
$('#mydiv').slideToggle();
return false;
});
});
As can be seen, the slideToggle() does the trick there for you :)
Executive summary: use a framework plugin
Long version:
You could use javascript -- more likely in a combination with a javascript framework, like jQuery. This would involve adding a click handler to the word (actually a span tag around it) and having a way to retrieve the extra information to show as a tooltip -- there are plenty of plugins for this. Search for "jquery tooltip" here or using google: here's one example.
Alternatively, you could simply surround the word with the span tag and add a title attribute to the tag. Hovering over the word (actually the tag) would bring up the browser's default tooltip. This might be an easy way to get started with it -- in fact, this could be the start of the javascript solution. Using the tag for the click event and taking the data from the title attribute -- probably by storing the title in jQuery data on page load, then grabbing the text from the data on click so that you don't have a conflict with the browser's tool tip behavior. Many of the plugins operate this way.
Another elegant approach using pure HTML and CSS without JavaScript.
HTML:
here goes text before
<label class="details">
<input type="checkbox" /><span>here come some details</span><em> </em>
</label>
and after
CSS:
.details input,
.details span {
display: none;
}
.details input:checked~span {
display: inline;
border-bottom: dotted 1px gray;
}
.details em:after {
content: "show...";
}
.details input:checked~em:after {
content: "...hide";
}
Quick idea how to do it when avoiding a JS-only solution. I'm using jQuery here, because it is faster to code in, but as I mentioned above, if this is your only JS functionality it would only add a heavy-weight file for some trivial extras.
<script type="text/javascript">
jQuery(function() {
$(".article .additional")
.hide()
.before("<a href='#'>")
.prev()
.text("more")
.click(function() {
$(this).next().toggle()
})
});
</script>
<div class="article">
<h2>Some headline</h2>
<p>Some intro text that is always visible</p>
<div class="additional">
<p>Some extra text that is hidden by JS</p>
<p>But will stay visible if the visitor doesn't have JS</p>
</div>
</div>
As you see, the HTML is completely stand-alone. Only if JavaScript is supported, a "more" link will be added and the additional content hidden, so that non-JS users still can read all the text and don't have an useless "more" link.

Categories

Resources