How to override jquery plugin options with html5 data-attributes - javascript

What I want to do is to set options into plugin like you normally would, but to also override those options with html5 data-attributes.
Here I have exactly that (jsfiddle), but there's a slight problem with that.
JSFiddle Code:
(function($){
$.fn.extend({
test: function(options) {
var defaults = {
background: null,
height: null
};
var options = $.extend( defaults, options);
return this.each(function() {
var o = options;
var obj = $(this);
var objD = obj.data();
// background
if ( !objD.background) {
var cBG = o.background;
}
else {
var cBG = objD.background;
}
// Opacity
if ( !objD.height) {
var cH = o.height;
}
else {
var cH = objD.height;
}
obj.css({
background: cBG,
height: cH
});
});
}
});
})(jQuery);
$(document).ready(function() {
$('.test').test({
background: 'red',
height: 400
});
});
The method that I'm using in the jsfiddle bloats the code so much, even with just 2 different options that are also using data.
Question is: How could I achieve same end result with less code, to determine wether or not data should be used?
Here is the part of the code from the jsfiddle, that determines wether or not to use data.
// objD is obj.data();
// background
if ( !objD.background) {
var cBG = o.background;
}
else {
var cBG = objD.background;
}
// Opacity
if ( !objD.height) {
var cH = o.height;
}
else {
var cH = objD.height;
}
obj.css({
background: cBG,
height: cH
});

I had a feeling that there might be a better solution, but I wasn't able to get it working, until now.
Of course I am no expert, but this seems to work and shortens the code even more, since it works the same way the plugin options normally do, it just adds the data-attribute in there.
Default - Initially use default options.
Custom - If set, use custom options to override defaults.
Data - If set, use data to override both.
http://jsfiddle.net/lollero/HvbdL/5/
o = $.extend(true, {}, options, $(this).data() );
Here's another jsfiddle example.
This one toggles the width of each box on click. The middle box has a data-attribute with bigger width value than the other divs.
As an extra example, here's the same thing with 1 more option added in to the mix.

Well, one common trick is the double-pipe "or" operator in your assignment. If the first value is true, the second one is ignored, since only one true statement is enough to make the entire expression true:
var cBG = objD.background || o.background;
var cH = objD.height || o.height;
In fact, if you don't need those variables anywhere else, just add the results into the last statement:
obj.css({
background: (objD.background || o.background),
height: (objD.height || o.height)
});
Your fiddle
Simplified example
Now if objD.background is any "falsey" value (such as null, undefined, false, zero, or the empty string), then o.background will automatically be used. If for some reason you don't want that, you should use the ternary operator with an appropriate test instead:
obj.css({
background: (objD.background!=undefined) ? objD.background : o.background,
height: (objD.height!=undefined) ? objD.height : o.height
});

An easy way would be using the jQuery HTML5data plugin from Mark Dalgleish. The pro side of this plugin is namespacing so your config will never clash with the config of some other plugin.

Related

Google places autocomplete, how to clean up pac-container?

