boomqueries
Advanced tools
Comparing version 0.0.3 to 0.0.4
@@ -1,2 +0,2 @@ | ||
/*! BoomQueries 0.0.3 | http://boomtownroi.github.io/boomqueries/ | (c) 2014 BoomTown | MIT License */ | ||
/*! BoomQueries 0.0.4 | http://boomtownroi.github.io/boomqueries/ | (c) 2014 BoomTown | MIT License */ | ||
(function (global, boomQueries) { | ||
@@ -13,9 +13,18 @@ if(typeof define === 'function' && define.amd) { | ||
var boomQueriesComponents = {}; | ||
var elementKey = 0; | ||
function boomQuery() { | ||
// Array of dom nodes which update their class when the window resizes | ||
this.nodes = []; | ||
function debounce(func, wait, immediate) { | ||
// Hash of selectors with their corresponding break points | ||
this.map = {}; | ||
// Dispatch our custom event when the window resizes | ||
window.addEventListener('resize', this.debounce(this, this.update, 100), false); | ||
} | ||
// Rate limit the amount of times our update method gets called on window resize | ||
boomQuery.prototype.debounce = function(context, func, wait, immediate) { | ||
var timeout; | ||
return function() { | ||
var context = this, args = arguments; | ||
var args = arguments; | ||
var later = function() { | ||
@@ -30,64 +39,174 @@ timeout = null; | ||
}; | ||
} | ||
}; | ||
function add(nameOrObject, descriptor) { | ||
var key; | ||
if (typeof nameOrObject === 'string') { | ||
key = nameOrObject; | ||
boomQueriesComponents[nameOrObject] = descriptor; | ||
} else { | ||
key = elementKey++; | ||
boomQueriesComponents[key] = nameOrObject; | ||
} | ||
return key; | ||
} | ||
// Called whenever we need to ensure all nodes have their proper class | ||
boomQuery.prototype.update = function(options) { | ||
var details = {}; | ||
// You can pass a custom object to each node when we dispatch the event | ||
if ( typeof options !== 'undefined' ) details["detail"] = options; | ||
var updateEvent = new CustomEvent("checkyourself", details); | ||
// Loop through our nodes firing a "checkyourself" event on each of them | ||
this.nodes.forEach(function(node) { | ||
node.dispatchEvent(updateEvent); | ||
}); | ||
}; | ||
function remove(key) { | ||
delete boomQueriesComponents[key]; | ||
} | ||
// Internal function that accepts a DOM node with break points | ||
// Adds custom properties and a listener to the node and then stores the node in an internal array | ||
boomQuery.prototype._add = function(node, breakPoints, selector) { | ||
// Check to ensure we aren't already tracking this node | ||
if ( node.breaks === undefined ) { | ||
// Store the break points array in the node | ||
node.breaks = breakPoints; | ||
function calculateElements() { | ||
var componentKeys = Object.keys(boomQueriesComponents); | ||
// If we pass a selector/name, add it to the node so we can reference it later | ||
if ( selector !== null ) node.selector = selector; | ||
var currentComponent; | ||
var $elements; | ||
// Attach an event listener with functionality to update it's own class | ||
node.addEventListener("checkyourself", function(event) { | ||
// event.detail is custom object if we have one | ||
function calculateElement(element, i) { | ||
if (element.offsetParent !== null) { | ||
var componentBreaksCounter = currentComponent.breaks.length, | ||
currentWidth = element.offsetWidth, | ||
currentBreak = -1; | ||
// Ensure we have a parent with layout to assess our offsetWidth from | ||
if ( this.offsetParent !== null ) { | ||
var componentBreaksCounter = this.breaks.length, | ||
currentWidth = this.offsetWidth, | ||
currentBreak = -1; | ||
while (componentBreaksCounter--) { | ||
if(currentWidth >= currentComponent.breaks[componentBreaksCounter][0]) { | ||
currentBreak++; | ||
while ( componentBreaksCounter-- ) { | ||
if ( currentWidth >= this.breaks[componentBreaksCounter][0] ) { | ||
currentBreak++; | ||
} | ||
this.classList.remove(this.breaks[componentBreaksCounter][1]); | ||
} | ||
element.classList.remove(currentComponent.breaks[componentBreaksCounter][1]); | ||
if ( currentBreak >= 0 ) { | ||
this.classList.add(this.breaks[currentBreak][1]); | ||
} | ||
// Create a custom object with details of our event to pass to our callback | ||
var details = { | ||
detail: { | ||
'offsetWidth': currentWidth, | ||
'currentBreak': this.breaks[currentBreak] | ||
} | ||
}; | ||
var completedEvent = new CustomEvent("nodeUpdated", details); | ||
// You can now attach an event listener to this node to catch when we have completed the event | ||
this.dispatchEvent(completedEvent); | ||
} | ||
}); | ||
if (currentBreak >= 0) { | ||
element.classList.add(currentComponent.breaks[currentBreak][1]); | ||
node.addEventListener("cleanup", function(event) { | ||
var self = this; | ||
this.breaks.forEach(function(br) { | ||
self.classList.remove(br[1]); | ||
}); | ||
}); | ||
// Push the node on to our stack of nodes | ||
this.nodes.push(node); | ||
} | ||
}; | ||
// Internal method that accepts a css selector, | ||
boomQuery.prototype._addSelector = function(selector, breakPoints) { | ||
// Add selector to internal map hash for refreshing | ||
this.map[selector] = breakPoints; | ||
// Loop through nodes adding them internally | ||
var nodes = document.querySelectorAll(selector), self = this; | ||
Array.prototype.forEach.call(nodes, function(node) { | ||
self._add(node, breakPoints, selector); | ||
}); | ||
}; | ||
// Main method for external use | ||
// Can add a css selector or DOM node as first par | ||
// Breakpoints is a multi dimensional array containing an offsetWidth int and a corresponding class name | ||
// Name is an optional parameter that allows you to specify or name a DOM node | ||
boomQuery.prototype.add = function (selector, breakPoints, name) { | ||
if ( typeof selector === 'string' ) { | ||
this._addSelector(selector, breakPoints); | ||
} else { | ||
var id = null, self = this; | ||
if ( name !== 'undefined' ) id = name; | ||
if ( selector.constructor === Array ) { | ||
selector.forEach(function(node){ | ||
self._add(node, breakPoints, id); | ||
}); | ||
} else { | ||
this._add(selector, breakPoints, id); | ||
} | ||
} | ||
this.update(); | ||
}; | ||
// Call refresh method when new DOM elements have been added | ||
boomQuery.prototype.refresh = function() { | ||
// First let's remove any nodes which have been removed from DOM | ||
this.remove(); | ||
// Now we need to roll through css selectors and requery them to see if there are a new nodes we need to consume | ||
var selectors = Object.keys(this.map), self = this; | ||
selectors.forEach(function(selector) { | ||
self._addSelector(selector, self.map[selector]); | ||
}); | ||
this.update(); | ||
}; | ||
// Internal method to stay DRY | ||
boomQuery.prototype._delete = function(i) { | ||
// Remove event listener before discarding to avoid zombies | ||
this.nodes[i].dispatchEvent(new CustomEvent("cleanup")); | ||
this.nodes[i].removeEventListener("checkyourself"); | ||
this.nodes[i].removeEventListener("cleanup"); | ||
this.nodes.splice(i, 1); | ||
return true; | ||
}; | ||
// Remove internal nodes based on selector or it's presence in the DOM | ||
boomQuery.prototype.remove = function(selector) { | ||
// Remove node based on selector or unique name provided | ||
if ( selector !== undefined ) { | ||
for ( var i = this.nodes.length; i--; ) { | ||
if ( this.nodes[i].selector === selector ) { | ||
this._delete(i); | ||
} | ||
} | ||
// Make sure our selector map actually has selector before deleting | ||
// If we pass an ID of DOM node to delete, it won't be contained in our selector map | ||
if ( this.map.hasOwnProperty(selector) ) delete this.map[selector]; | ||
// If a selector is not passed, let's remove the node if it is no longer in the DOM | ||
} else { | ||
for ( var i = this.nodes.length; i--; ) { | ||
if ( !document.body.contains(this.nodes[i]) ) { | ||
this._delete(i); | ||
} | ||
} | ||
} | ||
}; | ||
componentKeys.forEach(function(key) { | ||
currentComponent = boomQueriesComponents[key]; | ||
if (currentComponent.selector) { | ||
$elements = document.querySelectorAll(currentComponent.selector); | ||
} else if (currentComponent.element) { | ||
$elements = [currentComponent.element]; | ||
boomQuery.prototype.get = function(selector) { | ||
// Loop through internal array of nodes | ||
for ( var i = this.nodes.length; i--; ) { | ||
if ( this.nodes[i].selector === selector ) { | ||
return this.nodes[i]; | ||
} | ||
Array.prototype.forEach.call($elements, calculateElement); | ||
}); | ||
} | ||
} | ||
}; | ||
window.addEventListener('load', calculateElements); | ||
window.addEventListener('resize', debounce(calculateElements, 100), false); | ||
// Just logs the internal array of nodes for debug/inspection | ||
// You can specify which internal store you want to inspect: map or nodes | ||
boomQuery.prototype.inspect = function(which) { | ||
if ( typeof console !== "undefined" ) { | ||
if ( which === 'map' ) console.log(this.map); | ||
else console.log(this.nodes); | ||
} | ||
}; | ||
return { | ||
add: add, | ||
remove: remove, | ||
calculate: calculateElements | ||
}; | ||
})); | ||
return new boomQuery(); | ||
})); |
@@ -5,3 +5,3 @@ { | ||
"description": "BoomQueries is our take on element-queries; sizing elements based on their container.", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"homepage": "http://boomtownroi.github.io/boomqueries/", | ||
@@ -25,3 +25,3 @@ "author": { | ||
"type": "MIT", | ||
"url": "https://github.com/boomtownroi/boomqueries/blog/master/LICENSE.md" | ||
"url": "https://github.com/BoomTownROI/boomqueries/blob/master/LICENSE.md" | ||
} | ||
@@ -37,3 +37,2 @@ ], | ||
"gulp-less": "^1.3.6", | ||
"gulp-livereload": "^2.1.1", | ||
"gulp-rename": "^1.2.0", | ||
@@ -40,0 +39,0 @@ "gulp-uglify": "^1.0.1", |
114
README.md
# BoomQueries | ||
BoomQueries is our take on element-queries; sizing elements based on their container. | ||
BoomQueries is our take on element queries; sizing elements based on their container. | ||
@@ -22,31 +22,80 @@ As our product has grown to be more modular, we began to see the limitations of sizing those modular components across more granular scopes; main content areas, sidebars, etc. And most of all, the specificities of keeping up with all these variations started to take a toll on productivity and maintenance. While there are other implementations, we didn't find any that quite fit our needs. The benefits of our version are: | ||
Please see tests/kitchensink.html for a thorough example of usage. | ||
## Initializing/Adding Components | ||
Use `window.boomQueries.add()` to register your component(s) with the BoomQueries library. Each instance can house a `key`; can be used to interact after it has been registered, a `selector`, and a `breaks` array, which holds references to your desired `min-width` breakpoint and the class to be added to your component. | ||
Use `boomQueries.add()` to register your component(s) with the BoomQueries library. | ||
window.boomQueries.add("COMPONENTKEY", { | ||
selector: ".component", | ||
breaks: [ | ||
boomQueries.add('.component', [ | ||
[480, "component--md"], | ||
[600, "component--lg"] | ||
] | ||
}); | ||
]); | ||
Once you have added your components, you can initialize BoomQueries with: | ||
You can also register DOM nodes. | ||
window.boomQueries.calculate(); | ||
var component = document.createElement('div'); | ||
boomQueries.add(component, [ | ||
[480, "component--md"], | ||
[600, "component--lg"] | ||
]); | ||
When registering DOM nodes, you can pass an additional third parameter, id, to reference your node later in the application. | ||
var component = document.createElement('div'); | ||
boomQueries.add(component, [ | ||
[480, "component--md"], | ||
[600, "component--lg"] | ||
], 'myComponent'); | ||
// boomQueries.get('myComponent') you can get your node | ||
// boomQueries.remove('myComponent') you can remove your node | ||
You can also bulk add DOM nodes. | ||
var components = [document.createElement('div'), document.createElement('div'), document.createElement('div'), document.createElement('div')]; | ||
boomQueries.add(components, [ | ||
[480, "component--md"], | ||
[600, "component--lg"] | ||
], 'myComponents'); | ||
// boomQueries.remove('myComponents') to remove them | ||
## Refreshing Components | ||
When you are working with a dynamic application that has lots of DOM changes, you should refresh your boomQueries after change. | ||
boomQueries.refresh(); | ||
The refresh method will remove event listeners from watched nodes that are no longer in the DOM and grab newly added elements based on their css selector. | ||
## Component Callback | ||
There are times when you would want to fire additional functionality on a node after it's been updated. You can do this by attaching a custom event listener to that node. | ||
var component = document.createElement('div'); | ||
document.body.appendChild(component); | ||
component.addEventListener('nodeUpdated', function(event){ | ||
console.log(event.detail); | ||
}); | ||
We pass the callback an object about the recent update: 'offsetWidth' and 'currentBreak' | ||
## Removing Components | ||
You can remove components registered by BoomQueries by calling the `remove` method and specifying your component `key`. | ||
You can remove components registered by BoomQueries by calling the `remove` method and specifying either your custom id or css selector. | ||
window.boomQueries.remove("COMPONENTKEY"); | ||
boomQueries.remove('myComponent'); | ||
_You can freely add/remove components as needed throughout your app, so don't feel that you need to register them all at once!_ | ||
## Working with Dynamic Content | ||
Using Backbone, Angular, React, etc. to dynamically interact with DOM elements? You can easily "refresh" BoomQueries by calling the `calculate()` method again: | ||
Using Backbone, Angular, React, etc. to dynamically interact with DOM elements? You can easily "refresh" BoomQueries by calling the `refresh()` method again: | ||
window.boomQueries.calculate(); | ||
boomQueries.refresh(); | ||
@@ -60,15 +109,34 @@ ## CommonJS Usage | ||
boomQueries.add("COMPONENTKEY", { | ||
selector: ".component", | ||
breaks: [ | ||
[480, "component--md"], | ||
[600, "component--lg"] | ||
] | ||
}); | ||
boomQueries.add(".component", [ | ||
[480, "component--md"], | ||
[600, "component--lg"] | ||
]); | ||
boomQueries.remove("COMPONENTKEY"); | ||
boomQueries.remove(".component"); | ||
boomQueries.calculate(); | ||
boomQueries.refresh(); | ||
``` | ||
## Internal Inspection | ||
If you need to see what nodes are currently being watched, you can log `boomQueries.inspect()` | ||
If you need to see which css selectors are being followed/refreshed, you can log `boomQueries.inspect('map')` | ||
And although it is not recommended, you can access the internal data for debugging purposes: | ||
`boomQueries.nodes` | ||
`boomQueries.map` | ||
## Contributing | ||
Have something you want to add to BoomQueries? Great! Here's a few helpful tips to get started: | ||
_We use [GulpJS](http://gulpjs.com) to compile BoomQueries, so make sure you have that and [Node.js](http://nodejs.org/) installed._ | ||
* Clone the repo, `git clone git://github.com/boomtownroi/boomqueries.git` | ||
* `npm install` to add-in Gulp dependencies | ||
* `gulp server` to fire up a browser (using [BrowserSync](http://www.browsersync.io/)) which will take care of compiling and reloading the page. | ||
## Versioning | ||
@@ -80,2 +148,2 @@ | ||
Copyright 2014 [BoomTown](http://boomtownroi.com) under the [MIT License](https://github.com/boomtownroi/boomqueries/blob/LICENSE.md) | ||
Copyright 2014 [BoomTown](http://boomtownroi.com) under the [MIT License](https://github.com/BoomTownROI/boomqueries/blob/master/LICENSE.md) |
14277
11
181
147