I need to check if an HTML element (any element will do) is empty of contents. By contents I don't mean text content or any other HTML elements, because that is already a given. I mean want to check if it's simply a blank element. For example:
If I hide the link, the only thing left is the paragraph element, which is now blank. It does not have any styles on it, so it's just a transparent container with margin/padding/whatever, but no other visual styles. Is there a way to detect this in Javascript? Any weird browser quirk will do. Ugly solutions valid too.
You want to recursively walk the childNodes of your element and check them to see if they are all blank. The recursive walking logic isn't difficult. It really all depends on how you define a blank element:
Display: none
Or visibility: hidden
Or all off the following criteria are true:
Text
None
or only whitespace
or text color is transparent
or text color is the same as the parent's background color
Background
Transparent
Matches parent
Border
Style: none
or width 0px
or color transparent
or color matches parent background
Tag
Not <img />
Children
none
or all satisfy above criteria
So, your recursive function would look something like this:
if (!window.getComputedStyle)
{
window.getComputedStyle = function(el)
{
return el.currentStyle || {};
};
}
function isElementBlank(el)
{
var style = window.getComputedStyle ? getComputedStyle(el, null) : el.currentStyle;
if (style.display == "none" || style.visibility == "hidden")
{
return true;
}
if (el.tagName.toLowerCase() == "img")
{
return false;
}
var parentBG = "transparent";
for (var parent = el.parentNode; parent; parent = parent.parentNode)
{
var parStyle = getComputedStyle(parent, null);
if (parStyle.backgroundColor != "transparent")
{
parentBG = parStyle.backgroundColor;
break;
}
}
if (style.backgroundColor != "transparent" && style.backgroundColor != parentBG)
{
return false;
}
if (style.borderColor != "transparent" &&
style.borderColor != parentBG &&
parseInt(style.borderWidth) > 0 &&
style.borderStyle != "none")
{
return false;
}
for (var i = 0; i < el.childNodes.length; i++)
{
var child = el.childNodes[i];
if (child.nodeType == 3)
{
if (/\S/.test(child.nodeValue) &&
style.color != parentBG &&
style.color != "transparent")
{
return false;
}
}
else if (!isElementBlank(child))
{
return false;
}
}
return true;
}
I didn't test this, but it's a start at least.
You could use a method where you remove all HTML and then trim the result.
If the result is empty string then there are no visible text.
This would not detect a div with height and background-color though so of you need that kind of detection you would also need to go over the while subtree looking for elements with any visible css which would require a very verbose code.
To detect empty-of-content tags you can iterate all child nodes of every nodes and looking for not empty text nodes. If a not empty text node is found the test pass, otherwise the node is an empty of content one. I've done a test that seems to works fine in Firefox:
<div id="container">
<div>Text <span>buzz</span>
</div>
<p>Hello
</p>
</div>
<script>
var all = document.getElementsByTagName('body')[0].getElementsByTagName('*');
allNodes: for (var i = 0, l = all.length; i < l; i++) {
// for each childNode of every node...
for (var ii = 0, sub = all[i].childNodes, ll = sub.length; ii < ll; ii++) {
// is it a text node?
if (sub[ii].nodeType == 3) {
// Is the text node NOT empty?
if (/[^\s]+/.test(sub[ii].nodeValue)) {
continue allNodes;
}
}
}
console.log(all[i], ' fails');
}
</script>
The test result is:
<div id="container"> fails
<p> fails
Related
I have elements like below
<div class="one">send Message</div>
<div class="one">send Message</div>
<div class="one">send Message</div>
I have a web page where there is send Message buttons like above, in which only one button is visible at a time.Other two buttons are hidden via some javascript codes.So for example if 2nd button is visible , I should be able to get only that element.
So my code will be something like
document.querySelector(".one:visible");
In jquery the code is $(".one:visible"); , which works fine , But I need to know how to do this via pure javascript.
Here's something you can use, pure Javascript:
// Get all elements on the page (change this to another DOM element if you want)
var all = document.getElementsByTagName("*");
for (var i = 0, max = all.length; i < max; i++) {
if (isHidden(all[i]))
// hidden
else
// visible
}
function isHidden(el) {
var style = window.getComputedStyle(el);
return ((style.display === 'none') || (style.visibility === 'hidden'))
}
I have something shorter:
Array.from(document.querySelectorAll('.one')).filter(s =>
window.getComputedStyle(s).getPropertyValue('display') != 'none'
);
Returns all elements with attribute display block set.
Use getBoundingClientRect. It will return height and width of zero if the element is not in the DOM, or is not displayed.
Note that this cannot be used to determine if an element is not visible due to visibility: hidden or opacity: 0. AFAIK this behavior is identical to the jQuery :visible "selector". Apparently jQuery uses offsetHeight and offsetWidth of zero to check for non-visibility.
This solution will also not check if the item is not visible due to being off the screen (although you could check that easily enough), or if the element is hidden behind some other element.
See also Detect if an element is visible (without using jquery)
var $el = document.querySelectorAll('.one');
var visibleElements;
for (var i = 0; i < $el.length; i++) {
var currentElement = $el[i];
var $style = window.getComputedStyle(currentElement, null);
if (!currentElement) {
return false;
} else if (!$style) {
return false;
} else if ($style.display === 'none') {
return false;
} else {
visibleElements.push(currentElement);
}
}
First we get all the elements using document querySelectorAll. Then, we need to iterate over all the elements. To get the style, use getComputedStyle.
After that :visible check only for display and we do it the same way.
A more comprehensive approach:
function isVisible(el) {
while (el) {
if (el === document) {
return true;
}
var $style = window.getComputedStyle(el, null);
if (!el) {
return false;
} else if (!$style) {
return false;
} else if ($style.display === 'none') {
return false;
} else if ($style.visibility === 'hidden') {
return false;
} else if (+$style.opacity === 0) {
return false;
} else if (($style.display === 'block' || $style.display === 'inline-block') &&
$style.height === '0px' && $style.overflow === 'hidden') {
return false;
} else {
return $style.position === 'fixed' || isVisible(el.parentNode);
}
}
}
This would check for any possible way an element could be visible in the dom to my knowledge minus the z-index cases.
If you're using the hidden attribute :
document.querySelector(".one:not([hidden])");
So all jQuery's :visible selector does is check the display property.
If that's all you want, this is all you'd need.
(window.getComputedStyle(el).getPropertyValue('display') !== 'none')
However, this is lacking in many use cases. If you seek a more comprehensive solution, keep reading.
Both Element.getBoundingClientRect() and window.getComputedStyle() are useful for determining if the element is visible and in the viewport.
You can't use getBoundingRect() alone to determine the visibility, and while you could use getComputedStyle() solely, it's not the optimal solution in terms of performance.
Both of these functions used in conjunction with each other is the best option (around 22% faster than getComputedStyle() alone.
function inViewport(els) {
let matches = [],
elCt = els.length;
for (let i=0; i<elCt; ++i) {
let el = els[i],
b = el.getBoundingClientRect(), c;
if (b.width > 0 && b.height > 0 &&
b.left+b.width > 0 && b.right-b.width < window.outerWidth &&
b.top+b.height > 0 && b.bottom-b.width < window.outerHeight &&
(c = window.getComputedStyle(el)) &&
c.getPropertyValue('visibility') === 'visible' &&
c.getPropertyValue('opacity') !== 'none') {
matches.push(el);
}
}
return matches;
}
With a usage example of...
var els = document.querySelectorAll('.one'),
visibleEls = inViewport(els);
This ensures that the display is not set to "none", the visibility is "visible", the width and height are greater than 0, and the element is within the bounds of the viewport.
Let's say I have the following text:
Hi, <span class='blue_mark'>my name is Bob</span>.
Let's say I want to highlight Bob with a .red_mark. When I do this, the nearest parent would be .blue_mark and not the main parent. I want to calculate this because I don't want any spans nested inside of each other. Only from the main parent.
This is my code:
var selection = document.getSelection();
var range = selection.getRangeAt(0);
var contents = range.extractContents();
var node = document.createElement('span');
node.classList.add('blue_mark');
node.appendChild(contents);
range.insertNode(node);
selection.removeAllRanges(); //Clear the selection, showing highlight
Before I insertNode, I want to check if the span is nested inside another span. If so, don't insert and have an alert come up. If not, then insert the content/
So basically, you don't want to have any overlapping spans. So:
The start of the selection can't be inside a span
The end of the selection can't be inside a span
The selection cannot fully contain a span
First, a utility function:
function isInSpan(node) {
if (node && node.nodeType === 3) {
node = node.parentNode;
}
while (node && node.nodeType === 1) {
if (node.nodeName.toUpperCase() === "SPAN") {
return true;
}
node = node.parentNode;
}
return false;
}
Then, I believe you can check those like this:
if (isInSpan(range.startContainer) ||
isInSpan(range.endContainer) ||
range.cloneContents().querySelector("span")) {
// Do the alert
} else {
// Go ahead and create the span
}
(Was pleasantly surprised to see querySelector on DocumentFragment.)
I am facing an issue with the numbered list in ckeditor. When I try to bold some text in li, only the text is getting bold, without the preceding number. This is how it looks like,
One
Two
Three
It should be like this
2. Two
When I check the source, I found the code like below
<li><strong>Two</strong></li>
I would like to know is there any way to change the working of bold button, so that it will add something like below
<li style="font-weight:bold">Two</li>
<p> Hello <strong>World</strong></p>
I tried to solve your problem.
My solution isn't the best, because I guess that create a bold plugin, that takes care about list items would be the best solution.
I make it without using jQuery; however, using it the code should became simpler and more readable.
First of all, we need to define something useful for the main task:
String trim. See this.
if (!String.prototype.trim) {
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, '');
};
}
String contains. See this
String.prototype.contains = function(it) {
return this.indexOf(it) != -1;
};
First child element. The following function obtains the first child element, or not-empty text node, of the element passed as argument
function getFirstChildNotEmpty(el) {
var firstChild = el.firstChild;
while(firstChild) {
if(firstChild.nodeType == 3 && firstChild.nodeValue && firstChild.nodeValue.trim() != '') {
return firstChild;
} else if (firstChild.nodeType == 1) {
return firstChild;
}
firstChild = firstChild.nextSibling;
}
return firstChild;
}
Now, we can define the main two functions we need:
function removeBoldIfPresent(el) {
el = el.$;
var elStyle = el.getAttribute("style");
elStyle = (elStyle) ? elStyle : '';
if(elStyle.trim() != '' && elStyle.contains("font-weight:bold")) {
el.setAttribute("style", elStyle.replace("font-weight:bold", ''));
}
}
CKEDITOR.instances.yourinstance.on("change", function(ev) {
var liEls = ev.editor.document.find("ol li");
for(var i=0; i<liEls.count(); ++i) {
var el = liEls.getItem(i);
var nativeEl = el.$.cloneNode(true);
nativeEl.normalize();
var firstChild = getFirstChildNotEmpty(nativeEl);
if(firstChild.nodeType != 1) {
removeBoldIfPresent(el);
continue;
}
var firstChildTagName = firstChild.tagName.toLowerCase()
if(firstChildTagName == 'b' || firstChildTagName == 'strong') {
//TODO: you also need to consider the case in which the bold is done using the style property
//My suggest is to use jQuery; you can follow this question: https://stackoverflow.com/questions/10877903/check-if-text-in-cell-is-bold
var textOfFirstChild = (new CKEDITOR.dom.element(firstChild)).getText().trim();
var textOfLi = el.getText().trim();
if(textOfFirstChild == textOfLi) {
//Need to make bold
var elStyle = el.getAttribute("style");
elStyle = (elStyle) ? elStyle : '';
if(elStyle.trim() == '' || !elStyle.contains("font-weight:bold")) {
el.setAttribute("style", elStyle + ";font-weight:bold;");
}
} else {
removeBoldIfPresent(el);
}
} else {
removeBoldIfPresent(el);
}
}
});
You need to use the last release of CkEditor (version 4.3), and the onchange plugin (that is included by default in the full package).
CKEditor 4.1 remove your classes, styles, and attributes that is not specified in its rules.
If that's the problem, you might want to disable it by adding this line:
CKEDITOR.config.allowedContent = true;
Here is full code to use it:
window.onload = function() {
CKEDITOR.replace( 'txt_description' );
CKEDITOR.config.allowedContent = true; //please add this line after your CKEditor initialized
};
Please check it out here
<ul class="test">
<li><span>hello</span></li>
</ul>
.test li
{
font-weight:bold;
}
.test li span
{
font-weight:normal;
}
I'm using this solution by Tim Down to get selected html in a contenteditable div, and it's working fine (thank you Tim!)
But using Chrome, if I select a html string exactly at the boundaries of a html tag, as in this image: http://i.imgur.com/UiYzrcp.png?1:
what I get it's just plain text (test in this case).
If I expand the selection to a next character (letter c for example), instead I get the correct html (<strong>test</strong> c).
Can I get the full html in Webkit by selecting a word like in the image?
Thanks
Not really. WebKit normalizes each boundary of any range when it's added to the selection so that it conforms to WebKit's idea of valid selection/caret positions in the document. You could change the original function so that it detects the case of a selection containing all the text within an element and expanding the selection range to surround that element (without actually changing the selection). Here's a simple example (you may need something cleverer for a more general case, such as when the text is inside nested elements, detecting block/inline elements, etc.):
Demo: http://jsfiddle.net/btLeg/
Code:
function adjustRange(range) {
range = range.cloneRange();
// Expand range to encompass complete element if element's text
// is completely selected by the range
var container = range.commonAncestorContainer;
var parentElement = container.nodeType == 3 ?
container.parentNode : container;
if (parentElement.textContent == range.toString()) {
range.selectNode(parentElement);
}
return range;
}
function getSelectionHtml() {
var html = "", sel, range;
if (typeof window.getSelection != "undefined") {
sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
range = adjustRange( sel.getRangeAt(i) );
container.appendChild(range.cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
}
return html;
}
Code: http://jsfiddle.net/DerNalia/9vHK4/2/
Version showing: the selected html as text (expanded): http://jsfiddle.net/DerNalia/Wnd3j/
So, when I do a Selection like this:
My code generates a "highlight" like this:
Notice how it added an empty <li> before and after the selection.
What it should have done is completely surround the 6,7, 8 pool <li>'s with the selection span.
Here is the code handeling selection expansion:
TBR.Selection.get_HTML_from_range = function(range, sel){
var startEl = sel.anchorNode;
var endEl = sel.focusNode;
if (startEl != endEl){
if (startEl != range.commonAncestorContainer) {
while (startEl.parentNode != range.commonAncestorContainer) {
startEl = startEl.parentNode;
}
}
if (endEl != range.commonAncestorContainer) {
while (endEl.parentNode != range.commonAncestorContainer) {
endEl = endEl.parentNode;
}
}
range.setStartBefore(startEl);
range.setEndAfter(endEl);
}
sel.addRange(range);
var container = document.createElement("span");
container.appendChild(sel.getRangeAt(0).cloneContents());
html = container.innerHTML;
if (html.length < MINIMUM_SELECTION_LENGTH) {return false;}
return html;
}
basically if the start and end of the selection are on different DOM nodes, then the selection needs to expand such that the selection contains no broken DOM elements (all tags much have a matching end tag, etc)
But I have a feeling that the selection is breaking the HTML, and the browser is repairing the HTML before the selection expands all the way. I'm not sure.