-
+
+
-
-
+
-
+
@@ -80,715 +94,734 @@
diff --git a/examples/v0.2/index.js b/examples/v0.2/index.js
index ea6ddd627..3fc72573c 100644
--- a/examples/v0.2/index.js
+++ b/examples/v0.2/index.js
@@ -1,5 +1,61 @@
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o
div, div.dragon-list > ul > li, li.dragon-pop { line-height: 46px; }',
+ 'div.dragon-list > ul { top: 46px; }',
+ 'div.dragon-list > ul > li:not(:last-child)::before, li.dragon-pop::before {',
+ ' content: \'\\2b24\';', // BLACK LARGE CIRCLE
+ ' color: #b6b6b6;',
+ ' font-size: 30px;',
+ ' margin: 8px 14px 8px 8px; }',
+ 'li.dragon-pop { opacity:.8; }'
+ ]
+};
+
+function addStylesheet(key, referenceElement) {
+ cssInjector(stylesheets[key], key, referenceElement);
+}
+
+module.exports = addStylesheet;
+
+},{"css-injector":4}],2:[function(require,module,exports){
+module.exports = { // This file generated by gulp-imagine-64 at 6:03:02 AM on 2/1/2016
"calendar": {
type: "image/png",
data: "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAc0lEQVR4nIXQwQkCMRSE4U9ZLMCT9Xjaq2AfNhfYU5oQLMAOtoN48EWei5iBIRPe/yYQ3qrhf1lFG7iKcEaJxSfukUvMWgdHavt0uWHtg2QwxXnAnJZ2uOLyVZtybzzhgWNmfoFl0/YB87NbzR1cjP9xeQHSDC6mcL1xFQAAAABJRU5ErkJggg=="
@@ -34,7 +90,7 @@ module.exports = { // This file generated by gulp-imagine-64 at 1:11:59 PM on 1/
},
};
-},{}],2:[function(require,module,exports){
+},{}],3:[function(require,module,exports){
/* eslint-env browser */
'use strict';
@@ -59,7 +115,7 @@ images.filter = function(state) {
module.exports = images;
-},{"./images":1,"object-iterators":18}],3:[function(require,module,exports){
+},{"./images":2,"object-iterators":21}],4:[function(require,module,exports){
'use strict';
/* eslint-env browser */
@@ -140,7 +196,7 @@ cssInjector.idPrefix = 'injected-stylesheet-';
// Interface
module.exports = cssInjector;
-},{}],4:[function(require,module,exports){
+},{}],5:[function(require,module,exports){
'use strict';
/** @namespace extend-me **/
@@ -290,7 +346,7 @@ function initializePrototypeChain() {
module.exports = extend;
-},{}],5:[function(require,module,exports){
+},{}],6:[function(require,module,exports){
/* eslint-env browser */
// This is the main file, usable as is, such as by /test/index.js.
@@ -299,18 +355,14 @@ module.exports = extend;
'use strict';
-var cssInjector = require('css-injector');
+var unstrungify = require('unstrungify');
+var cssInjector = require('./js/css');
var FilterNode = require('./js/FilterNode');
var DefaultFilter = require('./js/FilterLeaf');
var template = require('./js/template');
var operators = require('./js/tree-operators');
-var css; // defined by code inserted by gulpfile between following comments
-/* inject:css */
-css = '.filter-tree{font-family:sans-serif;font-size:10pt;line-height:1.5em}.filter-tree label{font-weight:400}.filter-tree input[type=checkbox],.filter-tree input[type=radio]{left:3px;margin-right:3px}.filter-tree ol{margin-top:0}.filter-tree-add,.filter-tree-add-filter,.filter-tree-remove{cursor:pointer}.filter-tree-add,.filter-tree-add-filter{font-style:italic;color:#444;font-size:90%}.filter-tree-add-filter{margin:3px 0 3px 3em;width:120px;display:inline-block}.filter-tree-add-filter:hover,.filter-tree-add:hover{text-decoration:underline}.filter-tree-add-filter.as-menu-header,.filter-tree-add.as-menu-header{background-color:#fff;font-weight:700;font-style:normal}.filter-tree-add-filter.as-menu-header:hover{text-decoration:inherit}.filter-tree-add-filter>div,.filter-tree-add>div,.filter-tree-remove{display:inline-block;width:15px;height:15px;border-radius:8px;background-color:#8c8;font-size:11.5px;font-weight:700;color:#fff;text-align:center;line-height:normal;font-style:normal;font-family:sans-serif;text-shadow:0 0 1.5px grey;margin-right:4px}.filter-tree-add-filter>div:before,.filter-tree-add>div:before{content:\'\\ff0b\'}.filter-tree-remove{background-color:#e88;border:0}.filter-tree-remove:before{content:\'\\2212\'}.filter-tree li::after{font-size:70%;font-style:italic;font-weight:700;color:#080}.filter-tree>ol>li:last-child::after{display:none}.op-or>ol>li::after{margin-left:2.5em;content:\'— OR —\'}.op-and>ol>li::after{margin-left:2.5em;content:\'— AND —\'}.op-nor>ol>li::after{margin-left:2.5em;content:\'— NOR —\'}.filter-tree-default>:enabled{margin:0 .4em;background-color:#ddd;border:0}.filter-tree-default>input[type=text]{width:8em;padding:0 5px}.filter-tree-default>select{border:0}.filter-tree-default>.filter-tree-warning{background-color:#ffc}.filter-tree-default>.filter-tree-error{background-color:#Fcc}.filter-tree .footnotes{font-size:6pt;margin:2px 0 0;line-height:normal;white-space:normal;color:#999}.filter-tree .footnotes>ol{margin:0;padding-left:2em}.filter-tree .footnotes>ol>li{margin:2px 0}.filter-tree .footnotes .field-name,.filter-tree .footnotes .field-value{font-weight:700;color:#777}.filter-tree .footnotes .field-value:after,.filter-tree .footnotes .field-value:before{content:\'\"\'}.filter-tree .footnotes .field-value{font-family:monospace}.filter-tree-chooser{position:absolute;font-size:9pt;outline:0;box-shadow:5px 5px 10px grey}';
-/* endinject */
-
var ordinal = 0;
var reFilterTreeErrorString = /^filter-tree: /;
@@ -347,8 +399,8 @@ var reFilterTreeErrorString = /^filter-tree: /;
*/
var FilterTree = FilterNode.extend('FilterTree', {
- initialize: function(options) {
- cssInjector(css, 'filter-tree-base', options && options.cssStylesheetReferenceElement);
+ preInitialize: function(options) {
+ cssInjector('filter-tree-base', options && options.cssStylesheetReferenceElement);
if (options.editors) {
this.editors = options.editors;
@@ -376,6 +428,17 @@ var FilterTree = FilterNode.extend('FilterTree', {
this.el.addEventListener('click', catchClick.bind(this));
},
+ getState: unstrungify,
+
+ getJSON: function() {
+ var ready = JSON.stringify(this, null, this.JSONspace);
+ return ready ? ready : '';
+ },
+
+ setJSON: function(json) {
+ this.setState(JSON.parse(json));
+ },
+
load: function(state) {
if (!state) {
var filterEditorNames = Object.keys(this.editors),
@@ -389,13 +452,13 @@ var FilterTree = FilterNode.extend('FilterTree', {
// Validate `state.operator`
if (!(operators[state.operator] || state.operator === undefined && state.children.length === 1)) {
- throw this.Error('Expected `operator` property to be one of: ' + Object.keys(operators));
+ throw FilterNode.Error('Expected `operator` property to be one of: ' + Object.keys(operators));
}
this.operator = state.operator;
// Validate `state.children`
if (!(state.children instanceof Array && state.children.length)) {
- throw this.Error('Expected `children` property to be a non-empty array.');
+ throw FilterNode.Error('Expected `children` property to be a non-empty array.');
}
this.children = [];
var self = this;
@@ -487,6 +550,7 @@ var FilterTree = FilterNode.extend('FilterTree', {
},
/**
+ * @param {boolean} [object.rethrow=false] - Catch (do not throw) the error.
* @param {boolean} [object.alert=true] - Announce error via window.alert() before returning.
* @param {boolean} [object.focus=true] - Place the focus on the offending control and give it error color.
* @returns {undefined|string} `undefined` means valid or string containing error message.
@@ -496,6 +560,7 @@ var FilterTree = FilterNode.extend('FilterTree', {
var focus = options.focus === undefined || options.focus,
alert = options.alert === undefined || options.alert,
+ rethrow = options.rethrow === true,
result;
try {
@@ -504,7 +569,7 @@ var FilterTree = FilterNode.extend('FilterTree', {
result = err.message;
// Throw when not a filter tree error
- if (!reFilterTreeErrorString.test(result)) {
+ if (rethrow || !reFilterTreeErrorString.test(result)) {
throw err;
}
@@ -519,10 +584,12 @@ var FilterTree = FilterNode.extend('FilterTree', {
test: function test(dataRow) {
var operator = operators[this.operator],
- result = operator.seed;
+ result = operator.seed,
+ noChildrenDefined = true;
this.children.find(function(child) {
if (child) {
+ noChildrenDefined = false;
if (child instanceof DefaultFilter) {
result = operator.reduce(result, child.test(dataRow));
} else if (child.children.length) {
@@ -534,7 +601,7 @@ var FilterTree = FilterNode.extend('FilterTree', {
return false;
});
- return operator.negate ? !result : result;
+ return noChildrenDefined || (operator.negate ? !result : result);
},
toJSON: function toJSON() {
@@ -548,7 +615,10 @@ var FilterTree = FilterNode.extend('FilterTree', {
if (child instanceof DefaultFilter) {
state.children.push(child);
} else if (child.children.length) {
- state.children.push(toJSON.call(child));
+ var ready = toJSON.call(child);
+ if (ready) {
+ state.children.push(ready);
+ }
}
}
});
@@ -558,27 +628,29 @@ var FilterTree = FilterNode.extend('FilterTree', {
state[key] = metadata[key];
});
- return state;
+ return state.children.length ? state : undefined;
},
- toSQL: function toSQL() {
+ getSqlWhereClause: function getSqlWhereClause() {
var lexeme = operators[this.operator].SQL,
- where = lexeme.beg;
+ where = '';
this.children.forEach(function(child, idx) {
var op = idx ? ' ' + lexeme.op + ' ' : '';
if (child) {
if (child instanceof DefaultFilter) {
- where += op + child.toSQL();
+ where += op + child.getSqlWhereClause();
} else if (child.children.length) {
- where += op + toSQL.call(child);
+ where += op + getSqlWhereClause.call(child);
}
}
});
- where += lexeme.end;
+ if (!where) {
+ where = 'NULL IS NULL';
+ }
- return where;
+ return lexeme.beg + where + lexeme.end;
}
});
@@ -595,7 +667,7 @@ function throwIfJSON(state) {
if (typeof state === 'string') {
errMsg += ' See `JSON.parse()`.';
}
- throw this.Error(errMsg);
+ throw FilterNode.Error(errMsg);
}
}
@@ -610,6 +682,10 @@ function catchClick(evt) { // must be called with context
handler.call(this, evt);
evt.stopPropagation();
}
+
+ if (this.eventHandler) {
+ this.eventHandler(evt);
+ }
}
/**
@@ -704,25 +780,25 @@ function detachChooser() { // must be called with context
module.exports = FilterTree;
-},{"./js/FilterLeaf":6,"./js/FilterNode":7,"./js/template":8,"./js/tree-operators":9,"css-injector":3}],6:[function(require,module,exports){
+},{"./js/FilterLeaf":7,"./js/FilterNode":8,"./js/css":9,"./js/template":11,"./js/tree-operators":12,"unstrungify":26}],7:[function(require,module,exports){
/* eslint-env browser */
/* eslint-disable key-spacing */
'use strict';
-var regExpLIKE = require('regexp-like').cached;
-
var FilterNode = require('./FilterNode');
var template = require('./template');
+var operators = require('./leaf-operators');
/** @typedef {object} converter
* @property {function} to - Returns input value converted to type. Fails silently.
* @property {function} not - Tests input value against type, returning `false if type or `true` if not type.
*/
-/** @type converter */
+/** @type {converter} */
var numberConverter = { to: Number, not: isNaN };
-/** @type converter */
+
+/** @type {converter} */
var dateConverter = { to: function(s) { return new Date(s); }, not: isNaN };
/** @constructor
@@ -731,23 +807,19 @@ var dateConverter = { to: function(s) { return new Date(s); }, not: isNaN };
*/
var FilterLeaf = FilterNode.extend('FilterLeaf', {
- name: 'Column ? Literal',
+ name: 'column : value',
- operators: {
- '<' : { test: function(a, b) { return a < b; } },
- '\u2264' : { test: function(a, b) { return a <= b; }, SQL: '<=' },
- '=' : { test: function(a, b) { return a === b; } },
- '\u2265' : { test: function(a, b) { return a >= b; }, SQL: '>=' },
- '>' : { test: function(a, b) { return a > b; } },
- '\u2260' : { test: function(a, b) { return a !== b; }, SQL: '<>' },
- LIKE : { test: function(a, b) { return regExpLIKE(b).test(a); } },
- 'NOT LIKE': { test: function(a, b) { return !regExpLIKE(b).test(a); } }
+ preInitialize: function() {
+ this.onChange = cleanUpAndMoveOn.bind(this);
},
+ operators: operators,
+ operatorsOptions: operators.options,
+
destroy: function() {
if (this.controls) {
for (var key in this.controls) {
- this.controls[key].removeEventListener('change', cleanUpAndMoveOn);
+ this.controls[key].removeEventListener('change', this.onChange);
}
}
},
@@ -756,16 +828,16 @@ var FilterLeaf = FilterNode.extend('FilterLeaf', {
var fields = this.parent.nodeFields || this.fields;
if (!fields) {
- throw this.Error('Terminal node requires a fields list.');
+ throw FilterNode.Error('Terminal node requires a fields list.');
}
var root = this.el = document.createElement('span');
root.className = 'filter-tree-default';
this.controls = {
- column: this.makeElement(root, fields, 'column'),
- operator: this.makeElement(root, Object.keys(this.operators), 'operator'),
- argument: this.makeElement(root)
+ column: this.makeElement(root, fields, 'column', true),
+ operator: this.makeElement(root, this.operatorsOptions, 'operator'),
+ value: this.makeElement(root)
};
root.appendChild(document.createElement('br'));
@@ -799,7 +871,7 @@ var FilterLeaf = FilterNode.extend('FilterLeaf', {
* * Otherwise, creates a `` element with these options.
* @param {null|string} [prompt=''] - Adds an initial `` element to the drop-down with this value, parenthesized, as its `text`; and empty string as its `value`. Omitting creates a blank prompt; `null` suppresses.
*/
- makeElement: function(container, options, prompt) {
+ makeElement: function(container, options, prompt, sort) {
var el, option, span,
tagName = options ? 'select' : 'input';
@@ -816,8 +888,11 @@ var FilterLeaf = FilterNode.extend('FilterLeaf', {
container.appendChild(span);
} else {
- el = addOptions(tagName, options, prompt);
- this.el.addEventListener('change', cleanUpAndMoveOn);
+ el = addOptions(tagName, options, prompt, sort);
+ if (el.type === 'text' && this.eventHandler) {
+ this.el.addEventListener('keyup', this.eventHandler);
+ }
+ this.el.addEventListener('change', this.onChange);
FilterNode.setWarningClass(el);
container.appendChild(el);
}
@@ -887,57 +962,63 @@ var FilterLeaf = FilterNode.extend('FilterLeaf', {
* Caught by {@link FilterTree#validate|FilterTree.prototype.validate()}.
*
* Also performs the following compilation actions:
- * * Copies all the `this.controls`'s values from the DOM to similarly named properties of `this`.
- * * Pre-sets `this.operation`, `this.converter` and `this.sqlOperator` for efficient access in walks.
+ * * Copies all `this.controls`' values from the DOM to similarly named properties of `this`.
+ * * Pre-sets `this.op` and `this.converter` for use in `test`'s tree walk.
*
* @param {boolean} focus - Move focus to offending control.
* @returns {undefined} if valid
*/
validate: function(focus) {
- for (var elementName in this.controls) {
+ var elementName, fields, field;
+
+ for (elementName in this.controls) {
var el = this.controls[elementName],
value = controlValue(el).trim();
if (value === '') {
- if (focus) { focusOn(el); }
+ if (focus) { clickIn(el); }
throw new FilterNode.Error('Blank ' + elementName + ' control.\nComplete the filter or delete it.');
} else {
// Copy each controls's value to property of this object.
this[elementName] = value;
+ }
+ }
- switch (elementName) {
- case 'operator':
- var operator = this.operators[value];
- this.operation = operator.test; // for efficient access in this.test()
- this.sqlOperator = operator.SQL || value;
- break;
- case 'column':
- var fields = this.parent.nodeFields || this.fields,
- field = findField(fields, value);
- if (field && field.type) {
- this.converter = this.converters[field.type];
- }
+ this.op = this.operators[this.operator];
+
+ this.converter = undefined; // remains undefined when neither operator nor column is typed
+ if (this.op.type) {
+ this.converter = this.converters[this.op.type];
+ } else {
+ for (elementName in this.controls) {
+ if (/^column/.test(elementName)) {
+ fields = this.parent.nodeFields || this.fields;
+ field = findField(fields, this[elementName]);
+ if (field && field.type) {
+ this.converter = this.converters[field.type];
+ }
}
}
}
},
p: function(dataRow) { return dataRow[this.column]; },
- q: function() { return this.argument; },
+ q: function() { return this.value; },
test: function(dataRow) {
- var p = this.p(dataRow),
- q = this.q(dataRow),
+ var p, q, // untyped versions of args
P, Q, // typed versions of p and q
- convert = this.converter;
+ convert;
- return (
- convert &&
- !convert.not(P = convert.to(p)) &&
- !convert.not(Q = convert.to(q))
- )
- ? this.operation(P, Q)
- : this.operation(p, q);
+ return (p = this.p(dataRow)) === undefined || (q = this.q(dataRow)) === undefined
+ ? false
+ : (
+ (convert = this.converter) &&
+ !convert.not(P = convert.to(p)) &&
+ !convert.not(Q = convert.to(q))
+ )
+ ? this.op.test(P, Q)
+ : this.op.test(p, q);
},
toJSON: function(options) { // eslint-disable-line no-unused-vars
@@ -954,12 +1035,12 @@ var FilterLeaf = FilterNode.extend('FilterLeaf', {
return state;
},
- toSQL: function() {
- return [
- this.SQL_QUOTED_IDENTIFIER + this.column + this.SQL_QUOTED_IDENTIFIER,
- this.sqlOperator,
- ' \'' + this.argument.replace(/'/g, '\'\'') + '\''
- ].join(' ');
+ getSqlWhereClause: function() {
+ return this.SQL_QUOTED_IDENTIFIER + this.column + this.SQL_QUOTED_IDENTIFIER + ' ' + (
+ typeof this.op.sql === 'function'
+ ? this.op.sql(this.value)
+ : (this.op.sql || this.operator) + operators.sq(this.value)
+ );
}
});
@@ -1001,9 +1082,13 @@ function cleanUpAndMoveOn(evt) {
el.value = ''; // rid of any white space
FilterNode.clickIn(el);
}
+
+ if (this.eventHandler) {
+ this.eventHandler(evt);
+ }
}
-function focusOn(el) {
+function clickIn(el) {
setTimeout(function() {
el.classList.add('filter-tree-error');
FilterNode.clickIn(el);
@@ -1048,7 +1133,7 @@ function controlValue(el) {
* @param {null|string} [prompt=''] - Adds an initial `` element to the drop-down with this value in parentheses as its `text`; and empty string as its `value`. Omitting creates a blank prompt; `null` suppresses.
* @returns {Element} Either a `