I'm using the google places autocomplete control, and it creates an element for the drop down with a class pac-container.
I'm using the autocomplete in an ember app, and when I'm done with it the DOM element the autocomplete is bound to gets removed, but the pac-container element remains, even thought its hidden. Next time I instantiate a new autocomplete, a new pac-container is created and the old one remains. I can't seem to find anything like a dispose method on the API, so is there a way of doing this correctly? If not I guess I should just use jquery to clear up the elements.
I was having the same problem, and hopefully Google eventually provides an official means of cleanup, but for now I was able to solve the problem by manually removing the pac-container object, a reference to which can be found in the Autocomplete class returned from:
var autocomplete = new google.maps.places.Autocomplete(element, options);
The reference to the pac-container element can be found at:
autocomplete.gm_accessors_.place.Mc.gm_accessors_.input.Mc.L
Which I simply removed from the DOM in my widget destructor:
$(autocomplete.gm_accessors_.place.Mc.gm_accessors_.input.Mc.L).remove();
Hope this helps.
Update
I'm not sure how Google's obfuscation works, but parts of the above seem obfuscated, and obviously will fail if the obfuscation or internal structures of the API change. Can't do much about the latter, but for the former you could at least search the object properties by expected criteria. As we can see, some of the property names are not obfuscated, while some appear to be, such as "Mc" and "L". To make this a little more robust, I wrote the following code:
var obj = autocomplete.gm_accessors_.place;
$.each(Object.keys(obj), function(i, key) {
if(typeof(obj[key]) == "object" && obj[key].hasOwnProperty("gm_accessors_")) {
obj = obj[key].gm_accessors_.input[key];
return false;
}
});
$.each(Object.keys(obj), function(i, key) {
if($(obj[key]).hasClass("pac-container")) {
obj = obj[key];
return false;
}
});
$(obj).remove();
The code expects the general structure to remain the same, while not relying on the (possibly) obfuscated names "Mc" and "L". Ugly I know, but hopefully Google fixes this issue soon.
My implementation of code from above without jquery.
var autocomplete = new google.maps.places.Autocomplete(element, options);
export function getAutocompletePacContainer(autocomplete) {
const place: Object = autocomplete.gm_accessors_.place;
const placeKey = Object.keys(place).find((value) => (
(typeof(place[value]) === 'object') && (place[value].hasOwnProperty('gm_accessors_'))
));
const input = place[placeKey].gm_accessors_.input[placeKey];
const inputKey = Object.keys(input).find((value) => (
(input[value].classList && input[value].classList.contains('pac-container'))
));
return input[inputKey];
}
getAutocompletePacContainer(autocomplete).remove()
This works for now until Google changes the class name.
autocomplete.addListener('place_changed', function() {
$('.pac-container').remove();
});
Built this recursive function to locate element position inside autocomplete object.
Get first matching object
var elementLocator = function(prop, className, maxSearchLevel, level) {
level++;
if (level === (maxSearchLevel + 1) || !prop || !(Array.isArray(prop) || prop === Object(prop))) {
return;
}
if (prop === Object(prop) && prop.classList && prop.classList.contains && typeof prop.classList.contains === 'function' && prop.classList.contains(className)) {
return prop;
}
for (const key in prop) {
if (prop.hasOwnProperty(key)) {
var element = elementLocator(prop[key], className, maxSearchLevel, level);
if (element) {
return element;
}
}
}
};
Usage:
var elm = null;
try {
//set to search first 12 levels, pass -1 to search all levels
elm = elementLocator(this.autocomplete, 'pac-container', 12, null);
} catch(e) {
console.log(e);
}
I just encountered this issue as well. It may have something to do with my input field being inside of a flexbox but I haven't tried restructuring my page yet. Instead I added an onfocus listener to my input field as well as an onscroll listener to it's container. Inside I get the input field's position with getBoundingClientRect and then update my stylesheet with the values. I tried directly selecting and updating the .pac-container via document.querySelctor but that didn't seem to work. You may need a setTimeout to allow it to be added to the DOM first.
Here is my code:
let ruleIndex = null;
const form = document.body.querySelector('.form');
const input = document.body.querySelector('.form-input');
const positionAutoComplete = () => {
const { top, left, height } = inputField.getBoundingClientRect();
if(ruleIndex) document.styleSheets[0].deleteRule(ruleIndex);
ruleIndex = document.styleSheets[0].insertRule(`.pac-container { top: ${top + height}px !important; left: ${left}px !important; }`);
}
form.addEventListener('scroll', positionAutoComplete);
input.addEventListener('focus', positionAutoComplete);
As mentioned in an earlier answer, this breaks the minute google decides to rename .pac-container so not a perfect fix but works in the meantime.

Trying to make div background change color when 2 ids are true

