#Id#Id:Repeated Occurrences of the Same Simple Selector Should Increase Specificity But Don't for Ids in IE9

Can you specify a data-target for Bootstrap which refers to a sibling DOM element without using an ID?

While it is true that the selector in a data-target attribute is a jQuery selector, the data-api specification for this plugin provides no means of referencing back to this in the scope of execution (see lines 147-153 in bootstrap-collapse.js for its use).

However, I would like to offer another alternative approach, which is to extend the data-api with your own custom toggle specifier. Let's call it collapse-next.

JS (see update note)

$('body').on('click.collapse-next.data-api', '[data-toggle=collapse-next]', function (e) {
var $target = $(this).parent().next()
$target.data('collapse') ? $target.collapse('toggle') : $target.collapse()
})

HTML

<a class="accordion-toggle" data-toggle="collapse-next">

JSFiddle (updated)

Downside here is that it's a rather tightly coupled approach, since the JS presumes a specific structure to the markup.


Note about IE issues

As @slhck pointed out in his answer, IE9 and under apparently fail on the first click when using an earlier revision of my answer. The cause is actually not an IE issue at all, but rather a Bootstrap one. If one invokes .collapse('toggle') on a target whose Carousel object is uninitialized, the toggle() method will be called twice - once during initialization and then again explicitly after initialization. This is definitely a Bootstrap bug and hopefully will get fixed. The only reason it doesn't appear as a problem in Chrome, FF, IE10, etc, is because they all support CSS transitions, and hence when the second call is made it short-circuits because the first one is still active. The updated workaround above merely avoids the double-call problem by checking for initialization first and handling it differently.

How can I keep selected Bootstrap tab on page refresh?

I prefer storing the selected tab in the hashvalue of the window. This also enables sending links to colleagues, who than see "the same" page. The trick is to change the hash of the location when another tab is selected. If you already use # in your page, possibly the hash tag has to be split. In my app, I use ":" as hash value separator.

<ul class="nav nav-tabs" id="myTab">
<li class="active"><a href="#home">Home</a></li>
<li><a href="#profile">Profile</a></li>
<li><a href="#messages">Messages</a></li>
<li><a href="#settings">Settings</a></li>
</ul>

<div class="tab-content">
<div class="tab-pane active" id="home">home</div>
<div class="tab-pane" id="profile">profile</div>
<div class="tab-pane" id="messages">messages</div>
<div class="tab-pane" id="settings">settings</div>
</div>

JavaScript, has to be embedded after the above in a <script>...</script> part.

$('#myTab a').click(function(e) {
e.preventDefault();
$(this).tab('show');
});

// store the currently selected tab in the hash value
$("ul.nav-tabs > li > a").on("shown.bs.tab", function(e) {
var id = $(e.target).attr("href").substr(1);
window.location.hash = id;
});

// on load of the page: switch to the currently selected tab
var hash = window.location.hash;
$('#myTab a[href="' + hash + '"]').tab('show');

How can I tell if a DOM element is visible in the current viewport?

Update: Time marches on and so have our browsers. This technique is no longer recommended and you should use Dan's solution if you do not need to support version of Internet Explorer before 7.

Original solution (now outdated):

This will check if the element is entirely visible in the current viewport:

function elementInViewport(el) {
var top = el.offsetTop;
var left = el.offsetLeft;
var width = el.offsetWidth;
var height = el.offsetHeight;

while(el.offsetParent) {
el = el.offsetParent;
top += el.offsetTop;
left += el.offsetLeft;
}

return (
top >= window.pageYOffset &&
left >= window.pageXOffset &&
(top + height) <= (window.pageYOffset + window.innerHeight) &&
(left + width) <= (window.pageXOffset + window.innerWidth)
);
}

You could modify this simply to determine if any part of the element is visible in the viewport:

