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 imagesWhen 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_contentapplies to the/workand/work/categoryroutes.should_hide_default_contentapplies to the/work/1-titleand/aboutroutes, but not via AJAX/XHR.elseapplies to the/work/1-titleand/aboutroutes, only via AJAX/XHR.
# Helpers
module ApplicationHelper
PAGES_WITH_ONLY_DEFAULT_CONTENT = [
"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 *PAGES_WITH_ONLY_DEFAULT_CONTENT 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);
});Add this plugin to your project.
Include the javascript and css.
And then make an instance of the overlay.
(function() {
"use strict";
NAMESPACE.overlay = new Overlay();
}());<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
// replace 'NAMESPACE.overlay' with your overlay instance
var $sbio = $(".should-belong-in-overlay").detach();
var html = $sbio.html();
if (html) {
NAMESPACE.overlay.append_content(html);
NAMESPACE.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
endclass PagesController < ...
def action
@getting_stuff = FromTheDatabase.all
return_json_when_xhr
end
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
endHere 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.
// router.js
(function() {
"use strict";
function Router() {
this.state = {};
this.retrieve_return_pathname();
this.retrieve_return_document_title();
}
//
// Checks
//
Router.prototype.can_stay_on_the_same_page = function() {
return Modernizr.history;
};
//
// Getters
//
Router.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;
};
Router.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
//
Router.prototype.set_document_title = function(title) {
document.title = title;
};
Router.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;
}
}
};
Router.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_router = function() {
var instance = new Router();
window.NAMESPACE.router = 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;
NAMESPACE.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.router.can_stay_on_the_same_page()) {
NAMESPACE.overlay.show();
$.when(this.add_content_via_url(href))
.then(function(obj) {
NAMESPACE.router.go_to_page(href, obj.document_title);
});
} else {
NAMESPACE.router.go_to_page(href, null);
}
};
OT.prototype.overlay_hide_handler = function(e) {
NAMESPACE.router.go_to_return_page();
};
//
// Make an instance
//
window.NAMESPACE.initialize_overlay_triggers = function() {
var instance = new OT();
window.NAMESPACE.overlay_triggers = instance;
};
}());