Location.hash returns empty string - javascript

I am currently using jQuery, Twitter Bootstrap and CanJS for my web application. I'm trying to implement routing with CanJS. I'm using Bootstrap's tab, and when I click on a tab, it was supposed to bring #tabSearch, #tabUser or #tabPermissions, but the hash is returned with an empty string.
What am I doing wrong?
I´m using this to change the tabs:
TabControl = can.Control.extend({}, {
init: function (element, options) {
element.find('a[href="' + options.defaultTab + '"]').tab('show');
this.userSearch = null;
this.userForm = null;
}
, 'a[data-toggle="tab"] show': function (e) {
this.options.tabChangeCallback(e);
}
});
UserTab = new TabControl('#tabctrlUser', {
defaultTab: '#tabSearch',
tabChangeCallback: function (e) {
if (e.context.hash == '#tabSearch') {
if (!this.userSearch) {
this.userSearch = new FormUserQuery('#tabSearch', {});
}
} else if (e.context.hash == '#tabUser') {
if (!this.userForm) {
this.userForm = new FormUser('#tabUser', {});
}
this.userForm.renderView(currentUser);
} else if (e.context.hash == '#tabPermissions') {
new FormPermissions('#tabPermissions', {});
}
}
});
Here is the HTML part:
<ul id="tabctrlUser" class="nav nav-tabs">
<li>Pesquisar</li>
<li>Cadastrar/Alterar</li>
<li>Acessos</li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="tabSearch"></div>
<div class="tab-pane" id="tabUser"></div>
<div class="tab-pane" id="tabPermissions"></div>
</div>

You need to use can.route or can.Control.Route, the way you do it is complicated take a look at this question
Also there's 2 good articles about routing
look at this gist
http://jsfiddle.net/Z9Cv5/2/light/
var HistoryTabs = can.Control({
init: function( el ) {
// hide all tabs
var tab = this.tab;
this.element.children( 'li' ).each(function() {
tab( $( this ) ).hide();
});
// activate the first tab
var active = can.route.attr(this.options.attr);
this.activate(active);
},
"{can.route} {attr}" : function(route, ev, newVal, oldVal){
this.activate(newVal, oldVal)
},
// helper function finds the tab for a given li
tab: function( li ) {
return $( li.find( 'a' ).attr( 'href' ) );
},
// helper function finds li for a given id
button : function(id){
// if nothing is active, activate the first
return id ? this.element.find("a[href=#"+id+"]").parent() :
this.element.children( 'li:first' );
},
// activates
activate: function( active, oldActive ){
// deactivate the old active
var oldButton = this.button(oldActive).removeClass('active');
this.tab(oldButton).hide();
// activate new
var newButton = this.button(active).addClass('active');
this.tab(newButton).show();
},
"li click" : function(el, ev){
// prevent the default setting
ev.preventDefault();
// update the route data
can.route.attr(this.options.attr, this.tab(el)[0].id)
}
});
// configure routes
can.route(":component",{
component: "model",
person: "mihael"
});
can.route(":component/:person",{
component: "model",
person: "mihael"
});
// adds the controller to the element
new HistoryTabs( '#components',{attr: 'component'});
new HistoryTabs( '#people',{attr: 'person'});

