/* * We have a few competing goals: * * First, be as responsive as possible. Preload nearby post HTML and their images. * * If data in a post page changes, eg. if the user votes, then coming back to the page * later should retain the changes. This means either requesting the page again, or * retaining the document node and reusing it, so we preserve the changes that were * made in-place. * * Don't use too much memory. If we keep every document node in memory as we use it, * the images will probably be kept around too. Release older nodes, so the browser * is more likely to release images that havn't been used in a while. * * We do the following: * - When we load a new post, it's formatted and its scripts are evaluated normally. * - When we're replacing the displayed post, its node is stashed away in a node cache. * - If we come back to the post while it's in the node cache, we'll use the node directly. * - HTML and images for posts are preloaded. We don't use a simple mechanism like * Preload.preload_raw, because Opera's caching is broken for XHR and it'll always * do a slow revalidation. * - We don't depend on browser caching for HTML. That would require us to expire a * page when we switch away from it if we've made any changes (eg. voting), so we * don't pull an out-of-date page next time. This is slower, and would require us * to be careful about expiring the cache. */ BrowserView = function(container) { this.container = container; /* The post that we currently want to display. This will be either one of the * current html_preloads, or be the displayed_post_id. */ this.wanted_post_id = null; this.wanted_post_frame = null; /* The post that's currently actually being displayed. */ this.displayed_post_id = null; this.displayed_post_frame = null; this.current_ajax_request = null; this.last_preload_request = []; this.last_preload_request_active = false; this.image_pool = new ImgPoolHandler(); this.img_box = this.container.down(".image-box"); this.container.down(".image-canvas"); /* Pop. Show large by default { */ (function($, B){ /* MI: If mobile browser, do nothing. */ if (Prototype.BrowserIsMobile) return; var COOKIE_NAME = "browse_large_by_default"; B.large_by_default = jQuery.cookie(COOKIE_NAME) === "0" ? false : true; var li = $($('.post-info-right-edge li')[0]).css("position", "relative"); var div = $('.default_to_large_cont'), a = div.children('a'), s = a.children('span'); function toggle_show_large() { B.large_by_default = !B.large_by_default; (B.large_by_default ? s.show() : s.hide()); jQuery.cookie(COOKIE_NAME, (B.large_by_default ? 1 : 0)); } a.click(function(){ toggle_show_large(); return false; }); li.hover(function(){ div.show(); }, function(){ div.hide(); }); if (!B.large_by_default) s.hide(); })(jQuery, this); /* } */ /* In Opera 10.63, the img.complete property is not reset to false after changing the * src property. Blits from images to the canvas silently fail, with nothing being * blitted and no exception raised. This causes blank canvases to be displayed, because * we have no way of telling whether the image is blittable or if the blit succeeded. */ if(!Prototype.Browser.Opera) this.canvas = create_canvas_2d(); if(this.canvas) { this.canvas.hide(); this.img_box.appendChild(this.canvas); } this.zoom_level = 0; /* True if the post UI is visible. */ this.post_ui_visible = true; this.update_navigator = this.update_navigator.bind(this); Event.on(window, "resize", this.window_resize_event.bindAsEventListener(this)); document.on("viewer:vote", function(event) { if(this.vote_widget) this.vote_widget.set(event.memo.score); }.bindAsEventListener(this)); if(TagCompletion) TagCompletion.init(); /* Double-clicking the main image, or on nothing, toggles the thumb bar. */ this.container.down(".image-container").on("dblclick", ".image-container", function(event) { /* Watch out: Firefox fires dblclick events for all buttons, with the standard * button maps, but IE only fires it for left click and doesn't set button at * all, so event.isLeftClick won't work. */ if(event.button) return; event.stop(); document.fire("viewer:set-thumb-bar", {toggle: true}); }.bindAsEventListener(this)); /* Image controls: */ document.on("viewer:view-large-toggle", function(e) { this.toggle_view_large_image(); }.bindAsEventListener(this)); this.container.down(".post-info").on("click", ".toggle-zoom", function(e) { e.stop(); this.toggle_view_large_image(true); }.bindAsEventListener(this)); // Pop. this.container.down(".parent-post").down("A").on("click", this.parent_post_click_event.bindAsEventListener(this)); this.container.down(".child-posts").down("A").on("click", this.child_posts_click_event.bindAsEventListener(this)); this.container.down(".post-frames").on("click", ".post-frame-link", function(e, item) { e.stop(); /* Change the displayed post frame to the one that was clicked. Since all post frames * are usually displayed in the thumbnail view, set center_thumbs to true to recenter * on the thumb that was clicked, so it's clearer what's happening. */ document.fire("viewer:set-active-post", {post_id: this.displayed_post_id, post_frame: item.post_frame, center_thumbs: true}); }.bind(this)); /* We'll receive this message from the thumbnail view when the overlay is * visible on the bottom of the screen, to tell us how much space is covered up * by it. */ this.thumb_bar_height = 0; document.on("viewer:thumb-bar-changed", function(e) { /* Update the thumb bar height and rescale the image to fit the new area. */ this.thumb_bar_height = e.memo.height; this.update_image_window_size(); this.set_post_ui(e.memo.shown); this.scale_and_position_image(true); }.bindAsEventListener(this)); /* OnKey(79, null, function(e) { this.zoom_level -= 1; this.scale_and_position_image(true); this.update_navigator(); return true; }.bindAsEventListener(this)); OnKey(80, null, function(e) { this.zoom_level += 1; this.scale_and_position_image(true); this.update_navigator(); return true; }.bindAsEventListener(this)); */ /* Hide member-only and moderator-only controls: */ $(document.body).pickClassName("is-member", "not-member", User.is_member_or_higher()); $(document.body).pickClassName("is-moderator", "not-moderator", User.is_mod_or_higher()); var tag_span = this.container.down(".post-tags"); tag_span.on("click", ".post-tag", function(e, element) { e.stop(); document.fire("viewer:perform-search", {tags: element.tag_name}); }.bind(this)); /* These two links do the same thing, but one is shown to approve a pending post * and the other is shown to unflag a flagged post, so they prompt differently. */ this.container.down(".post-approve").on("click", function(e) { e.stop(); if(!confirm("Approve this post?")) return; var post_id = this.displayed_post_id; Post.approve(post_id, false); }.bindAsEventListener(this)); this.container.down(".post-unflag").on("click", function(e) { e.stop(); if(!confirm("Unflag this post?")) return; var post_id = this.displayed_post_id; Post.unflag(post_id); }.bindAsEventListener(this)); this.container.down(".post-delete").on("click", function(e) { e.stop(); var post = Post.posts.get(this.displayed_post_id); var default_reason = ""; if(post.flag_detail) default_reason = post.flag_detail.reason; var reason = prompt("Reason:", default_reason); if(!reason || reason == "") return; var post_id = this.displayed_post_id; Post.approve(post_id, reason); }.bindAsEventListener(this)); this.container.down(".post-undelete").on("click", function(e) { e.stop(); if(!confirm("Undelete this post?")) return; var post_id = this.displayed_post_id; Post.undelete(post_id); }.bindAsEventListener(this)); this.container.down(".flag-button").on("click", function(e) { e.stop(); var post_id = this.displayed_post_id; Post.flag(post_id); }.bindAsEventListener(this)); this.container.down(".activate-post").on("click", function(e) { e.stop(); var post_id = this.displayed_post_id; if(!confirm("Activate this post?")) return; Post.update_batch([{ id: post_id, is_held: false }], function() { var post = Post.posts.get(post_id); if(post.is_held) notice("Couldn't activate post"); else notice("Activated post"); }.bind(this)); }.bindAsEventListener(this)); this.container.down(".reparent-post").on("click", function(e) { e.stop(); if(!confirm("Make this post the parent?")) return; var post_id = this.displayed_post_id; var post = Post.posts.get(post_id); if(post == null) return; Post.reparent_post(post_id, post.parent_id, false); }.bindAsEventListener(this)); this.container.down(".pool-info").on("click", ".remove-pool-from-post", function(e, element) { e.stop(); var pool_info = element.up(".pool-info"); var pool = Pool.pools.get(pool_info.pool_id); var pool_name = pool.name.replace(/_/g, ' '); if(!confirm("Remove this post from pool #" + pool_info.pool_id + ": " + pool_name + "?")) return; Pool.remove_post(pool_info.post_id, pool_info.pool_id); }.bind(this)); /* Post editing: */ var post_edit = this.container.down(".post-edit"); post_edit.down("FORM").on("submit", function(e) { e.stop(); this.edit_save(); }.bindAsEventListener(this)); this.container.down(".show-tag-edit").on("click", function(e) { e.stop(); this.edit_show(true); }.bindAsEventListener(this)); this.container.down(".edit-save").on("click", function(e) { e.stop(); this.edit_save(); }.bindAsEventListener(this)); this.container.down(".edit-cancel").on("click", function(e) { e.stop(); this.edit_show(false); }.bindAsEventListener(this)); this.edit_post_area_changed = this.edit_post_area_changed.bind(this); post_edit.down(".edit-tags").on("paste", function(e) { this.edit_post_area_changed.defer(); }.bindAsEventListener(this)); post_edit.down(".edit-tags").on("keydown", function(e) { this.edit_post_area_changed.defer(); }.bindAsEventListener(this)); new TagCompletionBox(post_edit.down(".edit-tags")); this.container.down(".post-edit").on("keydown", function(e) { /* Don't e.stop() KEY_ESC, so we fall through and let handle_keypress unfocus the * form entry, if any. Otherwise, Chrome gets confused and leaves the focus on the * hidden input, where it'll steal keystrokes. */ if (e.keyCode == Event.KEY_ESC) { this.edit_show(false); } else if (e.keyCode == Event.KEY_RETURN) { e.stop(); this.edit_save(); } }.bindAsEventListener(this)); /* When the edit-post hotkey is pressed (E), force the post UI open and show editing. */ document.on("viewer:edit-post", function(e) { document.fire("viewer:set-thumb-bar", { set: true }); this.edit_show(true); }.bindAsEventListener(this)); /* When the post that's currently being displayed is updated by an API call, update * the displayed info. */ document.on("posts:update", function(e) { if(e.memo.post_ids.get(this.displayed_post_id) == null) return; this.set_post_info(); }.bindAsEventListener(this)); this.vote_widget = new Vote(jQuery(this.container.down(".vote-container"), null)); this.vote_widget.initShortcut(); this.blacklist_override_post_id = null; this.container.down(".show-blacklisted").on("click", function(e) { e.preventDefault(); }.bindAsEventListener(this)); this.container.down(".show-blacklisted").on("dblclick", function(e) { e.stop(); this.blacklist_override_post_id = this.displayed_post_id; var post = Post.posts.get(this.displayed_post_id); this.set_main_image(post, this.displayed_post_frame); }.bindAsEventListener(this)); this.img_box.on("viewer:center-on", function(e) { this.center_image_on(e.memo.x, e.memo.y); }.bindAsEventListener(this)); this.navigator = new Navigator(this.container.down(".image-navigator"), this.img_box); this.container.on("swipe:horizontal", function(e) { document.fire("viewer:show-next-post", { prev: e.memo.right }); }.bindAsEventListener(this)); // if(Prototype.BrowserFeatures.Touchscreen) /* MI: Using Prototype.BrowserIsMobile instead. */ if (Prototype.BrowserIsMobile) { this.create_voting_popup(); this.image_swipe = new SwipeHandler(this.container.down(".image-container")); } /* Create the frame editor. This must be created before image_dragger, since it takes priority * for drags. */ this.container.down(".edit-frames-button").on("click", function(e) { e.stop(); this.show_frame_editor(); }.bindAsEventListener(this)); this.frame_editor = new FrameEditor(this.container.down(".frame-editor"), this.img_box, this.container.down(".frame-editor-popup"), { onClose: function() { this.hide_frame_editor(); }.bind(this) }); /* If we're using dragging as a swipe gesture (see SwipeHandler), don't use it for * dragging too. */ if(this.image_swipe == null) this.image_dragger = new WindowDragElementAbsolute(this.img_box, this.update_navigator); } BrowserView.prototype.create_voting_popup = function() { /* Create the low-level voting widget. */ var popup_vote_widget_container = this.container.down(".vote-popup-container"); popup_vote_widget_container.show(); this.popup_vote_widget = new Vote(jQuery(popup_vote_widget_container), null); this.popup_vote_widget.initShortcut(); var flash = this.container.down(".vote-popup-flash"); /* vote-popup-expand is the part that's always present and is clicked to display the * voting popup. Create a dragger on it, and pass the position down to the voting * popup as we drag around. */ var popup_expand = this.container.down(".vote-popup-expand"); popup_expand.show(); var last_dragged_over = null; this.popup_vote_dragger = new DragElement(popup_expand, { ondown: function(drag) { /* Stop the touchdown/mousedown events, so this drag takes priority over any * others. In particular, we don't want this.image_swipe to also catch this * as a drag. */ drag.latest_event.stop(); flash.hide(); flash.removeClassName("flash-star"); this.popup_vote_widget.set_mouseover(null); last_dragged_over = null; popup_vote_widget_container.removeClassName("vote-popup-hidden"); }.bind(this), onup: function(drag) { /* If we're cancelling the drag, don't activate the vote, if any. */ if(drag.cancelling) { debug("cancelling drag"); last_dragged_over = null; } /* Call even if star_container is null or not a star, so we clear any mouseover. */ this.popup_vote_widget.set_mouseover(last_dragged_over); var star = this.popup_vote_widget.activate_item(last_dragged_over); /* If a vote was made, flash the vote star. */ if(star != null) { /* Set the star-# class to color the star. */ for(var i = 0; i < 4; ++i) flash.removeClassName("star-" + i); flash.addClassName("star-" + star); flash.show(); /* Center the element on the screen. */ var offset = this.image_window_size; var flash_x = offset.width/2 - flash.offsetWidth/2; var flash_y = offset.height/2 - flash.offsetHeight/2; flash.setStyle({left: flash_x + "px", top: flash_y + "px"}); flash.addClassName("flash-star"); } popup_vote_widget_container.addClassName("vote-popup-hidden"); last_dragged_over = null; }.bind(this), ondrag: function(drag) { last_dragged_over = document.elementFromPoint(drag.x, drag.y); this.popup_vote_widget.set_mouseover(last_dragged_over); }.bind(this) }); } BrowserView.prototype.set_post_ui = function(visible) { /* Disable the post UI by default on touchscreens; we don't have an interface * to toggle it. */ // if(Prototype.BrowserFeatures.Touchscreen) /* MI: Using Prototype.BrowserIsMobile instead. */ if (Prototype.BrowserIsMobile) visible = false; /* If we don't have a post displayed, always hide the post UI even if it's currently * shown. */ this.container.down(".post-info").show(visible && this.displayed_post_id != null); if(visible == this.post_ui_visible) return; this.post_ui_visible = visible; if(this.navigator) this.navigator.set_autohide(!visible); /* If we're hiding the post UI, cancel the post editor if it's open. */ if(!this.post_ui_visible) this.edit_show(false); } BrowserView.prototype.image_loaded_event = function(event) { /* Record that the image is completely available, so it can be blitted to the canvas. * This is different than img.complete, which is true if the image has completed downloading * but hasn't yet been decoded, so isn't yet completely available. This generally happens * if we query img.completed quickly after setting img.src and the image data is cached. */ this.img.fully_loaded = true; document.fire("viewer:displayed-image-loaded", { post_id: this.displayed_post_id, post_frame: this.displayed_post_frame }); this.update_canvas(); } /* Return true if last_preload_request includes [post_id, post_frame]. */ BrowserView.prototype.post_frame_list_includes = function(post_id_list, post_id, post_frame) { var found_preload = post_id_list.find(function(post) { return post[0] == post_id && post[1] == post_frame; }); return found_preload != null; } /* Begin preloading the HTML and images for the given post IDs. */ BrowserView.prototype.preload = function(post_ids) { /* We're being asked to preload post_ids. Only do this if it seems to make sense: if * the user is actually traversing posts that are being preloaded. Look at the previous * call to preload(). If it didn't include the current post, then skip the preload. */ var last_preload_request = this.last_preload_request; this.last_preload_request = post_ids; if(!this.post_frame_list_includes(last_preload_request, this.wanted_post_id, this.wanted_post_frame)) { // debug("skipped-preload(" + post_ids.join(",") + ")"); this.last_preload_request_active = false; return; } this.last_preload_request_active = true; // debug("preload(" + post_ids.join(",") + ")"); var new_preload_container = new PreloadContainer(); for(var i = 0; i < post_ids.length; ++i) { var post_id = post_ids[i][0]; var post_frame = post_ids[i][1]; var post = Post.posts.get(post_id); if(post_frame != -1) { var frame = post.frames[post_frame]; new_preload_container.preload(frame.url); } else new_preload_container.preload(post.sample_url); } /* If we already were preloading images, we created the new preloads before * deleting the old ones. That way, any images that are still being preloaded * won't be deleted and recreated, possibly causing the download to be interrupted * and resumed. */ if(this.preload_container) this.preload_container.destroy(); this.preload_container = new_preload_container; } BrowserView.prototype.load_post_id_data = function(post_id) { debug("load needed"); // If we already have a request in flight, don't start another; wait for the // first to finish. if(this.current_ajax_request != null) return; new Ajax.Request("/post.json", { parameters: { tags: "id:" + post_id, api_version: 2, filter: 1, include_tags: "1", include_votes: "1", include_pools: 1 }, method: "get", onCreate: function(resp) { this.current_ajax_request = resp.request; }.bind(this), onSuccess: function(resp) { if(this.current_ajax_request != resp.request) return; /* If no posts were returned, then the post ID we're looking up doesn't exist; * treat this as a failure. */ var resp = resp.responseJSON; this.success = resp.posts.length > 0; if(!this.success) { notice("Post #" + post_id + " doesn't exist"); return; } Post.register_resp(resp); }.bind(this), onComplete: function(resp) { if(this.current_ajax_request == resp.request) this.current_ajax_request = null; /* If the request failed and we were requesting wanted_post_id, don't keep trying. */ var success = resp.request.success() && this.success; if(!success && post_id == this.wanted_post_id) { /* As a special case, if the post we requested doesn't exist and we aren't displaying * anything at all, force the thumb bar open so we don't show nothing at all. */ if(this.displayed_post_id == null) document.fire("viewer:set-thumb-bar", {set: true}); return; } /* This will either load the post we just finished, or request data for the * one we want. */ this.set_post(this.wanted_post_id, this.wanted_post_frame); }.bind(this), onFailure: function(resp) { notice("Error " + resp.status + " loading post"); }.bind(this) }); } BrowserView.prototype.set_viewing_larger_version = function(b, from_toggle_zoom) // Pop. { this.viewing_larger_version = b; var post = Post.posts.get(this.displayed_post_id); var can_zoom = post != null && post.jpeg_url != post.sample_url; var viewing_larger_version = from_toggle_zoom || !this.large_by_default ? b : !b; // Pop. this.container.down(".zoom-icon-none").show(!can_zoom); this.container.down(".zoom-icon-in").show(can_zoom && !viewing_larger_version); // Pop. this.container.down(".zoom-icon-out").show(can_zoom && viewing_larger_version); // Pop. /* When we're on the regular version and we're on a touchscreen, disable drag * scrolling so we can use it to switch images instead. */ // if(Prototype.BrowserFeatures.Touchscreen && this.image_dragger) /* MI: Using Prototype.BrowserIsMobile instead. */ if (Prototype.BrowserIsMobile && this.image_dragger) this.image_dragger.set_disabled(!b); /* Only allow dragging to create new frames when not viewing the large version, * since we need to be able to drag the image. */ if(this.frame_editor) { this.frame_editor.set_drag_to_create(!b); this.frame_editor.set_show_corner_drag(!b); } } BrowserView.prototype.set_main_image = function(post, post_frame, from_toggle_zoom) // Pop. { /* * Clear the previous post, if any. Don't keep the old IMG around; create a new one, or * we may trigger long-standing memory leaks in WebKit, eg.: * https://bugs.webkit.org/show_bug.cgi?id=31253 * * This also helps us avoid briefly displaying the old image with the new dimensions, which * can otherwise take some hoop jumping to prevent. */ if(this.img != null) { this.img.stopObserving(); this.img.parentNode.removeChild(this.img); this.image_pool.release(this.img); this.img = null; } /* If this post is blacklisted, show a message instead of displaying it. */ var hide_post = Post.is_blacklisted(post.id) && post.id != this.blacklist_override_post_id; this.container.down(".blacklisted-message").show(hide_post); if(hide_post) return; this.img = this.image_pool.get(); this.img.className = "main-image"; if(this.canvas) this.canvas.hide(); this.img.show(); /* * Work around an iPhone bug. If a touchstart event is sent to this.img, and then * (due to a swipe gesture) we remove the image and replace it with a new one, no * touchend is ever delivered, even though it's the containing box listening to the * event. Work around this by setting the image to pointer-events: none, so clicks on * the image will actually be sent to the containing box directly. */ this.img.setStyle({pointerEvents: "none"}); this.img.on("load", this.image_loaded_event.bindAsEventListener(this)); if (!from_toggle_zoom && this.large_by_default) this.viewing_larger_version = true this.img.fully_loaded = false; if(post_frame != -1 && post_frame < post.frames.length) { var frame = post.frames[post_frame]; this.img.src = frame.url; this.img_box.original_width = frame.width; this.img_box.original_height = frame.height; this.img_box.show(); } else if(this.viewing_larger_version && post.jpeg_url) { this.img.src = post.jpeg_url; this.img_box.original_width = post.jpeg_width; this.img_box.original_height = post.jpeg_height; this.img_box.show(); } else if(!this.viewing_larger_version && post.sample_url) { this.img.src = post.sample_url; this.img_box.original_width = post.sample_width; this.img_box.original_height = post.sample_height; this.img_box.show(); } else { /* Having no sample URL is an edge case, usually from deleted posts. Keep the number * of code paths smaller by creating the IMG anyway, but not showing it. */ this.img_box.hide(); } this.container.down(".image-box").appendChild(this.img); if(this.viewing_larger_version) { this.navigator.set_image(post.preview_url, post.actual_preview_width, post.actual_preview_height); this.navigator.set_autohide(!this.post_ui_visible); } this.navigator.enable(this.viewing_larger_version); this.scale_and_position_image(); } /* * Display post_id. If post_frame is not null, set the specified frame. * * If no_hash_change is true, the UrlHash will not be updated to reflect the new post. * This should be used when this is called to load the post already reflected by the * URL hash. For example, the hash "#/pool:123" shows pool 123 in the thumbnails and * shows its first post in the view. It should *not* change the URL hash to reflect * the actual first post (eg. #12345/pool:123). This will insert an unwanted history * state in the browser, so the user has to go back twice to get out. * * no_hash_change should also be set when loading a state as a result of hashchange, * for similar reasons. */ BrowserView.prototype.set_post = function(post_id, post_frame, lazy, no_hash_change, replace_history) { if(post_id == null) throw "post_id must not be null"; /* If there was a lazy load pending, cancel it. */ this.cancel_lazily_load(); this.wanted_post_id = post_id; this.wanted_post_frame = post_frame; this.wanted_post_no_hash_change = no_hash_change; this.wanted_post_replace_history = replace_history; if(post_id == this.displayed_post_id && post_frame == this.displayed_post_frame) return; /* If a lazy load was requested and we're not yet loading the image for this post, * delay loading. */ var is_cached = this.last_preload_request_active && this.post_frame_list_includes(this.last_preload_request, post_id, post_frame); if(lazy && !is_cached) { this.lazy_load_timer = window.setTimeout(function() { this.lazy_load_timer = null; this.set_post(this.wanted_post_id, this.wanted_post_frame, false, this.wanted_post_no_hash_change, this.wanted_post_replace_history); }.bind(this), 500); return; } this.hide_frame_editor(); var post = Post.posts.get(post_id); if(post == null) { /* The post we've been asked to display isn't loaded. Request a load and come back. */ if(this.displayed_post_id == null) this.container.down(".post-info").hide(); this.load_post_id_data(post_id); return; } if(post_frame == null) { // If post_frame is unspecified and we have a frame, display the first. post_frame = this.get_default_post_frame(post_id); // We know what frame we actually want to display now, so update wanted_post_frame. this.wanted_post_frame = post_frame; } /* If post_frame doesn't exist, just display the main post. */ if(post_frame != -1 && post.frames.length <= post_frame) post_frame = -1; this.displayed_post_id = post_id; this.displayed_post_frame = post_frame; if(!no_hash_change) { var post_frame_hash = this.get_post_frame_hash(post, post_frame); UrlHash.set_deferred({"post-id": post_id, "post-frame": post_frame_hash}, replace_history); } this.set_viewing_larger_version(false); this.set_main_image(post, post_frame); if(this.vote_widget) { if (this.vote_widget.post_id) { Post.votes.set(this.vote_widget.post_id, this.vote_widget.data.vote); Post.posts.get(this.vote_widget.post_id).score = this.vote_widget.data.score; } this.vote_widget.post_id = post.id; this.vote_widget.updateWidget(Post.votes.get(post.id), post.score); }; if(this.popup_vote_widget) { this.popup_vote_widget.post_id = post.id; this.popup_vote_widget.updateWidget(Post.votes.get(post.id), post.score); }; document.fire("viewer:displayed-post-changed", { post_id: post_id, post_frame: post_frame }); this.set_post_info(); /* Hide the editor when changing posts. */ this.edit_show(false); } /* Return the frame spec for the hash, eg. "-0". * * If the post has no frames, then just omit the frame spec. If the post has any frames, * then return the frame number or "-F" for the full image. */ BrowserView.prototype.post_frame_hash = function(post, post_frame) { if(post.frames.length == 0) return ""; return "-" + (post_frame == -1? "F":post_frame); } /* Return the default frame to display for the given post. If the post isn't loaded, * we don't know which frame we'll display and null will be returned. This corresponds * to a hash of #1234, where no frame is specified (eg. #1234-F, #1234-0). */ BrowserView.prototype.get_default_post_frame = function(post_id) { var post = Post.posts.get(post_id); if(post == null) return null; return post.frames.length > 0? 0: -1; } BrowserView.prototype.get_post_frame_hash = function(post, post_frame) { /* * Omitting the frame in the hash selects the default frame: the first frame if any, * otherwise the full image. If we're setting the hash to a post_frame which would be * selected by this default, omit the frame so this default is used. For example, if * post #1234 has one frame and post_frame is 0, it would be selected by the default, * so omit the frame and use a hash of #1234, not #1234-0. * * This helps normalize the hash. Otherwise, loading /#1234 will update the hash to * /#1234-in set_post, causing an unwanted history entry. */ var default_frame = post.frames.length > 0? 0:-1; if(post_frame == default_frame) return null; else return post_frame; } /* Set the post info box for the currently displayed post. */ BrowserView.prototype.set_post_info = function() { var post = Post.posts.get(this.displayed_post_id); if(!post) return; this.container.down(".post-id").setTextContent(post.id); this.container.down(".post-id-link").href = "/post/show/" + post.id; this.container.down(".posted-by").show(); this.container.down(".posted-at").setTextContent(time_ago_in_words(new Date(post.created_at*1000))); /* Fill in the pool list. */ var pool_info = this.container.down(".pool-info"); while(pool_info.firstChild) pool_info.removeChild(pool_info.firstChild); if(post.pool_posts) { post.pool_posts.each(function(pp) { var pool_post = pp[1]; var pool_id = pool_post.pool_id; var pool = Pool.pools.get(pool_id); var pool_title = pool.name.replace(/_/g, " "); var sequence = pool_post.sequence; if(sequence.match(/^[0-9]/)) sequence = "#" + sequence; var html = '
Post ${sequence} in ${desc} ' + '(pool page)'; if(Pool.can_edit_pool(pool)) html += ' (remove)
'; var div = html.subst({ sequence: sequence, pool_id: pool_id, desc: pool_title.escapeHTML() }).createElement(); div.post_id = post.id; div.pool_id = pool_id; pool_info.appendChild(div); }.bind(this)); } if(post.creator_id != null) { this.container.down(".posted-by").down("A").href = "/user/show/" + post.creator_id; this.container.down(".posted-by").down("A").setTextContent(post.author); } else { this.container.down(".posted-by").down("A").href = "#" this.container.down(".posted-by").down("A").setTextContent('Anonymous'); } this.container.down(".post-dimensions").setTextContent(post.width + "x" + post.height); this.container.down(".post-source").show(post.source != ""); if(post.source != "") { var text = post.source; var url = null; var m = post.source.match(/^http:\/\/.*pixiv\.net\/(img\d+\/)?img\/(\w+)\/(\d+)(_.+)?\.\w+$/); if(m) { text = "pixiv #" + m[3] + " (" + m[2] + ")"; url = "http://www.pixiv.net/member_illust.php?mode=medium&illust_id=" + m[3]; } else if(post.source.substr(0, 7) == "http://") { text = text.substr(7); if(text.substr(0, 4) == "www.") text = text.substr(4); if(text.length > 20) text = text.substr(0, 20) + "..."; url = post.source; } var source_box = this.container.down(".post-source"); source_box.down("A").show(url != null); source_box.down("SPAN").show(url == null); if(url) { source_box.down("A").href = url; source_box.down("A").setTextContent(text); } else { source_box.down("SPAN").setTextContent(text); } } if(post.frames.length > 0) { /* Hide this with a class rather than by changing display, so show_frame_editor * and hide_frame_editor can hide and unhide this separately. */ this.container.down(".post-frames").removeClassName("no-frames"); var frame_list = this.container.down(".post-frame-list"); while(frame_list.firstChild) frame_list.removeChild(frame_list.firstChild); for(var i = -1; i < post.frames.length; ++i) { var text = i == -1? "main": (i+1); var a = document.createElement("a"); a.href = "/post/browse#" + post.id + this.post_frame_hash(post, i); a.className = "post-frame-link"; if(this.displayed_post_frame == i) a.className += " current-post-frame"; a.setTextContent(text); a.post_frame = i; frame_list.appendChild(a); } } else { this.container.down(".post-frames").addClassName("no-frames"); } var ratings = {s: "Safe", q: "Questionable", e: "Explicit"}; this.container.down(".post-rating").setTextContent(ratings[post.rating]); this.container.down(".post-score").setTextContent(post.score); this.container.down(".post-hidden").show(!post.is_shown_in_index); this.container.down(".post-info").show(this.post_ui_visible); var file_extension = function(path) { var m = path.match(/.*\.([^.]+)/); if(!m) return ""; return m[1]; } var has_sample = (post.sample_url != post.file_url); var has_jpeg = (post.jpeg_url != post.file_url); var has_image = post.file_url != null && !has_sample; /* Hide the whole download-links box if there are no downloads available, usually * because of a deleted post. */ this.container.down(".download-links").show(has_image || has_sample || has_jpeg); this.container.down(".download-image").show(has_image); if(has_image) { this.container.down(".download-image").href = post.file_url; this.container.down(".download-image-desc").setTextContent(number_to_human_size(post.file_size) + " " + file_extension(post.file_url.toUpperCase())); } this.container.down(".download-jpeg").show(has_sample); if(has_sample) { this.container.down(".download-jpeg").href = has_jpeg? post.jpeg_url: post.file_url; var image_desc = number_to_human_size(has_jpeg? post.jpeg_file_size: post.file_size) /*+ " " + post.jpeg_width + "x" + post.jpeg_height*/ + " JPG"; this.container.down(".download-jpeg-desc").setTextContent(image_desc); } this.container.down(".download-png").show(has_jpeg); if(has_jpeg) { this.container.down(".download-png").href = post.file_url; var png_desc = number_to_human_size(post.file_size) /*+ " " + post.width + "x" + post.height*/ + " " + file_extension(post.file_url.toUpperCase()); this.container.down(".download-png-desc").setTextContent(png_desc); } /* For links that are handled by click events, try to set the href so that copying the * link will give a similar effect. For example, clicking parent-post will call set_post * to display it, and the href links to /post/browse#12345. */ var parent_post = this.container.down(".parent-post"); parent_post.show(post.parent_id != null); if(post.parent_id) parent_post.down("A").href = "/post/browse#" + post.parent_id; var child_posts = this.container.down(".child-posts"); child_posts.show(post.has_children); if(post.has_children) child_posts.down("A").href = "/post/browse#/parent:" + post.id; /* Create the tag links. */ var tag_span = this.container.down(".post-tags"); var first = true; while(tag_span.firstChild) tag_span.removeChild(tag_span.firstChild); var tags_by_type = Post.get_post_tags_with_type(post); tags_by_type.each(function(t) { var tag = t[0]; var type = t[1]; var span = $(document.createElement("SPAN", "")); span = $(span); span.className = "tag-type-" + type; var space = document.createTextNode(" "); span.appendChild(space); var a = jQuery('', { text: tag, href: "/post/browse#/" + tag, class: "post-tag tag-type-" + type, }); /* Break tags with , so long tags can be wrapped. */ a.html(a.html().replace(/_/g, '_')); /* convert back to something Prototype or whatever can understand */ a = a[0]; a.tag_name = tag; span.appendChild(a); tag_span.appendChild(span); }); var flag_post = this.container.down(".flag-button"); flag_post.show(post.status == "active"); this.container.down(".post-approve").show(post.status == "flagged" || post.status == "pending"); this.container.down(".post-delete").show(post.status != "deleted"); this.container.down(".post-undelete").show(post.status == "deleted"); var flagged = this.container.down(".flagged-info"); flagged.show(post.status == "flagged"); if(post.status == "flagged" && post.flag_detail) { var by = flagged.down(".by"); flagged.down(".flagged-by-box").show(post.flag_detail.user_id != null); if(post.flag_detail.user_id != null) { by.setTextContent(post.flag_detail.flagged_by); by.href = "/user/show/" + post.flag_detail.user_id; } var reason = flagged.down(".reason"); reason.setTextContent(post.flag_detail.reason); } /* Moderators can unflag images, and the person who flags an image can unflag it himself. */ var is_flagger = post.flag_detail && post.flag_detail.user_id == User.get_current_user_id(); var can_unflag = flagged && (User.is_mod_or_higher() || is_flagger); flagged.down(".post-unflag").show(can_unflag); var pending = this.container.down(".status-pending"); pending.show(post.status == "pending"); this.container.down(".pending-reason-box").show(post.flag_detail && post.flag_detail.reason); if(post.flag_detail) this.container.down(".pending-reason").setTextContent(post.flag_detail.reason); var deleted = this.container.down(".status-deleted"); deleted.show(post.status == "deleted"); if(post.status == "deleted") { var by_container = deleted.down(".by-container"); by_container.show(post.flag_detail.flagged_by != null); var by = by_container.down(".by"); by.setTextContent(post.flag_detail.flagged_by); by.href = "/user/show/" + post.flag_detail.user_id; var reason = deleted.down(".reason"); reason.setTextContent(post.flag_detail.reason); } this.container.down(".status-held").show(post.is_held); var has_permission = User.get_current_user_id() == post.creator_id || User.is_mod_or_higher(); this.container.down(".activate-post").show(has_permission); } BrowserView.prototype.edit_show = function(shown) { var post = Post.posts.get(this.displayed_post_id); if(!post) shown = false; if(!User.is_member_or_higher()) shown = false; this.edit_shown = shown; this.container.down(".post-tags-box").show(!shown); this.container.down(".post-edit").show(shown); if(!shown) { /* Revert all changes. */ this.frame_editor.discard(); return; } this.select_edit_box(".post-edit-main"); /* This returns [tag, tag type]. We only want the tag; we call this so we sort the * tags consistently. */ var tags_by_type = Post.get_post_tags_with_type(post); var tags = tags_by_type.pluck(0); tags = tags.join(" ") + " "; this.container.down(".edit-tags").old_value = tags; this.container.down(".edit-tags").value = tags; this.container.down(".edit-source").value = post.source; this.container.down(".edit-parent").value = post.parent_id; this.container.down(".edit-shown-in-index").checked = post.is_shown_in_index; var rating_class = new Hash({ s: ".edit-safe", q: ".edit-questionable", e: ".edit-explicit" }); this.container.down(rating_class.get(post.rating)).checked = true; this.edit_post_area_changed(); this.container.down(".edit-tags").focus(); } /* Set the size of the tag edit area to the size of its contents. */ BrowserView.prototype.edit_post_area_changed = function() { var post_edit = this.container.down(".post-edit"); var element = post_edit.down(".edit-tags"); element.style.height = "0px"; element.style.height = element.scrollHeight + "px"; if(0) { var rating = null; var source = null; var parent_id = null; element.value.split(" ").each(function(tag) { /* This mimics what the server side does; it does prevent metatags from using * uppercase in source: metatags. */ tag = tag.toLowerCase(); /* rating:q or just q: */ var m = tag.match(/^(rating:)?([qse])$/); if(m) { rating = m[2]; return; } var m = tag.match(/^(parent):([0-9]+)$/); if(m) { if(m[1] == "parent") parent_id = m[2]; } var m = tag.match(/^(source):(.*)$/); if(m) { if(m[1] == "source") source = m[2]; } }.bind(this)); debug("rating: " + rating); debug("source: " + source); debug("parent: " + parent_id); } } BrowserView.prototype.edit_save = function() { var save_completed = function() { notice("Post saved"); /* If we're still showing the post we saved, hide the edit area. */ if(this.displayed_post_id == post_id) this.edit_show(false); }.bind(this); var post_id = this.displayed_post_id; /* If we're in the frame editor, save it. Don't save the hidden main editor. */ if(this.frame_editor) { if(this.frame_editor.is_opened()) { this.frame_editor.save(save_completed); return; } } var edit_tags = this.container.down(".edit-tags"); var tags = edit_tags.value; /* Opera doesn't blur the field automatically, even when we hide it later. */ edit_tags.blur(); /* Find which rating is selected. */ var rating_class = new Hash({ s: ".edit-safe", q: ".edit-questionable", e: ".edit-explicit" }); var selected_rating = "s"; rating_class.each(function(c) { if(this.container.down(c[1]).checked) selected_rating = c[0]; }.bind(this)); /* update_batch will give us updates for any related posts, as well as the one we're * updating. */ Post.update_batch([{ id: post_id, tags: this.container.down(".edit-tags").value, old_tags: this.container.down(".edit-tags").old_value, source: this.container.down(".edit-source").value, parent_id: this.container.down(".edit-parent").value, /* MI: Set value to int explicity, instead of bool, as it was converted to string, * then to int, ending up as 0. */ is_shown_in_index: this.container.down(".edit-shown-in-index").checked ? 1 : 0, rating: selected_rating }], save_completed); } BrowserView.prototype.window_resize_event = function(e) { if(e.stopped) return; this.update_image_window_size(); this.scale_and_position_image(true); } BrowserView.prototype.toggle_view_large_image = function(from_toggle_zoom)/*Pop.*/ { var post = Post.posts.get(this.displayed_post_id); if(post == null) return; if(this.img == null) return; if(post.jpeg_url == post.sample_url) { /* There's no larger version to display. */ return; } /* Toggle between the sample and JPEG version. */ this.set_viewing_larger_version(!this.viewing_larger_version, from_toggle_zoom); // Pop. this.set_main_image(post, null, from_toggle_zoom); // XXX frame // Pop. } /* this.image_window_size is the size of the area where the image is visible. */ BrowserView.prototype.update_image_window_size = function() { this.image_window_size = getWindowSize(); /* If the thumb bar is shown, exclude it from the window height and fit the image * in the remainder. Since the bar is at the bottom, we don't need to do anything to * adjust the top. */ this.image_window_size.height -= this.thumb_bar_height; this.image_window_size.height = Math.max(this.image_window_size.height, 0); /* clamp to 0 if there's no space */ /* When the window size changes, update the navigator since the cursor will resize to * match. */ this.update_navigator(); } BrowserView.prototype.scale_and_position_image = function(resizing) { var img_box = this.img_box; if(!this.img) return; var original_width = img_box.original_width; var original_height = img_box.original_height; var post = Post.posts.get(this.displayed_post_id); if(!post) { debug("unexpected: displayed post " + this.displayed_post_id + " unknown"); return; } var window_size = this.image_window_size; var ratio = 1.0; if(!this.viewing_larger_version) { /* Zoom the image to fit the viewport. */ var ratio = window_size.width / original_width; if (original_height * ratio > window_size.height) ratio = window_size.height / original_height; } ratio *= Math.pow(0.9, this.zoom_level); this.displayed_image_width = Math.round(original_width * ratio); this.displayed_image_height = Math.round(original_height * ratio); this.img.width = this.displayed_image_width; this.img.height = this.displayed_image_height; this.update_canvas(); if(this.frame_editor) this.frame_editor.set_image_dimensions(this.displayed_image_width, this.displayed_image_height); /* If we're resizing and showing the full-size image, don't snap the position * back to the default. */ if(resizing && this.viewing_larger_version) return; var x = 0.5; var y = 0.5; if(this.viewing_larger_version) { /* Align the image to the top of the screen. */ y = this.image_window_size.height/2; y /= this.displayed_image_height; } this.center_image_on(x, y); } /* x and y are [0,1]. */ BrowserView.prototype.update_navigator = function() { if(!this.navigator) return; if(!this.img) return; /* The coordinates of the image located at the top-left corner of the window: */ var scroll_x = -this.img_box.offsetLeft; var scroll_y = -this.img_box.offsetTop; /* The coordinates at the center of the window: */ x = scroll_x + this.image_window_size.width/2; y = scroll_y + this.image_window_size.height/2; var percent_x = x / this.displayed_image_width; var percent_y = y / this.displayed_image_height; var height_percent = this.image_window_size.height / this.displayed_image_height; var width_percent = this.image_window_size.width / this.displayed_image_width; this.navigator.image_position_changed(percent_x, percent_y, height_percent, width_percent); } /* * If Canvas support is available, we can accelerate drawing. * * Most browsers are slow when resizing large images. In the best cases, it results in * dragging the image around not being smooth (all browsers except Chrome). In the worst * case it causes rendering the page to be very slow; in Chrome, drawing the thumbnail * strip under a large resized image is unusably slow. * * If Canvas support is enabled, then once the image is fully loaded, blit the image into * the canvas at the size we actually want to display it at. This avoids most scaling * performance issues, because it's not rescaling the image constantly while dragging it * around. * * Note that if Chrome fixes its slow rendering of boxes *over* the image, then this may * be unnecessary for that browser. Rendering the image itself is very smooth; Chrome seems * to prescale the image just once, which is what we're doing. * * Limitations: * - If full-page zooming is being used, it'll still scale at runtime. * - We blit the entire image at once. It's more efficient to blit parts of the image * as necessary to paint, but that's a lot more work. * - Canvas won't blit partially-loaded images, so we do nothing until the image is complete. */ BrowserView.prototype.update_canvas = function() { if(!this.img.fully_loaded) { debug("image incomplete; can't render to canvas"); return false; } if(!this.canvas) return; /* If the contents havn't changed, skip the blit. This happens frequently when resizing * the window when not fitting the image to the screen. */ if(this.canvas.rendered_url == this.img.src && this.canvas.width == this.displayed_image_width && this.canvas.height == this.displayed_image_height) { // debug(this.canvas.rendered_url + ", " + this.canvas.width + ", " + this.canvas.height) // debug("Skipping canvas blit"); return; } this.canvas.rendered_url = this.img.src; this.canvas.width = this.displayed_image_width; this.canvas.height = this.displayed_image_height; var ctx = this.canvas.getContext("2d"); ctx.drawImage(this.img, 0, 0, this.displayed_image_width, this.displayed_image_height); this.canvas.show(); this.img.hide(); return true; } BrowserView.prototype.center_image_on = function(percent_x, percent_y) { var x = percent_x * this.displayed_image_width; var y = percent_y * this.displayed_image_height; var scroll_x = x - this.image_window_size.width/2; scroll_x = Math.round(scroll_x); var scroll_y = y - this.image_window_size.height/2; scroll_y = Math.round(scroll_y); this.img_box.setStyle({left: -scroll_x + "px", top: -scroll_y + "px"}); this.update_navigator(); } BrowserView.prototype.cancel_lazily_load = function() { if(this.lazy_load_timer == null) return; window.clearTimeout(this.lazy_load_timer); this.lazy_load_timer = null; } /* Update the window title when the display changes. */ WindowTitleHandler = function() { this.searched_tags = ""; this.post_id = null; this.post_frame = null; this.pool = null; document.on("viewer:searched-tags-changed", function(e) { this.searched_tags = e.memo.tags || ""; this.update(); }.bindAsEventListener(this)); document.on("viewer:displayed-post-changed", function(e) { this.post_id = e.memo.post_id; this.post_frame = e.memo.post_id; this.update(); }.bindAsEventListener(this)); document.on("viewer:displayed-pool-changed", function(e) { this.pool = e.memo.pool; this.update(); }.bindAsEventListener(this)); this.update(); } WindowTitleHandler.prototype.update = function() { var post = Post.posts.get(this.post_id); if(this.pool) { var title = this.pool.name.replace(/_/g, " "); if(post && post.pool_posts) { var pool_post = post.pool_posts.get(this.pool.id); if(pool_post) { var sequence = pool_post.sequence; title += " "; if(sequence.match(/^[0-9]/)) title += "#"; title += sequence; } } } else { var title = "/" + this.searched_tags.replace(/_/g, " "); } title += " - Browse"; document.title = title; } BrowserView.prototype.parent_post_click_event = function(event) { event.stop(); var post = Post.posts.get(this.displayed_post_id); if(post == null || post.parent_id == null) return; this.set_post(post.parent_id); } BrowserView.prototype.child_posts_click_event = function(event) { event.stop(); /* Search for this post's children. Set the results mode to center-on-current, so we * focus on the current item. */ document.fire("viewer:perform-search", { tags: "parent:" + this.displayed_post_id, results_mode: "center-on-current" }); } BrowserView.prototype.select_edit_box = function(className) { if(this.shown_edit_container) this.shown_edit_container.hide(); this.shown_edit_container = this.container.down(className); this.shown_edit_container.show(); } BrowserView.prototype.show_frame_editor = function() { this.select_edit_box(".frame-editor"); /* If we're displaying a frame and not the whole image, switch to the main image. */ var post_frame = null; if(this.displayed_post_frame != -1) { post_frame = this.displayed_post_frame; document.fire("viewer:set-active-post", {post_id: this.displayed_post_id, post_frame: -1}); } this.frame_editor.open(this.displayed_post_id); this.container.down(".post-frames").hide(); /* If we were on a frame when opened, focus the frame we were on. Otherwise, * leave it on the default. */ if(post_frame != null) this.frame_editor.focus(post_frame); } BrowserView.prototype.hide_frame_editor = function() { this.frame_editor.discard(); this.container.down(".post-frames").show(); } var Navigator = function(container, target) { this.container = container; this.target = target; this.hovering = false; this.autohide = false; this.img = this.container.down(".image-navigator-img"); this.container.show(); this.handlers = []; this.handlers.push(this.container.on("mousedown", this.mousedown_event.bindAsEventListener(this))); this.handlers.push(this.container.on("mouseover", this.mouseover_event.bindAsEventListener(this))); this.handlers.push(this.container.on("mouseout", this.mouseout_event.bindAsEventListener(this))); this.dragger = new DragElement(this.container, { snap_pixels: 0, onenddrag: this.enddrag.bind(this), ondrag: this.ondrag.bind(this) }); } Navigator.prototype.set_image = function(image_url, width, height) { this.img.src = image_url; this.img.width = width; this.img.height = height; } Navigator.prototype.enable = function(enabled) { this.container.show(enabled); } Navigator.prototype.mouseover_event = function(e) { if(e.relatedTarget && e.relatedTarget.isParentNode(this.container)) return; debug("over " + e.target.className + ", " + this.container.className + ", " + e.target.isParentNode(this.container)); this.hovering = true; this.update_visibility(); } Navigator.prototype.mouseout_event = function(e) { if(e.relatedTarget && e.relatedTarget.isParentNode(this.container)) return; debug("out " + e.target.className); this.hovering = false; this.update_visibility(); } Navigator.prototype.mousedown_event = function(e) { var x = e.pointerX(); var y = e.pointerY(); var coords = this.get_normalized_coords(x, y); this.center_on_position(coords); } Navigator.prototype.enddrag = function(e) { this.shift_lock_anchor = null; this.locked_to_x = null; this.update_visibility(); } Navigator.prototype.ondrag = function(e) { var coords = this.get_normalized_coords(e.x, e.y); if(e.latest_event.shiftKey != (this.shift_lock_anchor != null)) { /* The shift key has been pressed or released. */ if(e.latest_event.shiftKey) { /* The shift key was just pressed. Remember the position we were at when it was * pressed. */ this.shift_lock_anchor = [coords[0], coords[1]]; } else { /* The shift key was released. */ this.shift_lock_anchor = null; this.locked_to_x = null; } } this.center_on_position(coords); } Navigator.prototype.image_position_changed = function(percent_x, percent_y, height_percent, width_percent) { /* When the image is moved or the visible area is resized, update the cursor rectangle. */ var cursor = this.container.down(".navigator-cursor"); cursor.setStyle({ top: this.img.height * (percent_y - height_percent/2) + "px", left: this.img.width * (percent_x - width_percent/2) + "px", width: this.img.width * width_percent + "px", height: this.img.height * height_percent + "px" }); } Navigator.prototype.get_normalized_coords = function(x, y) { var offset = this.img.cumulativeOffset(); x -= offset.left; y -= offset.top; x /= this.img.width; y /= this.img.height; return [x, y]; } /* x and y are absolute window coordinates. */ Navigator.prototype.center_on_position = function(coords) { if(this.shift_lock_anchor) { if(this.locked_to_x == null) { /* Only change the coordinate with the greater delta. */ var change_x = Math.abs(coords[0] - this.shift_lock_anchor[0]); var change_y = Math.abs(coords[1] - this.shift_lock_anchor[1]); /* Only lock to moving vertically or horizontally after we've moved a small distance * from where shift was pressed. */ if(change_x > 0.1 || change_y > 0.1) this.locked_to_x = change_x > change_y; } /* If we've chosen an axis to lock to, apply it. */ if(this.locked_to_x != null) { if(this.locked_to_x) coords[1] = this.shift_lock_anchor[1]; else coords[0] = this.shift_lock_anchor[0]; } } coords[0] = Math.max(0, Math.min(coords[0], 1)); coords[1] = Math.max(0, Math.min(coords[1], 1)); this.target.fire("viewer:center-on", {x: coords[0], y: coords[1]}); } Navigator.prototype.set_autohide = function(autohide) { this.autohide = autohide; this.update_visibility(); } Navigator.prototype.update_visibility = function() { var box = this.container.down(".image-navigator-box"); var visible = !this.autohide || this.hovering || this.dragger.dragging; box.style.visibility = visible? "visible":"hidden"; } Navigator.prototype.destroy = function() { this.dragger.destroy(); this.handlers.each(function(h) { h.stop(); }); this.dragger = this.handlers = null; this.container.hide(); }