Dealing with detached dom elements, gridster.js plugin - javascript

I am working with the gridster.js plugin, which is great, however it seems to be giving me issues when I am removing items using it's remove method. I am using its built in method called remove_all_widgets to wipe out what I currently have on the page and load in new content.
You can see it here.
fn.remove_all_widgets = function(callback) {
this.$widgets.each($.proxy(function(i, el){
this.remove_widget(el, true, callback);
}, this));
return this;
};
It loops through the widgets and calls this remove_widget as seen here:
/**
* Remove a widget from the grid.
*
* #method remove_widget
* #param {HTMLElement} el The jQuery wrapped HTMLElement you want to remove.
* #param {Boolean|Function} silent If true, widgets below the removed one
* will not move up. If a Function is passed it will be used as callback.
* #param {Function} callback Function executed when the widget is removed.
* #return {Class} Returns the instance of the Gridster Class.
*/
fn.remove_widget = function(el, silent, callback) {
var $el = el instanceof $ ? el : $(el);
var wgd = $el.coords().grid;
// if silent is a function assume it's a callback
if ($.isFunction(silent)) {
callback = silent;
silent = false;
}
this.cells_occupied_by_placeholder = {};
this.$widgets = this.$widgets.not($el);
var $nexts = this.widgets_below($el);
this.remove_from_gridmap(wgd);
$el.fadeOut($.proxy(function() {
$el.remove();
if (!silent) {
$nexts.each($.proxy(function(i, widget) {
this.move_widget_up( $(widget), wgd.size_y );
}, this));
}
this.set_dom_grid_height();
if (callback) {
callback.call(this, el);
}
}, this));
return this;
};
I have small bits of javascript to run button functions and other assorted things. I realized soon after playing with it that it leaves the full html content of the widget in a detached dom tree thus keeping the js files running. I first found this because the buttons have the same names on new pages and it was running the click functions for both the newly loaded button and the one i had taken off the screen using gridsters remove_all_widgets method.
I can track the previous javascript to an (anonymous function) in chomes dev console, and within that i can see the entire html content inside of the detached tree. I am not refreshing the pages or anything, the new content is being brought in by ajax (I set ajax cache:false as well).
Is there any way around this? Would it be possible to clear the contents of the widgets before they get stuck like this? It would be ideal if it didn't happen at all or of there was some way to get rid of them completely when they get removed.
Thank you for taking the time to read this, any insight would be greatly helpful.
As per requests her is some of the code, the click functions on the lines button for example is double firing:
$(document).ready(function() {
$(document).on("change",".accountContries",function(e){
var countryPck = $("body > .addAccountForm1").find(".accountContries").find('option:selected').attr('id');
$.ajax(
{
cache: false,
url : "/listStates/" + countryPck,
type : "GET",
beforeSend: function(){
$("body").append("<div class='loadingNow'></div>");
},
success:function(data, textStatus, jqXHR) {
$('.loadingNow').remove();
$(".accountStates").empty();
$(".accountStates").append("<option value='' selected disabled> Select a State</option>");
$.each(data.states, function(){
$(".accountStates").append("<option value=" + this.id +" id=" + this.id + ">" + this.name +"</option>");
});
},
error: function(jqXHR, textStatus, errorThrown)
{
errorOffScreen("List States by account");
}
});
});
$(document).on("touchend click", ".lines-button", function(e){
e.stopImmediatePropagation();
if($(this).hasClass("close")){
$(this).removeClass("close");
$(".widget1x1Back").next(".actionsHolder3").slideUp("fast", function() {
$(this).remove();
});
}else{
var iconsList = $(this).closest(".top1x1").next(".hdnActnLst").find(".iconsHolder3").html();
$(this).closest(".widget1x1").append(iconsList);
$(this).closest(".widget1x1").find(".actionsHolder3").hide();
$(this).closest(".widget1x1").find(".actionsHolder3").slideDown(700,"easeOutBack");
$(this).addClass("close");
}
});
});
</script>
UPDATE : it seems I only the stuff inside the <Script> tag is being kept even after the elements are .removed

