diff --git a/source/Ox.UI/js/Core/Ox.URL.js b/source/Ox.UI/js/Core/Ox.URL.js index 95ab5107..e04d4a46 100644 --- a/source/Ox.UI/js/Core/Ox.URL.js +++ b/source/Ox.UI/js/Core/Ox.URL.js @@ -4,23 +4,46 @@ Ox.URL URL controller (options) -> URL controller options Options object - findKeys <[o]> Find keys {id: "", type: ""} - type can be "string" or "number" - getItemId Tests if a string matches an item + findKeys <[o]> Find keys + id Find key id + type Value type ("string" or "number") + getItem Tests if a string matches an item (string, callback) -> undefined string The string to be tested callback callback function id Matching item id, or empty - getSpanId Tests if a string matches a span - (string, callback) -> undefined + getSpan Tests if a string matches a span + (item, view, string, callback) -> undefined + item The item id, or empty + view The view, or empty string The string to be tested - callback callback function + callback Callback function id Matching span id, or empty + view Matching view, or empty pages <[s]> List of pages - sortKeys <[o]> Sort keys {id: "", operator: ""} - operator is the default operator ("+" or "-") + sortKeys Sort keys for list and item views for all types + typeA Sort keys for this type + list Sort keys for list views for this type + viewA <[o]> Sort keys for this view + id Sort key id + operator Default sort operator ("+" or "-") + item Sort keys for item views for this type + viewA <[o]> Sort keys for this view + id Sort key id + operator Default sort operator ("+" or "-") + spanType Span types for list and item views for all types + typeA Span types for this type + list Span types for list views for this type + viewA Span type for this view + Can be "date", "duration" or "location" + item Span types for item views for this type + viewA Span type for this view + Can be "date", "duration" or "location" types <[s]> List of types - views List of views {type: {'list': [...], 'item': [...]}} + views List and item views for all types + typeA Views for type "typeA" + list <[s]> List views for this type + item <[s]> Item views for this type @*/ /* @@ -82,12 +105,12 @@ example.com/clip/+clip.duration/subtitles=foo 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 +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. -example.com/calendar/1900,2000/clip.duration/event=hiroshima +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 @@ -95,6 +118,21 @@ example.com/calendar/1900,2000/clip.duration/event=hiroshima 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.) +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 +example.com/@paris/paris -> example.com/map/ABC/paris + paris matches a place name (case-insensitive), so we get map view, zoomed to + Paris, with find *=paris +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 + */ Ox.URL = function(options) { @@ -102,18 +140,19 @@ Ox.URL = function(options) { var self = {}, that = {}; self.options = Ox.extend({ + // fixme: find keys are also per type/list|item/view + // since one can search for layer properties in some item views findKeys: [], - getItemId: null, - getSpanId: null, + getItem: null, + getSpan: null, pages: [], - sortKeys: [], + spanType: {}, + sortKeys: {}, types: [], views: {} }, options); - self.sortKeyIds = self.options.sortKeys.map(function(sortKey) { - return sortKey.id; - }); + Ox.print('Ox.URL options', self.options) function constructCondition(condition) { var key = condition.key == '*' ? '' : condition.key, @@ -133,6 +172,10 @@ Ox.URL = function(options) { return [key, operator, value].join(''); } + function constructDate(date) { + return Ox.formatDate(date, '%Y-%m-%d', true); + } + function constructDuration(duration) { return Ox.formatDuration(duration, 3).replace(/\.000$/, ''); } @@ -149,44 +192,60 @@ Ox.URL = function(options) { }).join(find.operator); } - function constructSort(sort) { + function constructLocation(location) { + return location.join(','); + } + + function constructSort(sort, state) { return sort.map(function(sort) { return ( - sort.operator == Ox.getObjectById(self.options.sortKeys, sort.key).operator - ? '' : sort.operator + sort.operator == Ox.getObjectById(self.options.sortKeys[state.type][ + !state.item ? 'list' : 'item' + ][state.view], sort.key).operator ? '' : sort.operator ) + sort.key; }).join(',') } - function constructSpan(span) { - return span.map(function(point) { - return /^[0-9-\.:]+$/.test(str) ? constuctDuration(point) : point; + function constructSpan(span, state) { + var spanType = self.options.spanType[state.type][ + !state.item ? 'list' : 'item' + ][state.view]; + return (Ox.isArray(span) ? span : [span]).map(function(point) { + return Ox.isNumber(point) ? ( + spanType == 'date' ? constructDate(point) + : spanType == 'duration' ? constructDuration(point) + : constructLocation(point) + ) : point; }).join(','); } function constructURL(state) { var parts = []; - if (self.options.types.indexOf(state.type) > 0) { - parts.push(state.type); + if (state.page) { + parts.push(state.page); + } else { + if (self.options.types.indexOf(state.type) > 0) { + parts.push(state.type); + } + if (state.item) { + parts.push(state.item); + } + if (self.options.views[state.type][ + state.item ? 'item' : 'list' + ].indexOf(state.view) > -1) { + parts.push(state.view); + } + if (state.span) { + parts.push(constructSpan(state.span, state)); + } + if (state.sort && state.sort.length) { + parts.push(constructSort(state.sort, state)); + } + if (state.find) { + parts.push(constructFind(state.find)); + } } - 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('/'); + return '/' + parts.join('/'); } function decodeValue(str) { @@ -205,8 +264,29 @@ Ox.URL = function(options) { return ret; } + function isNumericalSpan(str) { + return str.split(',').every(function(str) { + return /^[0-9-\.:]+$/.test(str); + }); + } + + function getSpanType(str, types) { + Ox.print('getSpanType', str, types) + var canBeDate = types.indexOf('date') > -1, + canBeDuration = types.indexOf('duration') > -1, + canBeLocation = types.indexOf('location') > -1, + length = str.split(',').length; + 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) ? 'date' + : canBeLocation && length == 2 ? 'location' : ':' + } + function parseCondition(str) { - var condition, + var condition = {}, operators = ['!==', '==', '!=', '=', '!<', '<', '!>', '>'], split; Ox.forEach(operators, function(operator) { @@ -220,7 +300,11 @@ Ox.URL = function(options) { return false; } }); - if (!condition.operator) { + if ( + !condition.operator + || Ox.getPositionById(self.options.findKeys, condition.key) == -1 + ) { + // missing operator or unknown key condition = {key: '*', value: str, operator: '='}; } if (['=', '!='].indexOf(condition.operator) > -1) { @@ -233,11 +317,17 @@ Ox.URL = function(options) { } } if (condition.value.indexOf(':') > -1) { - condition.value = condition.value.split(':') + condition.value = condition.value.split(':').map(decodeValue); + } else { + condition.value = decodeValue(condition.value); } return condition; } + function parseDate(str) { + return Ox.formatDate(Ox.parseDate(str, true), '%Y-%m-%d'); + } + function parseDuration(str) { var parts = str.split(':').reverse(); while (parts.length > 3) { @@ -284,18 +374,36 @@ Ox.URL = function(options) { return find; } - function parseSort(str) { + function parseLocation(str) { + return str.split(',').map(function(str, i) { + return Ox.limit(parseInt(str, 10), -90 * (i + 1), 90 * (i + 1)); + }); + } + + function parseSort(str, state) { 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 + operator: hasOperator + ? str[0] + : Ox.getObjectById(self.options.sortKeys[state.type][ + !state.item ? 'list' : 'item' + ][state.view], str).operator }; }); } - function parseSpan(str, callback) { - return str.split(',').map(parseDuration); + 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 + : parseLocation + ); } function parseURL(str, callback) { @@ -305,72 +413,135 @@ Ox.URL = function(options) { state = {}; if (parts.length == 0) { state.page = ''; + callback(state); } else if (self.options.pages.indexOf(parts[0]) > -1) { state.page = parts[0]; + callback(state); } else { if (self.options.types.indexOf(parts[0]) > -1) { + // type state.type = parts[0]; parts.shift(); + } else { + // set to default type + state.type = self.options.types[0]; } if (parts.length) { - if (self.options.views.list.indexOf(parts[0]) > -1) { + if (self.options.views[state.type].list.indexOf(parts[0]) > -1) { + // list view state.item = ''; state.view = parts[0]; parts.shift(); - parseBeyondItem(false); + parseBeyondItem(); } else { - self.options.getItemId(parts[0], function(itemId) { - if (itemId) { - state.item = itemId; + // test for item id or name + self.options.getItem(parts[0], function(item) { + state.item = item; + if (item) { parts.shift(); } - parseBeyondItem(!!itemId); + parseBeyondItem(); }); } } else { callback(state); } } - function parseBeyondItem(itemId) { - if (parts.length && itemId && self.options.views.item.indexOf(parts[0]) > -1) { + function parseBeyondItem() { + 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(); } + Ox.print('pBI', state, parts.join('/')); 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; + 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))); + Ox.print('SPAN TYPE', spanType) + if (spanType) { + span = parseSpan(parts[0], spanType); + if (span) { + if (!state.view) { + // 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) { + state.view = view; + state.span = span; + parts.shift(); + return false; + } + }); + } else { + state.span = span; + parts.shift(); + } + } + } + } + if (!state.span && /^[A-Z@]/.test(parts[0])) { + // test for span id or name + self.options.getSpan(state.item, state.view, parts[0], function(span, view) { + if (span) { + if (!state.view) { + // set list or item view + state.view = view; + } + state.span = span; parts.shift(); } parseBeyondSpan(); }); + } else { + parseBeyondSpan(); } } else { callback(state); } } function parseBeyondSpan() { + if (!state.view) { + // set to default list or item view + state.view = self.options.views[state.type][ + !state.item ? 'list' : 'item' + ][0]; + } + Ox.print('pBS', state) + var sortKeyIds = (self.options.sortKeys[state.type][ + !state.item ? 'list' : 'item' + ][state.view] || []).map(function(sortKey) { + return sortKey.id; + }); if (parts.length && parts[0].split(',').every(function(str) { - return self.sortKeyIds.indexOf(str.replace(/^[\+-]/, '')) > -1; + return sortKeyIds.indexOf(str.replace(/^[\+-]/, '')) > -1; })) { - state.sort = parseSort(parts[0]); + // sort + state.sort = parseSort(parts[0], state); parts.shift(); } if (parts.length) { + // find state.find = parseFind(parts.join('/')); } callback(state); } } - that._constructURL = function(state) { + that._construct = function(state) { return constructURL(state); }; diff --git a/source/Ox.UI/js/Form/Ox.Filter.js b/source/Ox.UI/js/Form/Ox.Filter.js index a747d4f3..b1403049 100644 --- a/source/Ox.UI/js/Form/Ox.Filter.js +++ b/source/Ox.UI/js/Form/Ox.Filter.js @@ -49,22 +49,26 @@ Ox.Filter = function(options, self) { self.conditionOperators = { date: [ - {id: '', title: 'is'}, - {id: '!', title: 'is not'}, + {id: '=', title: 'is'}, + {id: '!=', title: 'is not'}, {id: '<', title: 'is before'}, + {id: '!<', title: 'is not before'}, {id: '>', title: 'is after'}, + {id: '!>', title: 'is not after'}, {id: '-', title: 'is between'}, {id: '!-', title: 'is not between'} ], list: [ - {id: '', title: 'is'}, - {id: '!', title: 'is not'} + {id: '=', title: 'is'}, + {id: '!=', title: 'is not'} ], number: [ - {id: '', title: 'is'}, - {id: '!', title: 'is not'}, + {id: '=', title: 'is'}, + {id: '!=', title: 'is not'}, {id: '<', title: 'is less than'}, + {id: '!<', title: 'is not less than'}, {id: '>', title: 'is greater than'}, + {id: '!>', title: 'is not greater than'}, {id: '-', title: 'is between'}, {id: '!-', title: 'is not between'}/*, {id: '^', title: 'starts with'}, @@ -73,18 +77,18 @@ Ox.Filter = function(options, self) { {id: '!$', title: 'does not end with'}*/ ], string: [ - {id: '=', title: 'is'}, - {id: '!=', title: 'is not'}, - {id: '', title: 'contains'}, - {id: '!', title: 'does not contain'}, + {id: '==', title: 'is'}, + {id: '!==', title: 'is not'}, + {id: '=', title: 'contains'}, + {id: '!=', title: 'does not contain'}, {id: '^', title: 'starts with'}, {id: '!^', title: 'does not start with'}, {id: '$', title: 'ends with'}, {id: '!$', title: 'does not end with'} ], text: [ - {id: '', title: 'contains'}, - {id: '!', title: 'does not contain'} + {id: '=', title: 'contains'}, + {id: '!=', title: 'does not contain'} ] }; self.operators = [ diff --git a/source/Ox.js b/source/Ox.js index d5e42ee7..899971bb 100644 --- a/source/Ox.js +++ b/source/Ox.js @@ -698,6 +698,12 @@ Ox.isEmpty Returns true if a collection is empty true > Ox.isEmpty(function() {}) true + > Ox.isEmpty(function(a) {}) + false + > Ox.isEmpty(null) + false + > Ox.isEmpty() + false @*/ Ox.isEmpty = function(val) { // fixme: what about deep isEmpty? @@ -771,8 +777,11 @@ Ox.len Returns the length of an array, function, object or string > Ox.len('abc') 3 @*/ -Ox.len = function(obj) { - return (Ox.isObject(obj) ? Ox.values(obj) : obj).length; +Ox.len = function(col) { + var type = Ox.typeOf(col); + return ['array', 'function', 'string'].indexOf(type) > -1 + ? col.length + : type == 'object' ? Ox.values(col).length : void 0; }; /*@ @@ -1253,6 +1262,10 @@ Ox.rgb = function(hsl) { //@ Ox.AMPM <[str]> ['AM', 'PM'] Ox.AMPM = ['AM', 'PM']; +//@ Ox.BASE_32_ALIASES Base 32 aliases +Ox.BASE_32_ALIASES = {'I': '1', 'L': '1', 'O': '0', 'U': 'V'}, +//@ Ox.BASE_32_DIGITS Base 32 digits +Ox.BASE_32_DIGITS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; //@ Ox.BCAD <[str]> ['BC', 'AD'] Ox.BCAD = ['BC', 'AD']; // fixme: this is unused, and probably unneeded @@ -1292,7 +1305,8 @@ Ox.KEYS = { 108: 'enter.numpad', 110: 'dot.numpad', 111: 'slash.numpad', 112: 'f1', 113: 'f2', 114: 'f3', 115: 'f4', 116: 'f5', 117: 'f6', 118: 'f7', 119: 'f8', 120: 'f9', 121: 'f10', - 122: 'f11', 123: 'f12', 124: 'f13', 125: 'f14', 126: 'f15', 127: 'f16', + 122: 'f11', 123: 'f12', 124: 'f13', 125: 'f14', 126: 'f15', + 127: 'f16', 128: 'f17', 129: 'f18', 130: 'f19', 131: 'f20', 144: 'numlock', 145: 'scrolllock', 186: 'semicolon', 187: 'equal', 188: 'comma', 189: 'minus', 190: 'dot', 191: 'slash', 192: 'backtick', 219: 'openbracket', @@ -1938,9 +1952,6 @@ Ox.element = function(str) { (function() { - var aliases = {'I': '1', 'L': '1', 'O': '0', 'U': 'V'}, - digits = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; - function cap(width, height) { // returns maximum encoding capacity of an image return parseInt(width * height * 3/8) - 4; @@ -1980,6 +1991,29 @@ Ox.element = function(str) { ); } + /*@ + Ox.encodeBase26 Encode a number as base26 + > Ox.encodeBase26(3758) + 'FOO' + @*/ + Ox.encodeBase26 = function(num) { + return Ox.map(num.toString(26), function(char) { + return Ox.char(65 + parseInt(char, 26)); + }).join(''); + }; + + /*@ + Ox.decodeBase26 Decodes a base26-encoded number + See Base 32. + > Ox.decodeBase26('foo') + 3758 + @*/ + Ox.decodeBase26 = function(str) { + return parseInt(Ox.map(str.toUpperCase(), function(char) { + return (char.charCodeAt(0) - 65).toString(26); + }).join(''), 26); + }; + /*@ Ox.encodeBase32 Encode a number as base32 See Base 32. @@ -1990,9 +2024,9 @@ Ox.element = function(str) { @*/ Ox.encodeBase32 = function(num) { return Ox.map(num.toString(32), function(char) { - return digits[parseInt(char, 32)]; + return Ox.BASE_32_DIGITS[parseInt(char, 32)]; }).join(''); - } + }; /*@ Ox.decodeBase32 Decodes a base32-encoded number @@ -2006,10 +2040,10 @@ Ox.element = function(str) { @*/ Ox.decodeBase32 = function(str) { return parseInt(Ox.map(str.toUpperCase(), function(char) { - var index = digits.indexOf(aliases[char] || char); + var index = Ox.BASE_32_DIGITS.indexOf(Ox.BASE_32_ALIASES[char] || char); return (index == -1 ? ' ' : index).toString(32); }).join(''), 32); - } + }; /*@ Ox.encodeBase64 Encode a number as base64 @@ -2018,7 +2052,7 @@ Ox.element = function(str) { @*/ Ox.encodeBase64 = function(num) { return btoa(Ox.encodeBase256(num)).replace(/=/g, ''); - } + }; /*@ Ox.decodeBase64 Decodes a base64-encoded number @@ -2027,7 +2061,7 @@ Ox.element = function(str) { @*/ Ox.decodeBase64 = function(str) { return Ox.decodeBase256(atob(str)); - } + }; /*@ Ox.encodeBase128 Encode a number as base128 @@ -2040,8 +2074,8 @@ Ox.element = function(str) { str = Ox.char(num & 127) + str; num >>= 7; } - return str; - } + return str || '0'; + }; /*@ Ox.decodeBase128 Decode a base128-encoded number @@ -2054,7 +2088,7 @@ Ox.element = function(str) { num += char.charCodeAt(0) << (len - i - 1) * 7; }); return num; - } + }; /*@ Ox.encodeBase256 Encode a number as base256 @@ -2068,7 +2102,7 @@ Ox.element = function(str) { num >>= 8; } return str; - } + }; /*@ Ox.decodeBase256 Decode a base256-encoded number @@ -2081,7 +2115,7 @@ Ox.element = function(str) { num += char.charCodeAt(0) << (len - i - 1) * 8; }); return num; - } + }; /*@ Ox.encodeDeflate Encodes a string, using deflate @@ -2127,7 +2161,7 @@ Ox.element = function(str) { } callback && callback(data); return data; - } + }; /*@ Ox.decodeDeflate Decodes an deflate-encoded string @@ -2179,7 +2213,7 @@ Ox.element = function(str) { } image.onerror = error; image.src = 'data:image/png;base64,' + btoa(data); - } + }; /*@ Ox.encodeHTML HTML-encodes a string @@ -2261,7 +2295,7 @@ Ox.element = function(str) { (and flip the second least significant bit, if at all) - write an extra png chunk containing some key */ - } + }; /*@ Ox.decodePNG Decodes an image, returns a string @@ -2306,7 +2340,7 @@ Ox.element = function(str) { } catch (e) { throw new RangeError('PNG codec can\'t decode image'); } - } + }; /*@ Ox.encodeUTF8 Encodes a string as UTF-8 @@ -2334,7 +2368,7 @@ Ox.element = function(str) { } return str; }).join(''); - } + }; /*@ Ox.decodeUTF8 Decodes an UTF-8-encoded string