function elementInViewport2(el) {
var top = el.offsetTop;
var left = el.offsetLeft;
var width = el.offsetWidth;
var height = el.offsetHeight;

while(el.offsetParent) {
el = el.offsetParent;
top += el.offsetTop;
left += el.offsetLeft;
}

return (
top < (window.pageYOffset + window.innerHeight) &&
left < (window.pageXOffset + window.innerWidth) &&
(top + height) > window.pageYOffset &&
(left + width) > window.pageXOffset
);
}

jQuery plugin template - best practice, convention, performance and memory impact

[Edit] 7 months later

Quoting from the github project

jQuery is no good, and jQuery plugins is not how do modular code.

Seriously "jQuery plugins" are not a sound architecture strategy. Writing code with a hard dependency on jQuery is also silly.

[Original]

Since I gave critique about this template I will propose an alternative.

To make live easier this relies on jQuery 1.6+ and ES5 (use the ES5 Shim).

I've spend some time re-designing the plugin template you've given and rolled out my own.

Links:

  • Github
  • Documentation
  • Unit tests Confirmed to pass in FF4, Chrome and IE9 (IE8 & OP11 dies. known bug).
  • Annotated Source Code
  • The PlaceKitten example plugin

Comparison:

I've refactored the template so that it's split into boilerplate (85%) and scaffolding code (15%). The intention is that you only have to edit the scaffolding code and you can keep leave boilerplate code untouched. To achieve this I've used

  • inheritance var self = Object.create(Base) Rather then editing the Internal class you have directly you should be editing a sub class. All your template / default functionality should be in a base class (called Base in my code).
  • convention self[PLUGIN_NAME] = main; By convention the plugin defined on jQuery will call the method define on self[PLUGIN_NAME] by default. This is considered the main plugin method and has a seperate external method for clarity.
  • monkey patching $.fn.bind = function _bind ... Use of monkey patching means that the event namespacing is done automatically for you under the hood. This functionality is free and does not come at the cost of readability (calling getEventNS all the time).

OO Techniques

It's better to stick to proper JavaScript OO rather then classical OO emulation. To achieve this you should use Object.create. (which ES5 just use the shim to upgrade old browsers).

var Base = (function _Base() {
var self = Object.create({});
/* ... */
return self;
})();

var Wrap = (function _Wrap() {
var self = Object.create(Base);
/* ... */
return self;
})();

var w = Object.create(Wrap);

This is different from the standard new and .prototype based OO people are used to. This approach is preferred because it re-inforces the concept that there are only Objects in JavaScript and it's a prototypical OO approach.

[getEventNs]