Based on your comments and updates, you have Javascript in the loaded content pages. As that includes the use of delegated event handlers, that code will live on.
This is one case were you would be better off with normal non-delegated event handlers like:
$(".lines-button").bind("touchend click", function(e){
e.stopImmediatePropagation();
These bindings will terminate when the elements are removed (as they are per-element handlers).
An alternative is to not have any code in the content pages, but only in the master page, and apply code as required when loading new content.

Related

How to to trigger page component events when loading template using Ajax on Oro Platform?

I'm currently facing an issue on Oro Platform v.4.1.10.
I have a specific form page where I'm performing an ajax reload on a specific field change.
The thing is that everything is working well except that the CSS and JS are not applied to my ajax section when reloaded.
When I first load the page, everything is OK :
When the section is reload using Ajax :
An OroDateTimeType field is used in the reloaded section, and according to my issue, the datepicker doesn't init on it.
Some details about the way my Ajax call is performed :
define(function (require) {
'use strict';
let SinisterAjaxRepairman,
BaseView = require('oroui/js/app/views/base/view');
SinisterAjaxRepairman = BaseView.extend({
autoRender: true,
/**
* Initializes SinisterAjaxRepairman component
*
* #param {Object} options
*/
initialize: function (options) {
// assign options to component object
this.$elem = options._sourceElement;
delete options._sourceElement;
SinisterAjaxRepairman.__super__.initialize.call(this, options);
this.options = options;
},
/**
* Renders the view - add event listeners here
*/
render: function () {
$(document).ready(function() {
let sectionTrigger = $('input.repair-section-trigger');
let sectionTargetWrapper = $('.repair-section-content');
sectionTrigger.on('click', function(e) {
$.ajax({
url: sectionTrigger.data('update-url'),
data: {
plannedRepair: sectionTrigger.is(':checked') ? 1 : 0,
id: sectionTrigger.data('sinister-id') ? sectionTrigger.data('sinister-id') : 0
},
success: function (html) {
if (!html) {
sectionTargetWrapper.html('').addClass('d-none');
return;
}
// Replace the current field and show
sectionTargetWrapper
.html(html)
.removeClass('d-none')
}
});
});
});
return SinisterAjaxRepairman.__super__.render.call(this);
},
/**
* Disposes the view - remove event listeners here
*/
dispose: function () {
if (this.disposed) {
// the view is already removed
return;
}
SinisterAjaxRepairman.__super__.dispose.call(this);
}
});
return SinisterAjaxRepairman;
});
The loaded template just contains the form row to update in the related section :
{{ form_row(form.repairman) }}
{{ form_row(form.reparationDate) }}
I think that my issue is related to the page load events used by Oro to trigger the page-component events and update their contents but I'm stuck at this point, I don't find how to trigger programmatically this update on my Ajax success code, in order to have the same rendering of the fields on an initial Page load and an Ajax reload of the section.
Thank you for your help 🙂
The final fix, that I found thanks to Andrey answer, was to update the JS file like this, with the addition of content:remove and content:changed events on ajax response (success section) :
success: function (html) {
if (!html) {
sectionTargetWrapper
.trigger('content:remove')
.html('')
.trigger('content:changed')
.addClass('d-none');
return;
}
// Replace the current field and show
sectionTargetWrapper
.trigger('content:remove')
.html(html)
.trigger('content:changed')
.removeClass('d-none')
}
Hope it could help ! 🙂

Dynamically added elements don't wrap

In my plug-in I need to wrapp all sidebar's children in a div to let them overflow but if those elements are loaded dynamically the function does not work and I don't know either how to make it work.
The code is:
<div class="sidebar">
</div>
var $sidebar = $( '.sidebar' );
$sidebar.load( 'external-page.ext' );
$sidebar.MyPlugin();
$.fn.MyPlugin = function() {
this.wrapInner( '<div />' );
});
If those elements are not loaded dynamically there is no problem.
Firstly the code was:
$sidebar.wrapInner( '<div/>' );
and this just works fine if elemens are not loaded dynamically, so I tried this way:
var children = $sidebar.children();
$( document ).on( 'load', children, function() {
$( this ).wrapAll( '<div />' );
});
but, of course it does not work.
Can you please help me?
I thought that this rule would have worked this time too but it didn't. What did I mistake?
You can find the whole code here.
And a demo here
MORE DETAILS
I want to handle this issue from the inside, not from the outside! I don't know if users will load content dinamically or not. that's the point.
So there is a way to handle this issue inside the plugin and not outside?
From the manual
http://api.jquery.com/load/
Callback Function
If a "complete" callback is provided, it is executed after
post-processing and HTML insertion has been performed. The callback is
fired once for each element in the jQuery collection, and this is set
to each DOM element in turn.
Try the following code and see if this works:
$sidebar.load( 'external-page.ext', function() { $sidebar.MyPlugin(); } );
Thanks.
$.load() makes an ajax call to load the data ,
So there is a possibility that your this.wrapInner( '<div />' ) method has invoked before any data is loaded inside the div.sidebar.
Make sure this.wrapInner( '<div />' ) is called after all data has been loaded successfully using the complete callback.
$.load() trigger callback for each div ,call your plugin from callback
$sidebar.load('http://fiddle.jshell.net/vikrant47/ncagab2y/1/show/', function () {
$(this).MyPlugin();
}
});
DEMO
OR
If you are using $.load() only to load inside multiple elements then you could probably use one of the more powerful jQuery ajax methods (i.e., get() or post() or ajax()).
$.get('http://fiddle.jshell.net/vikrant47/ncagab2y/1/show/', {}, function(data) {
$sidebar.html(data).MyPlugin();
});
DEMO using $.get() Method
UPDATE-
Answer to the comment-
You should not have to worry about weather user gonna call your plugin like this $sidebar.load(...).MyPlugin().User must be aware enough about how to handle asynchronous methods.
You can not make your plugin work until there is some data inside div.slider
but ,you can add ajax loading functionality inside your plugin like -
$(document).ready(function () {
$.fn.MyPlugin = function (options) {
var $elem=this;
var init = function () {
options.load = $.extend({}, $.fn.MyPlugin.defaultOptions.load, options.load);
load();
}
//adding load method to load data dynamically
var load = function () {
if (!options.load.url) {
alert("url can not be empty");
} else {
$.ajax({
url: options.load.url,
type: options.load.type,
data: options.load.data,
success: function (response) {
options.load.success.call(this, response);
$elem.html(response).wrapInner('<div class="wrapper"/>');//wrap after data has been loaded successfully
},
error : function (jqXHR, textStatus, errorThrown) {
alert("error occured" + textStatus + " ," + errorThrown)
}
})
}
}
init();
}
$.fn.MyPlugin.defaultOptions = {
load: {
tye: "get",
data: {},
success: function () {}
}
};
Now use your plugin like-
var $sidebar = $('.sidebar');
$sidebar.MyPlugin({
load: {
url: 'http://fiddle.jshell.net/vikrant47/ncagab2y/1/show/'
}
});
});
DEMO with load
Try adding adding below piece to plugin . Added at lines 84 - 110 at gist .
var target = $sidebar.get(0);
// create an observer instance
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
// do stuff when
// `childList` modified
// i.e.g.,
$.each(mutation.addedNodes, function (k, v) {
$(v)
.wrapInner('<div data-'
+ dataName
+ '="sub-wrapper"></div>')
})
});
});
// configuration of the observer:
var _config = {
childList: true
};
// pass in the target node, as well as the observer options
observer.observe(target, _config);
jsfiddle http://jsfiddle.net/guest271314/s5wzptc8/
See MutationObserver

