Sequenzia/lib/assets/javascripts/moe-legacy/browser-thumb-view.js
2013-10-26 18:06:58 -05:00

1194 lines
40 KiB
JavaScript
Executable File

/*
* Handle the thumbnail view, and navigation for the main view.
*
* Handle a large number (thousands) of entries cleanly. Thumbnail nodes are created
* as needed, and destroyed when they scroll off screen. This gives us constant
* startup time, loads thumbnails on demand, allows preloading thumbnails in advance
* by creating more nodes in advance, and keeps memory usage constant.
*/
ThumbnailView = function(container, view)
{
this.container = container;
this.view = view;
this.post_ids = [];
this.post_frames = [];
this.expanded_post_idx = null;
this.centered_post_idx = null;
this.centered_post_offset = 0;
this.last_mouse_x = 0;
this.last_mouse_y = 0;
this.thumb_container_shown = true;
this.allow_wrapping = true;
this.thumb_preload_container = new PreloadContainer();
this.unused_thumb_pool = [];
/* The [first, end) range of posts that are currently inside .post-browser-posts. */
this.posts_populated = [0, 0];
document.on("DOMMouseScroll", this.document_mouse_wheel_event.bindAsEventListener(this));
document.on("mousewheel", this.document_mouse_wheel_event.bindAsEventListener(this));
document.on("viewer:displayed-image-loaded", this.displayed_image_loaded_event.bindAsEventListener(this));
document.on("viewer:set-active-post", function(e) {
var post_id_and_frame = [e.memo.post_id, e.memo.post_frame];
this.set_active_post(post_id_and_frame, e.memo.lazy, e.memo.center_thumbs);
}.bindAsEventListener(this));
document.on("viewer:show-next-post", function(e) { this.show_next_post(e.memo.prev); }.bindAsEventListener(this));
document.on("viewer:scroll", function(e) { this.scroll(e.memo.left); }.bindAsEventListener(this));
document.on("viewer:set-thumb-bar", function(e) {
if(e.memo.toggle)
this.show_thumb_bar(!this.thumb_container_shown);
else
this.show_thumb_bar(e.memo.set);
}.bindAsEventListener(this));
document.on("viewer:loaded-posts", this.loaded_posts_event.bindAsEventListener(this));
this.hashchange_post_id = this.hashchange_post_id.bind(this);
UrlHash.observe("post-id", this.hashchange_post_id);
UrlHash.observe("post-frame", this.hashchange_post_id);
new DragElement(this.container, { ondrag: this.container_ondrag.bind(this) });
Element.on(window, "resize", this.window_resize_event.bindAsEventListener(this));
this.container.on("mousemove", this.container_mousemove_event.bindAsEventListener(this));
this.container.on("mouseover", this.container_mouseover_event.bindAsEventListener(this));
this.container.on("mouseout", this.container_mouseout_event.bindAsEventListener(this));
this.container.on("click", this.container_click_event.bindAsEventListener(this));
this.container.on("dblclick", ".post-thumb,.browser-thumb-hover-overlay",
this.container_dblclick_event.bindAsEventListener(this));
/* Prevent the default behavior of left-clicking on the expanded thumbnail overlay. It's
* handled by container_click_event. */
this.container.down(".browser-thumb-hover-overlay").on("click", function(event) {
if(event.isLeftClick())
event.preventDefault();
}.bindAsEventListener(this));
/*
* For Android browsers, we're set to 150 DPI, which (in theory) scales us to a consistent UI size
* based on the screen DPI. This means that we can determine the physical screen size from the
* window resolution: 150x150 is 1"x1". Set a thumbnail scale based on this. On a 320x480 HVGA
* phone screen the thumbnails are about 2x too big, so set thumb_scale to 0.5.
*
* For iOS browsers, there's no way to set the viewport based on the DPI, so it's fixed at 1x.
* (Note that on Retina screens the browser lies: even though we request 1x, it's actually at
* 0.5x and our screen dimensions work as if we're on the lower-res iPhone screen. We can mostly
* ignore this.) CSS inches aren't implemented (the DPI is fixed at 96), so that doesn't help us.
* Fall back on special-casing individual iOS devices.
*/
this.config = { };
if(navigator.userAgent.indexOf("iPad") != -1)
{
this.config.thumb_scale = 1.0;
}
else if(navigator.userAgent.indexOf("iPhone") != -1 || navigator.userAgent.indexOf("iPod") != -1)
{
this.config.thumb_scale = 0.5;
}
else if(navigator.userAgent.indexOf("Android") != -1)
{
/* We may be in landscape or portrait; use out the narrower dimension. */
var width = Math.min(window.innerWidth, window.innerHeight);
/* Scale a 320-width screen to 0.5, up to 1.0 for a 640-width screen. Remember
* that this width is already scaled by the DPI of the screen due to target-densityDpi,
* so these numbers aren't actually real pixels, and this scales based on the DPI
* and size of the screen rather than the pixel count. */
this.config.thumb_scale = scale(width, 320, 640, 0.5, 1.0);
debug("Unclamped thumb scale: " + this.config.thumb_scale);
/* Clamp to [0.5,1.0]. */
this.config.thumb_scale = Math.min(this.config.thumb_scale, 1.0);
this.config.thumb_scale = Math.max(this.config.thumb_scale, 0.5);
debug("startup, window size: " + window.innerWidth + "x" + window.innerHeight);
}
else
{
/* Unknown device, or not a mobile device. */
this.config.thumb_scale = 1.0;
}
debug("Thumb scale: " + this.config.thumb_scale);
this.config_changed();
/* Send the initial viewer:thumb-bar-changed event. */
this.thumb_container_shown = false;
this.show_thumb_bar(true);
}
ThumbnailView.prototype.window_resize_event = function(e)
{
if(e.stopped)
return;
if(this.thumb_container_shown)
this.center_on_post_for_scroll(this.centered_post_idx);
}
/* Show the given posts. If extending is true, post_ids are meant to extend a previous
* search; attempt to continue where we left off. */
ThumbnailView.prototype.loaded_posts_event = function(event)
{
var post_ids = event.memo.post_ids;
var old_post_ids = this.post_ids;
var old_centered_post_idx = this.centered_post_idx;
this.remove_all_posts();
/* Filter blacklisted posts. */
post_ids = post_ids.reject(Post.is_blacklisted);
this.post_ids = [];
this.post_frames = [];
for(var i = 0; i < post_ids.length; ++i)
{
var post_id = post_ids[i];
var post = Post.posts.get(post_id);
if(post.frames.length > 0)
{
for(var frame_idx = 0; frame_idx < post.frames.length; ++frame_idx)
{
this.post_ids.push(post_id);
this.post_frames.push(frame_idx);
}
}
else
{
this.post_ids.push(post_id);
this.post_frames.push(-1);
}
}
this.allow_wrapping = !event.memo.can_be_extended_further;
/* Show the results box or "no results". Do this before updating the results box to make sure
* the results box isn't hidden when we update, which will make offsetLeft values inside it zero
* and break things. If the reason we have no posts is because we didn't do a search at all,
* don't show no-results. */
this.container.down(".post-browser-no-results").show(event.memo.tags != null && this.post_ids.length == 0);
this.container.down(".post-browser-posts").show(this.post_ids.length != 0);
if(event.memo.extending)
{
/*
* We're extending a previous search with more posts. The new post list we get may
* not line up with the old one: the post we're focused on may no longer be in the
* search, or may be at a different index.
*
* Find a nearby post in the new results. Start searching at the post we're already
* centered on. If that doesn't match, move outwards from there. Only look forward
* a little bit, or we may match a post that was never seen and jump forward too far
* in the results.
*/
var post_id_search_order = sort_array_by_distance(old_post_ids.slice(0, old_centered_post_idx+3), old_centered_post_idx);
var initial_post_id = null;
for(var i = 0; i < post_id_search_order.length; ++i)
{
var post_id_to_search = post_id_search_order[i];
var post = Post.posts.get(post_id_to_search);
if(post != null)
{
initial_post_id = post.id;
break;
}
}
debug("center-on-" + initial_post_id);
/* If we didn't find anything that matched, go back to the start. */
if(initial_post_id == null)
{
this.centered_post_offset = 0;
initial_post_id = new_post_ids[0];
}
var center_on_post_idx = this.post_ids.indexOf(initial_post_id);
this.center_on_post_for_scroll(center_on_post_idx);
}
else
{
/*
* A new search has completed.
*
* results_mode can be one of the following:
*
* "center-on-first"
* Don't change the active post. Center the results on the first result. This is used
* when performing a search by clicking on a tag, where we don't want to center on the
* post we're on (since it'll put us at some random spot in the results when the user
* probably wants to browse from the beginning), and we don't want to change the displayed
* post either.
*
* "center-on-current"
* Don't change the active post. Center the results on the existing current item,
* if possible. This is used when we want to show a new search without disrupting the
* shown post, such as the "child posts" link in post info, and when loading the initial
* URL hash when we start up.
*
* "jump-to-first"
* Set the active post to the first result, and center on it. This is used after making
* a search in the tags box.
*/
var results_mode = event.memo.load_options.results_mode || "center-on-current";
var initial_post_id_and_frame;
if(results_mode == "center-on-first" || results_mode == "jump-to-first")
initial_post_id_and_frame = [this.post_ids[0], this.post_frames[0]];
else
initial_post_id_and_frame = this.get_current_post_id_and_frame();
var center_on_post_idx = this.get_post_idx(initial_post_id_and_frame);
if(center_on_post_idx == null)
center_on_post_idx = 0;
this.centered_post_offset = 0;
this.center_on_post_for_scroll(center_on_post_idx);
/* If no post is currently displayed and we just completed a search, set the current post.
* This happens when first initializing; we wait for the first search to complete to retrieve
* info about the post we're starting on, instead of making a separate query. */
if(results_mode == "jump-to-first" || this.view.wanted_post_id == null)
this.set_active_post(initial_post_id_and_frame, false, false, true);
}
if(event.memo.tags == null)
{
/* If tags is null then no search has been done, which means we're on a URL
* with a post ID and no search, eg. "/post/browse#12345". Hide the thumb
* bar, so we'll just show the post. */
this.show_thumb_bar(false);
}
}
ThumbnailView.prototype.container_ondrag = function(e)
{
this.centered_post_offset -= e.dX;
this.center_on_post_for_scroll(this.centered_post_idx);
}
ThumbnailView.prototype.container_mouseover_event = function(event)
{
var li = $(event.target);
if(!li.hasClassName(".post-thumb"))
li = li.up(".post-thumb");
if(li)
this.expand_post(li.post_idx);
}
ThumbnailView.prototype.container_mouseout_event = function(event)
{
/* If the mouse is leaving the hover overlay, hide it. */
var target = $(event.target);
if(!target.hasClassName(".browser-thumb-hover-overlay"))
target = target.up(".browser-thumb-hover-overlay");
if(target)
this.expand_post(null);
}
ThumbnailView.prototype.hashchange_post_id = function()
{
var post_id_and_frame = this.get_current_post_id_and_frame();
if(post_id_and_frame[0] == null)
return;
/* If we're already displaying this post, ignore the hashchange. Don't center on the
* post if this is just a side-effect of clicking a post, rather than the user actually
* changing the hash. */
var post_id = post_id_and_frame[0];
var post_frame = post_id_and_frame[1];
if(post_id == this.view.displayed_post_id &&
post_frame == this.view.displayed_post_frame)
{
// debug("ignored-hashchange");
return;
}
var new_post_idx = this.get_post_idx(post_id_and_frame);
this.centered_post_offset = 0;
this.center_on_post_for_scroll(new_post_idx);
this.set_active_post(post_id_and_frame, false, false, true);
}
/* Search for the given post ID and frame in the current search results, and return its
* index. If the given post isn't in post_ids, return null. */
ThumbnailView.prototype.get_post_idx = function(post_id_and_frame)
{
var post_id = post_id_and_frame[0];
var post_frame = post_id_and_frame[1];
var post_idx = this.post_ids.indexOf(post_id);
if(post_idx == -1)
return null;
if(post_frame == -1)
return post_idx;
/* A post-frame is specified. Search for a matching post-id and post-frame. We assume
* here that all frames for a post are grouped together in post_ids. */
var post_frame_idx = post_idx;
while(post_frame_idx < this.post_ids.length && this.post_ids[post_frame_idx] == post_id)
{
if(this.post_frames[post_frame_idx] == post_frame)
return post_frame_idx;
++post_frame_idx;
}
/* We found a matching post, but not a matching frame. Return the post. */
return post_idx;
}
/* Return the post and frame that's currently being displayed in the main view, based
* on the URL hash. If no post is displayed and no search results are available,
* return [null, null]. */
ThumbnailView.prototype.get_current_post_id_and_frame = function()
{
var post_id = UrlHash.get("post-id");
if(post_id == null)
{
if(this.post_ids.length == 0)
return [null, null];
else
return [this.post_ids[0], this.post_frames[0]];
}
post_id = parseInt(post_id);
var post_frame = UrlHash.get("post-frame");
// If no frame is set, attempt to resolve the post_frame we'll display, if the post data
// is already loaded. Otherwise, post_frame will remain null.
if(post_frame == null)
post_frame = this.view.get_default_post_frame(post_id);
return [post_id, post_frame];
}
/* Track the mouse cursor when it's within the container. */
ThumbnailView.prototype.container_mousemove_event = function(e)
{
var x = e.pointerX() - document.documentElement.scrollLeft;
var y = e.pointerY() - document.documentElement.scrollTop;
this.last_mouse_x = x;
this.last_mouse_y = y;
}
ThumbnailView.prototype.document_mouse_wheel_event = function(event)
{
event.stop();
var val;
if(event.wheelDelta)
{
val = event.wheelDelta;
} else if (event.detail) {
val = -event.detail;
}
if(this.thumb_container_shown)
document.fire("viewer:scroll", { left: val >= 0 });
else
document.fire("viewer:show-next-post", { prev: val >= 0 });
}
/* Set the post that's shown in the view. The thumbs will be centered on the post
* if center_thumbs is true. See BrowserView.prototype.set_post for an explanation
* of no_hash_change. */
ThumbnailView.prototype.set_active_post = function(post_id_and_frame, lazy, center_thumbs, no_hash_change, replace_history)
{
/* If no post is specified, do nothing. This will happen if a search returns
* no results. */
if(post_id_and_frame[0] == null)
return;
this.view.set_post(post_id_and_frame[0], post_id_and_frame[1], lazy, no_hash_change, replace_history);
if(center_thumbs)
{
var post_idx = this.get_post_idx(post_id_and_frame);
this.centered_post_offset = 0;
this.center_on_post_for_scroll(post_idx);
}
}
ThumbnailView.prototype.set_active_post_idx = function(post_idx, lazy, center_thumbs, no_hash_change, replace_history)
{
if(post_idx == null)
return;
var post_id = this.post_ids[post_idx];
var post_frame = this.post_frames[post_idx];
this.set_active_post([post_id, post_frame], lazy, center_thumbs, no_hash_change, replace_history);
}
ThumbnailView.prototype.show_next_post = function(prev)
{
if(this.post_ids.length == 0)
return;
var current_idx = this.get_post_idx([this.view.wanted_post_id, this.view.wanted_post_frame]);
/* If the displayed post isn't in the thumbnails and we're changing posts, start
* at the beginning. */
if(current_idx == null)
current_idx = 0;
var add = prev? -1:+1;
if(this.post_frames[current_idx] != this.view.wanted_post_frame && add == +1)
{
/*
* We didn't find an exact match for the frame we're displaying, which usually means
* we viewed a post frame, and then the user changed the view to the main post, and
* the main post isn't in the thumbnails.
*
* It's strange to be on the main post, to hit pgdn, and to end up on the second frame
* because the nearest match was the first frame. Instead, we should end up on the first
* frame. To do that, just don't add anything to the index.
*/
debug("Snapped the display to the nearest frame");
if(add == +1)
add = 0;
}
var new_idx = current_idx;
new_idx += add;
new_idx += this.post_ids.length;
new_idx %= this.post_ids.length;
var wrapped = (prev && new_idx > current_idx) || (!prev && new_idx < current_idx);
if(wrapped)
{
/* Only allow wrapping over the edge if we've already expanded the results. */
if(!this.allow_wrapping)
return;
if(!this.thumb_container_shown && prev)
notice("Continued from the end");
else if(!this.thumb_container_shown && !prev)
notice("Starting over from the beginning");
}
this.set_active_post_idx(new_idx, true, true, false, true);
}
/* Scroll the thumbnail view left or right. Don't change the displayed post. */
ThumbnailView.prototype.scroll = function(left)
{
/* There's no point in scrolling the list if it's not visible. */
if(!this.thumb_container_shown)
return;
var new_idx = this.centered_post_idx;
/* If we're not centered on the post, and we're moving towards the center,
* don't jump past the post. */
if(this.centered_post_offset > 0 && left)
;
else if(this.centered_post_offset < 0 && !left)
;
else
new_idx += (left? -1:+1);
// Snap to the nearest post.
this.centered_post_offset = 0;
/* Wrap the new index. */
if(new_idx < 0)
{
/* Only allow scrolling over the left edge if we've already expanded the results. */
if(!this.allow_wrapping)
new_idx = 0;
else
new_idx = this.post_ids.length - 1;
}
else if(new_idx >= this.post_ids.length)
{
if(!this.allow_wrapping)
new_idx = this.post_ids.length - 1;
else
new_idx = 0;
}
this.center_on_post_for_scroll(new_idx);
}
/* Hide the hovered post, if any, call center_on_post(post_idx), then hover over the correct post again. */
ThumbnailView.prototype.center_on_post_for_scroll = function(post_idx)
{
if(this.thumb_container_shown)
this.expand_post(null);
this.center_on_post(post_idx);
/*
* Now that we've re-centered, we need to expand the correct image. Usually, we can just
* wait for the mouseover event to fire, since we hid the expanded thumb overlay and the
* image underneith it is now under the mouse. However, browsers are badly broken here.
* Opera doesn't fire mouseover events when the element under the cursor is hidden. FF
* fires the mouseover on hide, but misses the mouseout when the new overlay is shown, so
* the next time it's hidden mouseover events are lost.
*
* Explicitly figure out which item we're hovering over and expand it.
*/
if(this.thumb_container_shown)
{
var element = document.elementFromPoint(this.last_mouse_x, this.last_mouse_y);
element = $(element);
if(element)
{
var li = element.up(".post-thumb");
if(li)
this.expand_post(li.post_idx);
}
}
}
ThumbnailView.prototype.remove_post = function(right)
{
if(this.posts_populated[0] == this.posts_populated[1])
return false; /* none to remove */
var node = this.container.down(".post-browser-posts");
if(right)
{
--this.posts_populated[1];
var node_to_remove = node.lastChild;
}
else
{
++this.posts_populated[0];
var node_to_remove = node.firstChild;
}
/* Remove the thumbnail that's no longer visible, and put it in unused_thumb_pool
* so we can reuse it later. This won't grow out of control, since we'll always use
* an item from the pool if available rather than creating a new one. */
var item = node.removeChild(node_to_remove);
this.unused_thumb_pool.push(item);
return true;
}
ThumbnailView.prototype.remove_all_posts = function()
{
while(this.remove_post(true))
;
}
/* Add the next thumbnail to the left or right side. */
ThumbnailView.prototype.add_post_to_display = function(right)
{
var node = this.container.down(".post-browser-posts");
if(right)
{
var post_idx_to_populate = this.posts_populated[1];
if(post_idx_to_populate == this.post_ids.length)
return false;
++this.posts_populated[1];
var thumb = this.create_thumb(post_idx_to_populate);
node.insertBefore(thumb, null);
}
else
{
if(this.posts_populated[0] == 0)
return false;
--this.posts_populated[0];
var post_idx_to_populate = this.posts_populated[0];
var thumb = this.create_thumb(post_idx_to_populate);
node.insertBefore(thumb, node.firstChild);
}
return true;
}
/* Fill the container so post_idx is visible. */
ThumbnailView.prototype.populate_post = function(post_idx)
{
if(this.is_post_idx_shown(post_idx))
return;
/* If post_idx is on the immediate border of what's already displayed, add it incrementally, and
* we'll cull extra posts later. Otherwise, clear all of the posts and populate from scratch. */
if(post_idx == this.posts_populated[1])
{
this.add_post_to_display(true);
return;
}
else if(post_idx == this.posts_populated[0])
{
this.add_post_to_display(false);
return;
}
/* post_idx isn't on the boundary, so we're jumping posts rather than scrolling.
* Clear the container and start over. */
this.remove_all_posts();
var node = this.container.down(".post-browser-posts");
var thumb = this.create_thumb(post_idx);
node.appendChild(thumb);
this.posts_populated[0] = post_idx;
this.posts_populated[1] = post_idx + 1;
}
ThumbnailView.prototype.is_post_idx_shown = function(post_idx)
{
if(post_idx >= this.posts_populated[1])
return false;
return post_idx >= this.posts_populated[0];
}
/* Return the total width of all thumbs to the left or right of post_idx, not
* including itself. */
ThumbnailView.prototype.get_width_adjacent_to_post = function(post_idx, right)
{
var post = $("p" + post_idx);
if(right)
{
var rightmost_node = post.parentNode.lastChild;
if(rightmost_node == post)
return 0;
var right_edge = rightmost_node.offsetLeft + rightmost_node.offsetWidth;
var center_post_right_edge = post.offsetLeft + post.offsetWidth;
return right_edge - center_post_right_edge
}
else
{
return post.offsetLeft;
}
}
/* Center the thumbnail strip on post_idx. If post_id isn't in the display, do nothing.
* Fire viewer:need-more-thumbs if we're scrolling near the edge of the list. */
ThumbnailView.prototype.center_on_post = function(post_idx)
{
if(!this.post_ids)
{
debug("unexpected: center_on_post has no post_ids");
return;
}
var post_id = this.post_ids[post_idx];
if(Post.posts.get(post_id) == null)
return;
if(post_idx > this.post_ids.length*3/4)
{
/* We're coming near the end of the loaded posts, so load more. We may be currently
* in the middle of setting up the post; defer this, so we finish what we're doing first. */
(function() {
document.fire("viewer:need-more-thumbs", { view: this });
}).defer();
}
this.centered_post_idx = post_idx;
/* If we're not expanded, we can't figure out how to center it since we'll have no width.
* Also, don't cause thumbnails to be loaded if we're hidden. Just set centered_post_idx,
* and we'll come back here when we're displayed. */
if(!this.thumb_container_shown)
return;
/* If centered_post_offset is high enough to put the actual center post somewhere else,
* adjust it towards zero and change centered_post_idx. This keeps centered_post_idx
* pointing at the item that's actually centered. */
while(1)
{
var post = $("p" + this.centered_post_idx);
if(!post)
break;
var pos = post.offsetWidth/2 + this.centered_post_offset;
if(pos >= 0 && pos < post.offsetWidth)
break;
var next_post_idx = this.centered_post_idx + (this.centered_post_offset > 0? +1:-1);
var next_post = $("p" + next_post_idx);
if(next_post == null)
break;
var current_post_center = post.offsetLeft + post.offsetWidth/2;
var next_post_center = next_post.offsetLeft + next_post.offsetWidth/2;
var distance = next_post_center - current_post_center;
this.centered_post_offset -= distance;
this.centered_post_idx = next_post_idx;
post_idx = this.centered_post_idx;
break;
}
this.populate_post(post_idx);
/* Make sure that we have enough posts populated around the one we're centering
* on to fill the display. If we have too many nodes, remove some. */
for(var direction = 0; direction < 2; ++direction)
{
var right = !!direction;
/* We need at least this.container.offsetWidth/2 in each direction. Load a little more, to
* reduce flicker. */
var minimum_distance = this.container.offsetWidth/2;
minimum_distance *= 1.25;
var maximum_distance = minimum_distance + 500;
while(true)
{
var added = false;
var width = this.get_width_adjacent_to_post(post_idx, right);
/* If we're offset to the right then we need more data to the left, and vice versa. */
width += this.centered_post_offset * (right? -1:+1);
if(width < 0)
width = 1;
if(width < minimum_distance)
{
/* We need another post. Stop if there are no more posts to add. */
if(!this.add_post_to_display(right))
break;
added = false;
}
else if(width > maximum_distance)
{
/* We have a lot of posts off-screen. Remove one. */
this.remove_post(right);
/* Sanity check: we should never add and remove in the same direction. If this
* happens, the distance between minimum_distance and maximum_distance may be less
* than the width of a single thumbnail. */
if(added)
{
alert("error");
break;
}
}
else
{
break;
}
}
}
this.preload_thumbs();
/* We always center the thumb. Don't clamp to the edge when we're near the first or last
* item, so we always have empty space on the sides for expanded landscape thumbnails to
* be visible. */
var thumb = $("p" + post_idx);
var center_on_position = this.container.offsetWidth/2;
var shift_pixels_right = center_on_position - thumb.offsetWidth/2 - thumb.offsetLeft;
shift_pixels_right -= this.centered_post_offset;
shift_pixels_right = Math.round(shift_pixels_right);
var node = this.container.down(".post-browser-scroller");
node.setStyle({left: shift_pixels_right + "px"});
}
/* Preload thumbs on the boundary of what's actually displayed. */
ThumbnailView.prototype.preload_thumbs = function()
{
var post_idxs = [];
for(var i = 0; i < 5; ++i)
{
var preload_post_idx = this.posts_populated[0] - i - 1;
if(preload_post_idx >= 0)
post_idxs.push(preload_post_idx);
var preload_post_idx = this.posts_populated[1] + i;
if(preload_post_idx < this.post_ids.length)
post_idxs.push(preload_post_idx);
}
/* Remove any preloaded thumbs that are no longer in the preload list. */
this.thumb_preload_container.get_all().each(function(element) {
var post_idx = element.post_idx;
if(post_idxs.indexOf(post_idx) != -1)
{
/* The post is staying loaded. Clear the value in post_idxs, so we don't load it
* again down below. */
post_idxs[post_idx] = null;
return;
}
/* The post is no longer being preloaded. Remove the preload. */
this.thumb_preload_container.cancel_preload(element);
}.bind(this));
/* Add new preloads. */
for(var i = 0; i < post_idxs.length; ++i)
{
var post_idx = post_idxs[i];
if(post_idx == null)
continue;
var post_id = this.post_ids[post_idx];
var post = Post.posts.get(post_id);
var post_frame = this.post_frames[post_idx];
var url;
if(post_frame != -1)
url = post.frames[post_frame].preview_url;
else
url = post.preview_url;
var element = this.thumb_preload_container.preload(url);
element.post_idx = post_idx;
}
}
ThumbnailView.prototype.expand_post = function(post_idx)
{
/* Thumbs on click for touchpads doesn't make much sense anyway--touching the thumb causes it
* to be loaded. It also triggers a bug in iPhone WebKit (covering up the original target of
* a mouseover during the event seems to cause the subsequent click event to not be delivered).
* Just disable hover thumbnails for touchscreens. */
// if(Prototype.BrowserFeatures.Touchscreen)
/* MI: Using Prototype.BrowserIsMobile instead. */
if (Prototype.BrowserIsMobile)
return;
if(!this.thumb_container_shown)
return;
var post_id = this.post_ids[post_idx];
var overlay = this.container.down(".browser-thumb-hover-overlay");
overlay.hide();
overlay.down("IMG").src = "/images/blank.gif";
this.expanded_post_idx = post_idx;
if(post_idx == null)
return;
var post = Post.posts.get(post_id);
if(post.status == "deleted")
return;
var thumb = $("p" + post_idx);
var bottom = this.container.down(".browser-bottom-bar").offsetHeight;
overlay.style.bottom = bottom + "px";
var post_frame = this.post_frames[post_idx];
var image_width, image_url;
if(post_frame != -1)
{
var frame = post.frames[post_frame];
image_width = frame.preview_width;
image_url = frame.preview_url;
}
else
{
image_width = post.actual_preview_width;
image_url = post.preview_url;
}
var left = thumb.cumulativeOffset().left - image_width/2 + thumb.offsetWidth/2;
overlay.style.left = left + "px";
/* If the hover thumbnail overflows the right edge of the viewport, it'll extend the document and
* allow scrolling to the right, which we don't want. overflow: hidden doesn't fix this, since this
* element is absolutely positioned. Set the max-width to clip the right side of the thumbnail if
* necessary. */
var max_width = document.viewport.getDimensions().width - left;
overlay.style.maxWidth = max_width + "px";
overlay.href = "/post/browse#" + post.id + this.view.post_frame_hash(post, post_frame);
overlay.down("IMG").src = image_url;
overlay.show();
}
ThumbnailView.prototype.create_thumb = function(post_idx)
{
var post_id = this.post_ids[post_idx];
var post_frame = this.post_frames[post_idx];
var post = Post.posts.get(post_id);
/*
* Reuse thumbnail blocks that are no longer in use, to avoid WebKit memory leaks: it
* doesn't like creating and deleting lots of images (or blocks with images inside them).
*
* Thumbnails are hidden until they're loaded, so we don't show ugly load-borders. This
* also keeps us from showing old thumbnails before the new image is loaded. Use visibility:
* hidden, not display: none, or the size of the image won't be defined, which breaks
* center_on_post.
*/
if(this.unused_thumb_pool.length == 0)
{
var div =
'<div class="inner">' +
'<a class="thumb" tabindex="-1">' +
'<img alt="" class="preview" onload="this.style.visibility = \'visible\';">' +
'</a>' +
'</div>';
var item = $(document.createElement("li"));
item.innerHTML = div;
item.className = "post-thumb";
}
else
{
var item = this.unused_thumb_pool.pop();
}
item.id = "p" + post_idx;
item.post_idx = post_idx;
item.down("A").href = "/post/browse#" + post.id + this.view.post_frame_hash(post, post_frame);
/* If the image is already what we want, then leave it alone. Setting it to what it's
* already set to won't necessarily cause onload to be fired, so it'll never be set
* back to visible. */
var img = item.down("IMG");
var url;
if(post_frame != -1)
url = post.frames[post_frame].preview_url;
else
url = post.preview_url;
if(img.src != url)
{
img.style.visibility = "hidden";
img.src = url;
}
this.set_thumb_dimensions(item);
return item;
}
ThumbnailView.prototype.set_thumb_dimensions = function(li)
{
var post_idx = li.post_idx;
var post_id = this.post_ids[post_idx];
var post_frame = this.post_frames[post_idx];
var post = Post.posts.get(post_id);
var width, height;
if(post_frame != -1)
{
var frame = post.frames[post_frame];
width = frame.preview_width;
height = frame.preview_height;
}
else
{
width = post.actual_preview_width;
height = post.actual_preview_height;
}
width *= this.config.thumb_scale;
height *= this.config.thumb_scale;
/* This crops blocks that are too wide, but doesn't pad them if they're too
* narrow, since that creates odd spacing.
*
* If the height of this block is changed, adjust .post-browser-posts-container in
* config_changed. */
var block_size = [Math.min(width, 200 * this.config.thumb_scale), 200 * this.config.thumb_scale];
var crop_left = Math.round((width - block_size[0]) / 2);
var pad_top = Math.max(0, block_size[1] - height);
var inner = li.down(".inner");
inner.actual_width = block_size[0];
inner.actual_height = block_size[1];
inner.setStyle({width: block_size[0] + "px", height: block_size[1] + "px"});
var img = inner.down("img");
img.width = width;
img.height = height;
img.setStyle({marginTop: pad_top + "px", marginLeft: -crop_left + "px"});
}
ThumbnailView.prototype.config_changed = function()
{
/* Adjust the size of the container to fit the thumbs at the current scale. They're the
* height of the thumb block, plus ten pixels for padding at the top and bottom. */
var container_height = 200*this.config.thumb_scale + 10;
this.container.down(".post-browser-posts-container").setStyle({height: container_height + "px"});
this.container.select("LI.post-thumb").each(this.set_thumb_dimensions.bind(this));
this.center_on_post_for_scroll(this.centered_post_idx);
}
/* Handle clicks and doubleclicks on thumbnails. These events are handled by
* the container, so we don't need to put event handlers on every thumb. */
ThumbnailView.prototype.container_click_event = function(event)
{
/* Ignore the click if it was stopped by the DragElement. */
if(event.stopped)
return;
if($(event.target).up(".browser-thumb-hover-overlay"))
{
/* The hover overlay was clicked. When the user clicks a thumbnail, this is
* usually what happens, since the hover overlay covers the actual thumbnail. */
this.set_active_post_idx(this.expanded_post_idx);
event.preventDefault();
return;
}
var li = $(event.target).up(".post-thumb");
if(li == null)
return;
/* An actual thumbnail was clicked. This can happen if we don't have the expanded
* thumbnails for some reason. */
event.preventDefault();
this.set_active_post_idx(li.post_idx);
}
ThumbnailView.prototype.container_dblclick_event = function(event)
{
if(event.button)
return;
event.preventDefault();
this.show_thumb_bar(false);
}
ThumbnailView.prototype.show_thumb_bar = function(shown)
{
if(this.thumb_container_shown == shown)
return;
this.thumb_container_shown = shown;
this.container.show(shown);
/* If the centered post was changed while we were hidden, it wasn't applied by
* center_on_post, so do it now. */
this.center_on_post_for_scroll(this.centered_post_idx);
document.fire("viewer:thumb-bar-changed", {
shown: this.thumb_container_shown,
height: this.thumb_container_shown? this.container.offsetHeight:0
});
}
/* Return the next or previous post, wrapping around if necessary. */
ThumbnailView.prototype.get_adjacent_post_idx_wrapped = function(post_idx, next)
{
post_idx += next? +1:-1;
post_idx = (post_idx + this.post_ids.length) % this.post_ids.length;
return post_idx;
}
ThumbnailView.prototype.displayed_image_loaded_event = function(event)
{
/* If we don't have a loaded search, then we don't have any nearby posts to preload. */
if(this.post_ids == null)
return;
var post_id = event.memo.post_id;
var post_frame = event.memo.post_frame;
var post_idx = this.get_post_idx([post_id, post_frame]);
if(post_idx == null)
return;
/*
* The image in the post we're displaying is finished loading.
*
* Preload the next and previous posts. Normally, one or the other of these will
* already be in cache.
*
* Include the current post in the preloads, so if we switch from a frame back to
* the main image, the frame itself will still be loaded.
*/
var post_ids_to_preload = [];
post_ids_to_preload.push([this.post_ids[post_idx], this.post_frames[post_idx]]);
var adjacent_post_idx = this.get_adjacent_post_idx_wrapped(post_idx, true);
if(adjacent_post_idx != null)
post_ids_to_preload.push([this.post_ids[adjacent_post_idx], this.post_frames[adjacent_post_idx]]);
var adjacent_post_idx = this.get_adjacent_post_idx_wrapped(post_idx, false);
if(adjacent_post_idx != null)
post_ids_to_preload.push([this.post_ids[adjacent_post_idx], this.post_frames[adjacent_post_idx]]);
this.view.preload(post_ids_to_preload);
}
/* This handler handles global keypress bindings, and fires viewer: events. */
function InputHandler()
{
TrackFocus();
/*
* Keypresses are aggrevating:
*
* Opera can only stop key events from keypress, not keydown.
*
* Chrome only sends keydown for non-alpha keys, not keypress.
*
* In Firefox, keypress's keyCode value for non-alpha keys is always 0.
*
* Alpha keys can always be detected with keydown. Don't use keypress; Opera only provides
* charCode to that event, and it's affected by the caps state, which we don't want.
*
* Use OnKey for alpha key bindings. For other keys, use keypress in Opera and FF and
* keydown in other browsers.
*/
var keypress_event_name = window.opera || Prototype.Browser.Gecko? "keypress":"keydown";
document.on(keypress_event_name, this.document_keypress_event.bindAsEventListener(this));
}
InputHandler.prototype.handle_keypress = function(e)
{
var key = e.charCode;
if(!key)
key = e.keyCode; /* Opera */
if(key == Event.KEY_ESC)
{
if(document.focusedElement && document.focusedElement.blur && !document.focusedElement.hasClassName("no-blur-on-escape"))
{
document.focusedElement.blur();
return true;
}
}
var target = e.target;
if(target.tagName == "INPUT" || target.tagName == "TEXTAREA")
return false;
if(key == 63) // ?, f
{
debug("xxx");
document.fire("viewer:show-help");
return true;
}
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey)
return false;
var grave_keycode = Prototype.Browser.WebKit? 192: 96;
if(key == 32) // space
document.fire("viewer:set-thumb-bar", { toggle: true });
else if(key == 49) // 1
document.fire("viewer:vote", { score: 1 });
else if(key == 50) // 2
document.fire("viewer:vote", { score: 2 });
else if(key == 51) // 3
document.fire("viewer:vote", { score: 3 });
else if(key == grave_keycode) // `
document.fire("viewer:vote", { score: 0 });
else if(key == 65 || key == 97) // A, b
document.fire("viewer:show-next-post", { prev: true });
else if(key == 69 || key == 101) // E, e
document.fire("viewer:edit-post");
else if(key == 83 || key == 115) // S, s
document.fire("viewer:show-next-post", { prev: false });
else if(key == 70 || key == 102) // F, f
document.fire("viewer:focus-tag-box");
else if(key == 86 || key == 118) // V, v
document.fire("viewer:view-large-toggle");
else if(key == Event.KEY_PAGEUP)
document.fire("viewer:show-next-post", { prev: true });
else if(key == Event.KEY_PAGEDOWN)
document.fire("viewer:show-next-post", { prev: false });
else if(key == Event.KEY_LEFT)
document.fire("viewer:scroll", { left: true });
else if(key == Event.KEY_RIGHT)
document.fire("viewer:scroll", { left: false });
else
return false;
return true;
}
InputHandler.prototype.document_keypress_event = function(e)
{
//alert(e.charCode + ", " + e.keyCode);
if(this.handle_keypress(e))
e.stop();
}