Skip to content

Instantly share code, notes, and snippets.

@kentcdodds
Last active December 21, 2015 05:29
Show Gist options
  • Save kentcdodds/6257506 to your computer and use it in GitHub Desktop.
Save kentcdodds/6257506 to your computer and use it in GitHub Desktop.
InfiniteWPM default files
(function(){
var ENTER_KEYCODE = 13;
var DEFAULT_CAL_LABELS = document.createElement("x-calendar").labels;
function cloneDataMap(data){
return JSON.parse(JSON.stringify(data));
}
// Date utils
/** isValidDateObj: (*) => Boolean
simply checks if the given parameter is a valid date object
**/
function isValidDateObj(d) {
return (d instanceof Date) && !!(d.getTime) && !isNaN(d.getTime());
}
function getYear(d) {
return d.getUTCFullYear();
}
function getMonth(d) {
return d.getUTCMonth();
}
function getDate(d) {
return d.getUTCDate();
}
/** pad: (Number, Number) => String
Pads a number with preceding zeros to be padSize digits long
If given a number with more than padSize digits, truncates the leftmost
digits to get to a padSize length
**/
function pad(n, padSize) {
var str = n.toString();
var padZeros = (new Array(padSize)).join('0');
return (padZeros + str).substr(-padSize);
}
/** iso: Date => String
returns the ISO format representation of a date ("YYYY-MM-DD")
**/
function iso(d) {
return [pad(getYear(d), 4),
pad(getMonth(d)+1, 2),
pad(getDate(d), 2)].join('-');
}
/** fromIso: String => Date/null
Given a string, attempts to parse out a date in YYYY-MM-DD format
If successful, returns the corresponding Date object, otherwise return null
**/
var ISO_DATE_REGEX = /(\d{4})[^\d]?(\d{2})[^\d]?(\d{2})/;
function fromIso(s){
if (isValidDateObj(s)) return s;
var d = ISO_DATE_REGEX.exec(s);
if (d) {
return new Date(d[1],d[2]-1,d[3]);
}
else{
return null;
}
}
/** parseSingleDate: String => Date/null
attempts to parse out the given string as a Date
If successful, returns the corresponding Date object, otherwise return null
Valid input formats include any format with a YYYY-MM-DD format or
is parseable by Date.parse
**/
function parseSingleDate(dateStr){
if(isValidDateObj(dateStr)) return dateStr;
// cross-browser check for ISO format that is not
// supported by Date.parse without implicit time zone
var isoParsed = fromIso(dateStr);
if(isoParsed){
return isoParsed;
}
else{
var parsedMs = Date.parse(dateStr);
if(!isNaN(parsedMs)){
return new Date(parsedMs);
}
return null;
}
}
/** _validateDatepicker: DOM element => Boolean
checks the value of the datepicker and toggles the datepicker's "invalid"
attribute depending on if the value is a valid parsable date string or not
also returns true if the date passes validation and false otherwise
**/
function _validateDatepicker(datepicker){
var input = (datepicker.polyfill) ?
datepicker.xtag.polyfillInput : datepicker.xtag.dateInput;
var inputDate = parseSingleDate(input.value);
if(inputDate)
{
datepicker.removeAttribute("invalid");
}
else{
datepicker.setAttribute("invalid", true);
}
return !!inputDate;
}
/** _updateDatepicker: (DOM element, boolean)
based on the value of the datepicker's input elements, update the value
attribute/property of the datepicker itself
if sendParsed is true, we preparse the value before assigning to
value, otherwise, send the original value
- (ie: if this is true, update the text value of the inputs with the
parsed version as well)
**/
function _updateDatepicker(datepicker, sendParsed){
var input = (datepicker.polyfill) ?
datepicker.xtag.polyfillInput :
datepicker.xtag.dateInput;
var rawVal = input.value;
var parsedDate = parseSingleDate(rawVal);
datepicker.value = (sendParsed && parsedDate) ? parsedDate : rawVal;
}
/* pass this a callback function to execute while being observed;
* if the submit value of the datepicker is changed during the callback,
* fire the change event on the datepicker;
* if alsoWatchRawVal is true, will also trigger a change event if the
* .value changes, not just if the .submitValue changes */
function _watchForChange(datepicker, callback, alsoWatchRawVal){
var oldVal = datepicker.submitValue;
var oldRawVal = datepicker.value;
callback();
var newVal = datepicker.submitValue;
var newRawVal = datepicker.value;
if(oldVal !== newVal || (alsoWatchRawVal && oldRawVal !== newRawVal))
{
xtag.fireEvent(datepicker, "change");
}
}
function getPlaceholderText(datepicker){
var labels = datepicker.xtag._labels;
return (new Array(5).join(labels.yearAbbr) + "-" +
new Array(3).join(labels.monthAbbr) + "-" +
new Array(3).join(labels.dayAbbr));
}
xtag.register("x-datepicker", {
lifecycle: {
created: function(){
this.innerHTML = "";
var dateInput = document.createElement("input");
dateInput.setAttribute("type", "date");
xtag.addClass(dateInput, "x-datepicker-input");
this.appendChild(dateInput);
this.xtag.dateInput = dateInput;
// datepicker unique labels
this.xtag._labels = {
yearAbbr: "Y",
monthAbbr: "M",
dayAbbr: "D"
};
// calendar-specific labels
this.xtag._polyfillCalLabels = cloneDataMap(DEFAULT_CAL_LABELS);
this.xtag.polyfillInput = null;
this.xtag.polyfillUI = null;
// initialize polyfill with detected support
this.polyfill = (this.hasAttribute("polyfill") ||
dateInput.type.toLowerCase() !== "date");
}
},
events: {
// handle calendar UI input
"datetoggleon:delegate(x-calendar)": function(e){
var xCal = this;
var datepicker = e.currentTarget;
if((!e.detail) || (!e.detail.date)){
return;
}
var selectedDate = parseSingleDate(e.detail.date);
_watchForChange(datepicker, function(){
datepicker.value = (selectedDate) ? iso(selectedDate) : "";
xtag.fireEvent(datepicker, "input");
});
},
"datetoggleoff:delegate(x-calendar)": function(e){
e.currentTarget.value = null;
},
"focus": function(e){
e.currentTarget.setAttribute("focused", true);
},
"blur:delegate(.x-datepicker-input)": function(e){
e.currentTarget.removeAttribute("focused");
},
"blur:delegate(.x-datepicker-polyfill-input)": function(e){
var datepicker = e.currentTarget;
datepicker.removeAttribute("focused");
// send parsed version to ensure that text of input matches
_watchForChange(datepicker, function(){
_updateDatepicker(datepicker, true);
}, true);
},
// force readonly state as soon as touch event is detected
// to prevent mobile keyboard from popping up from now on
"touchstart:delegate(.x-datepicker-polyfill-input)": function(e){
this.setAttribute("readonly", true);
},
"tapstart:delegate(x-calendar)": function(e){
e.preventDefault(); // prevent blurring of polyfill input
},
"keypress:delegate(.x-datepicker-polyfill-input)": function(e){
var keyCode = e.keyCode;
var datepicker = e.currentTarget;
if(keyCode === ENTER_KEYCODE){
// send parsed version to ensure that text of input matches
_watchForChange(datepicker, function(){
_updateDatepicker(datepicker, true);
}, true);
}
},
// handle UI changes to the native date input
"input:delegate(.x-datepicker-input)": function(e){
var datepicker = e.currentTarget;
_watchForChange(datepicker, function(){
// send parsed version to ensure that value of native
// input matches
_updateDatepicker(datepicker, true);
// redirect event target
e.stopPropagation();
xtag.fireEvent(datepicker, "input");
});
},
// handles UI changes to the polyfill input
"input:delegate(.x-datepicker-polyfill-input)": function(e){
// _DONT_ send parsed verison when using polyfill in order to
// prevent the input from constantly overriding the user's
// text as they are typing
var datepicker = e.currentTarget;
_watchForChange(datepicker, function(){
_updateDatepicker(datepicker, false);
// redirect event target
e.stopPropagation();
xtag.fireEvent(datepicker, "input");
});
},
// simply redirect change event target if native
"change:delegate(.x-datepicker-input)": function(e){
e.stopPropagation();
xtag.fireEvent(e.currentTarget, "change");
},
"change:delegate(.x-datepicker-polyfill-input)": function(e){
// change event is different for text input,
// so prevent from bubbling
e.stopPropagation();
// _DONT_ send parsed verison when using polyfill in order to
// prevent the input from constantly overriding the user's
// text as they are typing
var datepicker = e.currentTarget;
_watchForChange(datepicker, function(){
_updateDatepicker(datepicker, false);
});
}
},
accessors: {
"name": {
attribute: {selector: ".x-datepicker-input"},
set: function(newName){
var dateInput = this.xtag.dateInput;
if(newName === null || newName === undefined){
dateInput.removeAttribute("name");
}
else{
dateInput.setAttribute("name", newName);
}
}
},
// returns the value that should be submitted to a form
// note: even if no name attribute is present, still return what
// should be submitted for cases of dynamic submission
"submitValue":{
get: function(){
return this.xtag.dateInput.value;
}
},
// handles the currently displayed value of the datepicker
"value": {
attribute: {
skip: true
},
get: function(){
return (this.polyfill) ? this.xtag.polyfillInput.value :
this.xtag.dateInput.value;
},
// if given null/undefined, deletes the value;
// always saves either the date in ISO-format or empty
// if the input date is invalid to the dateinput
// (this is what actually gets submitted);
// if given a value different than the user-input-box value,
// updates the input's value to the parsed ISO date, otherwise
// leaves it alone (this is what the user sees)
set: function(rawDateVal){
var parsedDate = parseSingleDate(rawDateVal);
var isoStr = (parsedDate) ? iso(parsedDate) : null;
var dateInput = this.xtag.dateInput;
var polyfillInput = this.xtag.polyfillInput;
var polyfillUI = this.xtag.polyfillUI;
// if prompted to remove value
if(rawDateVal === null || rawDateVal === undefined){
this.removeAttribute("value");
dateInput.value = "";
if(polyfillInput){
polyfillInput.value = "";
}
if(polyfillUI){
polyfillUI.chosen = null;
// note that we don't reset calendar's view,
// we may want to choose where we left off
}
}
else{
var finalVal = (isoStr) ? isoStr : rawDateVal;
// only override input's text value if given something
// different, in order to prevent having us override
// text as the user types
var attrVal;
if(polyfillInput){
if(rawDateVal !== polyfillInput.value){
polyfillInput.value = finalVal;
attrVal = finalVal;
}
// otherwise, match the value attribute to whats
// displayed
else{
attrVal = rawDateVal;
}
}
else{
attrVal = finalVal;
}
this.setAttribute("value", attrVal);
// make sure the date input (ie: what actually
// would submit in a form) either contains a valid date
// or is blanked; also make sure calendar displays
// a valid date
if(isoStr){
dateInput.value = isoStr;
if(polyfillUI){
polyfillUI.chosen = parsedDate;
polyfillUI.view = parsedDate;
}
}
else{
dateInput.value = "";
if(polyfillUI){
polyfillUI.chosen = null;
}
// note that we don't reset calendar view, as we
// may want to choose from where we left off
}
}
// update the "invalid" class of the datepicker
_validateDatepicker(this);
}
},
// handles whether to display as a polyfill or not
// note: in polyfill mode, we essentially keep the original
// input, but as a hidden input that receives parsed dates only.
// In order to allow user-input, we show a second text input
// (which is nameless to prevent form submission) that is tied to
// a calendar UI element
"polyfill": {
attribute: {boolean: true},
set: function(isPolyfill){
var dateInput = this.xtag.dateInput;
// turn on polyfill elements (creating them if they
// aren't initialized yet)
if(isPolyfill){
// hide the "true" submitted input from UI view
dateInput.setAttribute("type", "hidden");
dateInput.setAttribute("readonly", true);
// create the "fake" input to act as a middleman
// between user input and the parsed-date-only input
if(!this.xtag.polyfillInput){
var polyfillInput = document.createElement("input");
xtag.addClass(polyfillInput,
"x-datepicker-polyfill-input");
polyfillInput.setAttribute("type", "text");
polyfillInput.setAttribute("placeholder",
getPlaceholderText(this));
polyfillInput.value = this.xtag.dateInput.value;
this.xtag.polyfillInput = polyfillInput;
this.appendChild(polyfillInput);
}
this.xtag.polyfillInput.removeAttribute("disabled");
// creates the calendar UI element to associate with
// the datepicker
if(!this.xtag.polyfillUI){
var polyfillUI=document.createElement("x-calendar");
xtag.addClass(polyfillUI,
"x-datepicker-polyfill-ui");
polyfillUI.chosen = this.value;
polyfillUI.view = this.xtag.dateInput.value;
polyfillUI.controls = true;
polyfillUI.labels = this.xtag._polyfillCalLabels;
this.xtag.polyfillUI = polyfillUI;
this.appendChild(polyfillUI);
}
}
// turn off polyfill elements (but don't remove them)
else{
dateInput.setAttribute("type", "date");
dateInput.removeAttribute("readonly");
var polyInput = this.xtag.polyfillInput;
if(polyInput){
polyInput.setAttribute("disabled", true);
}
}
}
},
"labels": {
get: function(){
// merge both the datepicker-only and calendar-only
// labels into a single dict
var allLabels = {};
var datepickerLabels = this.xtag._labels;
var calendarLabels = this.xtag._polyfillCalLabels;
for(var key in datepickerLabels){
allLabels[key] = datepickerLabels[key];
}
for(var key in calendarLabels){
allLabels[key] = calendarLabels[key];
}
// prevent any aliases from slipping through
return cloneDataMap(allLabels);
},
set: function(newLabelData){
var calendar = this.xtag.polyfillUI;
var polyInput = this.xtag.polyfillInput;
// set calendar labels
if(calendar){
calendar.labels = newLabelData;
this.xtag._polyfillCalLabels = calendar.labels;
}
else{
var polyLabels = this.xtag._polyfillCalLabels;
for(var key in polyLabels){
if(key in newLabelData){
polyLabels[key] = newLabelData[key];
}
}
}
// replace datepicker specific labels
var datepickerLabels = this.xtag._labels;
for(var key in datepickerLabels){
if(key in newLabelData){
datepickerLabels[key] = newLabelData[key];
}
}
// rerender placeholder text
if(polyInput){
polyInput.setAttribute("placeholder",
getPlaceholderText(this));
}
}
}
}
});
})();
class ArticlesController < ContentController
before_filter :login_required, :only => [:preview]
before_filter :auto_discovery_feed, :only => [:show, :index]
before_filter :verify_config
layout :theme_layout, :except => [:comment_preview, :trackback]
cache_sweeper :blog_sweeper
caches_page :index, :read, :archives, :view_page, :redirect, :if => Proc.new {|c|
c.request.query_string == ''
}
helper :'admin/base'
def index
respond_to do |format|
format.html { @limit = this_blog.limit_article_display }
format.rss { @limit = this_blog.limit_rss_display }
format.atom { @limit = this_blog.limit_rss_display }
end
unless params[:year].blank?
@noindex = 1
@articles = Article.published_at(params.values_at(:year, :month, :day)).page(params[:page]).per(@limit)
else
@noindex = 1 unless params[:page].blank?
@articles = Article.published.page(params[:page]).per(@limit)
end
@page_title = index_title
@description = index_description
@keywords = (this_blog.meta_keywords.empty?) ? "" : this_blog.meta_keywords
suffix = (params[:page].nil? and params[:year].nil?) ? "" : "/"
@canonical_url = url_for(:only_path => false, :controller => 'articles', :action => 'index', :page => params[:page], :year => params[:year], :month => params[:month], :day => params[:day]) + suffix
respond_to do |format|
format.html { render_paginated_index }
format.atom do
render_articles_feed('atom')
end
format.rss do
auto_discovery_feed(:only_path => false)
render_articles_feed('rss')
end
end
end
def search
@canonical_url = url_for(:only_path => false, :controller => 'articles', :action => 'search', :page => params[:page], :q => params[:q])
@articles = this_blog.articles_matching(params[:q], :page => params[:page], :per_page => @limit)
return error(_("No posts found..."), :status => 200) if @articles.empty?
@page_title = this_blog.search_title_template.to_title(@articles, this_blog, params)
@description = this_blog.search_desc_template.to_title(@articles, this_blog, params)
respond_to do |format|
format.html { render 'search' }
format.rss { render "index_rss_feed", :layout => false }
format.atom { render "index_atom_feed", :layout => false }
end
end
def live_search
@search = params[:q]
@articles = Article.search(@search)
render :live_search, :layout => false
end
def preview
@article = Article.last_draft(params[:id])
@canonical_url = ""
render 'read'
end
def check_password
return unless request.xhr?
@article = Article.find(params[:article][:id])
if @article.password == params[:article][:password]
render :partial => 'articles/full_article_content', :locals => { :article => @article }
else
render :partial => 'articles/password_form', :locals => { :article => @article }
end
end
def redirect
from = split_from_path params[:from]
match_permalink_format from, this_blog.permalink_format
return show_article if @article
# Redirect old version with /:year/:month/:day/:title to new format,
# because it's changed
["%year%/%month%/%day%/%title%", "articles/%year%/%month%/%day%/%title%"].each do |part|
match_permalink_format from, part
return redirect_to @article.permalink_url, :status => 301 if @article
end
r = Redirect.find_by_from_path(from.join("/"))
return redirect_to r.full_to_path, :status => 301 if r
render "errors/404", :status => 404
end
### Deprecated Actions ###
def archives
@articles = Article.find_published
@page_title = this_blog.archives_title_template.to_title(@articles, this_blog, params)
@keywords = (this_blog.meta_keywords.empty?) ? "" : this_blog.meta_keywords
@description = this_blog.archives_desc_template.to_title(@articles, this_blog, params)
@canonical_url = url_for(:only_path => false, :controller => 'articles', :action => 'archives')
end
def comment_preview
if (params[:comment][:body].blank? rescue true)
render :nothing => true
return
end
set_headers
@comment = Comment.new(params[:comment])
@controller = self
end
def category
redirect_to categories_path, :status => 301
end
def tag
redirect_to tags_path, :status => 301
end
def view_page
if(@page = Page.find_by_name(Array(params[:name]).map { |c| c }.join("/"))) && @page.published?
@page_title = @page.title
@description = (this_blog.meta_description.empty?) ? "" : this_blog.meta_description
@keywords = (this_blog.meta_keywords.empty?) ? "" : this_blog.meta_keywords
@canonical_url = @page.permalink_url
else
render "errors/404", :status => 404
end
end
# TODO: Move to TextfilterController?
def markup_help
render :text => TextFilter.find(params[:id]).commenthelp
end
private
def verify_config
if ! this_blog.configured?
redirect_to :controller => "setup", :action => "index"
elsif User.count == 0
redirect_to :controller => "accounts", :action => "signup"
else
return true
end
end
# See an article We need define @article before
def show_article
@comment = Comment.new
@page_title = this_blog.article_title_template.to_title(@article, this_blog, params)
@description = this_blog.article_desc_template.to_title(@article, this_blog, params)
article_meta
auto_discovery_feed
respond_to do |format|
format.html { render "articles/#{@article.post_type}" }
format.atom { render_feedback_feed('atom') }
format.rss { render_feedback_feed('rss') }
format.xml { render_feedback_feed('atom') }
end
rescue ActiveRecord::RecordNotFound
error("Post not found...")
end
def article_meta
groupings = @article.categories + @article.tags
@keywords = groupings.map { |g| g.name }.join(", ")
@canonical_url = @article.permalink_url
end
def render_articles_feed format
if this_blog.feedburner_url.empty? or request.env["HTTP_USER_AGENT"] =~ /FeedBurner/i
render "index_#{format}_feed", :layout => false
else
redirect_to "http://feeds2.feedburner.com/#{this_blog.feedburner_url}"
end
end
def render_feedback_feed format
@feedback = @article.published_feedback
render "feedback_#{format}_feed", :layout => false
end
def set_headers
headers["Content-Type"] = "text/html; charset=utf-8"
end
def render_paginated_index(on_empty = _("No posts found..."))
return error(on_empty, :status => 200) if @articles.empty?
if this_blog.feedburner_url.empty?
auto_discovery_feed(:only_path => false)
else
@auto_discovery_url_rss = "http://feeds2.feedburner.com/#{this_blog.feedburner_url}"
@auto_discovery_url_atom = "http://feeds2.feedburner.com/#{this_blog.feedburner_url}"
end
render 'index'
end
def index_title
if params[:year]
return this_blog.archives_title_template.to_title(@articles, this_blog, params)
elsif params[:page]
return this_blog.paginated_title_template.to_title(@articles, this_blog, params)
else
this_blog.home_title_template.to_title(@articles, this_blog, params)
end
end
def index_description
if params[:year]
return this_blog.archives_desc_template.to_title(@articles, this_blog, params)
elsif params[:page]
return this_blog.paginated_desc_template.to_title(@articles, this_blog, params)
else
this_blog.home_desc_template.to_title(@articles, this_blog, params)
end
end
def time_delta(year, month = nil, day = nil)
from = Time.mktime(year, month || 1, day || 1)
to = from.next_year
to = from.next_month unless month.blank?
to = from + 1.day unless day.blank?
to = to - 1 # pull off 1 second so we don't overlap onto the next day
return from..to
end
def split_from_path path
parts = path.split '/'
parts.delete('')
if parts.last =~ /\.atom$/
request.format = 'atom'
parts.last.gsub!(/\.atom$/, '')
elsif parts.last =~ /\.rss$/
request.format = 'rss'
parts.last.gsub!(/\.rss$/, '')
end
parts
end
def match_permalink_format parts, format
specs = format.split('/')
specs.delete('')
return if parts.length != specs.length
article_params = {}
specs.zip(parts).each do |spec, item|
if spec =~ /(.*)%(.*)%(.*)/
before_format = $1
format_string = $2
after_format = $3
result = item.gsub(/^#{before_format}(.*)#{after_format}$/, '\1')
article_params[format_string.to_sym] = result
else
return unless spec == item
end
end
begin
@article = this_blog.requested_article(article_params)
rescue
#Not really good.
# TODO :Check in request_article type of DATA made in next step
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment