diff --git a/examples/v0.2/index.html b/examples/v0.2/index.html
index 36f1f71e7..948c963b4 100644
--- a/examples/v0.2/index.html
+++ b/examples/v0.2/index.html
@@ -209,6 +209,10 @@
];
jsonModel.setBottomTotals(bottomTotals);
+ jsonGrid.registerFormatter('USD', accounting.formatMoney);
+ jsonGrid.registerFormatter('GBP', function(value) {
+ return accounting.formatMoney(value, "€", 2, ".", ",");
+ });
// setInterval(function(){
// topTotals[1][5] = Math.round(Math.random()*100);
// jsonModel.changed();
@@ -218,13 +222,10 @@
//sort ascending on the first column (first name)
//jsonModel.toggleSort(0);
- var leadingZeroIfNecessary = function(number) {
- return number < 10 ? '0' + number : number + '';
- };
- var upDown = fin.Hypergrid.images['up-down'];
+ var upDown = fin.Hypergrid.images['down-rectangle'];
var upDownSpin = fin.Hypergrid.images['up-down-spin'];
- var downArrow = fin.Hypergrid.images['down-rectangle'];
+ var downArrow = fin.Hypergrid.images['calendar'];
//all formatting and rendering per cell can be overridden in here
cellProvider.getCell = function(config) {
var renderer = cellProvider.cellCache.simpleCellRenderer;
@@ -263,8 +264,7 @@
config.value = [null, config.value, upDownSpinIMG];
} else if (x === 3 && !doAggregates) {
config.halign = 'left';
- var dateString = value = config.value.getFullYear() + '-' + leadingZeroIfNecessary(config.value.getMonth() + 1) + '-' + leadingZeroIfNecessary(config.value.getDay());
- config.value = [null, dateString, downArrowIMG];
+ config.value = [null, config.value, downArrowIMG];
} else if (x === 6) {
renderer = cellProvider.cellCache.buttonRenderer;
} else if (x === 7) {
@@ -273,14 +273,12 @@
config.halign = 'right';
config.backgroundColor = '#00' + bcolor + '00';
config.color = '#FFFFFF';
- config.value = accounting.formatMoney(config.value);
} else if (x === 8) {
var travel = 105 + Math.round(config.value*150/1000);
var bcolor = travel.toString(16);
config.halign = 'right';
config.backgroundColor = '#' + bcolor+ '0000';
config.color = '#FFFFFF';
- config.value = accounting.formatMoney(config.value, "€", 2, ".", ",");
}
return renderer;
};
@@ -288,46 +286,62 @@
//custom column filter....
function MyCustomFilter() {
var self = this;
- this.value = '';
this.alias = 'MyCustomFilter';
this.getDisplayString = function() {
return '< ' + this.value;
};
- this.template = function() {
- /*
-
cell value must be less than:
- */
+ this.initialize = function(dialog) {
+ this.filterTree = new fin.FilterTree({
+ fields: dialog.fields
+ });
+ delete this.filter;
};
- this.onShow = function(dialog) {
- var input = dialog.querySelector('#value');
- input.value = this.value;
+ this.onShow = function(dialog, container) {
+ container.appendChild(this.filterTree.el);
};
this.onOk = function(dialog) {
- var limit = parseInt(dialog.querySelector('#value').value);
- this.value = limit;
+
+ };
+
+ this.onReset = function(dialog) {
+
};
- this.onClear = function(dialog) {
- this.value = '';
+ this.onDelete = function(dialog) {
+ delete this.filter;
+ delete this.filterTree;
};
- this.create = function() {
- return function(data) {
- return data < self.value;
- };
- }
+ this.onCancel = function(dialog) {
+ delete this.filterTree;
+ };
+
+ this.create = function(state) {
+ if (!this.filter) {
+ var filterTree = new fin.FilterTree({
+ json: JSON.parse(state)
+ });
+ this.filter = filterTree.test.bind(filterTree); // called with 1 param: function(data)
+ }
+ return this.filter;
+ };
this.getState = function() {
- return this.value;
+ var jsonString = JSON.stringify(this.filterTree);
+ delete this.filterTree;
+ return jsonString;
};
this.setState = function(state) {
- this.value = state;
+ this.filterTree = new fin.FilterTree({
+ json: JSON.parse(state)
+ });
};
- };
+ }
+
var customFilter = new MyCustomFilter();
jsonGrid.registerFilter(customFilter);
@@ -350,13 +364,8 @@
cellEditor.setItems(['', true, false]); //editor here
return cellEditor;
}
- }
- if (x === 0) {
- cellEditor.setItems(lastNames);
- } else if (x === 4 || x === 5) {
- cellEditor.setItems(states);
} else if (x === 6) {
- return null; //editor here
+ return null;
} else if (x === 2) {
cellEditor.getInput().setAttribute('min', 0);
cellEditor.getInput().setAttribute('max', 10);
@@ -417,6 +426,11 @@
jsonGrid.repaint();
});
+
+ jsonGrid.addFinEventListener('fin-filter-applied', function(e) {
+ console.log('fin-filter-applied', e);
+ });
+
jsonGrid.addFinEventListener('fin-keydown', function(e) {
var key = e.detail.char;
var keys = e.detail.currentKeys;
@@ -584,10 +598,10 @@
fieldsMap.birthDate,
fieldsMap.birthState,
// fieldsMap.residenceState,
- // fieldsMap.employed,
+ fieldsMap.employed,
// fieldsMap.first_name,
- // fieldsMap.income,
- // fieldsMap.travel,
+ fieldsMap.income,
+ fieldsMap.travel,
// fieldsMap.squareOfIncome
],
@@ -621,7 +635,9 @@
jsonGrid.addProperties({
fixedRowCount: 4,
- showRowNumbers: true
+ showRowNumbers: true,
+ singleRowSelectionMode: false,
+ checkboxOnlyRowSelections: true
});
// properties that can be set
// use a function or a value
@@ -667,6 +683,34 @@
columnHeaderColor: 'white'
});
+ jsonModel.setColumnProperties(0,{
+ autopopulateEditor: true
+ });
+
+ jsonModel.setColumnProperties(1,{
+ autopopulateEditor: true
+ });
+
+ jsonModel.setColumnProperties(3,{
+ format: 'date'
+ });
+
+ jsonModel.setColumnProperties(4,{
+ autopopulateEditor: true
+ });
+
+ jsonModel.setColumnProperties(5,{
+ autopopulateEditor: true
+ });
+
+ jsonModel.setColumnProperties(7,{
+ format: 'USD'
+ });
+
+ jsonModel.setColumnProperties(8,{
+ format: 'GBP'
+ });
+
console.log(jsonModel.getHeaders());
console.log(jsonModel.getFields());
diff --git a/examples/v0.2/index.js b/examples/v0.2/index.js
index 775c79459..d62fc5b02 100644
--- a/examples/v0.2/index.js
+++ b/examples/v0.2/index.js
@@ -1,5 +1,9 @@
(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;odiv,.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:#900}.filter-tree>ol>li:last-child::after{display:none}.op-or>ol>li::after{content:\'\\A0— OR —\'}.op-and>ol>li::after{content:\'\\A0— AND —\'}.op-nor>ol>li::after{content:\'\\A0— NOR —\'}.filter-tree-default>*{margin:0 .4em}.filter-tree-chooser{position:absolute;font-style:italic;background-color:#8c8;color:#fff;font-size:11.5px;outline:0;box-shadow:5px 5px 10px grey}';
+/* endinject */
+
+/** @constructor
+ *
+ * @summary A node in a filter tree (including the root node), representing a complex filter expression.
+ *
+ * @desc A `FilterTree` is an n-ary tree with a single `operator` to be applied to all its `children`.
+ *
+ * Also known as a "subtree" or a "subexpression".
+ *
+ * Each of the `children` can be either:
+ *
+ * * a terninal node `Filter` (or an object inheriting from `Filter`) representing a simple conditional expression; or
+ * * a nested `FilterTree` representing a complex subexpression.
+ *
+ * The `operator` must be one of the {@link operators|tree operators} or may be left undefined iff there is only one child node.
+ *
+ * Notes:
+ * 1. A `FilterTree` may consist of a single leaf, in which case the `operator` is not used and may be left undefined. However, if a second child is added and the operator is still undefined, it will be set to the default (`'op-and'`).
+ * 2. The order of the children is undefined as all operators are commutative. For the '`op-or`' operator, evaluation ceases on the first positive result and for efficiency, all simple conditional expressions will be evaluated before any complex subexpressions.
+ * 3. A nested `FilterTree` is distinguished in the JSON object from a `Filter` by the presence of a `children` member.
+ * 4. Nesting a `FilterTree` containing a single child is valid (albeit pointless).
+ *
+ * @param {string[]} [localFields] - A list of field names for `Filter` objects to use. May be overridden by defining `json.localFields` here or in the `json` parameter of any descendant (including terminal nodes). If no such definition, will search up the tree for the first node with a defined `fields` member. In practice this parameter is not used herein; it may be used by the caller for the top-level (root) tree.
+ * @param {JSON} [json] - If ommitted, loads an empty filter (a `FilterTree` consisting of a single terminal node and the default `operator` value (`'op-and'`).
+ * @param {FilterTree} [parent] - Used internally to insert element when creating nested subtrees. For the top level tree, you don't give a value for `parent`; you are responsible for inserting the top-level `.el` into the DOM.
+ *
+ * @property {FilterTree} parent
+ * @property {number} ordinal
+ * @property {string} operator
+ * @property {FilterNode[]} children - Each one is either a `Filter` (or an object inheriting from `Filter`) or another `FilterTree`..
+ * @property {Element} el - The root element of this (sub)tree.
+ */
+var FilterTree = FilterNode.extend('FilterTree', {
+
+ initialize: function(options) {
+ cssInjector(css, 'filter-tree-base', options && options.cssStylesheetReferenceElement);
+
+ if (options.editors) {
+ FilterTree.prototype.editors = options.editors;
+ chooser = makeChooser();
+ } else if (!chooser) {
+ chooser = makeChooser();
+ }
+ },
+
+ editors: {
+ Default: DefaultFilter
+ },
+
+ newView: function() {
+ this.el = template('tree', ++ordinal);
+ this.el.addEventListener('click', catchClick.bind(this));
+ },
+
+ fromJSON: function(json) {
+ if (json) {
+ // Validate the JSON object
+ if (typeof json !== 'object') {
+ var errMsg = 'Expected `json` parameter to be an object.';
+ if (typeof json === 'string') {
+ errMsg += ' See `JSON.parse()`.';
+ }
+ throw this.Error(errMsg);
+ }
+
+ // Validate `json.children`
+ if (!(json.children instanceof Array && json.children.length)) {
+ throw this.Error('Expected `children` field to be a non-empty array.');
+ }
+ this.children = [];
+ var self = this;
+ json.children.forEach(function(json) { // eslint-disable-line no-shadow
+ var Constructor;
+ if (typeof json !== 'object') {
+ throw self.Error('Expected child to be an object containing either `children`, `type`, or neither.');
+ }
+ if (json.children) {
+ Constructor = FilterTree;
+ } else {
+ Constructor = self.editors[json.type || 'Default'];
+ }
+ self.children.push(new Constructor({
+ json: json,
+ parent: self
+ }));
+ });
+
+ // Validate `json.operator`
+ if (!(operators[json.operator] || json.operator === undefined && json.children.length === 1)) {
+ throw this.Error('Expected `operator` field to be one of: ' + Object.keys(operators));
+ }
+ this.operator = json.operator;
+ } else {
+ var filterEditorNames = Object.keys(this.editors),
+ onlyOneFilterEditor = filterEditorNames.length === 1;
+ this.children = onlyOneFilterEditor ? [new this.editors[filterEditorNames[0]]({
+ parent: this
+ })] : [];
+ this.operator = 'op-and';
+ }
+ },
+
+ render: function() {
+ // simulate click on the operator to display strike-through and operator between filters
+ var radioButton = this.el.querySelector('input[value=' + this.operator + ']');
+ radioButton.checked = true;
+ this['filter-tree-choose-operator']({
+ target: radioButton
+ });
+
+ // when multiple filter editors available, simulate click on the new "add conditional" link
+ if (!this.children.length && Object.keys(this.editors).length > 1) {
+ var addFilterLink = this.el.querySelector('.filter-tree-add-filter');
+ this['filter-tree-add-filter']({
+ target: addFilterLink
+ });
+ }
+
+ // proceed with render
+ FilterNode.prototype.render.call(this);
+ },
+
+ 'filter-tree-choose-operator': function(evt) {
+ var radioButton = evt.target;
+
+ this.operator = radioButton.value;
+
+ // display strike-through
+ var radioButtons = this.el.querySelectorAll('label>input.filter-tree-choose-operator[name=' + radioButton.name + ']');
+ Array.prototype.slice.call(radioButtons).forEach(function(radioButton) { // eslint-disable-line no-shadow
+ radioButton.parentElement.style.textDecoration = radioButton.checked ? 'none' : 'line-through';
+ });
+
+ // display operator between filters by adding operator string as a CSS class of this tree
+ for (var key in operators) {
+ this.el.classList.remove(key);
+ }
+ this.el.classList.add(this.operator);
+ },
+
+ 'filter-tree-add-filter': function(evt) { // eslint-disable-line
+ var filterEditorNames = Object.keys(this.editors);
+ if (filterEditorNames.length === 1) {
+ this.children.push(new this.editors[filterEditorNames[0]]({
+ parent: this
+ }));
+ } else {
+ attachChooser.call(this, evt);
+ }
+ },
+
+ 'filter-tree-add': function() {
+ this.children.push(new FilterTree({
+ parent: this
+ }));
+ },
+
+ 'filter-tree-remove': function(evt) {
+ var deleteButton = evt.target,
+ listItem = deleteButton.parentElement,
+ children = this.children,
+ el = deleteButton.nextElementSibling;
+
+ children.forEach(function(child, idx) {
+ if (child.el === el) {
+ delete children[idx];
+ listItem.remove();
+ }
+ });
+ },
+
+ test: function(string) {
+ var number = Number(string);
+ return test.call(this, string, number, isNaN(number));
+ },
+
+ toJSON: function toJSON() {
+ var json = {
+ operator: this.operator,
+ children: []
+ };
+
+ this.children.forEach(function(child) {
+ var isTerminalNode = !(child instanceof FilterTree);
+ if (isTerminalNode || child.children.length) {
+ json.children.push(isTerminalNode ? child : toJSON.call(child));
+ }
+ });
+
+ var tree = this;
+ ['fields', 'nodeFields'].forEach(function(prop) {
+ if (!tree.parent || tree[prop] && tree[prop] !== tree.parent[prop]) {
+ json[prop] = tree[prop];
+ }
+ });
+
+ return json;
+ },
+
+ toSQL: function toSQL() {
+ var lexeme = operators[this.operator].SQL,
+ where = lexeme.beg;
+
+ this.children.forEach(function(child, idx) {
+ var isTerminalNode = !(child instanceof FilterTree);
+ if (isTerminalNode || child.children.length) {
+ if (idx) {
+ where += ' ' + lexeme.op + ' ';
+ }
+ where += isTerminalNode ? child.toSQL() : toSQL.call(child);
+ }
+ });
+
+ where += lexeme.end;
+
+ return where;
+ }
+
+});
+
+function catchClick(evt) {
+ var elt = evt.target;
+
+ var handler = this[elt.className] || this[elt.parentNode.className];
+ if (handler) {
+ detachChooser();
+ handler.call(this, evt);
+ evt.stopPropagation();
+ }
+}
+
+function test(s, n, textCompare) {
+ var operator = operators[this.operator],
+ result = operator.seed;
+
+ for (var i = 0; i < this.children.length && result !== operator.abort; ++i) {
+ var child = this.children[i],
+ isTerminalNode = !(child instanceof FilterTree);
+
+ if (isTerminalNode || child.children.length) {
+ var method = isTerminalNode ? child.test : test;
+ result = operator.reduce(result, method.call(child, s, n, textCompare));
+ }
+ }
+
+ if (operator.negate) {
+ result = !result;
+ }
+
+ return result;
+}
+
+function makeChooser() {
+ var $ = document.createElement('select'),
+ editors = Object.keys(FilterTree.prototype.editors);
+
+ $.className = 'filter-tree-chooser';
+ $.size = editors.length;
+
+ editors.forEach(function(key) {
+ $.add(new Option(key));
+ });
+
+ $.onmouseover = function(evt) {
+ evt.target.selected = true;
+ };
+
+ return $;
+}
+
+var chooserParent;
+
+function attachChooser(evt) {
+ var tree = this,
+ rect = evt.target.getBoundingClientRect();
+
+ if (!rect.width) {
+ // not in DOM yet so try again later
+ setTimeout(function() {
+ attachChooser.call(tree, evt);
+ }, 50);
+ return;
+ }
+
+ chooser.style.left = rect.left + 19 + 'px';
+ chooser.style.top = rect.bottom + 'px';
+
+ window.addEventListener('click', detachChooser); // detach chooser if click outside
+
+ chooser.onclick = function() {
+ tree.children.push(new tree.editors[chooser.value]({
+ parent: tree
+ }));
+ // click bubbles up to window where it detaches chooser
+ };
+
+ chooser.onmouseout = function() {
+ chooser.selectedIndex = -1;
+ };
+
+ chooserParent = this.el;
+ chooserParent.appendChild(chooser);
+ var link = chooserParent.querySelector('.filter-tree-add-filter');
+ link.style.backgroundColor = window.getComputedStyle(chooser).backgroundColor;
+}
+
+function detachChooser() {
+ if (chooserParent) {
+ chooser.selectedIndex = -1;
+ chooserParent.querySelector('.filter-tree-add-filter').style.backgroundColor = null;
+ chooserParent.removeChild(chooser);
+ chooser.onclick = chooser.onmouseout = chooserParent = null;
+ window.removeEventListener('click', detachChooser);
+ }
+}
+
+module.exports = FilterTree;
+
+},{"./js/FilterLeaf":6,"./js/FilterNode":7,"./js/template":8,"./js/tree-operators":9,"css-injector":3}],6:[function(require,module,exports){
+/* eslint-env browser */
+
+'use strict';
+
+var FilterNode = require('./FilterNode');
+
+var operators = {
+ '<': { test: function(a, b) { return a < b; } },
+ '≤': { test: function(a, b) { return a <= b; }, SQL: '<=' },
+ '=': { test: function(a, b) { return a === b; } },
+ '≥': { test: function(a, b) { return a >= b; }, SQL: '>=' },
+ '>': { test: function(a, b) { return a > b; } },
+ '≠': { test: function(a, b) { return a !== b; }, SQL: '<>' }
+};
+
+/** @constructor
+ * @summary A terminal node in a filter tree, representing a conditional expression.
+ * @desc Also known as a "filter."
+ */
+var FilterLeaf = FilterNode.extend('FilterLeaf', {
+
+ newView: function() {
+ var root = this.el = document.createElement('span');
+ root.className = 'filter-tree-default';
+
+ this.bindings = {
+ field: makeElement(root, this.parent.nodeFields || this.fields),
+ operator: makeElement(root, Object.keys(operators)),
+ argument: makeElement(root)
+ };
+
+ root.appendChild(document.createElement('br'));
+ },
+
+ fromJSON: function(json) {
+ var value, element, i;
+ if (json) {
+ for (var key in json) {
+ if (key !== 'fields' && key !== 'type') {
+ value = json[key];
+ element = this.bindings[key];
+ switch (element.type) {
+ case 'checkbox':
+ case 'radio':
+ element = document.querySelectorAll('input[name=\'' + element.name + '\']');
+ for (i = 0; i < element.length; i++) {
+ element[i].checked = value.indexOf(element[i].value) >= 0;
+ }
+ break;
+ case 'select-multiple':
+ element = element.options;
+ for (i = 0; i < element.length; i++) {
+ element[i].selected = value.indexOf(element[i].value) >= 0;
+ }
+ break;
+ default:
+ element.value = value;
+ }
+ }
+ }
+ }
+ },
+
+ test: function(Ls, Ln, textCompare) {
+ var test = operators[this.bindings.operator.value].test,
+ Rs = this.bindings.argument.value,
+ Rn;
+
+ return textCompare || isNaN(Rn = Number(Rs)) ? test(Ls, Rs) : test(Ln, Rn);
+ },
+
+ toJSON: function() {
+ var element, value, i, key, json = {};
+ if (this.type) {
+ json.type = this.type;
+ }
+ for (key in this.bindings) {
+ element = this.bindings[key];
+ switch (element.type) {
+ case 'checkbox':
+ case 'radio':
+ element = document.querySelectorAll('input[name=\'' + element.name + '\']:enabled:checked');
+ for (value = [], i = 0; i < element.length; i++) {
+ value.push(element[i].value);
+ }
+ break;
+ case 'select-multiple':
+ element = element.options;
+ for (value = [], i = 0; i < element.length; i++) {
+ if (!element.disabled && element.selected) {
+ value.push(element[i].value);
+ }
+ }
+ break;
+ default:
+ value = element.value;
+ }
+ json[key] = value;
+ }
+ if (!this.parent.nodeFields && this.fields !== this.parent.fields) {
+ json.fields = this.fields;
+ }
+ return json;
+ },
+
+ toSQL: function() {
+ return [
+ this.bindings.field.value,
+ operators[this.bindings.operator.value].SQL || this.bindings.operator.value,
+ ' \'' + this.bindings.argument.value.replace(/'/g, '\'\'') + '\''
+ ].join(' ');
+ }
+});
+
+/** @typedef valueOption
+ * @property {string} value
+ * @property {string} text
+ */
+/** @typedef optionGroup
+ * @property {string} label
+ * @property {fieldOption[]} options
+ */
+/** @typedef {string|valueOption|optionGroup|string[]} fieldOption
+ * @desc If a simple array of string, you must add a `label` property to the array.
+ */
+/**
+ * @summary HTML form control factory.
+ * @desc Creates and appends a text box or a drop-down.
+ * @returns The new element.
+ * @param {Element} container - An element to which to append the new element.
+ * @param {fieldOption|fieldOption[]} [options] - Overloads:
+ * * If omitted, will create an `` (text box) element.
+ * * If a single option (either as a scalar or as the only element in an array), will create a `...` element containing the string and a `` containing the value.
+ * * Otherwise, creates a `` element with these strings added as `` elements. Option groups may be specified as nested arrays.
+ * @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.
+ */
+function makeElement(container, options, prompt) {
+ var el,
+ tagName = options ? 'select' : 'input';
+
+ if (options && options.length === 1) {
+ var option = options[0];
+ el = document.createElement('span');
+ el.innerHTML = option.text || option;
+
+ var input = document.createElement('input');
+ input.type = 'hidden';
+ input.value = option.value || option;
+ el.appendChild(input);
+ } else {
+ el = addOptions(tagName, options, prompt);
+ }
+ container.appendChild(el);
+ return el;
+}
+
+/**
+ * @summary Creates a new element and adds options to it.
+ * @param {string} tagName - Must be one of:
+ * * `'input'` for a text box
+ * * `'select'` for a drop-down
+ * * `'optgroup'` (for internal use only)
+ * @param {fieldOption[]} [options] - Strings to add as `` elements. Omit when creating a text box.
+ * @param {null|string} [prompt=''] - Adds an initial `` element to the drop-down with `text` this value in parentheses, as its `text`; and empty string as its `value`. Omitting creates a blank prompt; `null` suppresses.
+ * @returns {Element} Either a `