I am trying to make each div's background change color when 2 ids exist. it is not changing the color. I cannot figure out what I am doing wrong. I am brand new to javascript. I have an embedded stylesheet and dont know if the javascript will override the css.
Also, I know some PHP and want to 'echo' the variables throughout the program so that I can see what the string value is in order to debug my own code. what is the easiest way to do this?
function drop(ev){
ev.preventDefault();
var image = ev.dataTransfer.getData("content");
ev.target.appendChild(document.getElementById(image));
var mydiv = '';
for (var i=0;i<9;i++)
{
if ($('#target'.i).find('#answer'.i).length == 1)
{
mydiv = document.getElementById('target'+i);
mydiv.style.backgroundColor = '#00CC00';
}
else
{
mydiv = document.getElementById('target'+i);
mydiv.style.backgroundColor = '#FF0000';
}
}
}
I think your problem may be on this line you have . not + to build the id's correctly.
if ($('#target'.i).find('#answer'.i).length == 1)
so your code should be:
if ($('#target'+i).find('#answer'+i).length == 1)
Keeping in mind I'm no jQuery wizard, my first notion was something like this:
$('div[id^=target]').each(function() {
var el = $(this).find('div[id^=answer]').addBack();
el.css('backgroundColor', el.length > 1 ? '#00CC00' : '#FF0000');
});
...but then I noticed that unlike your example, I was changing both the parent and child div. Something like this might be closer to your intent:
$('div[id^=target]').css('backgroundColor', function () {
return $(this).find('div[id^=answer]').length ? '#00CC00' : '#FF0000';
});
You also could retain the for loop if that's your preference:
for (var i = 0; i < 9; ++i) {
$('div#target' + i).css('backgroundColor', function() {
return $(this).find('div#answer' + i).length ? '#00CC00' : '#FF0000';
});
}
...and, just for fun, something kinda esoteric:
$('div[id^=target]:has(div[id^=answer])').css('backgroundColor', '#00CC00');
$('div[id^=target]:not(:has(div[id^=answer]))').css('backgroundColor', '#FF0000');
Fiddle!
Your code should work (see fiddle) with the correct operator for concatenation, i.e. with + instead of ., however here are a few points you should bear in mind :
Point 1 :
Among all the i variables you're iterating over in your for loop, if there is no div with id equal to "target" + i you will end up in the following else block :
else
{
mydiv = document.getElementById('target'+i); // null
mydiv.style.backgroundColor = '#FF0000';
}
At that place mydiv will be null and mydiv.style will throw an error.
Point 2 :
It seems you used jQuery to find the answers elements, while you used document.getElementById, which is part of the DOM API, to select then the target element. It would have been more consistent to use jQuery there too.
Point 3 :
If you want to simply output the value of some variable you can use console.log, which will output in the javascript console of the browser. The console object is provided by the browser, therefore you may not have the console.log method, but if you are using an up to date browser there is a good chance you will have it.
To summarize, see this fiddle for an example that takes these points into account.

Get border width from a div with plain javascript

