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

1067 lines
40 KiB
JavaScript
Raw Normal View History

2011-11-05 16:46:53 +00:00
'use strict';
/*@
Ox.URL <f> URL controller
(options) -> <o> URL controller
options <o> Options object
2011-09-22 03:20:27 +00:00
findKeys <[o]> Find keys
id <s> Find key id
type <s> Value type (like "string" or "integer")
getHash <f> Tests if a hash is valid
May modify the state's hash property
(state, callback) -> <u> undefined
state <o> State object
hash <o> The hash to be tested
callback <f> callback function
2011-09-22 03:20:27 +00:00
getItem <f> Tests if a string matches an item
May modify the state's item property
(state, string, callback) -> <u> undefined
state <o> State object
string <s> The string to be tested
callback <f> callback function
getPart <f> Tests if a string matches a part (page section)
May modify the state's part property
(state, string, callback) -> <u> undefined
state <o> State object
string <s> The string to be tested
callback <f> callback function
2013-07-12 09:00:51 +00:00
getSpan <f> Tests if a value matches a span
May modify the state's view and span properties
(state, string, callback) -> <u> undefined
state <o> State object
2013-07-12 09:00:51 +00:00
value <[n]|s> The value to be tested
2011-09-22 03:20:27 +00:00
callback <f> Callback function
pages <[s]> List of pages
2011-09-22 03:20:27 +00:00
sortKeys <o> Sort keys for list and item views for all types
typeA <o> Sort keys for this type
list <o> Sort keys for list views for this type
viewA <[o]> Sort keys for this view
id <s> Sort key id
operator <s> Default sort operator ("+" or "-")
item <o> Sort keys for item views for this type
viewA <[o]> Sort keys for this view
id <s> Sort key id
operator <s> Default sort operator ("+" or "-")
spanType <o> Span types for list and item views for all types
typeA <o> Span types for this type
list <o> Span types for list views for this type
viewA <s> Span type for this view
2013-03-04 11:19:09 +00:00
Can be "date", "duration", "location" or "number"
2011-09-22 03:20:27 +00:00
item <o> Span types for item views for this type
viewA <s> Span type for this view
2013-03-04 11:19:09 +00:00
Can be "date", "duration", "location" or "number"
types <[s]> List of types
2011-09-22 03:20:27 +00:00
views <o> List and item views for all types
typeA <o> Views for type "typeA"
list <[s]> List views for this type
item <[s]> Item views for this type
2012-10-31 11:57:55 +00:00
<script>
Ox.test.url = Ox.URL({
findKeys: [
{id: 'name', type: 'string'},
{id: 'population', type: 'integer'}
],
getHash: function(state, callback) {
2013-03-05 06:05:35 +00:00
if (state.hash) {
if (state.hash.anchor == 'invalid') {
delete state.hash.anchor;
2013-03-05 06:05:35 +00:00
}
if (state.hash.query) {
state.hash.query = state.hash.query.filter(function(v) {
return v.key != 'invalid'
});
if (Ox.isEmpty(state.hash.query)) {
delete state.hash.query;
}
}
if (Ox.isEmpty(state.hash)) {
delete state.hash;
}
}
2013-03-05 06:05:35 +00:00
callback();
2012-10-31 11:57:55 +00:00
},
getItem: function(state, str, callback) {
state.item = /^\d+$/.test(str) ? str : '';
callback();
},
getSpan: function(state, str, callback) {
if (!state.item && (!state.view || state.view == 'map')) {
state.view = 'map';
state.span = str.replace(/^@/, '');
2012-10-31 11:57:55 +00:00
}
callback();
2012-10-31 11:57:55 +00:00
},
pages: ['about', 'faq', 'help'],
sortKeys: {
countries: {
list: {
grid: [
{id: 'name', operator: '+'},
{id: 'population', operator: '-'},
{id: 'cities', operator: '-'}
]
},
item: {
cities: [
{id: 'name', operator: '+'},
{id: 'population', operator: '-'}
]
}
},
cities: {
list: {
grid: [
{id: 'name', operator: '+'},
{id: 'population', operator: '-'}
]
}
}
},
spanType: {
countries: {
list: {map: 'location'},
item: {map: 'location'}
},
cities: {
list: {map: 'location'},
item: {map: 'location'}
}
},
types: ['countries', 'cities'],
views: {
countries: {
list: ['grid', 'map'],
item: ['info', 'map', 'cities']
},
cities: {
list: ['grid', 'map'],
item: ['info', 'map']
}
}
});
Ox.test.result = {
'/': {},
'/faq#1': {
page: 'faq',
hash: {anchor: '1'}
},
'/cities': {
type: 'cities',
item: '',
view: 'grid'
},
'/map': {
type: 'countries',
item: '',
view: 'map'
},
'/-45,-90,45,90': {
type: 'countries',
item: '',
view: 'map',
span: [[-45, -90], [45, 90]]
},
'/@New%20York': {
type: 'countries',
item: '',
view: 'map',
span: 'New York'
},
'/name': {
type: 'countries',
item: '',
view: 'grid',
sort: [{key: 'name', operator: '+'}]
},
'/-name,population': {
type: 'countries',
item: '',
view: 'grid',
sort: [
{key: 'name', operator: '-'},
{key: 'population', operator: '-'}
]
},
'/2342': {
type: 'countries',
item: '2342',
view: 'info'
},
'/2342/map': {
type: 'countries',
item: '2342',
view: 'map'
},
'/2342/name': {
type: 'countries',
item: '2342',
view: 'cities',
sort: [{key: 'name', operator: '+'}]
},
'/foo': {
type: 'countries',
item: '',
view: 'grid',
find: {
conditions: [
{key: '*', operator: '=', value: 'foo'}
],
operator: '&'
}
},
'/population=1000,2000': {
type: 'countries',
item: '',
view: 'grid',
find: {
conditions: [
{key: 'population', operator: '=', value: ['1000', '2000']}
],
operator: '&'
}
},
'/population>0&(name=a*|name=*z)': {
type: 'countries',
item: '',
view: 'grid',
find: {
conditions: [
{key: 'population', operator: '>', value: '0'},
{
conditions: [
{key: 'name', operator: '^', value: 'a'},
{key: 'name', operator: '$', value: 'z'}
],
operator: '|'
}
],
operator: '&'
}
},
'/#a?k=v&l=w': {
hash: {
anchor: 'a',
query: [
{key: 'k', value: 'v'},
{key: 'l', value: 'w'}
]
}
2013-02-18 09:29:15 +00:00
},
2013-03-05 06:05:35 +00:00
'/#?a=[1,2]&b=true&n=1.2&o={"k":"v"}&s1="foo"&s2=bar': {
2013-02-18 09:29:15 +00:00
hash: {
query: [
{key: 'a', value: [1, 2]},
{key: 'b', value: true},
{key: 'n', value: 1.2},
{key: 'o', value: {k: 'v'}},
{key: 's1', value: 'foo'},
{key: 's2', value: 'bar'}
]
}
2013-03-05 06:05:35 +00:00
},
'/#invalid?invalid=true': {}
2012-10-31 11:57:55 +00:00
};
</script>
> !!Ox.test.url.parse('/', function(o) { Ox.test(o, Ox.test.result['/']); })
true
> !!Ox.test.url.parse('/faq#1', function(o) { Ox.test(o, Ox.test.result['/faq#1']); })
true
> !!Ox.test.url.parse('/cities', function(o) { Ox.test(o, Ox.test.result['/cities']); })
true
> !!Ox.test.url.parse('/map', function(o) { Ox.test(o, Ox.test.result['/map']); })
true
> !!Ox.test.url.parse('/-45,-90,45,90', function(o) { Ox.test(o, Ox.test.result['/-45,-90,45,90']); })
true
> !!Ox.test.url.parse('/@New%20York', function(o) { Ox.test(o, Ox.test.result['/@New%20York']); })
true
> !!Ox.test.url.parse('/name', function(o) { Ox.test(o, Ox.test.result['/name']); })
true
> !!Ox.test.url.parse('/-name,population', function(o) { Ox.test(o, Ox.test.result['/-name,population']); })
true
> !!Ox.test.url.parse('/2342', function(o) { Ox.test(o, Ox.test.result['/2342']); })
true
> !!Ox.test.url.parse('/2342/map', function(o) { Ox.test(o, Ox.test.result['/2342/map']); })
true
> !!Ox.test.url.parse('/2342/name', function(o) { Ox.test(o, Ox.test.result['/2342/name']); })
true
> !!Ox.test.url.parse('/foo', function(o) { Ox.test(o, Ox.test.result['/foo']); })
true
> !!Ox.test.url.parse('/population=1000,2000', function(o) { Ox.test(o, Ox.test.result['/population=1000,2000']); })
true
> !!Ox.test.url.parse('/population>0&(name=a*|name=*z)', function(o) { Ox.test(o, Ox.test.result['/population>0&(name=a*|name=*z)']); })
true
> !!Ox.test.url.parse('/#a?k=v&l=w', function(o) { Ox.test(o, Ox.test.result['/#a?k=v&l=w']); })
true
2013-03-05 06:05:35 +00:00
> !!Ox.test.url.parse('/#?a=[1,2]&b=true&n=1.2&o={"k":"v"}&s1="foo"&s2=bar', function(o) { Ox.test(o, Ox.test.result['/#?a=[1,2]&b=true&n=1.2&o={"k":"v"}&s1="foo"&s2=bar']); })
true
> !!Ox.test.url.parse('/#invalid?invalid=true', function(o) { Ox.test(o, Ox.test.result['/#invalid?invalid=true']); })
2013-02-18 09:29:15 +00:00
true
@*/
/*
2012-10-31 11:57:55 +00:00
example.com[/page][#hash]
or
2013-02-18 09:29:15 +00:00
example.com[/type][/item][/view][/span][/sort][/find][#hash]
page Special page, like "about" or "contact".
part Part (section) of a page, like in "help/accounts".
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.
2013-02-18 09:29:15 +00:00
hash Anchor and/or query, like 'a' or '?k=v' or 'a?k=v&l=w' or
'?a=[1,2]&b=true&f=1.2&i=3&o={"k":"v"}&s="foo"'. Values are evaluated as
JSON, and if that fails interpreted as a string. So strings (outside
arrays or objects) work without quotes as well, unless their value is
valid JSON, like 'true' or '1'.
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), contains (string or text)
k!=v,w k [v,w] != is not between (number), does not contain (string or text)
All parts of the URL can be omitted, as long as the order is preserved.
example.com/foo
2011-09-22 04:45:56 +00:00
If "foo" is not a type, item (of the default type), list view (of the
default type), span id (of any list view of the default type) or sort key
(of any list view of the default type), 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.)
2011-09-22 03:20:27 +00:00
example.com/map/@paris/duration/title!=london
If "map" is a default type list view and "paris" is a place name, 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.
2011-09-22 03:20:27 +00:00
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.)
2011-09-22 03:20:27 +00:00
example.com/2001/2001 -> example.com/0062622/video/00:33:21
2001 matches an item title (word match), the second 2001 is a valid duration
example.com/2002/2002 -> example.com/calendar/2002/2002
2002 is a valid duration, but no list view supports durations. Then it is
read as a year, and we get calendar view with find *=2002
2012-01-06 11:58:53 +00:00
example.com/@paris/london -> example.com/map/@paris/london
paris matches place ABC (case-insensitive), but (assuming) find *=london
does not match place ABC, "paris" becomes the map query
2011-09-22 03:20:27 +00:00
example.com/@paris/paris -> example.com/map/ABC/paris
2012-01-06 11:58:53 +00:00
paris matches place ABC (case-insensitive), so we get map view, zoomed to
ABC/Paris, which is selected, with find *=paris
2011-09-22 03:20:27 +00:00
example.com/@renaissance/renaissance -> example.com/calendar/ABC/renaissance
renaissaince matches an event name (case-insensitive), so we get calendar
view, zoomed to the Renaissance, with find *=renaissance
example.com/@foo/foo -> example.com/map/@foo/foo
foo doesn't match a place or event name, but getSpan() sets the map query to
foo and returns @foo, so we get map view, zoomed to Foo, with find *=foo
2011-09-22 04:45:56 +00:00
example.com/clip:duration -> example.com/clip/clip:duration
clip:duration is not a sort key of the default view (grid), so the view is
set to the first list view that accepts this sort key
2011-09-22 03:20:27 +00:00
*/
Ox.URL = function(options) {
var self = {}, that = {};
self.options = Ox.extend({
2011-09-22 03:20:27 +00:00
// fixme: find keys are also per type/list|item/view
// since one can search for layer properties in some item views
findKeys: [],
2013-02-20 04:51:43 +00:00
getHash: null,
2011-09-22 03:20:27 +00:00
getItem: null,
getPart: null,
2011-09-22 03:20:27 +00:00
getSpan: null,
pages: [],
2011-09-22 03:20:27 +00:00
spanType: {},
sortKeys: {},
types: [],
views: {}
}, options);
2012-10-31 11:57:55 +00:00
if (Ox.every(self.options.findKeys, function(findKey) {
return findKey.id != '*';
})) {
self.options.findKeys.push({id: '*', type: 'string'});
}
2011-11-09 22:32:45 +00:00
self.previousTitle = '';
2011-09-23 10:43:57 +00:00
self.previousURL = '';
2013-03-02 10:53:19 +00:00
window.addEventListener('popstate', function() {
2011-11-09 22:32:45 +00:00
self.previousTitle = document.title;
self.previousURL = document.location.pathname
+ document.location.search
+ document.location.hash;
2013-03-02 10:53:19 +00:00
});
2011-11-09 22:32:45 +00:00
function constructCondition(condition) {
var key = condition.key == '*' ? '' : condition.key,
operator = condition.operator,
value;
value = (
Ox.isArray(condition.value) ? condition.value : [condition.value]
).map(function(value) {
return encodeValue(constructValue(value, condition.key));
}).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('');
}
2011-09-22 03:20:27 +00:00
function constructDate(date) {
return Ox.formatDate(date, '%Y-%m-%d', true);
}
function constructDuration(duration) {
return Ox.formatDuration(duration, 3).replace(/\.000$/, '');
}
function constructFind(find) {
return find.conditions.map(function(condition) {
2012-01-06 11:58:53 +00:00
return condition.conditions
? '(' + constructFind(condition) + ')'
: constructCondition(condition);
}).join(find.operator);
}
2012-10-31 11:57:55 +00:00
function constructHash(hash) {
2013-02-18 09:29:15 +00:00
var obj = {};
if (hash.query) {
hash.query.forEach(function(condition) {
obj[condition.key] = condition.value;
});
}
return hash.anchor || hash.query
2013-03-05 14:31:21 +00:00
? '#' + (
hash.anchor || ''
) + (
2013-07-12 12:02:17 +00:00
hash.query ? '?' + Ox.serialize(obj, true) : ''
)
: '';
}
2011-09-22 03:20:27 +00:00
function constructLocation(location) {
return location.join(',');
}
function constructSort(sort, state) {
2011-09-23 10:43:57 +00:00
var sortKeys = self.options.sortKeys[state.type][
!state.item ? 'list' : 'item'
][state.view];
2011-10-08 17:19:18 +00:00
return sortKeys ? sort.map(function(sort) {
return (
2011-09-23 10:43:57 +00:00
Ox.getObjectById(sortKeys, sort.key).operator == sort.operator
? '' : sort.operator
) + sort.key;
2011-10-08 17:19:18 +00:00
}).join(',') : '';
}
2011-09-22 03:20:27 +00:00
function constructSpan(span, state) {
var view = state.view || self.options.views[state.type][
2011-09-22 03:20:27 +00:00
!state.item ? 'list' : 'item'
][0],
spanType = self.options.spanType[state.type][
!state.item ? 'list' : 'item'
][view];
2011-09-22 03:20:27 +00:00
return (Ox.isArray(span) ? span : [span]).map(function(point) {
return Ox.isNumber(point) ? (
spanType == 'date' ? constructDate(point)
: spanType == 'duration' ? constructDuration(point)
2013-03-04 11:19:09 +00:00
: spanType == 'location' ? constructLocation(point)
: point
2011-09-22 03:20:27 +00:00
) : point;
}).join(',');
}
function constructURL(state) {
var parts = [];
2011-09-22 03:20:27 +00:00
if (state.page) {
parts.push(state.page);
if (state.part) {
parts.push(state.part);
}
2011-09-22 03:20:27 +00:00
} else {
if (self.options.types.indexOf(state.type) > 0) {
parts.push(state.type);
}
if (state.item) {
parts.push(encodeValue(state.item, true));
2011-09-22 03:20:27 +00:00
}
2011-09-23 10:43:57 +00:00
if (state.type && self.options.views[state.type][
2011-09-22 03:20:27 +00:00
state.item ? 'item' : 'list'
].indexOf(state.view) > -1) {
parts.push(state.view);
}
2011-09-28 17:31:35 +00:00
if (state.span && state.span.length) {
2011-09-22 03:20:27 +00:00
parts.push(constructSpan(state.span, state));
}
if (state.sort && state.sort.length) {
parts.push(constructSort(state.sort, state));
2011-11-03 15:42:41 +00:00
}
2011-09-22 03:20:27 +00:00
if (state.find) {
parts.push(constructFind(state.find));
}
}
return '/' + Ox.filter(parts).join('/') + (
state.hash ? constructHash(state.hash) : ''
);
}
function constructValue(str, key) {
var findKey = Ox.getObjectById(self.options.findKeys, key),
type = Ox.isArray(findKey.type) ? findKey.type[0] : findKey.type,
2011-11-10 22:47:38 +00:00
value = str,
values = findKey.values;
2012-01-06 11:58:53 +00:00
return type == 'enum' ? values[value] : value;
}
2013-02-18 09:29:15 +00:00
function decodeValue(value) {
return Ox.decodeURIComponent(value)
.replace(/_/g, ' ').replace(/\t/g, '_')
.replace(/\x0E/g, '<').replace(/\x0F/g, '>');
}
function encodeValue(value, isItem) {
2013-08-26 17:15:56 +00:00
var chars = isItem ? '/#%' : '*=&|()#%',
ret = '';
2013-02-18 09:29:15 +00:00
value.toString().split('').forEach(function(char) {
var index = chars.indexOf(char);
ret += index > -1
? '%' + char.charCodeAt(0).toString(16).toUpperCase()
: char;
});
ret = ret.replace(/_/g, '%09').replace(/\s/g, '_');
if (!isItem) {
ret = ret.replace(/</g, '%0E').replace(/>/g, '%0F');
}
return ret;
}
2011-09-22 03:20:27 +00:00
function isNumericalSpan(str) {
return str.split(',').every(function(str) {
return /^[0-9-\.:]+$/.test(str);
});
}
function getSpanType(str, types) {
2011-11-04 15:54:28 +00:00
Ox.Log('Core', 'getSpanType', str, types)
2011-09-22 03:20:27 +00:00
var canBeDate = types.indexOf('date') > -1,
canBeDuration = types.indexOf('duration') > -1,
canBeLocation = types.indexOf('location') > -1,
canBeNumber = types.indexOf('number') > -1,
2011-09-22 03:20:27 +00:00
length = str.split(',').length;
// !/^\d{7}$/.test(str) avoids matching imdb ids
2011-09-22 03:20:27 +00:00
return canBeDate && /\d-/.test(str) ? 'date'
: canBeDuration && /:/.test(str) ? 'duration'
: canBeLocation && length == 4 ? 'location'
// leaves us with [-]D[.D][,[-]D[.D]]
: canBeDuration ? 'duration'
: canBeDate && !/\./.test(str) && !/^\d{7}$/.test(str) ? 'date'
: canBeLocation && length == 2 ? 'location'
2013-03-04 11:19:09 +00:00
: canBeNumber && /^\d+$/.test(str) ? 'number'
: '';
2011-09-22 03:20:27 +00:00
}
function parseCondition(str) {
2011-11-04 15:54:28 +00:00
Ox.Log('Core', 'PARSE COND', str)
2011-09-22 03:20:27 +00:00
var condition = {},
operators = ['!==', '==', '!=', '=', '!<', '<', '!>', '>'],
split;
2013-02-18 10:56:20 +00:00
str = str.replace(/%3C/g, '<').replace(/%3E/g, '>');
Ox.forEach(operators, function(operator) {
2013-02-18 09:29:15 +00:00
if (str.indexOf(operator) > -1) {
split = str.split(operator);
condition = {
key: split.shift(),
value: split.join(operator),
operator: operator
};
2012-07-05 08:58:08 +00:00
return false; // break
}
});
if (
!condition.operator
|| Ox.getIndexById(self.options.findKeys, condition.key) == -1
) {
// missing operator or unknown key
condition = {key: '*', value: str, operator: '='};
}
if (['=', '!='].indexOf(condition.operator) > -1) {
if (Ox.startsWith(condition.value, '*') && !Ox.endsWith(condition.value, '*')) {
2012-05-24 09:47:33 +00:00
condition.value = condition.value.slice(1);
condition.operator = condition.operator.replace('=', '$')
} else if (Ox.endsWith(condition.value, '*') && !Ox.startsWith(condition.value, '*')) {
2012-05-24 09:47:33 +00:00
condition.value = condition.value.slice(0, -1);
condition.operator = condition.operator.replace('=', '^')
}
}
if (
['date', 'enum', 'float', 'integer', 'time', 'year'].indexOf(
Ox.getObjectById(self.options.findKeys, condition.key).type
) > -1
&& condition.value.indexOf(',') > -1
) {
condition.value = condition.value.split(',').map(function(value) {
return parseValue(decodeValue(value), condition.key);
});
2011-09-22 03:20:27 +00:00
} else {
condition.value = parseValue(decodeValue(condition.value), condition.key);
}
2013-02-18 09:29:15 +00:00
Ox.Log('Core', 'PARSE COND', str, condition);
return condition;
}
2011-09-22 03:20:27 +00:00
function parseDate(str) {
return Ox.formatDate(Ox.parseDate(str, true), '%Y-%m-%d');
}
function parseDuration(str) {
2012-01-02 14:05:14 +00:00
return Ox.parseDuration(str);
}
function parseFind(str) {
2012-02-19 15:38:08 +00:00
str = (str || '').replace(/%7C/g, '|');
var counter = 0,
find = {conditions: [], operator: '&'},
salt = Ox.range(16).map(function() {
return Ox.char(65 + Ox.random(26));
}).join(''),
regexp = new RegExp(salt + '(\\d+)'),
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('');
}
});
2012-02-19 15:38:08 +00:00
subconditions = subconditions.filter(function(subcondition) {
// make sure empty brackets don't throw errors
return !!subcondition;
});
subconditions.forEach(function(subcondition, i) {
str = str.replace(subcondition, salt + i);
});
find.operator = str.indexOf('|') > -1 ? '|' : '&'
find.conditions = str.split(find.operator).map(function(condition, i) {
condition = condition.replace(regexp, function(match) {
return subconditions[parseInt(arguments[1])];
});
return condition[0] == '('
? parseFind(condition.slice(1, -1))
: parseCondition(condition);
});
}
return find;
}
2012-10-31 11:57:55 +00:00
function parseHash(str) {
2013-02-18 09:29:15 +00:00
var hash = {},
split = str.split('?');
if (split[0]) {
hash.anchor = decodeValue(split[0]);
}
if (split[1]) {
Ox.forEach(Ox.unserialize(split[1], true), function(value, key) {
hash.query = hash.query || [];
2013-02-18 09:29:15 +00:00
hash.query.push({
key: key,
value: value
});
});
}
return hash;
}
2011-09-22 03:20:27 +00:00
function parseLocation(str) {
return str.split(',').map(function(str, i) {
return Ox.limit(parseInt(str, 10), -90 * (i + 1), 90 * (i + 1));
});
}
2013-03-04 11:19:09 +00:00
function parseNumber(str) {
return parseInt(str);
}
2011-09-22 03:20:27 +00:00
function parseSort(str, state) {
return str.split(',').map(function(str) {
var hasOperator = /^[\+-]/.test(str);
return {
2012-05-24 09:47:33 +00:00
key: hasOperator ? str.slice(1) : str,
2011-09-22 03:20:27 +00:00
operator: hasOperator
? str[0]
: Ox.getObjectById(self.options.sortKeys[state.type][
!state.item ? 'list' : 'item'
][state.view], str).operator
};
});
}
2011-09-22 03:20:27 +00:00
function parseSpan(str, type) {
var split = str.split(',');
if (split.length == 4) {
split = [split[0] + ',' + split[1], split[2] + ',' + split[3]];
}
return split.map(
type == 'date' ? parseDate
: type == 'duration' ? parseDuration
2013-03-04 11:19:09 +00:00
: type == 'location' ? parseLocation
: parseNumber
2011-09-22 03:20:27 +00:00
);
}
function parseURL(str, callback) {
2012-05-25 21:42:10 +00:00
// fixme: removing trailing slash makes it impossible to search for '/'
2013-02-18 09:29:15 +00:00
var split = str.split('#'),
parts = split.shift().replace(/(^\/|\/$)/g, '').split('/'),
state = split.length && split[0].length
? {hash: parseHash(split.join('#'))}
: {};
2011-09-23 10:43:57 +00:00
if (parts[0] == '') {
// empty URL
2013-02-20 04:51:43 +00:00
getHash();
} else if (self.options.pages.indexOf(parts[0]) > -1) {
2011-09-23 10:43:57 +00:00
// page
state.page = parts[0];
parts.shift();
if (parts.length) {
// may modify state.part
2013-07-12 09:00:51 +00:00
self.options.getPart(state, decodeValue(parts[0]), getHash);
} else {
getHash();
}
} else {
if (self.options.types.indexOf(parts[0]) > -1) {
2011-09-22 03:20:27 +00:00
// type
state.type = parts[0];
parts.shift();
2011-09-22 03:20:27 +00:00
} else {
// set to default type
state.type = self.options.types[0];
}
if (parts.length) {
2011-11-04 15:54:28 +00:00
Ox.Log('Core', 'ST', state.type, self.options.views)
2011-09-22 03:20:27 +00:00
if (self.options.views[state.type].list.indexOf(parts[0]) > -1) {
// list view
state.item = '';
state.view = parts[0];
parts.shift();
2011-09-22 03:20:27 +00:00
parseBeyondItem();
} else {
2011-09-22 03:20:27 +00:00
// test for item id or name
2013-07-12 09:00:51 +00:00
self.options.getItem(state, decodeValue(parts[0]), function() {
// may have modified state.item
if (state.item) {
parts.shift();
}
2011-09-22 03:20:27 +00:00
parseBeyondItem();
});
}
} else {
2012-10-31 11:57:55 +00:00
state.item = '';
// set to default view
state.view = self.options.views[state.type].list[0];
2013-02-20 04:51:43 +00:00
getHash();
}
}
2011-09-22 03:20:27 +00:00
function parseBeyondItem() {
2011-11-04 15:54:28 +00:00
Ox.Log('Core', 'pBI', state, parts.join('/'));
2011-09-22 03:20:27 +00:00
var span, spanType, spanTypes;
if (
parts.length && state.item
&& self.options.views[state.type].item.indexOf(parts[0]) > -1
) {
// item view
state.view = parts[0];
parts.shift();
}
if (parts.length) {
2011-09-22 03:20:27 +00:00
if (isNumericalSpan(parts[0])) {
// test for numerical span
spanTypes = self.options.spanType[state.type][
!state.item ? 'list' : 'item'
];
// if no view is given then parse the span anyway,
// but make sure the span type could match a view
spanType = state.view
? spanTypes[state.view]
: getSpanType(parts[0], Ox.unique(Ox.values(spanTypes)));
2013-03-04 11:19:09 +00:00
Ox.Log('URL', 'SPAN TYPE', spanType)
2011-09-22 03:20:27 +00:00
if (spanType) {
span = parseSpan(parts[0], spanType);
if (span) {
2011-09-22 04:45:56 +00:00
if (state.view) {
state.span = span;
parts.shift();
} else {
2011-09-22 03:20:27 +00:00
// if no view is given then switch to the first
// view that supports a span of this type
Ox.forEach(self.options.views[state.type][
!state.item ? 'list' : 'item'
], function(view) {
if (spanTypes[view] == spanType) {
2011-11-03 15:42:41 +00:00
state.view = view;
2011-09-22 03:20:27 +00:00
state.span = span;
parts.shift();
2012-07-05 08:58:08 +00:00
return false; // break
2011-09-22 03:20:27 +00:00
}
});
}
}
}
}
2013-07-12 09:00:51 +00:00
if (state.span && spanType == 'duration') {
// test duration, may modify state.span
self.options.getSpan(state, state.span, parseBeyondSpan);
} else if (!state.span && /^[A-Z@]/.test(parts[0])) {
2011-09-22 03:20:27 +00:00
// test for span id or name
self.options.getSpan(state, decodeValue(parts[0]), function() {
// may have modified state.view and state.span
if (state.span) {
parts.shift();
}
parseBeyondSpan();
});
2011-09-22 03:20:27 +00:00
} else {
parseBeyondSpan();
}
} else {
if (!state.view) {
// set to default item view
state.view = self.options.views[state.type].item[0];
}
2013-02-20 04:51:43 +00:00
getHash();
}
}
function parseBeyondSpan() {
2013-02-16 01:21:08 +00:00
Ox.Log('Core', 'pBS', state, parts)
2011-09-22 04:45:56 +00:00
var sortKeyIds, sortParts;
if (parts.length) {
sortParts = parts[0].split(',');
sortKeyIds = Ox.map(self.options.sortKeys[state.type][
!state.item ? 'list' : 'item'
], function(sortKeys) {
return sortKeys.map(function(sortKey) {
return sortKey.id;
});
});
// test if sort keys match the given view,
// or any view if no view is given
Ox.forEach(
state.view ? [state.view]
: self.options.views[state.type][!state.item ? 'list' : 'item'],
function(view) {
if (sortKeyIds[view] && sortParts.every(function(part) {
return sortKeyIds[view].indexOf(part.replace(/^[\+-]/, '')) > -1;
})) {
if (!state.view) {
// set list or item view
state.view = view;
}
// sort
state.sort = parseSort(parts[0], state);
parts.shift();
2012-07-05 08:58:08 +00:00
return false; // break
2011-09-22 04:45:56 +00:00
}
}
);
}
2011-09-22 03:20:27 +00:00
if (!state.view) {
// set to default list or item view
state.view = self.options.views[state.type][
!state.item ? 'list' : 'item'
][0];
}
if (parts.length) {
2011-09-22 03:20:27 +00:00
// find
state.find = parseFind(parts.join('/'));
}
2013-02-20 04:51:43 +00:00
getHash();
}
2013-02-20 04:55:33 +00:00
function getHash() {
self.options.getHash(state, function() {
// may have modified state.hash
2013-03-04 11:57:20 +00:00
callback(state);
});
}
}
function parseValue(str, key) {
var findKey = Ox.getObjectById(self.options.findKeys, key),
type = Ox.isArray(findKey.type) ? findKey.type[0] : findKey.type,
2011-11-10 22:47:38 +00:00
value = str,
values = findKey.values;
if (type == 'boolean') {
value = ['', 'false'].indexOf(str) == -1;
} else if (type == 'date') {
value = Ox.formatDate(Ox.parseDate(str, true), '%F', true);
2011-11-10 22:47:38 +00:00
} else if (type == 'enum') {
value = Math.max(values.map(function(value) {
return value.toLowerCase();
}).indexOf(str.toLowerCase()), 0);
} else if (type == 'float') {
value = parseFloat(str) || 0;
} else if (type == 'integer') {
value = Math.round(str) || 0;
} else if (type == 'time') {
2013-03-03 11:02:44 +00:00
value = Ox.formatDuration(Ox.parseDuration(value));
} else if (type == 'year') {
value = Math.round(str) || 1970;
}
return value.toString();
}
2011-09-23 10:43:57 +00:00
function saveURL() {
2011-10-08 17:19:18 +00:00
2011-09-23 10:43:57 +00:00
}
2012-05-21 10:38:18 +00:00
/*@
parse <f> parse
(callback) -> <o> parse state from document.location
(url, callback) -> <o> parse state from passed url
@*/
2011-09-22 04:45:56 +00:00
that.parse = function() {
var str = arguments.length == 2 ? arguments[0]
: document.location.pathname
+ document.location.search
+ document.location.hash,
callback = arguments[arguments.length - 1];
2011-09-28 17:31:35 +00:00
parseURL(str, callback);
2011-09-23 10:43:57 +00:00
return that;
2013-02-16 10:58:40 +00:00
};
2011-09-23 10:43:57 +00:00
/*@
pop <f> Sets the URL to the previous URL
@*/
that.pop = function() {
2011-11-09 22:32:45 +00:00
if (self.previousURL) {
2012-05-25 17:58:43 +00:00
history.pushState && history.pushState(
{}, self.previousTitle, self.previousURL
);
2011-11-09 22:32:45 +00:00
document.title = self.previousTitle;
}
return !!self.previousURL;
};
2011-09-23 10:43:57 +00:00
/*@
push <f> Pushes a new URL
2011-09-28 17:31:35 +00:00
(state, title, url, callback) -> <o> URL controller
state <o> State for the new URL
2011-11-09 17:39:06 +00:00
If state is null, it will be derived from url
2011-09-28 17:31:35 +00:00
title <s> Title for the new URL
2011-11-09 17:39:06 +00:00
url <s|o> New URL
If url is null, it will be derived from state
2011-09-23 10:43:57 +00:00
callback <f> callback function
state <o> New state
@*/
2011-09-28 17:31:35 +00:00
that.push = function(state, title, url, callback) {
2011-11-09 17:39:06 +00:00
if (!state) {
parseURL(url, function(state) {
2011-09-28 17:31:35 +00:00
pushState(state, title, url);
2011-11-09 17:39:06 +00:00
});
2011-09-28 17:31:35 +00:00
} else {
2011-11-09 17:39:06 +00:00
url = url || constructURL(state);
2011-09-28 17:31:35 +00:00
pushState(state, title, url);
}
function pushState(state, title, url) {
2011-11-09 22:32:45 +00:00
self.previousTitle = document.title;
2011-10-08 17:19:18 +00:00
self.previousURL = document.location.pathname
+ document.location.search
+ document.location.hash;
2011-11-09 17:39:06 +00:00
if (url != self.previousURL) {
2012-05-25 17:58:43 +00:00
history.pushState && history.pushState(
Ox.extend(state, {title: title}), '', url
);
2011-11-09 17:39:06 +00:00
document.title = title;
callback && callback(state);
}
2011-09-28 17:31:35 +00:00
}
}
2011-09-28 17:31:35 +00:00
/*@
replace <f> Replaces the URL with a new URL
(state, title, url, callback) -> <o> URL controller
state <o> State for the new URL
2011-11-09 17:39:06 +00:00
If state is null, it will be derived from url
2011-09-28 17:31:35 +00:00
title <s> Title for the new URL
2011-11-09 17:39:06 +00:00
url <s|o> New URL
If url is null, it will be derived from state
2011-09-28 17:31:35 +00:00
callback <f> callback function
state <o> New state
@*/
that.replace = function(state, title, url, callback) {
2011-11-09 17:39:06 +00:00
if (!state) {
parseURL(url, function(state) {
2011-09-28 17:31:35 +00:00
replaceState(state, title, url);
2011-11-09 17:39:06 +00:00
});
2011-09-28 17:31:35 +00:00
} else {
2011-11-09 17:39:06 +00:00
url = url || constructURL(state);
2011-09-28 17:31:35 +00:00
replaceState(state, title, url);
}
function replaceState(state, title, url) {
2012-05-25 16:51:26 +00:00
history.replaceState && history.replaceState(
Ox.extend(state, {title: title}), '', url
);
2011-10-08 17:19:18 +00:00
document.title = title;
2011-09-28 17:31:35 +00:00
callback && callback(state);
}
}
return that;
2011-11-03 15:42:41 +00:00
2012-02-16 16:16:12 +00:00
};