Best way to silently bind window resize event to jQuery plugin without keeping a reference to the targeted element

I'm looking for best-practice advice.
I'm writing a small jQuery plugin to manage horizontal scroll on elements.
I need all the dom elements targeted by that plugin to update on window resize.
Fact is, my website is a full ajax 'app' so when I remove DOM elements, I need them gone so memory doesn't leak.
But I can't find a way to bind the resize event without keeping a reference to the DOM node.
EDIT :
Actually I need the resize handler to get the plugin-targeted elements at 'call' time, coz I don't want to keep any reference to those elements in memory, because I might call .html('') on a parent of theirs...
I did not paste all my code, just an empty shell. I already have a destroy method that unbinds handlers. But I'm generating, removing and appending html nodes dynamically and I the the elements targeted by the plugin to remove silently.
Kevin B stated I could override jQuery .remove method to deal with the handlers, but would have to load jQuery UI for it to work. I don't want that either..
Here is what I tried (attempts commented):
(function($) {
// SOLUTION 2 (see below too)
// Not good either coz elements are not removed until resize is triggered
/*
var hScrolls = $([]);
$(window).bind('resize.hScroll',function(){
if(!hScrolls.length) return;
hScrolls.each(function(){
if($(this).data('hScroll')) $(this).hScroll('updateDimensions');
else hScrolls = hScrolls.not($(this));
});
});
*/
// END SOLUTION 2
// SOLUTION 3 (not implemented but I think I'm on the right path)
$(window).bind('resize.hScroll',function(){
// need to get hScroll'ed elements via selector...
$('[data-hScroll]').hScroll('updateDimensions');
// I don't know how....
});
// END SOLUTION 3
var methods = {
init : function(options) {
var settings = $.extend( {
defaults: true
}, options);
return this.each(function() {
var $this = $(this),
data = $this.data('hScroll');
if (!data) {
$this.data('hScroll', {
target: $this
});
// SOLUTION 1
// This is not good: it keeps a reference to $this when I remove it...
/*
$(window).bind('resize.hScroll', function(){
$this.hScroll('updateDimensions');
});
*/
// END SOLUTION 1
$this.hScroll('updateDimensions');
// SOLUTION 2 (see above too)
hScrolls = hScrolls.add(this);
}
});
},
updateDimensions: function(){
var hScroll = this.data('hScroll');
// do stuff with hScroll.target
}
}
$.fn.hScroll = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if ( typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.hScroll');
}
};
})(jQuery);​
Thanks all in advance!
jQuery calls cleanData any time you do something that removes or replaces elements (yes, even if you use parent.html("") ). You can take advantage of that by extending it and having it trigger an event on the target elements.
// This is taken from https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js 10/17/2012
if (!$.widget) { // prevent duplicating if jQuery ui widget is already included
var _cleanData = $.cleanData;
$.cleanData = function( elems ) {
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
try {
$( elem ).triggerHandler( "remove" );
// http://bugs.jquery.com/ticket/8235
} catch( e ) {}
}
_cleanData( elems );
};
}
Now you can bind to the remove event when setting up your plugin and have it run your destroy method.
$(elem).bind("remove",methods.destroy)
You might use a class name and forward the resize event:
$.fn.hScroll = function(method) {
this
.addClass('hScroll')
.data('method', arguments)
};
var methods['alert_text'] = function(config){
alert( config + " " + $(this).text() );
}
$(window).bind('resize.hScroll',function(){
$(".hScroll").each(function(){
var method_config = $(this).data('method');
var method = method_config.shift();
// Forward the resize event with all resize event arguments:
methods[method].apply(this, method_config);
})
})
// Register a resize event for all a.test elements:
$("a.test").hScroll('alert_text', "hey");
// Would alert "hey you" for <a class="test">you</a> on every resize
Update
If you change the dom and want to keep the selector you might try this one:
var elements = [];
$.fn.hScroll = function(method) {
elements.push({'selector' : this.selector, 'arguments' : arguments });
};
var methods['alert_text'] = function(config){
alert( config + " " + $(this).text() );
}
$(window).bind('resize.hScroll',function(){
$.each(elements,function(i, element){
$(element.selector).each(function(){
var method_config = element.arguments;
var method = method_config.shift();
// Forward the resize event with all resize event arguments:
methods[method].apply(this, method_config);
})
})
})
// Register a resize event for all a.test elements:
$("a.test").hScroll('alert_text', "hey");
$(document.body).html("<a class='test'>you</a>");
// Would alert "hey you" for every window resize
You should have the scroll event bound in the extension. Also, you will want to add a "destroy" method to your extension as well. Before you remove the element from the DOM, you will want to call this method. Inside the detroy method is where you will want to unbind the resize event.
One important thing in making this work is that you have a reference to each handler method that is bound to the resize event. Alternatively, you can unbind All resize events upon the removal on an element and then rebind the scroll event to the remaining elements that require it.

How to place a jQuery snippet into a global file

I have a JavaScript file here http://www.problemio.com/js/problemio.js and I am trying to place some jQuery code into it that looks like this:
$(document).ready(function()
{
queue = new Object;
queue.login = false;
var $dialog = $('#loginpopup')
.dialog({
autoOpen: false,
title: 'Login Dialog'
});
var $problemId = $('#theProblemId', '#loginpopup');
$("#newprofile").click(function ()
{
$("#login_div").hide();
$("#newprofileform").show();
});
// Called right away after someone clicks on the vote up link
$('.vote_up').click(function()
{
var problem_id = $(this).attr("data-problem_id");
queue.voteUp = $(this).attr('problem_id');
voteUp(problem_id);
//Return false to prevent page navigation
return false;
});
var voteUp = function(problem_id)
{
alert ("In vote up function, problem_id: " + problem_id );
queue.voteUp = problem_id;
var dataString = 'problem_id=' + problem_id + '&vote=+';
if ( queue.login = false)
{
// Call the ajax to try to log in...or the dialog box to log in. requireLogin()
}
else
{
// The person is actually logged in so lets have him vote
$.ajax({
type: "POST",
url: "/problems/vote.php",
dataType: "json",
data: dataString,
success: function(data)
{
alert ("vote success, data: " + data);
// Try to update the vote count on the page
//$('p').each(function()
//{
//on each paragraph in the page:
// $(this).find('span').each()
// {
//find each span within the paragraph being iterated over
// }
//}
},
error : function(data)
{
alert ("vote error");
errorMessage = data.responseText;
if ( errorMessage == "not_logged_in" )
{
//set the current problem id to the one within the dialog
$problemId.val(problem_id);
// Try to create the popup that asks user to log in.
$dialog.dialog('open');
alert ("after dialog was open");
// prevent the default action, e.g., following a link
return false;
}
else
{
alert ("not");
}
} // End of error case
}
}); // Closing AJAX call.
};
$('.vote_down').click(function()
{
alert("down");
problem_id = $(this).attr("data-problem_id");
var dataString = 'problem_id='+ problem_id + '&vote=-';
//Return false to prevent page navigation
return false;
});
$('#loginButton', '#loginpopup').click(function()
{
alert("in login button fnction");
$.ajax({
url:'url to do the login',
success:function() {
//now call cote up
voteUp($problemId.val());
}
});
});
});
</script>
There are two reasons why I am trying to do that:
1) I am guessing this is just good practice (hopefully it will be easier to keep track of my global variables, etc.
2) More importantly, I am trying to call the voteUp(someId) function in the original code from the problemio.js file, and I am getting an error that it is an undefined function, so I figured I'd have better luck calling that function if it was in a global scope. Am I correct in my approach?
So can I just copy/paste the code I placed into this question into the problemio.js file, or do I have to remove certain parts of it like the opening/closing tags? What about the document.ready() function? Should I just have one of those in the global file? Or should I have multiple of them and that won't hurt?
Thanks!!
1) I am guessing this is just good practice (hopefully it will be
easier to keep track of my global variables, etc.
Yes and no, you now have your 'global' variables in one spot but the chances that you're going to collide with 'Global' variables (ie those defined by the browser) have increased 100% :)
For example say you decided to have a variable called location, as soon as you give that variable a value the browser decides to fly off to another URL because location is a reserved word for redirecting.
The solution to this is to use namespacing, as described here
2) More importantly, I am trying to call the voteUp(someId) function
in the original code from the problemio.js file, and I am getting an
error that it is an undefined function, so I figured I'd have better
luck calling that function if it was in a global scope. Am I correct
in my approach?
Here's an example using namespacing that will call the voteUp function:
(function($) {
var myApp = {};
$('.vote_up').click(function(e) {
e.preventDefault();
myApp.voteUp();
});
myApp.voteUp = function() {
console.log("vote!");
}
})(jQuery);
What about the document.ready() function? Should I just have one of
those in the global file? Or should I have multiple of them and that
won't hurt?
You can have as many document.ready listeners as you need, you are not overriding document.ready you are listening for that event to fire and then defining what will happen. You could even have them in separate javascript files.
Be sure your page is finding the jquery file BEFORE this file is included in the page. If jquery is not there first you will get function not defined. Otherwise, you might have other things conflicting with your jquery, I would look into jquery noConflict.
var j = jQuery.noConflict();
as seen here:
http://api.jquery.com/jQuery.noConflict/
Happy haxin
_wryteowl
Extending what KreeK has already provided: there's no need to define your "myApp" within the document ready function. Without testing, I don't know off the top of my head if doing so is a potential source for scope issues. However, I CAN say that the pattern below will not have scope problems. If this doesn't work, the undefined is possibly a script-loading issue (loading in the right order, for example) rather than scope.
var myApp = myApp || {}; // just adds extra insurance, making sure "myApp" isn't taken
myApp.voteUp = function() {
console.log("vote!");
}
$(function() { // or whatever syntax you prefer for document ready
$('.vote_up').click(function(e) {
e.preventDefault();
myApp.voteUp();
});
});

