beginning to unify pandora.URL and pandora.Query as Ox.URL
This commit is contained in:
parent
fdecafe4d7
commit
b7100792c5
5 changed files with 422 additions and 17 deletions
|
@ -17,7 +17,10 @@ Ox.Clipboard = function() {
|
|||
Ox.print('copy', JSON.stringify(clipboard));
|
||||
},
|
||||
paste: function(type) {
|
||||
return clipboard;
|
||||
return type ? clipboard.type : clipboard;
|
||||
},
|
||||
type: function(type) {
|
||||
return type in clipboard;
|
||||
}
|
||||
};
|
||||
}();
|
||||
|
|
|
@ -1,5 +1,400 @@
|
|||
// vim: et:ts=4:sw=4:sts=4:ft=javascript
|
||||
/***
|
||||
Ox.URL
|
||||
***/
|
||||
|
||||
/*@
|
||||
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;
|
||||
|
||||
};
|
|
@ -295,14 +295,17 @@ Ox.Filter = function(options, self) {
|
|||
newType = Ox.getObjectById(self.options.findKeys, key).type,
|
||||
oldConditionType = getConditionType(oldType),
|
||||
newConditionType = getConditionType(newType),
|
||||
changeConditionType = oldConditionType != newConditionType;
|
||||
changeConditionType = oldConditionType != newConditionType,
|
||||
wasUselessCondition = isUselessCondition(pos, subpos);
|
||||
Ox.print('old new', oldConditionType, newConditionType)
|
||||
condition.key = key;
|
||||
if (changeConditionType) {
|
||||
renderConditions();
|
||||
//self.$conditions[pos].replaceElement(1, constructConditionOperator(pos, oldOperator));
|
||||
}
|
||||
triggerChangeEvent();
|
||||
if (!(wasUselessCondition && isUselessCondition(pos, subpos))) {
|
||||
triggerChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function changeConditionOperator(pos, subpos, operator) {
|
||||
|
@ -311,7 +314,8 @@ Ox.Filter = function(options, self) {
|
|||
var condition = subpos == -1
|
||||
? self.options.query.conditions[pos]
|
||||
: self.options.query.conditions[pos].conditions[subpos],
|
||||
oldOperator = condition.operator
|
||||
oldOperator = condition.operator,
|
||||
wasUselessCondition = isUselessCondition(pos, subpos);
|
||||
condition.operator = operator;
|
||||
if (oldOperator.indexOf('-') == -1 && operator.indexOf('-') > -1) {
|
||||
condition.value = [condition.value, condition.value]
|
||||
|
@ -320,7 +324,9 @@ Ox.Filter = function(options, self) {
|
|||
condition.value = condition.value[0]
|
||||
renderConditions();
|
||||
}
|
||||
triggerChangeEvent();
|
||||
if (!(wasUselessCondition && isUselessCondition(pos, subpos))) {
|
||||
triggerChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function changeConditionValue(pos, subpos, value) {
|
||||
|
@ -341,7 +347,7 @@ Ox.Filter = function(options, self) {
|
|||
}
|
||||
});
|
||||
changeGroupOperator && renderConditions();
|
||||
triggerChangeEvent();
|
||||
self.options.query.conditions.length > 1 && triggerChangeEvent();
|
||||
}
|
||||
|
||||
function getConditionType(type) {
|
||||
|
@ -360,11 +366,13 @@ Ox.Filter = function(options, self) {
|
|||
: [self.options.query.conditions[pos].conditions[subpos]],
|
||||
isUseless = false;
|
||||
Ox.forEach(conditions, function(condition) {
|
||||
isUseless = ['string', 'text'].indexOf(
|
||||
isUseless = ['string', 'text'].indexOf(getConditionType(
|
||||
Ox.getObjectById(self.options.findKeys, condition.key).type
|
||||
) > -1
|
||||
&& condition.operator == (self.options.query.operator == '&' ? '' : '!')
|
||||
&& condition.value == ''
|
||||
)) > -1
|
||||
&& (
|
||||
self.options.query.operator == '&' ? ['', '^', '$'] : ['!', '!^', '!$']
|
||||
).indexOf(condition.operator) > -1
|
||||
&& condition.value === ''
|
||||
return isUseless;
|
||||
});
|
||||
Ox.print('isUseless', isUseless);
|
||||
|
|
|
@ -296,8 +296,8 @@ Ox.SplitPanel = function(options, self) {
|
|||
'collapsed': element.collapsed
|
||||
});
|
||||
element = self.options.elements[pos == 0 ? 1 : pos - 1];
|
||||
element.element.triggerEvent('resize', {size:
|
||||
element.element[self.dimensions[0]]()
|
||||
element.element.triggerEvent('resize', {
|
||||
size: element.element[self.dimensions[0]]()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -2429,9 +2429,8 @@ Ox.formatColor = function(val, type) {
|
|||
});
|
||||
color = Ox.range(3).map(function() {
|
||||
var v = Math.round(val * 255);
|
||||
return val < 0.5 ? 128 + v : 255 - v;
|
||||
return val < 0.5 ? 128 + v : v - 128;
|
||||
});
|
||||
Ox.print('COLOR', color)
|
||||
}
|
||||
element = Ox.element('<div>')
|
||||
.css({
|
||||
|
|
Loading…
Reference in a new issue