diff --git a/source/Ox.UI/js/Core/Ox.Clipboard.js b/source/Ox.UI/js/Core/Ox.Clipboard.js index b1438309..6dce708a 100644 --- a/source/Ox.UI/js/Core/Ox.Clipboard.js +++ b/source/Ox.UI/js/Core/Ox.Clipboard.js @@ -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; } }; }(); diff --git a/source/Ox.UI/js/Core/Ox.URL.js b/source/Ox.UI/js/Core/Ox.URL.js index ea40ebaa..95ab5107 100644 --- a/source/Ox.UI/js/Core/Ox.URL.js +++ b/source/Ox.UI/js/Core/Ox.URL.js @@ -1,5 +1,400 @@ // vim: et:ts=4:sw=4:sts=4:ft=javascript -/*** -Ox.URL -***/ +/*@ +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 + (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 + string The string to be tested + callback callback function + id 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 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) +kv 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; + +}; \ No newline at end of file diff --git a/source/Ox.UI/js/Form/Ox.Filter.js b/source/Ox.UI/js/Form/Ox.Filter.js index 8faa6715..a747d4f3 100644 --- a/source/Ox.UI/js/Form/Ox.Filter.js +++ b/source/Ox.UI/js/Form/Ox.Filter.js @@ -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); diff --git a/source/Ox.UI/js/Panel/Ox.SplitPanel.js b/source/Ox.UI/js/Panel/Ox.SplitPanel.js index 0b92d599..42140413 100644 --- a/source/Ox.UI/js/Panel/Ox.SplitPanel.js +++ b/source/Ox.UI/js/Panel/Ox.SplitPanel.js @@ -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]]() }); }); }; diff --git a/source/Ox.js b/source/Ox.js index 9df7114b..d5e42ee7 100644 --- a/source/Ox.js +++ b/source/Ox.js @@ -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('
') .css({