As mentioned this method has been refactored away by overriding .bind and .unbind to automatically inject namespaces. These methods are overwritten on the private version of jQuery $.sub(). The overwritten methods behave the same way as your namespacing does. It namespaces events uniquely based on plugin and instance of a plugin wrapper around a HTMLElement (Using .ns.

[getData]

This method has been replaced with a .data method that has the same API as jQuery.fn.data. The fact that it's the same API makes it easier to use, its basically a thin wrapper around jQuery.fn.data with namespacing. This allows you to set key/value pair data that is immediatley stored for that plugin only. Multiple plugins can use this method in parallel without any conflicts.

[publicMethods]

The publicMethods object has been replaced by any method being defined on Wrap being automatically public. You can call any method on a Wrapped object directly but you do not actually have access to the wrapped object.

[$.fn[PLUGIN_NAME]]

This has been refactored so it exposes a more standardized API. This api is

$(selector).PLUGIN_NAME("methodName", {/* object hash */}); // OR
$(selector).PLUGIN_NAME({/* object hash */}); // methodName defaults to PLUGIN_NAME

the elements in the selector are automatically wrapped in the Wrap object, the method is called or each selected element from the selector and the return value is always a $.Deferred element.

This standardizes the API and the return type. You can then call .then on the returned deferred to get out the actual data you care about. The use of deferred here is very powerful for abstraction away whether the plugin is synchronous or asynchronous.

_create

A caching create function has been added. This is called to turn a HTMLElement into a Wrapped element and each HTMLElement will only be wrapped once. This caching gives you a solid reduction in memory.

$.PLUGIN_NAME

Added another public method for the plugin (A total of two!).

$.PLUGIN_NAME(elem, "methodName", {/* options */});
$.PLUGIN_NAME([elem, elem2, ...], "methodName", {/* options */});
$.PLUGIN_NAME("methodName", {
elem: elem, /* [elem, elem2, ...] */
cb: function() { /* success callback */ }
/* further options */
});

All parameters are optional. elem defaults to <body>, "methodName" defaults to "PLUGIN_NAME" and {/* options */} defaults to {}.

This API is very flexible (with 14 method overloads!) and standard enough to get used to the syntnax for every method your plugin will expose.

Public exposure

The Wrap, create and $ objects are exposed globally. This will allow advanced plugin users maximum flexibility with your plugin. They can use create and the modified subbed $ in their development and they can also monkey patch Wrap. This allows for i.e. hooking into your plugin methods. All three of these are marked with a _ in front of their name so they are internal and using them breaks the garantuee that your plugin works.

The internal defaults object is also exposed as $.PLUGIN_NAME.global. This allows users to override your defaults and set plugin global defaults. In this plugin setup all hashes past into methods as objects are merged with the defaults, so this allows users to set global defaults for all your methods.

Actual Code

(function($, jQuery, window, document, undefined) {
var PLUGIN_NAME = "Identity";
// default options hash.
var defaults = {
// TODO: Add defaults
};

// -------------------------------
// -------- BOILERPLATE ----------
// -------------------------------

var toString = Object.prototype.toString,
// uid for elements
uuid = 0,
Wrap, Base, create, main;

(function _boilerplate() {
// over-ride bind so it uses a namespace by default
// namespace is PLUGIN_NAME_<uid>
$.fn.bind = function _bind(type, data, fn, nsKey) {
if (typeof type === "object") {
for (var key in type) {
nsKey = key + this.data(PLUGIN_NAME)._ns;
this.bind(nsKey, data, type[key], fn);
}
return this;
}

nsKey = type + this.data(PLUGIN_NAME)._ns;
return jQuery.fn.bind.call(this, nsKey, data, fn);
};

// override unbind so it uses a namespace by default.
// add new override. .unbind() with 0 arguments unbinds all methods
// for that element for this plugin. i.e. calls .unbind(_ns)
$.fn.unbind = function _unbind(type, fn, nsKey) {
// Handle object literals
if ( typeof type === "object" && !type.preventDefault ) {
for ( var key in type ) {
nsKey = key + this.data(PLUGIN_NAME)._ns;
this.unbind(nsKey, type[key]);
}
} else if (arguments.length === 0) {
return jQuery.fn.unbind.call(this, this.data(PLUGIN_NAME)._ns);
} else {
nsKey = type + this.data(PLUGIN_NAME)._ns;
return jQuery.fn.unbind.call(this, nsKey, fn);
}
return this;
};

// Creates a new Wrapped element. This is cached. One wrapped element
// per HTMLElement. Uses data-PLUGIN_NAME-cache as key and
// creates one if not exists.
create = (function _cache_create() {
function _factory(elem) {
return Object.create(Wrap, {
"elem": {value: elem},
"$elem": {value: $(elem)},
"uid": {value: ++uuid}
});
}
var uid = 0;
var cache = {};

return function _cache(elem) {
var key = "";
for (var k in cache) {
if (cache[k].elem == elem) {
key = k;
break;
}
}
if (key === "") {
cache[PLUGIN_NAME + "_" + ++uid] = _factory(elem);
key = PLUGIN_NAME + "_" + uid;
}
return cache[key]._init();
};
}());

// Base object which every Wrap inherits from
Base = (function _Base() {
var self = Object.create({});
// destroy method. unbinds, removes data
self.destroy = function _destroy() {
if (this._alive) {
this.$elem.unbind();
this.$elem.removeData(PLUGIN_NAME);
this._alive = false;
}
};

// initializes the namespace and stores it on the elem.
self._init = function _init() {
if (!this._alive) {
this._ns = "." + PLUGIN_NAME + "_" + this.uid;
this.data("_ns", this._ns);
this._alive = true;
}
return this;
};

// returns data thats stored on the elem under the plugin.
self.data = function _data(name, value) {
var $elem = this.$elem, data;
if (name === undefined) {
return $elem.data(PLUGIN_NAME);
} else if (typeof name === "object") {
data = $elem.data(PLUGIN_NAME) || {};
for (var k in name) {
data[k] = name[k];
}
$elem.data(PLUGIN_NAME, data);
} else if (arguments.length === 1) {
return ($elem.data(PLUGIN_NAME) || {})[name];
} else {
data = $elem.data(PLUGIN_NAME) || {};
data[name] = value;
$elem.data(PLUGIN_NAME, data);
}
};
return self;
})();

// Call methods directly. $.PLUGIN_NAME(elem, "method", option_hash)
var methods = jQuery[PLUGIN_NAME] = function _methods(elem, op, hash) {
if (typeof elem === "string") {
hash = op || {};
op = elem;
elem = hash.elem;
} else if ((elem && elem.nodeType) || Array.isArray(elem)) {
if (typeof op !== "string") {
hash = op;
op = null;
}
} else {
hash = elem || {};
elem = hash.elem;
}

hash = hash || {}
op = op || PLUGIN_NAME;
elem = elem || document.body;
if (Array.isArray(elem)) {
var defs = elem.map(function(val) {
return create(val)[op](hash);
});
} else {
var defs = [create(elem)[op](hash)];
}

return $.when.apply($, defs).then(hash.cb);
};

// expose publicly.
Object.defineProperties(methods, {
"_Wrap": {
"get": function() { return Wrap; },
"set": function(v) { Wrap = v; }
},
"_create":{
value: create
},
"_$": {
value: $
},
"global": {
"get": function() { return defaults; },
"set": function(v) { defaults = v; }
}
});

// main plugin. $(selector).PLUGIN_NAME("method", option_hash)
jQuery.fn[PLUGIN_NAME] = function _main(op, hash) {
if (typeof op === "object" || !op) {
hash = op;
op = null;
}
op = op || PLUGIN_NAME;
hash = hash || {};

// map the elements to deferreds.
var defs = this.map(function _map() {
return create(this)[op](hash);
}).toArray();

// call the cb when were done and return the deffered.
return $.when.apply($, defs).then(hash.cb);

};
}());

// -------------------------------
// --------- YOUR CODE -----------
// -------------------------------

main = function _main(options) {
this.options = options = $.extend(true, defaults, options);
var def = $.Deferred();

// Identity returns this & the $elem.
// TODO: Replace with custom logic
def.resolve([this, this.elem]);

return def;
}

Wrap = (function() {
var self = Object.create(Base);

var $destroy = self.destroy;
self.destroy = function _destroy() {
delete this.options;
// custom destruction logic
// remove elements and other events / data not stored on .$elem

$destroy.apply(this, arguments);
};

// set the main PLUGIN_NAME method to be main.
self[PLUGIN_NAME] = main;

// TODO: Add custom logic for public methods

return self;
}());

})(jQuery.sub(), jQuery, this, document);

As can be seen the code your supposed to edit is below the YOUR CODE line. The Wrap object acts similarly to your Internal object.

The function main is the main function called with $.PLUGIN_NAME() or $(selector).PLUGIN_NAME() and should contain your main logic.

How to get child element by class name?

Use doc.childNodes to iterate through each span, and then filter the one whose className equals 4:

var doc = document.getElementById("test");
var notes = null;
for (var i = 0; i < doc.childNodes.length; i++) {
if (doc.childNodes[i].className == "4") {
notes = doc.childNodes[i];
break;
}
}



Related Topics



Leave a reply



Submit