I am not familiar with CanJS, but this could have something to do that anchor click event happens before navigation, and location.hash is set after. E.g. if (as a test) you define your link as
Pesquisar
And in plain vanilla JS try
function tabChangeCallback() {
alert(location.hash)
}
It will display blank.
But if you try something like
function tabChangeCallback() {
setTimeout(function() {
alert(location.hash)
}, 100)
}
It will display the hash. So in your case either set your code to be executed on timeout after main event or (and I think it's a better option) instead of hash you should read href attribute of the anchor element that caused the event.
Update The plain JS example below shows how to get the hash value without timeout:
function tabChangeCallback(e) {
var elem = e ? e.target : event.srcElement
alert(elem.getAttribute("href"))
}

Related

BackboneJS not creating the Wrapper div

I am creating a simple backbone template popup. Backbone is not creating the Wrapping <div> element that it was suppose to create. When the template is being generated there is no <div class="theme-overlay"> is generated. Backbone dumps the html from template without any wrapper.
I have searched around but I haven't found any similar issue. I am very new with Backbone so I think I am missing something.
NOTE: I am working with WordPress environment that is why there is an wp global variable wp.Backbone is an internal adaptation of Backbone for avoiding conflict with PHP. Using Backbone.View.extend() instead of wp.Backbone.View.extend() gives me the same result.
Find Code Below
window.wp = window.wp || {};
(function($){
var importer = {};
importer.data = _kallzuDemoSettings;
_.extend( importer, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
importer.View = wp.Backbone.View.extend({
template: wp.template('demo'),
el: '#theme-overlay',
className: 'theme-overlay',
events: {
'click .close' : 'collapse'
},
render: function(demo_title){
var data = _.find(importer.data.demos, function(item){
return item.name == demo_title;
});
if( data == undefined ){
alert( 'No data found!');
return;
}
this.$el.html( this.template( data ) ); // insert into dom
},
collapse: function( event ) {
var self = this;
event = event || window.event;
if ( $( event.target ).is( '.close' ) ) {
// Add a temporary closing class while overlay fades out
$( 'body' ).addClass( 'closing-overlay' );
// With a quick fade out animation
this.$el.fadeOut( 130, function() {
// Clicking outside the modal box closes the overlay
$( 'body' ).removeClass( 'closing-overlay' );
// Handle event cleanup
self.closeOverlay();
});
}
},
closeOverlay: function() {
$( 'body' ).removeClass( 'modal-open' );
this.remove();
this.unbind();
this.trigger( 'importer:collapse' );
},
});
window.installDemo = function( demo_title ){
var view = new importer.View();
view.render( demo_title );
}
})(jQuery);
I don't think other part of the script like the template isn't necessary to show. But if you need them let me know in the comment.
You should remove the el: '#theme-overlay' and instead append the view to it after init.
window.installDemo = function( demo_title ){
var view = new importer.View();
view.$el.appendTo('#theme-overlay')
view.render( demo_title );
}
This way, your wrapper div will be created and then appended to the div.
The DOM should end up looking like:
<div id="theme-overlay">
<div class="theme-overlay"><!-- your rendered content --></div>
</div>

Stay on same tab upon refresh (Tabs within Tabs)

Upon page submit the main tab(Register, lock, unlock etc) stays on the current one but the sub tab(list, locked, checked out etc) goes to default(Search) upon page submit. example if I do a submit on List or Locked tab it defaults to Search tab.
Problem: I have to keep track of both the horizontal tab and the vertical tab and currently I am able to track only the horizontal tab
HTML Main tab(Horizontal):
<div id="tabs">
<ul>
<li>Register<div class="hidden_area">Register a device</div></li>
<li>Lock<div class="hidden_area">Lock a device</div></li>
<li>Unlock<div class="hidden_area">Unlock a device</div></li>
<li>Transfer<div class="hidden_area">Transfer device to a new warehouse</div></li>
<li>Manage Users<div class="hidden_area">Change/Add regions to a user</div></li>
<li>Reporting<div class="hidden_area">Reporting Services</div></li>
</ul>
HTML Sub tab(Vertical):
<div id="tabs1">
<ul>
<li class="first current">Search<div class="hidden_area1">Search a device</div></li>
<li>List<div class="hidden_area1">List all devices</div></li>
<li>Locked<div class="hidden_area1">List locked devices</div></li>
<li>Checked Out<div class="hidden_area1">List checked out devices</div></li>
<li>Repair<div class="hidden_area1">List repair devices</div></li>
<li class="last">No Check In<div class="hidden_area1">Devices checked out but not checked in</div></li>
</ul>
jQuery Main Tab:
var index = 'ui.newTab.index()';
// Define data store name
var dataStore = window.sessionStorage;
var oldIndex = 0;
try {
// getter: Fetch previous value
oldIndex = dataStore.getItem(index);
} catch(e) {}
$( "#tabs" ).tabs({
active: oldIndex,
activate: function(event, ui) {
// Get future value
var newIndex = ui.newTab.parent().children().index(ui.newTab);
// Set future value
try {
dataStore.setItem( index, newIndex );
} catch(e) {}
}
});
jQuery Sub Tab:
$( "#tabs1" ).tabs().addClass( "ui-tabs-vertical ui-helper-clearfix" );
$( "#tabs1 li" ).removeClass( "ui-corner-top" ).addClass( "ui-corner-left" );
I assume that you are using jQuery UI tabs ,
here is an example of using tabs + cookies to save the last clicked tab
http://jqueryui.com/demos/tabs/#cookie
demo : open this link http://jqueryui.com/demos/tabs/cookie.html
the close it and re open it and you will see the same clicked tab
update: after 3 years of this answer jquery ui has deprecated the cookie option : http://jqueryui.com/upgrade-guide/1.9/#deprecated-cookie-option.
but you can still append take a look here if this fits your needs or not https://stackoverflow.com/a/14313315/109217
OR
You could try this
$(function() {
// jQueryUI 1.10 and HTML5 ready
// http://jqueryui.com/upgrade-guide/1.10/#removed-cookie-option
// Documentation
// http://api.jqueryui.com/tabs/#option-active
// http://api.jqueryui.com/tabs/#event-activate
// http://balaarjunan.wordpress.com/2010/11/10/html5-session-storage-key-things-to-consider/
//
// Define friendly index name
var index = 'key';
// Define friendly data store name
var dataStore = window.sessionStorage;
// Start magic!
try {
// getter: Fetch previous value
var oldIndex = dataStore.getItem(index);
} catch(e) {
// getter: Always default to first tab in error state
var oldIndex = 0;
}
$('#tabs').tabs({
// The zero-based index of the panel that is active (open)
active : oldIndex,
// Triggered after a tab has been activated
activate : function( event, ui ){
// Get future value
var newIndex = ui.newTab.parent().children().index(ui.newTab);
// Set future value
dataStore.setItem( index, newIndex )
}
});
});
Solved:
Horizontal tabs worked fine, but for vertical tabs the control was going to the default tab hence had to use ui.oldtab.index()
jQuery:
var index1 = 'ui.oldTab.index()';
// Define data store name
var dataStore = window.sessionStorage;
var oldIndex = 0;
try {
// getter: Fetch previous value
oldIndex1 = dataStore.getItem(index1);
} catch(e) {}
$( "#tabs1" ).tabs({
active: oldIndex1,
activate: function(event, ui) {
// Get future value
var newIndex1 = ui.newTab.parent().children().index(ui.newTab);
// Set future value
try {
dataStore.setItem( index1, newIndex1);
} catch(e) {}
}
});

How to re-run JavaScript when DOM mutates?

I'm using Template.rendered to setup a dropdown replacement like so:
Template.productEdit.rendered = function() {
if( ! this.rendered) {
$('.ui.dropdown').dropdown();
this.rendered = true;
}
};
But how do I re-run this when the DOM mutates? Helpers return new values for the select options, but I don't know where to re-execute my .dropdown()
I think you don't want this to run before the whole DOM has rendered, or else the event handler will run on EVERY element being inserted:
var rendered = false;
Template.productEdit.rendered = function() {rendered: true};
To avoid rerunning this on elements which are already dropdowns, you could give new ones a class which you remove when you make them into dropdowns
<div class="ui dropdown not-dropdownified"></div>
You could add an event listener for DOMSubtreeModified, which will do something only after the page has rendered:
Template.productEdit.events({
"DOMSubtreeModified": function() {
if (rendered) {
var newDropdowns = $('.ui.dropdown.not-dropdownified');
newDropdowns.removeClass("not-dropdownified");
newDropdowns.dropdown();
}
}
});
This should reduce the number of operations done when the event is triggered, and could stop the callstack from being exhausted
Here's my tentative answer, it works but I'm still hoping Meteor has some sort of template mutation callback instead of this more cumbersome approach:
Template.productEdit.rendered = function() {
if( ! this.rendered) {
$('.ui.dropdown').dropdown();
var mutationOptions = {
childList: true,
subtree: true
}
var mutationObserver = new MutationObserver(function(mutations, observer){
observer.disconnect(); // otherwise subsequent DOM changes will recursively trigger this callback
var selectChanged = false;
mutations.map(function(mu) {
var mutationTargetName = Object.prototype.toString.call(mu.target).match(/^\[object\s(.*)\]$/)[1];
if(mutationTargetName === 'HTMLSelectElement') {
console.log('Select Changed');
selectChanged = true;
}
});
if(selectChanged) {
console.log('Re-init Select');
$('.ui.dropdown').dropdown('restore defaults');
$('.ui.dropdown').dropdown('refresh');
$('.ui.dropdown').dropdown('setup select');
}
mutationObserver.observe(document, mutationOptions); // Start observing again
});
mutationObserver.observe(document, mutationOptions);
this.rendered = true;
}
};
This approach uses MutationObserver with some syntax help I found here
Taking ad educated guess, and assuming you are using the Semantic UI Dropdown plugin, there are four callbacks you can define:
onChange(value, text, $choice): Is called after a dropdown item is selected. receives the name and value of selection and the active menu element
onNoResults(searchValue): Is called after a dropdown is searched with no matching values
onShow: Is called after a dropdown is shown.
onHide: Is called after a dropdown is hidden.
To use them, give the dropdown() function a parameter:
$(".ui.dropdown").dropdown({
onChange: function(value, text, $choice) {alert("You chose " + text + " with the value " + value);},
onNoResults: function(searchValue) {alert("Your search for " + searchValue + " returned no results");}
onShow: function() {alert("Dropdown shown");},
onHide: function() {alert("Dropdown hidden");}
});
I suggest you read the documentation of all plugins you use.

backbone collection url manipulation with input field text

I'm new to Backbone. I have a collection whose url function depends on the textfield text. How do i get that text from my textfield. No i don't want to use JQuery selectors as accessing outside selectors from your views aint a good practice. My HTML stucture is like:
<div id='outer'>
<input type='text' id='xyz'>
<div id='image123'></div>
<div id='div1'>
<ul>
</ul>
</div>
<div id='div2'></div>
</div>
So i got 2 views, 1 collections & 1 model.
How do i get the input text in the collection without using JQuery selectors from my 'outer' view.
[Post updated with View code]
var outerView = Backbone.View.extend
({
el: '#outer',
initialize: function()
{
},
events:
{
'keyup #xyz' : 'keyfunc'
},
keyfunc: function()
{
// inputtext is a global variable & i don't want it that way
inputtext = $('#xyz').val();
},
render: function()
{
},
});
I couldn't figure what are you doing , but only if you want to send the value to the collection with out making it global and changing the url ,try doing it this way
var outerView = Backbone.View.extend
({
el: '#outer',
initialize: function()
{
},
events:
{
'keyup #xyz' : 'keyfunc'
},
keyfunc: function()
{
// not global now
var inputtext = $('#xyz').val();
var myclooction = new MainCollection({
text : inputtext
});
},
render: function()
{
},
});
in the collection
var MainCollection = Backbone.extend.collection({
url : function(){
return "someurl/"+this.text;
},
// receive the value here
initialize:function(options){
this.text = options.text;
}
});
Backbone passes information about the element that triggered the event, and there you can find that value like so:
keyfunc: function(e)
{
inputtext = $(e.currentTarget).val();
this.model.trigger('textChanged', {id: this.myID, data: inputtext});
}
and your model can listen that event like this in its initialize-function.
this.listenTo(this, 'textChanged', this.textChangedHandler);
And the model can then decide what to do with that event. For example to send it to that another view by another event.
textChangeHandler: function (e) {
this.trigger('someTextChanged', e);
}
And your views or collections can listen that event in their initialize-function:
this.listenTo(this.model, 'someTextChanged', this.textChangedHandler);
And the handler would be something like:
textChangeHandler: function (e) {
if (e.id !== this.myID) {
//do stuff
}
}

Ready function called multiple times on click

I am using the following code to convert unoredered html list into a select drop down list:
jQuery(document).ready( function($) {
//build dropdown - main navigation
$("<select />").appendTo(".region-menu-inner nav");
// Create default option "Go to..."
$("<option />", {
"selected": "selected",
"value" : "",
"text" : "Navigate..."
}).appendTo("nav select");
// Populate dropdowns with the first menu items
$(".region-menu-inner li a").each(function() {
var el = $(this);
$("<option />", {
"value" : el.attr("href"),
"text" : el.text()
}).appendTo(".region-menu-inner select");
});
//make responsive dropdown menu actually work
$(".region-menu-inner select").change(function() {
window.location = $(this).find("option:selected").val();
});
});
At the same time, I am using Simple dialog module for Drupal to create modular window. This module comes with only one js file. The code this module is using is below:
/*
#file
Defines the simple modal behavior
*/
(function ($) {
/*
Add the class 'simple-dialog' to open links in a dialog
You also need to specify 'rev="<selector>"' where the <selector>
is the unique id of the container to load from the linked page.
Any additional jquery ui dialog options can be passed through
the rel tag using the format:
rel="<option_name1>:<value1>;<option_name2>:<value2>;"
e.g. <a href="financing/purchasing-options" class="simple-dialog"
rel="width:900;resizable:false;position:[60,center]"
rev="content-area" title="Purchasing Options">Link</a>
NOTE: This method doesn't not bring javascript files over from
the target page. You will need to make sure your javascript is
either inline in the html that's being loaded, or in the head tag
of the page you are on.
ALSO: Make sure the jquery ui.dialog library has been added to the page
*/
Drupal.behaviors.simpleDialog = {
attach: function (context, settings) {
// Create a container div for the modal if one isn't there already
if ($("#simple-dialog-container").length == 0) {
// Add a container to the end of the body tag to hold the dialog
$('body').append('<div id="simple-dialog-container" style="display:none;"></div>');
try {
// Attempt to invoke the simple dialog
$( "#simple-dialog-container", context).dialog({
autoOpen: false,
modal: true,
close: function(event, ui) {
// Clear the dialog on close. Not necessary for your average use
// case, butis useful if you had a video that was playing in the
// dialog so that it clears when it closes
$('#simple-dialog-container').html('');
}
});
var defaultOptions = Drupal.simpleDialog.explodeOptions(settings.simpleDialog.defaults);
$('#simple-dialog-container').dialog('option', defaultOptions);
}
catch (err) {
// Catch any errors and report
Drupal.simpleDialog.log('[error] Simple Dialog: ' + err);
}
}
// Add support for custom classes if necessary
var classes = '';
if (settings.simpleDialog.classes) {
classes = ', .' + settings.simpleDialog.classes;
}
$('a.simple-dialog' + classes, context).each(function(event) {
if (!event.metaKey && !$(this).hasClass('simpleDialogProcessed')) {
// Add a class to show that this link has been processed already
$(this).addClass('simpleDialogProcessed');
$(this).click(function(event) {
// prevent the navigation
event.preventDefault();
// Set up some variables
var url = $(this).attr('href');
// Use default title if not provided
var title = $(this).attr('title') ? $(this).attr('title') : settings.simpleDialog.title;
if (!title) {
title = $(this).text();
}
// Use defaults if not provided
var selector = $(this).attr('name') ? $(this).attr('name') : settings.simpleDialog.selector;
var options = $(this).attr('rel') ? Drupal.simpleDialog.explodeOptions($(this).attr('rel')) : Drupal.simpleDialog.explodeOptions(settings.simpleDialog.defaults);
if (url && title && selector) {
// Set the custom options of the dialog
$('#simple-dialog-container').dialog('option', options);
// Set the title of the dialog
$('#simple-dialog-container').dialog('option', 'title', title);
// Add a little loader into the dialog while data is loaded
$('#simple-dialog-container').html('<div class="simple-dialog-ajax-loader"></div>');
// Change the height if it's set to auto
if (options.height && options.height == 'auto') {
$('#simple-dialog-container').dialog('option', 'height', 200);
}
// Use jQuery .get() to request the target page
$.get(url, function(data) {
// Re-apply the height if it's auto to accomodate the new content
if (options.height && options.height == 'auto') {
$('#simple-dialog-container').dialog('option', 'height', options.height);
}
// Some trickery to make sure any inline javascript gets run.
// Inline javascript gets removed/moved around when passed into
// $() so you have to create a fake div and add the raw data into
// it then find what you need and clone it. Fun.
$('#simple-dialog-container').html( $( '<div></div>' ).html( data ).find( '#' + selector ).clone() );
// Attach any behaviors to the loaded content
Drupal.attachBehaviors($('#simple-dialog-container'));
});
// Open the dialog
$('#simple-dialog-container').dialog('open');
// Return false for good measure
return false;
}
});
}
});
}
}
// Create a namespace for our simple dialog module
Drupal.simpleDialog = {};
// Convert the options to an object
Drupal.simpleDialog.explodeOptions = function (opts) {
var options = opts.split(';');
var explodedOptions = {};
for (var i in options) {
if (options[i]) {
// Parse and Clean the option
var option = Drupal.simpleDialog.cleanOption(options[i].split(':'));
explodedOptions[option[0]] = option[1];
}
}
return explodedOptions;
}
// Function to clean up the option.
Drupal.simpleDialog.cleanOption = function(option) {
// If it's a position option, we may need to parse an array
if (option[0] == 'position' && option[1].match(/\[.*,.*\]/)) {
option[1] = option[1].match(/\[(.*)\]/)[1].split(',');
// Check if positions need be converted to int
if (!isNaN(parseInt(option[1][0]))) {
option[1][0] = parseInt(option[1][0]);
}
if (!isNaN(parseInt(option[1][1]))) {
option[1][1] = parseInt(option[1][1]);
}
}
// Convert text boolean representation to boolean
if (option[1] === 'true') {
option[1]= true;
}
else if (option[1] === 'false') {
option[1] = false;
}
return option;
}
Drupal.simpleDialog.log = function(msg) {
if (window.console) {
window.console.log(msg);
}
}
})(jQuery);
Link that is using this module, in the source looks like this:
<a href='/user' name='user-login' id='user-login' class='simple-dialog' title='Login ' rel='width:400;resizable:false;position:[center,60]'>Log in</a>
The problem is that when you click on that link, it takes a second or two to load the popup and when it actually loads, second set of select dropdown list is being generated. If you click login link one more time, it generates third select list. Basically it duplicates whatever is converted from ul li into select list.
Thanks for help in advance.
jQuery(document).ready( function($) {
$(".region-menu-inner nav").empty(); //empty here
//build dropdown - main navigation
$("<select />").appendTo(".region-menu-inner nav");
// Create default option "Go to..."
$("<option />", {
"selected": "selected",
"value" : "",
"text" : "Navigate..."
}).appendTo("nav select");
// Populate dropdowns with the first menu items
$(".region-menu-inner li a").each(function() {
var el = $(this);
$("<option />", {
"value" : el.attr("href"),
"text" : el.text()
}).appendTo(".region-menu-inner select");
});
//make responsive dropdown menu actually work
$(".region-menu-inner select").change(function() {
window.location = $(this).find("option:selected").val();
});
});

Categories