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.
Related
I am getting pretty deep into an employee talent management system website and I am finding out really quick why OOP is so talked about. Being able to efficiently utilize it would change that game for me and make my codebase much more maintainable. I am currently working on a dropdown menu section, and while I have implemented OOP, I can tell from a mile away it is not "good code." It gets the job done but is very messy. I will have an example of the Object and Implementation down below. I have tried watching a few videos on implementing OOP in an existing project, but I keep coming up dry. I understand the basic concepts, but I am not sure how to implement these ideas in a real world application.
Here are some sticking points I am having:
I am not confident I instantiated my objects correctly/efficiently. I have a list of toggleable menus, each representing a candidate in the portal. Well, I created an object for each menu using a for loop. This worked, but it felt unintuitive. When I wanted to attach click events to these menus, I had to loop through them a second time to attached to objects methods as click events.
I felt I had a lot of 'if' statements in my Object. This is because I see myself using the object again in the future, but I do not think each menu will have all the same features. For example, this specific menu had a fade in animation when clicked. But, I doubt every toggleable menu will have this feature. I don't know, I just felt it made the code way more difficult to navigate.
I did not like having to loop through my menu elements and attach methods as click events. This made me have to run an additional for loop and felt 'off'. I was tempted to handle all the event attachments inside the object itself, but I wasn't sure if this is bad practice. It seemed like a good idea and would save a lot of code, but I'm not sure.
Long story short, I can see myself using OOP for forms, buttons, toggleable menus, navbars, (ect.) all over the place. The problem is I am not sure how to actually carry out the process of handling HTML elements and converting them to Objects in a clear and concise manner.
I looked into some design patterns on refactoring.guru, but I felt these concepts were past the scope of what I am trying to do. They all felt like more advanced concepts to take on after getting a solid grip on objects. It was helpful and I really liked the builder pattern. I am currently working on a branch which implements the builder pattern into my current scenario.
Any thoughts, advice, or direction?
Here is an example of the Object and it's Implementation:
TOGGLE MENU OBJECT
class ToggleMenu {
constructor(wrapper, menu, openIcon, closeIcon, title, hiddenMenu) {
this.wrapper = wrapper
this.menu = menu
this.openIcon = openIcon
this.closeIcon = closeIcon
this.title = title
this.hiddenMenu = hiddenMenu
this.toggled = false
}
props() {
console.log(this.menu)
console.log(this.openIcon)
console.log(this.closeIcon)
console.log(this.title)
console.log(this.hiddenMenu)
console.log(this.toggled)
}
openHiddenMenu(config) {
if (config == undefined){
config = {}
}
if (config.menuBackGroundColor !== undefined){
this.menu.style.backgroundColor = config.menuBackGroundColor
}
if (config.titleColor !== undefined){
this.title.style.color = config.titleColor
}
if (config.hiddenMenuDisplay !== undefined){
this.hiddenMenu.style.display = config.hiddenMenuDisplay
}
if (config.hiddenMenuAnimation !== undefined){
this.hiddenMenu.style.animationName = config.hiddenMenuAnimation
}
if(config.menuAnimation !== undefined){
this.menu.style.animationName = config.menuAnimation
}
this.openIcon.style.display = 'none'
this.closeIcon.style.display = 'block'
this.toggled = true
}
closeHiddenMenu(config){
if (config == undefined){
config = {}
}
if (config.menuAnimation !== undefined && this.toggled == true){
this.menu.style.animationName = config.menuAnimation
}
this.menu.style.backgroundColor = ''
this.openIcon.style.display = ''
this.closeIcon.style.display = ''
this.title.style.color = ''
this.hiddenMenu.style.display = ''
this.toggled = false
}
}
IMPLEMENTATION
const initCandidateMenus = () => {
let candidateToggleMenus = document.getElementsByClassName('candidate-toggle-menu')
let candidateToggleWrappers = document.getElementsByClassName('candidate-toggle-menu-wrapper')
let hiddenMenus = document.getElementsByClassName('hidden-candidate-menu')
let toggleMenus = []
//collecting toggle menu objects
for (x = 0; x < candidateToggleMenus.length; x++){
let toggleMenu = new ToggleMenu(
candidateToggleWrappers[x],
candidateToggleMenus[x],
candidateToggleMenus[x].getElementsByClassName('candidate-open-icon')[0],
candidateToggleMenus[x].getElementsByClassName('candidate-close-icon')[0],
candidateToggleMenus[x].getElementsByClassName('candidate-name')[0],
hiddenMenus[x],
)
toggleMenus.push(toggleMenu)
}
//looping through toggle menu objects
for (x = 0; x < toggleMenus.length; x++){
let currentMenu = toggleMenus[x]
currentMenu.openIcon.addEventListener('click', () => {
//closing all other toggle menus
for (y = 0; y < toggleMenus.length; y++){
toggleMenus[y].closeHiddenMenu()
}
//opening current menu
currentMenu.openHiddenMenu({
menuBackGroundColor: 'var(--main-clr)',
titleColor: 'var(--white)',
menuAnimation: 'fade-title-color',
hiddenMenuDisplay: 'flex',
hiddenMenuAnimation: 'open-hidden-menu'
})
})
currentMenu.closeIcon.addEventListener('click', () => {
//closing current menu
currentMenu.closeHiddenMenu()
})
}
}
ok this isn’t really an answer, but more of just advice that I need to use code for.
I noticed that you didn’t implement a lot of common features that can reduce lines and characters, so I’ll point them out here
For the class functions, you did this:
openHiddenMenu(config) {
if (config == undefined){
config = {}
}
Which can be simplified to this:
openHiddenMenu(config = {}) {
// Completely remove the entire if statement
Putting an equal sign in the parameter will assign a default value if it isn’t defined.
Another thing is checking if something is undefined:
if (config.menuBackGroundColor !== undefined){
this.menu.style.backgroundColor = config.menuBackGroundColor
}
if (config.titleColor !== undefined){
this.title.style.color = config.titleColor
}
if (config.hiddenMenuDisplay !== undefined){
this.hiddenMenu.style.display = config.hiddenMenuDisplay
}
if (config.hiddenMenuAnimation !== undefined){
this.hiddenMenu.style.animationName = config.hiddenMenuAnimation
}
if(config.menuAnimation !== undefined){
this.menu.style.animationName = config.menuAnimation
}
Instead you could remove the ‘!== undefined’ part, because any value that is ‘falsey’ (0, NULL, undefined, NaN) will also return false. So if it has a value and isnt 0, it will return true.
Plus since it’s only 1 line, you can remove brackets:
if (config.menuBackGroundColor)
this.menu.style.backgroundColor = config.menuBackGroundColor
if (config.titleColor)
this.title.style.color = config.titleColor
if (config.hiddenMenuDisplay)
this.hiddenMenu.style.display = config.hiddenMenuDisplay
if (config.hiddenMenuAnimation)
this.hiddenMenu.style.animationName = config.hiddenMenuAnimation
if(config.menuAnimation)
this.menu.style.animationName = config.menuAnimation
If you wanted to simplify even further, you could use an or operator ||, so if the value is falsey, it will become the other value. This doubles as a default value.
this.menu.style.backgroundColor = config.menuBackGroundColor || // Default value
this.title.style.color = config.titleColor || // Default value
this.hiddenMenu.style.display = config.hiddenMenuDisplay || // Default value
this.hiddenMenu.style.animationName = config.hiddenMenuAnimation || // Default value
this.menu.style.animationName = config.menuAnimation || // Default value
There are some other things you can do, but this answer is insanely long and I’m writing this late at night. Hope this helps though
Here's how you get one css attribute using jQuery:
$('someObject').css('attribute')
How do you get them all? (without specifying and preferably in the following format so it can be reapplied with jQuery later):
cssObj = {
'overflow':'hidden',
'height':'100%',
'position':'absolute',
}
Thanks!!
EDIT
The methods I'm trying to get are declared in a style sheet (they are not inline). Sorry for not specifying.
See this live example using the jQuery attribute selector
$(document).ready(function() {
alert($("#stylediv").attr('style'));
});
What about something like this:
jQuery CSS plugin that returns computed style of element to pseudo clone that element?
It is ugly, but it appeared to work for the poster...
This also may be of interest:
https://developer.mozilla.org/en/DOM:window.getComputedStyle
Not sure how cross-browser this one is, but it works in Chrome -
https://gist.github.com/carymrobbins/223de0b98504ac9bd654
var getCss = function(el) {
var style = window.getComputedStyle(el);
return Object.keys(style).reduce(function(acc, k) {
var name = style[k],
value = style.getPropertyValue(name);
if (value !== null) {
acc[name] = value;
}
return acc;
}, {});
};
window.getComputedStyle(element);
// For example
var element = document.getElementById('header');
window.getComputedStyle(element);
For a platform (the name is subject to nondisclosure) where the Chrome or Safari DevTools/WebInspector are not available, and you need to dump the styles to the log.
dumpCssFromId (id) {
const el = document.getElementById(id);
const styles = window.getComputedStyle(el);
return Object.keys(styles).forEach((index) => {
const value = styles.getPropertyValue(index);
if (value !== null && value.length > 0) {
console.log(`style dump for ${id} - ${index}: ${value}`);
}
}, {});
}
I have a JSON object which I loop through and works correct
iGenerateChilds = function (obj, div, $new) {
var $new = $();
$.each(obj.objects, function (p, par) {
$(div).append(iGenerateObject(par, ""));
alert(par.objects.length);
if (par.objects.length != 0) {
iGenerateChilds(par, div, $new);
}
});
return div;
};
With this I want to approach that the objects are getting attached to each other, which it now does, however wrong and can't manage to figure out how to do this correct.
Currently it generates
<table></table><tr></tr><td></td><td></td>
and I want
<table><tr><td></td><td></td></tr></table>
Somebody has the solution on this?
Try this:
iGenerateChilds = function (obj, div) {
$.each(obj.objects, function (p, par) {
var genObj = iGenerateObject(par, "");
$(div).append(genObj);
alert(par.objects.length);
if (par.objects.length != 0) {
// add the children to the newly added genObj-element
iGenerateChilds(par, genObj);
}
});
return div;
};
If you could post more information, like your HTML structure and your JSON object, that'd be helpful.
But at first glance I'd say that you're appending the elements at too high of a level. With this line $(div).append(iGenerateObject(par, "")); It looks like you're appending it to the element that's wraps your table, correct? It needs to be appended to the <table> within that passed element.
I created a custom css selector engine function for my custom javascript library like so,
var catchEl = function(el) { // Catching elements by identifying the first character of a string
var firstChar = el[0],
actualNode = el.substring(1, el.length),
elements,
tempElems = [];
if (!document.querySelectorAll) {
try{
if(firstChar === "#") {//So, we can look for ids
tempElems.push(document.getElementById(actualNode));
} else if(firstChar === ".") {//or classes
elements = document.getElementsByClassName(actualNode);
for(i=0;i<elements.length;i++) tempElems.push(elements[i]);
} else {//or tags
elements = document.getElementsByTagName(el);
for(i=0;i<elements.length;i++) tempElems.push(elements[i]);
}
} catch(e) {};
} else {//but before everything we must check if the best function is available
try{
elements = document.querySelectorAll(el);
for(i=0;i<elements.length;i++) tempElems.push(elements[i]);
} catch(e) {};
}
return tempElems;
}
This function returns an array of elements. However, I turned my head around and tried to make it more flexible so that it can also return the window, document or this object, but was unsuccessful. Whenever I try to push the window object into the tempElems array, the array is still empty.
So, I want to know how to make this function return an array of elements when a string is passed through it or return the respective objects(window, document or this) as desired.
Note: I don't want to work with jQuery. So, please don't post any answers regarding jQuery.
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];
}
}
}