diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 0ebae79..e06302a 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -1,12 +1,15 @@ class PostsController < ApplicationController def index - @posts = Post.latest + @posts = Post.scoped + @posts = @posts.latest.limit(20).includes(:tags) + @posts = @posts.offset(params[:offset].to_i) if params[:offset] + render :partial => "listed_post", :collection => @posts.all if params[:list] end def show @post = Post.find_by_id(params[:id]) - @posts = Post.latest - [@post] + @posts = Post.latest.limit(20).includes(:tags) - [@post] end end diff --git a/app/views/admin/posts/_form.html.haml b/app/views/admin/posts/_form.html.haml index ce07a22..6806672 100644 --- a/app/views/admin/posts/_form.html.haml +++ b/app/views/admin/posts/_form.html.haml @@ -1,18 +1,17 @@ +- stylesheet "posts" +- javascript "elastic" + = form_for [:admin, @post] do |f| = f.label :title - %br = f.text_field :title - %br = f.label :tag_list - %br = f.text_field :tag_list - %br = f.label :body - %br = f.text_area :body - %br = f.label :published_on - %br = f.date_select :published_on %br - = f.submit "Save", :class => "button" \ No newline at end of file + = f.submit "Save", :class => "button medium" + +:javascript + $('textarea#post_body').elastic(); \ No newline at end of file diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index c627d0a..a6471bc 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -13,7 +13,7 @@ #header_container #header = image_tag "fade.png", :id => "fade" - = image_tag "logo.png", :id => "logo" + = link_to image_tag("logo.png", :id => "logo"), root_path #container #menu= yield :menu #content= yield diff --git a/app/views/posts/_listed_post.html.haml b/app/views/posts/_listed_post.html.haml index b6335b1..1afcdb3 100644 --- a/app/views/posts/_listed_post.html.haml +++ b/app/views/posts/_listed_post.html.haml @@ -8,10 +8,8 @@ %h1= link_to listed_post.title, listed_post, :title => listed_post.title %span.detailed_info by - %span.focus Martin - - unless listed_post.tags.empty? - and tagged with - = tag_sentence(listed_post) + %span.focus= listed_post.user.full_name + = raw "(" + link_to("Comments", post_path(listed_post, :anchor => "disqus_thread")) + ")" .fadeout .text %p= truncate(strip_tags(textilize(listed_post.body)), :length => 150) \ No newline at end of file diff --git a/app/views/posts/_post.html.haml b/app/views/posts/_post.html.haml index 03c8ff0..1f30c03 100644 --- a/app/views/posts/_post.html.haml +++ b/app/views/posts/_post.html.haml @@ -1,12 +1,13 @@ = stylesheet "coderay" - content_for(:menu) do - %p - = link_to "Write a new post", [:new, :admin, :post], :class => "button in_menu" - %br - = link_to "Edit this one", [:edit, :admin, post], :class => "button in_menu" - %br - = link_to "Delete this one", [:admin, post], :method => :delete, :confirm => "Are you sure?", :class => "button red in_menu" + - if current_user && current_user.admin + %p + = link_to "Write a new post", [:new, :admin, :post], :class => "button in_menu" + %br + = link_to "Edit this one", [:edit, :admin, post], :class => "button in_menu" + %br + = link_to "Delete this one", [:admin, post], :method => :delete, :confirm => "Are you sure?", :class => "button red in_menu" #title = image_tag "http://www.gravatar.com/avatar/eb8c26db448303abe1d2f5df15c2a0a5?s=70", :class => "profile_image" diff --git a/app/views/posts/_posts_list.html.haml b/app/views/posts/_posts_list.html.haml index 5af206a..c28b7e0 100644 --- a/app/views/posts/_posts_list.html.haml +++ b/app/views/posts/_posts_list.html.haml @@ -1,4 +1,5 @@ - stylesheet "posts" +- javascript "mousewheel", "jScrollPane" - unless @posts.empty? - javascript "posts" @@ -13,4 +14,7 @@ %button#patrik.button By Patrik %ul#posts - = render :partial => "listed_post", :collection => @posts \ No newline at end of file + = render :partial => "listed_post", :collection => @posts + %li#loading_post + %h2 Loading more posts... + = image_tag "/images/loader.gif" \ No newline at end of file diff --git a/app/views/posts/index.html.haml b/app/views/posts/index.html.haml index a3d58de..f621954 100644 --- a/app/views/posts/index.html.haml +++ b/app/views/posts/index.html.haml @@ -1 +1,7 @@ -= render "posts_list" \ No newline at end of file +- content_for(:menu) do + %p + = link_to "Write a new post", [:new, :admin, :post], :class => "button in_menu" + + +- content_for(:bottom) do + = render "posts_list" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index f878db0..5db18c4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,7 +54,7 @@ # You can have the root of your site routed with "root" # just remember to delete public/index.html. - # root :to => "welcome#index" + root :to => "posts#index" # See how all your routes lay out with "rake routes" diff --git a/public/images/loader.gif b/public/images/loader.gif new file mode 100644 index 0000000..09d621e Binary files /dev/null and b/public/images/loader.gif differ diff --git a/public/images/scroll_background.png b/public/images/scroll_background.png new file mode 100644 index 0000000..1ad2e13 Binary files /dev/null and b/public/images/scroll_background.png differ diff --git a/public/javascripts/elastic.js b/public/javascripts/elastic.js new file mode 100644 index 0000000..d4ba339 --- /dev/null +++ b/public/javascripts/elastic.js @@ -0,0 +1,117 @@ +/** +* @name Elastic +* @descripton Elastic is Jquery plugin that grow and shrink your textareas automaticliy +* @version 1.6.4 +* @requires Jquery 1.2.6+ +* +* @author Jan Jarfalk +* @author-email jan.jarfalk@unwrongest.com +* @author-website http://www.unwrongest.com +* +* @licens MIT License - http://www.opensource.org/licenses/mit-license.php +*/ + +(function(jQuery){ + jQuery.fn.extend({ + elastic: function() { + + // We will create a div clone of the textarea + // by copying these attributes from the textarea to the div. + var mimics = [ + 'paddingTop', + 'paddingRight', + 'paddingBottom', + 'paddingLeft', + 'fontSize', + 'lineHeight', + 'fontFamily', + 'width', + 'fontWeight']; + + return this.each( function() { + + // Elastic only works on textareas + if ( this.type != 'textarea' ) { + return false; + } + + var $textarea = jQuery(this), + $twin = jQuery('
').css({'position': 'absolute','display':'none','word-wrap':'break-word'}), + lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'), + minheight = parseInt($textarea.css('height'),10) || lineHeight*3, + maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE, + goalheight = 0, + i = 0; + + // Opera returns max-height of -1 if not set + if (maxheight < 0) { maxheight = Number.MAX_VALUE; } + + // Append the twin to the DOM + // We are going to meassure the height of this, not the textarea. + $twin.appendTo($textarea.parent()); + + // Copy the essential styles (mimics) from the textarea to the twin + var i = mimics.length; + while(i--){ + $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString())); + } + + + // Sets a given height and overflow state on the textarea + function setHeightAndOverflow(height, overflow){ + curratedHeight = Math.floor(parseInt(height,10)); + if($textarea.height() != curratedHeight){ + $textarea.css({'height': curratedHeight + 'px','overflow':overflow}); + + } + } + + + // This function will update the height of the textarea if necessary + function update() { + + // Get curated content from the textarea. + var textareaContent = $textarea.val().replace(/&/g,'&').replace(/ /g, ' ').replace(/<|>/g, '>').replace(/\n/g, '
'); + + var twinContent = $twin.html(); + + if(textareaContent+' ' != twinContent){ + + // Add an extra white space so new rows are added when you are at the end of a row. + $twin.html(textareaContent+' '); + + // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height + if(Math.abs($twin.height()+lineHeight - $textarea.height()) > 3){ + + var goalheight = $twin.height()+lineHeight; + if(goalheight >= maxheight) { + setHeightAndOverflow(maxheight,'auto'); + } else if(goalheight <= minheight) { + setHeightAndOverflow(minheight,'hidden'); + } else { + setHeightAndOverflow(goalheight,'hidden'); + } + + } + + } + + } + + // Hide scrollbars + $textarea.css({'overflow':'hidden'}); + + // Update textarea size on keyup + $textarea.keyup(function(){ update(); }); + + // And this line is to catch the browser paste event + $textarea.live('input paste',function(e){ setTimeout( update, 250); }); + + // Run update once when elastic is initialized + update(); + + }); + + } + }); +})(jQuery); \ No newline at end of file diff --git a/public/javascripts/jScrollPane.js b/public/javascripts/jScrollPane.js new file mode 100644 index 0000000..fded612 --- /dev/null +++ b/public/javascripts/jScrollPane.js @@ -0,0 +1,702 @@ +/* Copyright (c) 2009 Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com) + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * See http://kelvinluck.com/assets/jquery/jScrollPane/ + * $Id: jScrollPane.js 90 2010-01-25 03:52:10Z kelvin.luck $ + */ + +/** + * Replace the vertical scroll bars on any matched elements with a fancy + * styleable (via CSS) version. With JS disabled the elements will + * gracefully degrade to the browsers own implementation of overflow:auto. + * If the mousewheel plugin has been included on the page then the scrollable areas will also + * respond to the mouse wheel. + * + * @example jQuery(".scroll-pane").jScrollPane(); + * + * @name jScrollPane + * @type jQuery + * @param Object settings hash with options, described below. + * scrollbarWidth - The width of the generated scrollbar in pixels + * scrollbarMargin - The amount of space to leave on the side of the scrollbar in pixels + * wheelSpeed - The speed the pane will scroll in response to the mouse wheel in pixels + * showArrows - Whether to display arrows for the user to scroll with + * arrowSize - The height of the arrow buttons if showArrows=true + * animateTo - Whether to animate when calling scrollTo and scrollBy + * dragMinHeight - The minimum height to allow the drag bar to be + * dragMaxHeight - The maximum height to allow the drag bar to be + * animateInterval - The interval in milliseconds to update an animating scrollPane (default 100) + * animateStep - The amount to divide the remaining scroll distance by when animating (default 3) + * maintainPosition- Whether you want the contents of the scroll pane to maintain it's position when you re-initialise it - so it doesn't scroll as you add more content (default true) + * tabIndex - The tabindex for this jScrollPane to control when it is tabbed to when navigating via keyboard (default 0) + * enableKeyboardNavigation - Whether to allow keyboard scrolling of this jScrollPane when it is focused (default true) + * animateToInternalLinks - Whether the move to an internal link (e.g. when it's focused by tabbing or by a hash change in the URL) should be animated or instant (default false) + * scrollbarOnLeft - Display the scrollbar on the left side? (needs stylesheet changes, see examples.html) + * reinitialiseOnImageLoad - Whether the jScrollPane should automatically re-initialise itself when any contained images are loaded (default false) + * topCapHeight - The height of the "cap" area between the top of the jScrollPane and the top of the track/ buttons + * bottomCapHeight - The height of the "cap" area between the bottom of the jScrollPane and the bottom of the track/ buttons + * observeHash - Whether jScrollPane should attempt to automagically scroll to the correct place when an anchor inside the scrollpane is linked to (default true) + * @return jQuery + * @cat Plugins/jScrollPane + * @author Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com) + */ + +(function($) { + +var allow_load_posts = true; +var loading_posts = false; +function loadPosts() { + if(loading_posts || allow_load_posts == false) { + loading_posts = false; + return; + } + else { + $("#loading_post").show(); + loading_posts = true; + } + + var currently_loaded_posts = $("li.post").size() + $.get("/posts", "list=true&offset=" + currently_loaded_posts, function(data) { + $("#loading_post").hide(); + if(data == " ") + allow_load_posts = false; + else { + $("#loading_post").before(data); + $("#posts").jScrollPane({reload: true}); + } + loading_posts = false; + }); +} + +$.jScrollPane = { + active : [] +}; +$.fn.jScrollPane = function(settings) +{ + settings = $.extend({}, $.fn.jScrollPane.defaults, settings); + + var rf = function() { return false; }; + + return this.each( + function() + { + var $this = $(this); + var paneEle = this; + var currentScrollPosition = 0; + var lastPosition = 0; + var paneWidth; + var paneHeight; + var trackHeight; + var trackOffset = settings.topCapHeight; + var $container; + + if(settings.reload) + $this.parent().unbind('mousewheel').unbind('mousedown.jScrollPane').unbind('keydown.jscrollpane').unbind('keyup.jscrollpane'); + + + if ($(this).parent().is('.jScrollPaneContainer')) { + $container = $(this).parent(); + currentScrollPosition = settings.maintainPosition ? $this.position().top : 0; + var $c = $(this).parent(); + paneWidth = $c.innerWidth(); + paneHeight = $c.outerHeight(); + $('>.jScrollPaneTrack, >.jScrollArrowUp, >.jScrollArrowDown, >.jScrollCap', $c).remove(); + $this.css({'top':0}); + } else { + $this.data('originalStyleTag', $this.attr('style')); + // Switch the element's overflow to hidden to ensure we get the size of the element without the scrollbars [http://plugins.jquery.com/node/1208] + $this.css('overflow', 'hidden'); + this.originalPadding = $this.css('paddingTop') + ' ' + $this.css('paddingRight') + ' ' + $this.css('paddingBottom') + ' ' + $this.css('paddingLeft'); + this.originalSidePaddingTotal = (parseInt($this.css('paddingLeft')) || 0) + (parseInt($this.css('paddingRight')) || 0); + paneWidth = $this.innerWidth(); + paneHeight = $this.innerHeight(); + $container = $('
') + .attr({'className':'jScrollPaneContainer'}) + .css( + { + 'height':paneHeight+'px', + 'width':paneWidth+'px' + } + ); + if (settings.enableKeyboardNavigation) { + $container.attr( + 'tabindex', + settings.tabIndex + ); + } + $this.wrap($container); + $container = $this.parent(); + // deal with text size changes (if the jquery.em plugin is included) + // and re-initialise the scrollPane so the track maintains the + // correct size + $(document).bind( + 'emchange', + function(e, cur, prev) + { + $this.jScrollPane(settings); + } + ); + + } + trackHeight = paneHeight; + + if (settings.reinitialiseOnImageLoad) { + // code inspired by jquery.onImagesLoad: http://plugins.jquery.com/project/onImagesLoad + // except we re-initialise the scroll pane when each image loads so that the scroll pane is always up to size... + // TODO: Do I even need to store it in $.data? Is a local variable here the same since I don't pass the reinitialiseOnImageLoad when I re-initialise? + var $imagesToLoad = $.data(paneEle, 'jScrollPaneImagesToLoad') || $('img', $this); + var loadedImages = []; + + if ($imagesToLoad.length) { + $imagesToLoad.each(function(i, val) { + $(this).bind('load readystatechange', function() { + if($.inArray(i, loadedImages) == -1){ //don't double count images + loadedImages.push(val); //keep a record of images we've seen + $imagesToLoad = $.grep($imagesToLoad, function(n, i) { + return n != val; + }); + $.data(paneEle, 'jScrollPaneImagesToLoad', $imagesToLoad); + var s2 = $.extend(settings, {reinitialiseOnImageLoad:false}); + $this.jScrollPane(s2); // re-initialise + } + }).each(function(i, val) { + if(this.complete || this.complete===undefined) { + //needed for potential cached images + this.src = this.src; + } + }); + }); + }; + } + + var p = this.originalSidePaddingTotal; + var realPaneWidth = paneWidth - settings.scrollbarWidth - settings.scrollbarMargin - p; + + var cssToApply = { + 'height':'auto', + 'width': realPaneWidth + 'px' + } + + if(settings.scrollbarOnLeft) { + cssToApply.paddingLeft = settings.scrollbarMargin + settings.scrollbarWidth + 'px'; + } else { + cssToApply.paddingRight = settings.scrollbarMargin + 'px'; + } + + $this.css(cssToApply); + + var contentHeight = $this.outerHeight(); + var percentInView = paneHeight / contentHeight; + + var isScrollable = percentInView < .99; + $container[isScrollable ? 'addClass' : 'removeClass']('jScrollPaneScrollable'); + + if (isScrollable) { + $container.append( + $('
').addClass('jScrollCap jScrollCapTop').css({height:settings.topCapHeight}), + $('
').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth+'px'}).append( + $('
').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth+'px'}).append( + $('
').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth+'px'}), + $('
').attr({'className':'jScrollPaneDragBottom'}).css({'width':settings.scrollbarWidth+'px'}) + ) + ), + $('
').addClass('jScrollCap jScrollCapBottom').css({height:settings.bottomCapHeight}) + ); + + var $track = $('>.jScrollPaneTrack', $container); + var $drag = $('>.jScrollPaneTrack .jScrollPaneDrag', $container); + + + var currentArrowDirection; + var currentArrowTimerArr = [];// Array is used to store timers since they can stack up when dealing with keyboard events. This ensures all timers are cleaned up in the end, preventing an acceleration bug. + var currentArrowInc; + var whileArrowButtonDown = function() + { + if (currentArrowInc > 4 || currentArrowInc % 4 == 0) { + positionDrag(dragPosition + currentArrowDirection * mouseWheelMultiplier); + } + currentArrowInc++; + }; + + if (settings.enableKeyboardNavigation) { + $container.bind( + 'keydown.jscrollpane', + function(e) + { + switch (e.keyCode) { + case 38: //up + currentArrowDirection = -1; + currentArrowInc = 0; + whileArrowButtonDown(); + currentArrowTimerArr[currentArrowTimerArr.length] = setInterval(whileArrowButtonDown, 100); + return false; + case 40: //down + currentArrowDirection = 1; + currentArrowInc = 0; + whileArrowButtonDown(); + currentArrowTimerArr[currentArrowTimerArr.length] = setInterval(whileArrowButtonDown, 100); + return false; + case 33: // page up + case 34: // page down + // TODO + return false; + default: + } + } + ).bind( + 'keyup.jscrollpane', + function(e) + { + if (e.keyCode == 38 || e.keyCode == 40) { + for (var i = 0; i < currentArrowTimerArr.length; i++) { + clearInterval(currentArrowTimerArr[i]); + } + return false; + } + } + ); + } + + if (settings.showArrows) { + + var currentArrowButton; + var currentArrowInterval; + + var onArrowMouseUp = function(event) + { + $('html').unbind('mouseup', onArrowMouseUp); + currentArrowButton.removeClass('jScrollActiveArrowButton'); + clearInterval(currentArrowInterval); + }; + var onArrowMouseDown = function() { + $('html').bind('mouseup', onArrowMouseUp); + currentArrowButton.addClass('jScrollActiveArrowButton'); + currentArrowInc = 0; + whileArrowButtonDown(); + currentArrowInterval = setInterval(whileArrowButtonDown, 100); + }; + $container + .append( + $('') + .attr( + { + 'href':'javascript:;', + 'className':'jScrollArrowUp', + 'tabindex':-1 + } + ) + .css( + { + 'width':settings.scrollbarWidth+'px', + 'top':settings.topCapHeight + 'px' + } + ) + .html('Scroll up') + .bind('mousedown', function() + { + currentArrowButton = $(this); + currentArrowDirection = -1; + onArrowMouseDown(); + this.blur(); + return false; + }) + .bind('click', rf), + $('') + .attr( + { + 'href':'javascript:;', + 'className':'jScrollArrowDown', + 'tabindex':-1 + } + ) + .css( + { + 'width':settings.scrollbarWidth+'px', + 'bottom':settings.bottomCapHeight + 'px' + } + ) + .html('Scroll down') + .bind('mousedown', function() + { + currentArrowButton = $(this); + currentArrowDirection = 1; + onArrowMouseDown(); + this.blur(); + return false; + }) + .bind('click', rf) + ); + var $upArrow = $('>.jScrollArrowUp', $container); + var $downArrow = $('>.jScrollArrowDown', $container); + } + + if (settings.arrowSize) { + trackHeight = paneHeight - settings.arrowSize - settings.arrowSize; + trackOffset += settings.arrowSize; + } else if ($upArrow) { + var topArrowHeight = $upArrow.height(); + settings.arrowSize = topArrowHeight; + trackHeight = paneHeight - topArrowHeight - $downArrow.height(); + trackOffset += topArrowHeight; + } + trackHeight -= settings.topCapHeight + settings.bottomCapHeight; + $track.css({'height': trackHeight+'px', top:trackOffset+'px'}) + + var $pane = $(this).css({'position':'absolute', 'overflow':'visible'}); + + var currentOffset; + var maxY; + var mouseWheelMultiplier; + // store this in a seperate variable so we can keep track more accurately than just updating the css property.. + var dragPosition = 0; + var dragMiddle = percentInView*paneHeight/2; + + // pos function borrowed from tooltip plugin and adapted... + var getPos = function (event, c) { + var p = c == 'X' ? 'Left' : 'Top'; + return event['page' + c] || (event['client' + c] + (document.documentElement['scroll' + p] || document.body['scroll' + p])) || 0; + }; + + var ignoreNativeDrag = function() { return false; }; + + var initDrag = function() + { + ceaseAnimation(); + currentOffset = $drag.offset(false); + currentOffset.top -= dragPosition; + maxY = trackHeight - $drag[0].offsetHeight; + mouseWheelMultiplier = 2 * settings.wheelSpeed * maxY / contentHeight; + }; + + var onStartDrag = function(event) + { + initDrag(); + dragMiddle = getPos(event, 'Y') - dragPosition - currentOffset.top; + $('html').bind('mouseup', onStopDrag).bind('mousemove', updateScroll); + if ($.browser.msie) { + $('html').bind('dragstart', ignoreNativeDrag).bind('selectstart', ignoreNativeDrag); + } + return false; + }; + var onStopDrag = function() + { + $('html').unbind('mouseup', onStopDrag).unbind('mousemove', updateScroll); + dragMiddle = percentInView*paneHeight/2; + if ($.browser.msie) { + $('html').unbind('dragstart', ignoreNativeDrag).unbind('selectstart', ignoreNativeDrag); + } + }; + var positionDrag = function(destY) + { + $container.scrollTop(0); + destY = destY < 0 ? 0 : (destY > maxY ? maxY : destY); + dragPosition = destY; + $drag.css({'top':destY+'px'}); + var p = destY / maxY; + $this.data('jScrollPanePosition', (paneHeight-contentHeight)*-p); + $pane.css({'top':((paneHeight-contentHeight)*p) + 'px'}); + $this.trigger('scroll'); + if (settings.showArrows) { + $upArrow[destY == 0 ? 'addClass' : 'removeClass']('disabled'); + $downArrow[destY == maxY ? 'addClass' : 'removeClass']('disabled'); + } + if(p == 1 && p != lastPosition) { + loadPosts(); + } + lastPosition = p; + }; + var updateScroll = function(e) + { + positionDrag(getPos(e, 'Y') - currentOffset.top - dragMiddle); + }; + + var dragH = Math.max(Math.min(percentInView*(paneHeight-settings.arrowSize*2), settings.dragMaxHeight), settings.dragMinHeight); + + $drag.css( + {'height':dragH+'px'} + ).bind('mousedown', onStartDrag); + + var trackScrollInterval; + var trackScrollInc; + var trackScrollMousePos; + var doTrackScroll = function() + { + if (trackScrollInc > 8 || trackScrollInc%4==0) { + positionDrag((dragPosition - ((dragPosition - trackScrollMousePos) / 2))); + } + trackScrollInc ++; + }; + var onStopTrackClick = function() + { + clearInterval(trackScrollInterval); + $('html').unbind('mouseup', onStopTrackClick).unbind('mousemove', onTrackMouseMove); + }; + var onTrackMouseMove = function(event) + { + trackScrollMousePos = getPos(event, 'Y') - currentOffset.top - dragMiddle; + }; + var onTrackClick = function(event) + { + initDrag(); + onTrackMouseMove(event); + trackScrollInc = 0; + $('html').bind('mouseup', onStopTrackClick).bind('mousemove', onTrackMouseMove); + trackScrollInterval = setInterval(doTrackScroll, 100); + doTrackScroll(); + return false; + }; + + $track.bind('mousedown', onTrackClick); + + $container.bind( + 'mousewheel', + function (event, delta) { + delta = delta || (event.wheelDelta ? event.wheelDelta / 120 : (event.detail) ? +-event.detail/3 : 0); + initDrag(); + ceaseAnimation(); + var d = dragPosition; + positionDrag(dragPosition - delta * mouseWheelMultiplier); + var dragOccured = d != dragPosition; + return !dragOccured; + } + ); + + var _animateToPosition; + var _animateToInterval; + function animateToPosition() + { + var diff = (_animateToPosition - dragPosition) / settings.animateStep; + if (diff > 1 || diff < -1) { + positionDrag(dragPosition + diff); + } else { + positionDrag(_animateToPosition); + ceaseAnimation(); + } + } + var ceaseAnimation = function() + { + if (_animateToInterval) { + clearInterval(_animateToInterval); + delete _animateToPosition; + } + }; + var scrollTo = function(pos, preventAni) + { + if (typeof pos == "string") { + // Legal hash values aren't necessarily legal jQuery selectors so we need to catch any + // errors from the lookup... + try { + $e = $(pos, $this); + } catch (err) { + return; + } + if (!$e.length) return; + pos = $e.offset().top - $this.offset().top; + } + ceaseAnimation(); + var maxScroll = contentHeight - paneHeight; + pos = pos > maxScroll ? maxScroll : pos; + $this.data('jScrollPaneMaxScroll', maxScroll); + var destDragPosition = pos/maxScroll * maxY; + if (preventAni || !settings.animateTo) { + positionDrag(destDragPosition); + } else { + $container.scrollTop(0); + _animateToPosition = destDragPosition; + _animateToInterval = setInterval(animateToPosition, settings.animateInterval); + } + }; + $this[0].scrollTo = scrollTo; + + $this[0].scrollBy = function(delta) + { + var currentPos = -parseInt($pane.css('top')) || 0; + scrollTo(currentPos + delta); + }; + + initDrag(); + + scrollTo(-currentScrollPosition, true); + + // Deal with it when the user tabs to a link or form element within this scrollpane + $('*', this).bind( + 'focus', + function(event) + { + var $e = $(this); + + // loop through parents adding the offset top of any elements that are relatively positioned between + // the focused element and the jScrollPaneContainer so we can get the true distance from the top + // of the focused element to the top of the scrollpane... + var eleTop = 0; + + while ($e[0] != $this[0]) { + eleTop += $e.position().top; + $e = $e.offsetParent(); + } + + var viewportTop = -parseInt($pane.css('top')) || 0; + var maxVisibleEleTop = viewportTop + paneHeight; + var eleInView = eleTop > viewportTop && eleTop < maxVisibleEleTop; + if (!eleInView) { + var destPos = eleTop - settings.scrollbarMargin; + if (eleTop > viewportTop) { // element is below viewport - scroll so it is at bottom. + destPos += $(this).height() + 15 + settings.scrollbarMargin - paneHeight; + } + scrollTo(destPos); + } + } + ) + + + if (settings.observeHash) { + if (location.hash && location.hash.length > 1) { + setTimeout(function(){ + scrollTo(location.hash); + }, $.browser.safari ? 100 : 0); + } + + // use event delegation to listen for all clicks on links and hijack them if they are links to + // anchors within our content... + $(document).bind('click', function(e){ + $target = $(e.target); + if ($target.is('a')) { + var h = $target.attr('href'); + if (h && h.substr(0, 1) == '#' && h.length > 1) { + setTimeout(function(){ + scrollTo(h, !settings.animateToInternalLinks); + }, $.browser.safari ? 100 : 0); + } + } + }); + } + + // Deal with dragging and selecting text to make the scrollpane scroll... + function onSelectScrollMouseDown(e) + { + $(document).bind('mousemove.jScrollPaneDragging', onTextSelectionScrollMouseMove); + $(document).bind('mouseup.jScrollPaneDragging', onSelectScrollMouseUp); + + } + + var textDragDistanceAway; + var textSelectionInterval; + + function onTextSelectionInterval() + { + direction = textDragDistanceAway < 0 ? -1 : 1; + $this[0].scrollBy(textDragDistanceAway / 2); + } + + function clearTextSelectionInterval() + { + if (textSelectionInterval) { + clearInterval(textSelectionInterval); + textSelectionInterval = undefined; + } + } + + function onTextSelectionScrollMouseMove(e) + { + var offset = $this.parent().offset().top; + var maxOffset = offset + paneHeight; + var mouseOffset = getPos(e, 'Y'); + textDragDistanceAway = mouseOffset < offset ? mouseOffset - offset : (mouseOffset > maxOffset ? mouseOffset - maxOffset : 0); + if (textDragDistanceAway == 0) { + clearTextSelectionInterval(); + } else { + if (!textSelectionInterval) { + textSelectionInterval = setInterval(onTextSelectionInterval, 100); + } + } + } + + function onSelectScrollMouseUp(e) + { + $(document) + .unbind('mousemove.jScrollPaneDragging') + .unbind('mouseup.jScrollPaneDragging'); + clearTextSelectionInterval(); + } + + $container.bind('mousedown.jScrollPane', onSelectScrollMouseDown); + + + $.jScrollPane.active.push($this[0]); + + } else { + $this.css( + { + 'height':paneHeight+'px', + 'width':paneWidth-this.originalSidePaddingTotal+'px', + 'padding':this.originalPadding + } + ); + $this[0].scrollTo = $this[0].scrollBy = function() {}; + // clean up listeners + $this.parent().unbind('mousewheel').unbind('mousedown.jScrollPane').unbind('keydown.jscrollpane').unbind('keyup.jscrollpane'); + } + + } + ) +}; + +$.fn.jScrollPaneRemove = function() +{ + $(this).each(function() + { + $this = $(this); + var $c = $this.parent(); + if ($c.is('.jScrollPaneContainer')) { + $this.css( + { + 'top':'', + 'height':'', + 'width':'', + 'padding':'', + 'overflow':'', + 'position':'' + } + ); + $this.attr('style', $this.data('originalStyleTag')); + $c.after($this).remove(); + } + }); +} + +$.fn.jScrollPane.defaults = { + scrollbarWidth : 11, + scrollbarMargin : 5, + wheelSpeed : 18, + showArrows : false, + arrowSize : 0, + animateTo : false, + dragMinHeight : 1, + dragMaxHeight : 99999, + animateInterval : 100, + animateStep: 3, + maintainPosition: true, + scrollbarOnLeft: false, + reinitialiseOnImageLoad: false, + reload: false, + tabIndex : 0, + enableKeyboardNavigation: true, + animateToInternalLinks: false, + topCapHeight: 0, + bottomCapHeight: 0, + observeHash: true +}; + +// clean up the scrollTo expandos +$(window) + .bind('unload', function() { + var els = $.jScrollPane.active; + for (var i=0; i