In order to do this we need a few things. These will be explained in detail later on.
- The default content (the content which is there when closing the overlay)
- The overlay content
- A return path, i.e. the desired path when closing the overlay
TODO:
- History.pushState should be used instead of replaceState
- Support navigating through history and show/hide overlay
Say that you have the following routes, having a grid of images on every page and on some pages there's an overlay where content will be loaded into.
"/work" # work — grid with all images
"/work/category" # work_category — grid with images from this category
"/work/1-title" # work_detail — work detail overlay + grid with all images
"/about" # about — about overlay + grid with all images
When I visit /work
, I should see all the items in the grid and no overlay.
When I visit /work/category
, I should see all the items from that category and no overlay.
When I visit /work/1-title
, I should see all items below the overlay and the work item content in the overlay.
When I visit /about
, I should see all items below the overlay and the about content in the overlay.
When I fetch /work
, I should do nothing special, because there is no overlay content here.
When I fetch /work/category
, I should do nothing special, because there is no overlay content here.
When I fetch /work/1-title
, I should present only the overlay content.
When I fetch /about
, I should present only the overlay content.
For /work
, I should get all the items in the grid.
For /work/category
, I should get all the items from that category in the grid.
For /work/1-title
, I should get the work item content and nothing else.
For /about
, I should get the about content and nothing else.
# Helpers
module ApplicationHelper
def return_path
case controller.action_name
when "work_detail" then "/work"
when "about" then "/"
else request.path
end
end
def return_title
...
end
end
<body data-return-path="<%= return_path %>" data-return-title="<% return_title %>">
...
</body>
When you visit a page that routes to the work_detail action, an overlay will be shown. When we close this overlay, we will go to the /work
path.
Say that the grid is a partial that you can render into your html.
Then we can do the following:
<!-- Grid Partial -->
<grid>
<% images.each do |img| %>
<img src="<%= img.src %>" />
<% end %>
</grid>
<!-- Page HTML, layout, whatever, ... -->
<% if should_show_default_content %>
<%= render partial: "grid" %>
<% elsif should_hide_default_content %>
<script class="hidden-default-content" type="text/html">
<%= render partial: "grid" %>
</script>
<% else %>
<%# Don't render anything %>
<% end %>
should_show_default_content
applies to the/work
and/work/category
routes.should_hide_default_content
applies to the/work/1-title
and/about
routes, but not via AJAX/XHR.else
applies to the/work/1-title
and/about
routes, only via AJAX/XHR.
# Helpers
module ApplicationHelper
DEFAULT_PAGES = ["work", "work_category"]
def should_show_default_content
# true if if the current page is a default page (i.e. not a overlay page)
case controller.action_name
when *DEFAULT_PAGES then true
else false
end
end
def should_hide_default_content
# true if it is not an AJAX request
!request.xhr?
end
end
// show_hidden_default_content.js
$(".hidden-default-content").each(function() {
$(this).replaceWith(this.innerHTML);
});
<div class="should-belong-in-overlay" style="display: none;">
E.G. ABOUT
</div>
This way search engines can see the html for the actual content on that route, but you won't see an initial flash of the content in the browser. The next step is to move this html into the overlay, by using javascript.
// should_belong_in_overlay.js
var $sbio = $(".should-belong-in-overlay").detach();
var html = $sbio.html();
if (html) {
overlay.append_content(html);
overlay.show()
}
Intercept the request if it's an xhr request and then return json instead of html. The following piece of code will return the title and the html of the template that was supposed to be rendered (without the layout).
module ApplicationHelper
def title
"Title and stuff"
end
end
class PagesController < ...
before_filter :return_json_when_xhr
...
private
def return_json_when_xhr
if request.xhr?
render json: {
title: view_context.title,
html: render_to_string(template: "pages/#{self.action_name}", layout: false)
}
# nothing should happen after this
return false
end
end
end
Here we will use the History API, without a fallback. That is, on old browsers the page will reload, but it will still work. You can copy the following pretty much completely. You only have to replace the NAMESPACE
parts, etc.
// navigation.js
(function() {
"use strict";
function NS() {
this.state = {};
this.retrieve_return_pathname();
this.retrieve_return_document_title();
}
//
// Checks
//
NS.prototype.can_stay_on_the_same_page = function() {
return Modernizr.history;
};
//
// Getters
//
NS.prototype.retrieve_return_pathname = function() {
var return_pathname =
document.body.getAttribute("data-return-path") ||
window.location.pathname;
// chomp it
return_pathname = return_pathname.length > 1 ?
return_pathname.replace(/\/$/, "") :
return_pathname;
// state
this.state.return_pathname = return_pathname;
};
NS.prototype.retrieve_return_document_title = function() {
var return_document_title =
document.body.getAttribute("data-return-document-title") ||
document.title;
// state
this.state.return_document_title = return_document_title;
};
//
// Setters
//
NS.prototype.set_document_title = function(title) {
document.title = title;
};
NS.prototype.go_to_page = function(pathname, title, skip_replace) {
var associated_path, associated_path_split,
$header, $li, $previous_li, $active_li;
// chomp
pathname = pathname.length > 1 ?
pathname.replace(/\/$/, "") :
pathname;
// document title
this.set_document_title(title, pathname);
// replace url if needed
if (!skip_replace) {
if (this.can_stay_on_the_same_page()) {
history.replaceState({}, title, pathname);
} else {
window.location.href = pathname;
}
}
};
NS.prototype.go_to_return_page = function(skip_replace) {
this.go_to_page(this.state.return_pathname, this.state.return_document_title, skip_replace);
};
//
// Make an instance
//
window.NAMESPACE.initialize_navigation = function() {
var instance = new NS();
window.NAMESPACE.navigation = instance;
};
}());
// overlay_triggers.js
(function() {
"use strict";
function OT() {
this.bind_events();
}
//
// Content
//
OT.prototype.add_content_via_url = function(url) {
var dfd = $.Deferred();
var ot = this;
$
.when(this.get_content(url))
.then(function(obj) {
ot.add_content(obj);
dfd.resolve(obj);
}, function() {
dfd.reject();
});
return dfd.promise();
};
OT.prototype.get_content = function(url) {
var dfd = $.Deferred();
$.ajax(url, {
contentType: "json",
success: function(response) {
dfd.resolve({
content_html: response.html,
document_title: response.title
});
},
error: function() {
dfd.reject();
}
});
return dfd.promise();
};
OT.prototype.add_content = function(obj) {
var $elem = $(obj.content_html).css("display", "none"),
self = this;
overlay.append_content($elem);
$elem.css("display", "block");
$elem = null;
};
//
// Events
//
OT.prototype.bind_events = function() {
$(document.body).on(
"click.overlay_trigger",
".overlay-trigger",
$.proxy(this.overlay_trigger_click_handler, this)
);
$(window).on(
"overlay.hide.default",
$.proxy(this.overlay_hide_handler, this)
);
};
OT.prototype.overlay_trigger_click_handler = function(e) {
var href = e.currentTarget.getAttribute("href");
var title;
// prevent default
e.preventDefault();
// show overlay and load content
if (NAMESPACE.navigation.can_stay_on_the_same_page()) {
overlay.show();
$.when(this.add_content_via_url(href))
.then(function(obj) {
NAMESPACE.navigation.go_to_page(href, obj.document_title);
});
} else {
NAMESPACE.navigation.go_to_page(href, null);
}
};
OT.prototype.overlay_hide_handler = function(e) {
NAMESPACE.navigation.go_to_return_page();
};
//
// Make an instance
//
window.NAMESPACE.initialize_overlay_triggers = function() {
var instance = new OT();
window.NAMESPACE.overlay_triggers = instance;
};
}());