oxjs/source/Ox.UI/js/Core/Ox.URL.js

400 lines
14 KiB
JavaScript
Raw Normal View History

2011-07-29 18:48:43 +00:00
// vim: et:ts=4:sw=4:sts=4:ft=javascript
2011-04-22 22:03:10 +00:00
/*@
Ox.URL <f> URL controller
(options) -> <o> URL controller
options <o> Options object
findKeys <[o]> Find keys {id: "", type: ""}
type can be "string" or "number"
getItemId <f> Tests if a string matches an item
(string, callback) -> <u> undefined
string <s> The string to be tested
callback <f> callback function
id <s> Matching item id, or empty
getSpanId <f> Tests if a string matches a span
(string, callback) -> <u> undefined
string <s> The string to be tested
callback <f> callback function
id <s> Matching span id, or empty
pages <[s]> List of pages
sortKeys <[o]> Sort keys {id: "", operator: ""}
operator is the default operator ("+" or "-")
types <[s]> List of types
views <o> List of views {type: {'list': [...], 'item': [...]}}
@*/
/*
example.com[/page]
or
example.com[/type][/item][/view][/span][/sort][/find]
page Special page, like "about" or "contact"
type Section a.k.a. item type, like "movies", "edits", "texts" etc.
item Item id or title, like in '/movies/0060304', '/movies/inception' or
'texts/ABC'. Testing this is asynchonous.
view List or item view, like "clips" or "map". Both list and item views are
per type.
span Position or selection in a view, either one or two coordinates or one
id, like in "video/01:00", "video/-01:00", "video/01:00,-01:00",
"video/annotationABC", "video/subtitles:23", "text/chapter42",
"map/0,0", "map/-45,-90,45,90", "map/Barcelona", "image/100,100" etc.
Testing id is asynchronous.
sort Sort, like "title" or "-director" or "country,year,-language,+runtime"
find Query, like a=x or a=x&b=y or a=x&(b=y|c=z). A query object has the form
{conditions: [], operator: ''} (logical operator), and a condition
object has the form {key: '', value: '' or ['', ''], operator: ''}
(comparison operator) or {conditions: [], operator: ''} (logical
operator). Condition strings can be more than just "k=v", see below.
String Key Value Operator
v * v = any text or string contains or any number is
!v * v != no text or string contains and no number is
k=v k v = contains (text or string), is (number)
k!=v k v != does not contain (text or string), is not (number)
k==v k v == is (string)
k!==v k v !== is not (string)
k=v* k v ^ starts with (string)
k!=v* k v !^ does not start with (string)
k=*v k v $ ends with (string)
k!=*v k v !$ does not end with (string)
k<v k v < is less than (number)
k!<v k v !< is not less than (number)
k>v k v > is more than (number)
k!>v k v !> is not more than (number)
k=v:w k [v,w] = is between (number), is (string)
k!=v:w k [v,w] != is not between (number), is not (string)
All parts of the URL can be omitted, as long as the order is preserved.
example.com/foo
If "foo" is not a type, item (of the default type), view (of the default
type), span id (of the default type's default view) or sort key (of the
default type's default view), then this means find *=foo
example.com/title, or example.com/+title or example.com/-title
If this neither matches a type or default type item, then this will be sort
example.com/clip/+duration/title=foo
If "clip" is a default type list view, this will show all clips of items
that match title=foo, sorted by item duration in ascending order
example.com/clip/+clip.duration/subtitles=foo
If "clip" is a default type list view and "subtitles" is an annotation type,
this will show all clips that match subtitles=foo, sorted by clip duration
in ascending order. (In pan.do/ra's clip view, annotation=foo is always per
clip. There is no way to show all clips of all items where any clip matches
subtitles=foo, this doesn't seem to be needed.)
example.com/map/Paris/duration/title!=london
Ff "map" is a default type list view and "Paris" is a place id, this will
zoom the map to Paris, show all places of items that match title!=london,
and when a place is selected sort matching clips by item duration in
default order.
example.com/calendar/1900,2000/clip.duration/event=hiroshima
If "calendar" is a default type list view, this will zoom the calendar to
the 20th century, show all events of all items that match event=hiroshima,
and when an event is selected sort matching clips by clip duration in
default order. (In pan.do/ra's map and calendar view, annotation=foo is
always per item. There is no way to show all events of all clips that match
event=hiroshima, this doesn't seem to be needed.)
*/
Ox.URL = function(options) {
var self = {}, that = {};
self.options = Ox.extend({
findKeys: [],
getItemId: null,
getSpanId: null,
pages: [],
sortKeys: [],
types: [],
views: {}
}, options);
self.sortKeyIds = self.options.sortKeys.map(function(sortKey) {
return sortKey.id;
});
function constructCondition(condition) {
var key = condition.key == '*' ? '' : condition.key,
operator = condition.operator,
value = (
Ox.isArray(condition.value) ? condition.value : [condition.value]
).map(encodeValue).join(':');
if (!key) {
operator = operator.replace('=', '');
} else if (operator.indexOf('^') > -1) {
operator = operator.replace('^', '=');
value += '*';
} else if (operator.indexOf('$') > -1) {
operator = operator.replace('$', '=');
value = '*' + value;
}
return [key, operator, value].join('');
}
function constructDuration(duration) {
return Ox.formatDuration(duration, 3).replace(/\.000$/, '');
}
function constructFind(find) {
return find.conditions.map(function(condition) {
var ret;
if (condition.conditions) {
ret = '(' + constructFind(condition) + ')';
} else {
ret = constructCondition(condition);
}
return ret;
}).join(find.operator);
}
function constructSort(sort) {
return sort.map(function(sort) {
return (
sort.operator == Ox.getObjectById(self.options.sortKeys, sort.key).operator
? '' : sort.operator
) + sort.key;
}).join(',')
}
function constructSpan(span) {
return span.map(function(point) {
return /^[0-9-\.:]+$/.test(str) ? constuctDuration(point) : point;
}).join(',');
}
function constructURL(state) {
var parts = [];
if (self.options.types.indexOf(state.type) > 0) {
parts.push(state.type);
}
if (state.item) {
parts.push(item);
}
if (self.options.views[state.type][
state.item ? 'item' : 'list'
].indexOf(state.view) > 0) {
parts.push(state.view);
}
if (state.span) {
parts.push(constructSpan(state.span));
}
if (state.sort.length) {
parts.push(constructSort(state.sort));
}
if (state.find) {
parts.push(constructFind(state.find));
}
return parts.join('/');
}
function decodeValue(str) {
return decodeURIComponent(str);
}
function encodeValue(str) {
var chars = '/&|()=*:';
ret = '';
str.split('').forEach(function(char) {
var index = chars.indexOf(char);
ret += index > -1
? '%' + char.charCodeAt(0).toString(16).toUpperCase()
: char;
});
return ret;
}
function parseCondition(str) {
var condition,
operators = ['!==', '==', '!=', '=', '!<', '<', '!>', '>'],
split;
Ox.forEach(operators, function(operator) {
if (str.indexOf(operator) > - 1) {
split = str.split(operator);
condition = {
key: split.shift(),
value: split.join(operator),
operator: operator
};
return false;
}
});
if (!condition.operator) {
condition = {key: '*', value: str, operator: '='};
}
if (['=', '!='].indexOf(condition.operator) > -1) {
if (condition.value[0] == '*') {
condition.value = condition.value.substr(1);
condition.operator = condition.operator.replace('=', '$')
} else if (condition.value[condition.value.length - 1] == '*') {
condition.value = condition.value.substr(0, condition.value.length - 1);
condition.operator = condition.operator.replace('=', '^')
}
}
if (condition.value.indexOf(':') > -1) {
condition.value = condition.value.split(':')
}
return condition;
}
function parseDuration(str) {
var parts = str.split(':').reverse();
while (parts.length > 3) {
parts.pop();
}
return parts.reduce(function(prev, curr, i) {
return prev + (parseFloat(curr) || 0) * Math.pow(60, i);
}, 0);
}
function parseFind(str) {
var conditions, counter = 0,
find = {conditions: [], operator: '&'},
subconditions = [];
if (str.length) {
// replace subconditions with placeholder,
// so we can later split by main operator
Ox.forEach(str, function(c, i) {
if (c == ')') {
counter--;
}
if (counter >= 1) {
subconditions[subconditions.length - 1] += c;
}
if (c == '(') {
(++counter == 1) && subconditions.push('');
}
});
subconditions.forEach(function(subcondition, i) {
str = str.replace(subcondition, i);
});
find.operator = str.indexOf('|') > -1 ? '|' : '&'
find.conditions = str.split(find.operator).map(function(condition, i) {
var ret;
if (condition[0] == '(') {
// re-insert subcondition
ret = parseFind(subconditions[parseInt(Ox.sub(condition, 1, -1))]);
} else {
ret = parseCondition(condition);
}
return ret;
});
}
return find;
}
function parseSort(str) {
return str.split(',').map(function(str) {
var hasOperator = /^[\+-]/.test(str);
return {
key: hasOperator ? str.substr(1) : str,
operator: hasOperator ? str[0] : Ox.getObjectById(self.options.sortKeys, str).operator
};
});
}
function parseSpan(str, callback) {
return str.split(',').map(parseDuration);
}
function parseURL(str, callback) {
str = str || document.location.pathname
+ document.location.search + document.location.hash;
var parts = str.substr(1).split('/'),
state = {};
if (parts.length == 0) {
state.page = '';
} else if (self.options.pages.indexOf(parts[0]) > -1) {
state.page = parts[0];
} else {
if (self.options.types.indexOf(parts[0]) > -1) {
state.type = parts[0];
parts.shift();
}
if (parts.length) {
if (self.options.views.list.indexOf(parts[0]) > -1) {
state.item = '';
state.view = parts[0];
parts.shift();
parseBeyondItem(false);
} else {
self.options.getItemId(parts[0], function(itemId) {
if (itemId) {
state.item = itemId;
parts.shift();
}
parseBeyondItem(!!itemId);
});
}
} else {
callback(state);
}
}
function parseBeyondItem(itemId) {
if (parts.length && itemId && self.options.views.item.indexOf(parts[0]) > -1) {
state.view = parts[0];
parts.shift();
}
if (parts.length) {
if (parts.split(',').every(function(str) {
return /^[0-9-\.:]+$/.test(str);
})) {
state.span = parseSpan(parts[0]);
parts.shift()
parseBeyondSpan();
} else {
self.options.getSpanId(parts[0], function(spanId) {
if (spanId) {
state.span = spanId;
parts.shift();
}
parseBeyondSpan();
});
}
} else {
callback(state);
}
}
function parseBeyondSpan() {
if (parts.length && parts[0].split(',').every(function(str) {
return self.sortKeyIds.indexOf(str.replace(/^[\+-]/, '')) > -1;
})) {
state.sort = parseSort(parts[0]);
parts.shift();
}
if (parts.length) {
state.find = parseFind(parts.join('/'));
}
callback(state);
}
}
that._constructURL = function(state) {
return constructURL(state);
};
that.parse = function(str, callback) {
parseURL(str, callback);
}
that.pop = function() {
};
that.push = function(url) {
};
that.replace = function(url) {
};
that.update = function(state) {
// pushes a new URL, constructed from state
// state can have type, item, view, span, sort, find
};
return that;
};