Hover effect not working from the second time in Safari

I'm facing a weird problem with safari
I'm now developing a website that loads its contents with Ajax I'm applying this by using Hashchange methodology and when hashchange
I'm getting the url and then load the content for the page
jQuery(window).bind('hashchange', function () {
url = window.location.hash.substring(1);
if (!url) {
return;
}
var tsTimeStamp= new Date().getTime();
jQuery.get(url, { action: "get", time: tsTimeStamp,"ajaxed": "true"} , function (data, status, xmlHttp) {
var container = jQuery("#hidden");
container.html(xmlHttp.responseText);
var content = jQuery(".inner", container).html();}
and then after loading the content I'm applying some jquery stuff like
var ids = " ";
var ids_2 = " ";
for (var i = 0; i <= jQuery(".cats").length; i++) {
ids += "#c" + i + ",";
ids_2 += "#l" + i + ",";
}
ids = ids.substr(0, (ids.length) - 1);
ids_2 = ids_2.substr(0, (ids_2.length) - 1);
jQuery(ids).hover(function () {
href = jQuery(this).attr("href");
id = jQuery('a[href="' + href + '"]').attr("id");
jQuery("img").not(jQuery("img."+id)).addClass("op");
}, function () {
time = setTimeout(remove, 200);
});
jQuery(ids_2).hover(function () {
clearTimeout(time);
jQuery("img.op").removeClass("op");
href = jQuery(this).attr("href");
id = jQuery('a[href="' + href + '"]').attr("id");
jQuery("img").not(jQuery("img." + id)).addClass("op");
}, function () {
jQuery("img.op").delay(200).removeClass("op");
});
function remove() {
jQuery("img.op").removeClass("op");
}
The above code is applying hover over effect on map areas for images
(this code is applicable to 4 pages).
All the above code is working fine with all the browser except Safari
The problem is when the first page that contains the maps loaded the code is working fine but when you load another page contains the same areas it will stop working until the whole page being refreshed.
seems like it caches the handler for the first time and then does not apply it to the new selectors
Keep in mind that when Alert ids & ids_2 it gives the correct values but when using alert inside the .hover it does not fire in the second time.
I know its complicated but really I'm stuck with this issue.
Well, it would be better to have small code snippets, but from what I can see you're having problem on binding jQuery handlers on newly created elements. To solve this problem you have to use the jQuery .live().
From jQuery documentation:
This method is a variation on the
basic .bind() method for attaching
event handlers to elements. When
.bind() is called, the elements that
the jQuery object refers to get the
handler attached; elements that get
introduced later do not, so they would
require another .bind() call.
The .live() method provides an
alternative to this behavior. To bind
a click handler to the target element
using this method:
$('.hoverme').live('hover', function(){
// Live handler called.
});
And then later add a new element:
$('body').append('<div class="hoverme">Another target</div>');
Then clicks on the new element will
also trigger the handler.

Categories