I got this style applied to a div
div#content {
border: 1px solid skyblue;
}
and i want to be able to alert the width of the border, I have tried with this:
window.alert( document.getElementById( "content" ).style.borderWidth );
I heard that depends of the browser maybe you can help me
I'm using Firefox 18
Please try the below javascript:
alert($("#content").css("border-left-width")); //using jquery.
or
alert(getComputedStyle(document.getElementById('content'),null).getPropertyValue('border-left-width'));//without jquery.
getComputedStyle(element, pseudo)
element:The element to get a styling for
pseudo:A pseudo-selector like ‘hover’ or null if not needed.
Reference link: http://javascript.info/tutorial/styles-and-classes-getcomputedstyle
I might be too late but as you never marked it as answered, I thought I could give it a try.
If your problem was compatibility between browser I would create a custom method that I could use in almost every browser there is (that means going back to the very basics).
I actually dug a lot to do this. I use some of the code from jQuery because I did not want to use jQuery but still have the backwards compatibility that jQuery does.
This function solves your question and at the bottom there are some examples on how to use it.
This functions uses the "module pattern" through the immediate function that will be run as soon as the script loads creating a method that will NOT polute the global scope but extend its functionality through a function to do what you wanted.
// I give it a name but it can also be anonymous
(function preloadedFunctions(){
// Preseted methods.
if(window.getComputedStyle){
window.getComputedStylePropertyValue = function(element, prop){
var computedStyle = window.getComputedStyle(element, null);
if(!computedStyle) return null;
if(computedStyle.getPropertyValue) {
return computedStyle.getPropertyValue(prop);
} else if (computedStyle.getAttribute) {
return computedStyle.getAttribute(prop);
} else if(computedStyle[prop]) {
return computedStyle[prop];
};
};
}
// jQuery JavaScript Library v1.9.0
// http://www.minhacienda.gov.co/portal/pls/portal/PORTAL.wwsbr_imt_services.GenericView?p_docname=6240612.JS&p_type=DOC&p_viewservice=VAHWSTH&p_searchstring=
// For IE8 or less
else if ( document.documentElement.currentStyle ) {
var rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
rposition = /^(top|right|bottom|left)$/,
core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source;
window.getComputedStylePropertyValue = function(element, prop){
var left, rsLeft,
ret = element.currentStyle && element.currentStyle[ prop ],
style = element.style;
if ( ret == null && style && style[ prop ] ) {
ret = style[ prop ];
}
if ( rnumnonpx.test( ret ) && !rposition.test( prop ) ) {
left = style.left;
rsLeft = element.runtimeStyle && element.runtimeStyle.left;
if ( rsLeft ) {
element.runtimeStyle.left = element.currentStyle.left;
}
style.left = prop === "fontSize" ? "1em" : ret;
ret = style.pixelLeft + "px";
style.left = left;
if ( rsLeft ) {
element.runtimeStyle.left = rsLeft;
}
}
return ret === "" ? "auto" : ret;
};
};
})();
i.e.
1.-
var borderWidth = getComputedStylePropertyValue(document.getElementsByTagName("div")[0], "border-width");
console.log(borderWidth);
2.-
var div = document.getElementById("someID");
console.log(getComputedStylePropertyValue(div, "border-width"));
If somebody is still looking, this seems to be easiest way to do it with plain JS.
let border =
+getComputedStyle((document.getElementById("idOfYourElement")))
.borderTopWidth.slice(0, -2)
Explanation below:
document.getElementById("idOfYourElement") - Return our HTML element.
getComputedStyle - Return css attributes of chosen element as object.
.borderTopWidth - Corresponding attribute from getComputedStyle object (return array like this: ("10px")).
.slice(0, -2) - Cut the last 2 characters from our array so we get rid of px at the end.
And + at the start - Parse rest of our string, that contains number we want, to the integer.
You can try this:
var border = document.getElementById("yourDiv").clientWidth - document.getElementById("yourDiv").offsetWidth;
alert(border);
Note, that the value will be rounded to an integer. If fractional value is required, you need to use getComputedStyle instead (see other answers).
Very old question, but anyway...
This solution is plain JavaScript, and should work in older browsers too.
It measures the size of the element, with, and without borders.
The following example should work correctly if the borders around the element are all the same size.
If not, the procedure doesn't change much, you just have to set the borders equal to zero, one by one.
var ele=document.getElementById("content");
// measures the element width, WITH borders
var w=ele.offsetWidth;
var cssBackup=ele.style.cssText;
// sets the borders to zero
ele.style.border="0px";
// computes the border size
var bord=(w-ele.offsetWidth)/2;
// resets the css
ele.style.cssText=cssBackup;
alert(bord);
When left & right border has same width:
function getWidth(div) {
return (div.offsetWidth - div.clientWidth) /2
}
getWidth(document.querySelector('#content'))
According to W3Schools, this property is supported by major browsers. Thus you shouldn't have any difficulty in using it.
However, using a JavaScript framework like jQuery would always help you not worrying about trivial issues like this.
Works for me
let content = document.querySelector('#content');
// here 'borderWidth' is similar to element.style syntax
let contentBorderWidth = getComputedStyle(content).borderWidth; // 1px
// number, without 'px'
let contentBorderWidthNumber = parseFloat(getComputedStyle(content).borderWidth); // 1
// demo
content.innerHTML = contentBorderWidth +', '+ contentBorderWidthNumber;
// in your case, to alert
window.alert(contentBorderWidth +', '+ contentBorderWidthNumber);
div#content {
border: 1px solid skyblue;
}
<div id="content"></div>
More about getComputedStyle.

