<article>
<h1 class="post-title">Weekly Widget 1 - TreeCombo</h1>
<div class="author-time">18 January 2013 by justinbmeyer</div>
<p>I, <a href="http://twitter.com/justinbmeyer">@justinbmeyer</a>, am
going to post a weekly widget made with CanJS. I hope to continue
this for as long as I have widgets to write. If you
<strong>want to see something</strong>, please tweet it to
<a href="http://twitter.com/justinbmeyer">@canjs</a>. These posts
are going to be quick and dirty. Eventually, I will put
these up on <a href="http://canjs.us/recipes.html">CanJS’s recipe page</a> with
a full description.
</p>
<p>I’m starting with the <code>TreeCombo</code> below:</p>
<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/sTLhX/35/embedded/result,html,js"
allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>
<h2 id="what-it-does">What it does</h2>
<p>The tree combo allows you to select items within and
navigate through a hierarchial collection
of data. Click “→” to see child items. Click the
breadcrumb navigation at the top to return to parent items.</p>
<p>It also displays a list of the items the user has selected under
“You’ve selected:”.</p>
</article>
<article>
<h1 class="post-title">Weekly Widget 2 - 2-Way Mustache Helpers
</h1>
<div class="author-time">27 January 2013 by justinbmeyer</div>
<p>This article shows how to make 2-way binding mustache helpers
and use them in a basic form.</p>
<p>By default, CanJS supports 1-way binding templates.
1-way binding means that if data changes,
the template is automatically updated. For example,
if <code>attendee.name</code> changes, the following input’s value is updated:
</p>
<pre><code class="xml"><span class="tag"><<span class="title">input</span> <span
class="attribute">type</span>=<span class="value">'text'</span> <span
class="attribute">value</span>=<span
class="value">"{{attendee.name}}"</span>/></span>
</code></pre>
<p>2-way binding means the reverse is also true. If the user
types a new value, the <code>attendee.name</code> is automatically updated.
This article shows how roll your own
2-way mustache helpers that can be used like:</p>
<pre><code class="xml"><span class="tag"><<span class="title">input</span> <span
class="attribute">type</span>=<span class="value">'text'</span> {{<span
class="attribute">value</span> <span
class="attribute">attendee.name</span>}}/></span>
<span
class="tag"><<span class="title">input</span> <span
class="attribute">value</span>=<span class="value">"yes"</span> <span
class="attribute">type</span>=<span class="value">"radio"</span> {{<span
class="attribute">checked</span> <span
class="attribute">attendee.attending</span>}}/></span>
</code></pre>
</article>
<article>
<h1 class="post-title">Weekly Widget 3 - Paginated Grid
</h1>
<div class="author-time">01 February 2013 by justinbmeyer</div>
<p>This weekly widget was suggested
by <a href="https://twitter.com/Cherif_b/status/296765112386203649">@Cherif_b</a>.
The article covers how to create a basic paginated
grid. But even more important, it gives a glipse
at how we organize large applications. </p>
<h2 id="the-app">The app</h2>
<p>The app allows you to paginate through a list of links:</p>
<iframe style="width: 100%; height: 300px"
src="http://jsfiddle.net/SyEXx/4/embedded/result,html,js"
allowfullscreen="allowfullscreen"
frameborder="0">JSFiddle</iframe>
<h2 id="what-it-does">What it does</h2>
<p>You can go through each page by clicking the “Next” and “Prev”
links. Those buttons become gray when clicking on one would
move to a page without data.</p>
<p>The app also supports bookmarking of the current page and
forward/back button support.</p>
</article>
(function(){
/**
* $('.body').more({
* moreHTML: "<a href='javascript://' class='more'>...</a>",
* moreWidth: 50,
* lessHTML: " <a href='javascript://' class='less'>less</a>",
* lines: 2
* })
*/
$.fn.more = function(options){
// setup defaults
options = $.extend({
lessHTML: " <a href='javascript://' class='less'>-</a>",
moreHTML: " <a href='javascript://' class='more'>+</a>",
moreWidth: 50,
lines: 2
},options||{});
this.each(function(el){
var $el = $(this);
// save current HTML for later
$el.data('originalHTML', $el.html())
// the active range we will be moving around
var range = $el.range(),
// the end of the body's text for comparison
end = range.clone().collapse(false).start("-1"),
// the start of the body's text for comparison
start = nextChar(range.collapse().end("+1"),end).clone(),
// the current line's first character's coordinates
prevRect = start.rect(),
// how many lines we've come across
lines = 0;
// go until we reach the end of the body
while(range.compare("START_TO_START",end) != 0){
range.end("+1").start("+1");
var rect = range.rect();
// if the charcter is on a new line
if( rect && (rect.top -prevRect.top > 4) ) {
lines++;
// quit on second line
if(lines == options.lines){
break;
}
prevStart = range.clone()
prevRect = rect;
}
}
if(lines === options.lines){
// backup to previous line
range.end('-1').start('-1');
}
// get the last visible text element
prevChar(range, start)
var movedLeft = false,
offset = $el.offset(),
width = $el.width();
// keep moving back until there is room for more
while(range.compare("START_TO_START",start) != -1 ){
if( range.rect(true).left <= (offset.left+width-options.moreWidth) ) {
break;
}
movedLeft = true;
range.end("-1").start("-1")
}
// exit if we don't need to add more button
if(!movedLeft && (lines < options.lines ) ) {
return
}
var parent = range.start().container;
// remove remaining text
if( parent.nodeType === Node.TEXT_NODE ||
parent.nodeType === Node.CDATA_SECTION_NODE ) {
parent.nodeValue = parent.nodeValue.slice(0,range.start().offset+1)
}
var removeAfter = parent;
// remove everything after
while(removeAfter !== this){
var parentEl = removeAfter.parentNode,
childNodes = parentEl.childNodes,
index = $.inArray(removeAfter,childNodes );
for(var i = parentEl.childNodes.length-1; i > index; i--){
parentEl.removeChild( childNodes[i] );
}
removeAfter = parentEl;
}
// add more after parent
if( parent.nodeType === Node.TEXT_NODE ||
parent.nodeType === Node.CDATA_SECTION_NODE ) {
parent = parent.parentElement
}
$(parent).append(options.moreHTML);
$el.data('shortenedHTML',$el.html())
// show more / hide listeners
.on("click","a.more",function(){
$el.html($el.data('originalHTML')+options.lessHTML)
})
.on("click","a.less",function(){
$el.html($el.data('shortenedHTML'))
});
})
};
// Moves the range until it hits something other than a space
var nextChar = function(range, boundary){
while(/[\s\n]/.test(range) && range.compare("END_TO_END",boundary) != 1){
range.start("+1").end("+1")
}
return range;
};
// Moves the range until it hits something other than a space
var prevChar = function(range, boundary){
while(/[\s\n]/.test(range) && range.compare("START_TO_START",boundary) != -1){
range.start("-1").end("-1")
}
return range;
}
})();
$("article").more({lines: 5, moreWidth: 25})