Get border value with getComputedStyle().getPropertyValue()? (Mozilla, FF)

In some browsers (namely, Firefox) the getComputedStyle().getPropertyValue() doesn't report anything for shorthand CSS, like border. Is there a non-specific-code way of getting these shorthand CSS values? I've considered making a whitelist of shorthand CSS and their respective longhand CSS values. But I realize doing that would be both a big pain and a non-forward-compatible design.
I'm wondering, what do you want to do with a string like border: 1px solid #000?
Say you want to reproduce an elems border in order to copy it copyStyle(el2, el, "border"):
// Copies a set of styles from one element to another.
function copyStyle(dest, source, shorthand) {
var computed = window.getComputedStyle(source, null);
for (var i = computed.length; i--;) {
var property = camelize(computed[i]);
if (property.indexOf(shorthand) > -1) {
console.log(property)
dest.style[property] = computed[property];
}
}
}
// prototype.js
function camelize(text) {
return text.replace(/-+(.)?/g, function (match, chr) {
return chr ? chr.toUpperCase() : '';
});
}
Comparing if two element's given set of styles matches can be done in the same manner. Other than that, I really can't see the use a string, which should be parsed if you want to compute anything with it.

Duplicating an element (and its style) with JavaScript

For a JavaScript library I'm implementing, I need to clone an element which has exactly the same applied style than the original one. Although I've gained a rather decent knowledge of JavaScript, as a programming language, while developing it, I'm still a DOM scripting newbie, so any advice about how this can be achieved would be extremely helpful (and it has to be done without using any other JavaScript library).
Thank you very much in advance.
Edit: cloneNode(true) does not clone the computed style of the element. Let's say you have the following HTML:
<body>
<p id="origin">This is the first paragraph.</p>
<div id="destination">
<p>The cloned paragraph is below:</p>
</div>
</body>
And some style like:
body > p {
font-size: 1.4em;
font-family: Georgia;
padding: 2em;
background: rgb(165, 177, 33);
color: rgb(66, 52, 49);
}
If you just clone the element, using something like:
var element = document.getElementById('origin');
var copy = element.cloneNode(true);
var destination = document.getElementById('destination');
destination.appendChild(copy);
Styles are not cloned.
Not only will you need to clone, but you'll probably want to do deep cloning as well.
node.cloneNode(true);
Documentation is here.
If deep is set to false, none of the
child nodes are cloned. Any text that
the node contains is not cloned
either, as it is contained in one or
more child Text nodes.
If deep evaluates to true, the whole
subtree (including text that may be in
child Text nodes) is copied too. For
empty nodes (e.g. IMG and INPUT
elements) it doesn't matter whether
deep is set to true or false but you
still have to provide a value.
Edit: OP states that node.cloneNode(true) wasn't copying styles. Here is a simple test that shows the contrary (and the desired effect) using both jQuery and the standard DOM API:
var node = $("#d1");
// Add some arbitrary styles
node.css("height", "100px");
node.css("border", "1px solid red");
// jQuery clone
$("body").append(node.clone(true));
// Standard DOM clone (use node[0] to get to actual DOM node)
$("body").append(node[0].cloneNode(true));
Results are visible here: http://jsbin.com/egice3/
Edit 2
Wish you would have mentioned that before ;) Computed style is completely different. Change your CSS selector or apply that style as a class and you'll have a solution.
Edit 3
Because this problem is a legitimate one that I didn't find any good solutions for, it bothered me enough to come up with the following. It's not particularily graceful, but it gets the job done (tested in FF 3.5 only).
var realStyle = function(_elem, _style) {
var computedStyle;
if ( typeof _elem.currentStyle != 'undefined' ) {
computedStyle = _elem.currentStyle;
} else {
computedStyle = document.defaultView.getComputedStyle(_elem, null);
}
return _style ? computedStyle[_style] : computedStyle;
};
var copyComputedStyle = function(src, dest) {
var s = realStyle(src);
for ( var i in s ) {
// Do not use `hasOwnProperty`, nothing will get copied
if ( typeof s[i] == "string" && s[i] && i != "cssText" && !/\d/.test(i) ) {
// The try is for setter only properties
try {
dest.style[i] = s[i];
// `fontSize` comes before `font` If `font` is empty, `fontSize` gets
// overwritten. So make sure to reset this property. (hackyhackhack)
// Other properties may need similar treatment
if ( i == "font" ) {
dest.style.fontSize = s.fontSize;
}
} catch (e) {}
}
}
};
var element = document.getElementById('origin');
var copy = element.cloneNode(true);
var destination = document.getElementById('destination');
destination.appendChild(copy);
copyComputedStyle(element, copy);
See PPK's article entitled Get Styles for more information and some caveats.
After looking at a couple of good solutions across the WEB, I decided to combine all the best aspects of each and come up with this.
I left my solution in plain super fast Javascript, so that everybody can translate to their latest and great JS flavour of the month.
Representing the vanilla from manilla.....
* #problem: Sometimes .cloneNode(true) doesn't copy the styles and your are left
* with everything copied but no styling applied to the clonedNode (it looks plain / ugly). Solution:
*
* #solution: call synchronizeCssStyles to copy styles from source (src) element to
* destination (dest) element.
*
* #author: Luigi D'Amico (www.8bitplatoon.com)
*
*/
function synchronizeCssStyles(src, destination, recursively) {
// if recursively = true, then we assume the src dom structure and destination dom structure are identical (ie: cloneNode was used)
// window.getComputedStyle vs document.defaultView.getComputedStyle
// #TBD: also check for compatibility on IE/Edge
destination.style.cssText = document.defaultView.getComputedStyle(src, "").cssText;
if (recursively) {
var vSrcElements = src.getElementsByTagName("*");
var vDstElements = destination.getElementsByTagName("*");
for (var i = vSrcElements.length; i--;) {
var vSrcElement = vSrcElements[i];
var vDstElement = vDstElements[i];
// console.log(i + " >> " + vSrcElement + " :: " + vDstElement);
vDstElement.style.cssText = document.defaultView.getComputedStyle(vSrcElement, "").cssText;
}
}
}
None of those worked for me, but I came up with this based on Luigi's answer.
copyStyles(source: HTMLElement, destination: HTMLElement) {
// Get a list of all the source and destination elements
const srcElements = <HTMLCollectionOf<HTMLElement>>source.getElementsByTagName('*');
const dstElements = <HTMLCollectionOf<HTMLElement>>destination.getElementsByTagName('*');
// For each element
for (let i = srcElements.length; i--;) {
const srcElement = srcElements[i];
const dstElement = dstElements[i];
const sourceElementStyles = document.defaultView.getComputedStyle(srcElement, '');
const styleAttributeKeyNumbers = Object.keys(sourceElementStyles);
// Copy the attribute
for (let j = 0; j < styleAttributeKeyNumbers.length; j++) {
const attributeKeyNumber = styleAttributeKeyNumbers[j];
const attributeKey: string = sourceElementStyles[attributeKeyNumber];
dstElement.style[attributeKey] = sourceElementStyles[attributeKey];
}
}
}

Categories