Commit 4be6b8a3 authored by S Anand's avatar S Anand
Browse files

Merge branch 'dev'

parents c0e152ad 87e0d614
Pipeline #48397 passed with stage
in 2 minutes and 29 seconds
# Change log
- `0.7.0`: 19 May 2018
- [$.formhandler](#formhandler) supports grids via `table: 'grid'`
- [$.formhandler](#formhandler) tables can be edited by the user via `edit: true`
- `0.6.0`: 15 Apr 2018
- [sanddance](#sanddance) smoothly animates selections into pre-defined and custom layouts
- [$.formhandler](#formhandler) and [g1.datafilter](#datafilter) support namespaces
......
......@@ -112,9 +112,167 @@ URLFilter containers uses these attributes:
- You can also specify `data-mode`, `data-remove` and `data-src`. This is the same as specifying on
every `.urlfilter` trigger.
## url.parse
`g1.url` provides URL manipulation utilities.
```js
var url = g1.url.parse("https://username:password@example.com:80/~folder/subfolder/filename.html?a=1&a=2&b=3%2E&d#hash")
```
### url object attributes
This parses the URL and returns an object with the following attributes matching `window.location`:
| Attribute | Value |
|------------|------------------------------------|
| `href` | the original URL |
| `protocol` | `https` |
| `origin` | `username:password@example.com:80` |
| `username` | `username` |
| `password` | `password` |
| `hostname` | `example.com` |
| `port` | `80` |
| `pathname` | `folder/subfolder/filename.html` |
| `search` | `a=1&a=2&b=3%2E&d` |
| `hash` | `hash` |
... and additional attributes:
| Attribute | Value |
|--------------|--------------------------------------------------------|
| `userinfo` | `username:password` |
| `relative` | `folder/subfolder/filename.html?a=1&a=2&b=3%2E&d#hash` |
| `directory` | `folder/subfolder/` |
| `file` | `filename.html` |
| `searchKey` | `{'a:'2', b:'3.', d:''}` |
| `searchList` | `{'a:['1', '2'], b:['3.'], d:['']}` |
It can also parse URL query strings.
```js
var url = g1.url.parse('?a=1&a=2&b=3%2E&d#hash')
```
| Attribute | Value |
|--------------|----------------------------------|
| `search` | `a=1&a=2&b=3%2E&d` |
| `hash` | `hash` |
| `searchKey` | `{a:'2', b:'3.', d:''}` |
| `searchList` | `a:['1', '2'], b:['3.'], d:['']` |
These attributes are **not mutable**. To change the URL, use
[url.join](#urljoin) or [url.update](#urlupdate).
### url object methods
The url object has a `.toString()` method that converts the object back into a
string.
## url.join
```js
var url = url.join(another_url)
```
updates the `url` with the attributes from `another_url`. For example:
| url | joined with | gives |
|------------------------|----------------------|----------------------------|
| `/path/p` | `a/b/c` | `/path/a/b/c` |
| `/path/p/q/` | `../a/..` | `/path/p/` |
| `http://host1/p` | `http://host2/q` | `http://host2/q` |
| `https://a:b@host1/p` | `//c:d@host2/q?x=1` | `https://c:d@host2/q?x=1` |
| `/path/p?b=1` | `./?a=1#top` | `/path/?a=1#top` |
`.join()` updates the query parameters and hash fragment as well. To prevent this, use:
```js
url.join(another_url, {query: false, hash: false})
```
For example:
```js
g1.url.parse('/').join('/?x=1#y=1', {hash: false}).toString() == '/?x=1'
g1.url.parse('/').join('/?x=1#y=1', {query: false}).toString() == '/#y=1'
```
## url.update
```js
var url = url.update(object)
```
updates the `url` query parameters with the attributes from `object`. For example:
| url | updated with | gives |
|--------------|----------------------|-----------------------|
| `/` | `{a:1}` | `/?a=1` |
| `/?a=1&b=2` | `{b:3, a:4, c:''}` | `/?a=4&b=3&c=` |
| `/?a=1&b=2` | `{a:null}` | `/?b=2` |
| `/?a=1&b=2` | `{a:[3,4], b:[4,5]}` | `/?a=3&a=4&b=4&b=5` |
By default, it *updates* the query parameters. But:
- `url.update(object, 'add')` *adds* the query parameters instead of updating
- `url.update(object, 'del')` *deletes* the query parameters instead of updating
- `url.update(object, 'toggle')` *toggles* the query parameters (i.e. adds if missing, deletes if present)
For example:
| url | updated with | in mode | gives |
|---------------------|----------------------|---------------------|-----------------------|
| `/?a=1&a=2` | `{a:3, b:1}` | `add` | `/?a=1&a=2&a=3&b=1` |
| `/?a=1&a=2'` | `{a:[3,4]}` | `add` | `/?a=1&a=2&a=3&a=4` |
| `/?a=1&a=2&b=1` | `{a:2, b:2}` | `del` | `/?a=1&b=1` |
| `/?a=1&a=2&b=1` | `{a:[1,4]}` | `del` | `/?a=2&b=1` |
| `/?a=1&a=2` | `{a:1, b:1}` | `toggle` | `/?a=2&b=1` |
| `/?a=1&a=2&b=1&b=2` | `{a:[2,3], b:[1,3]}` | `toggle` | `/?a=1&a=3&b=2&b=3` |
You can specify different modes for different query parameters.
```js
g1.url.parse('/?a=1&b=2&c=3&d=4') // Update this URL
.update({a:1, b:[2,3], c:6, d:7}, // With this object
'a=del&b=toggle&c=add') // Delete ?a, Toggle ?b, add ?c, update ?d (default)
// Returns /?b=3&c=3&c=6&d=7
```
## Interaction conventions
All interaction components use this naming convention:
- Interactions are enabled on a *container*, typically `body`. For example,
`$('body').urlfilter()`. Containers have these common attributes:
- `data-selector`: selector to identify triggers. e.g. `.urlfilter`, `.highlight`
- `data-target`: selector that all triggers act on by default
- `data-mode`: mode of interaction for all triggers
- `data-attr`: attribute that contains the interaction data, e.g. `href` for `.urlfilter`
- Interactions are triggered on a *trigger*. For example, `.urlfilter` for `$().urlfilter()`.
Clicking / hovering on / typing in a trigger triggers the interaction.
- `data-target`: selector that this trigger acts on
- `data-mode`: mode of interaction for this trigger
- Interactions change a *target*. For example, `urlfilter` changes `location.href` by default. The
`data-target` on containers and triggers define this.
- Interactions data is contained in an attribute. This is applied to the target. For example,
`.urlfilter` applied `href` to the target. The attribute name is stored in `data-attr`.
- Interactions have *modes*. This can be controlled using `data-mode=`.
All container `data-` attributes can also be passed as an option to the
function. For example, `<body data-selector=".link">` is the same as
`$('body').urlfilter({selector: '.link'})`.
### $.urlfilter events
- `urlfilter` is fired on the trigger when the URL is changed. Attributes:
- `urlfilter` is fired on the trigger when the URL is changed.
Note: if the page is reloaded (e.g. if there is no `data-target=`),
the page is reloaded and the event is lost. Attributes:
- `url`: the new URL
- `load` is fired on the target when the URL is loaded -- only if the `data-target=` is a selector. Attributes:
- `url`: the new URL
......@@ -158,6 +316,47 @@ Highlight containers use these attributes:
- `target`: elements that match the `data-target=` selector
## types
`g1.types(data)` returns the column data types. For example:
```js
var data = [
{a: 1, b: 1.1, c: 'c', d: '2014-04-04', e: true},
{a: 2, b: 2},
]
g1.types(data) // Returns {"a": "number", "b": "number", "c": "string", "d": "date", "e": "boolean"}
```
### types options
`types()` accepts 2 parameters:
- `data`: a list of objects
- `options`: a dictionary that may contain these keys:
- `convert`: converts values to the right type. For example, "1" is converted to 1. default: `false`
- `limit`: number of rows to evaluate. default: 1000
- `ignore`: list of values that should be ignored. default: `[null, undefined]`
Rules:
- Evaluate up to `limit` rows
- Ignore values that are keys in the `ignore` option. Only consider the rest
- If `convert` is `false`, then for each column:
- If all values are Date objects -> `date`
- Else if all values are numbers -> `number`
- Else if all values are strings -> `string`
- Else if all values are bools -> `boolean`
- Else if there are no values or is undefined or null -> `null`
- Else -> `mixed`
- Else if `convert` is `true`, then for each column:
- If all values can be converted to Date -> `date`
- Else if all values can be converted to numbers -> `number`
- Else if all values are bools -> `boolean`
- Else if there are no values or is undefined or null -> `null`
- Else -> `string`
## datafilter
`g1.datafilter(data, filters)` returns the filtered data based on the filters. While urlfilter on [$.formhandler](#formhandler) applies filtering on data server side, `datafilter` applies urlfilter on frontend loaded data.
......@@ -277,14 +476,25 @@ In the above example, `data-page-size="10"` over-rides `pageSize: 20`.
The full list of options is below. Simple options can be specified as `data-` attributes as well.
- `src`: [FormHandler][formhandler] URL endpoint
- `namespace`: (Optional) If the URL has `?name:key=value`, the filter
`key=value` only applies to formhandlers with namespace as `name`.
Filters without a namespace like `?key=value` will apply to all formhandlers.
- `columns`: comma-separated column names to display, or a list of objects with these keys:
- `name`: column name. `"*"` is a special column placeholder for "all columns"
- `name`: column name. `"*"` is a special column placeholder for "all columns" (options given for `"*"` are applied for all columns)
- `title`: for header display. Defaults to the same value as `name`
- `type`: `text` (default) / `number` / `date`. Data type. Determines filters to be used
- `format`: string / function that renders the cell contents.
- functions are applied to the value and the return value is used
- `format`: string / function that returns formatted value.
- function(row, data) returns formatted value
- strings specify a numeral.js format if the value is a number (you must include numeral.js)
- strings specify a moment.js format if the value is a date (you must include moment.js)
- `editable`: `true` (default) / `false`. When `true`, edit and save buttons appears at end of each row.
- `template`: string template / function that renders the cell.
- function accepts an object with these keys:
- `value`: cell data value
- `format`: cell display value
- `link`: cell link value (if applicable)
- `data`: the dataset from `src`
- string template can use the above variables
- `sort`: `true` / `false` / operators dict with:
- `{'': 'Sort ascending', '-': 'Sort descending'}` (default)
- `filters`: `true` (default) / `false` / operators dict with:
......@@ -293,12 +503,19 @@ The full list of options is below. Simple options can be specified as `data-` at
- `link`: string / function that generates a link for this each cell.
- If no `link:` is specified, clicking on the cell filters by that cell.
- If `link:` is a string, opens a new window with the string URL interpolated as a lodash template with `row` as data.
Example: `"https://example.org/city/<%- city >"`
Example: `"https://example.org/city/<%- row.city >"`
- If `link:` is a function, opens a new window with the URL as `fn(row)`.
Example: `function(row) { return 'https://example.org/city/' + row.city }`
- `hideable`: `true` (default) / `false`. Hides the column
- `hideable`: `true` (default) / `false`. Show or hide `Hide` option in header dropdown
- `hide`: `true` / `false` (default). Hides the column
- `unique`: TODO: {dict of query parameter and display value} or [list of values] or function?
- `table`: Shows the table control. Can be `true` (default) / `false`
- `edit`: Shows the edit control. Can be `true` / `false` (default)
- `actions`: A list of objects. If `data-action='delete'` action name is delete, you need not add it to actions
- `actionName`: a function() that gets triggered on clicking the element with `data-action='actionName` attribute. The function has arguments ... //TODO
- `table`: Shows the table control. Can be:
- `true`: displays a table (default)
- `'grid'`: renders a grid instead of a table
- `false`: disables the table (and shows nothing for the main content)
- `count`: Shows the number of rows. Can be `true` (default) / `false`
- `page`: Shows the page control. Can be `true` (default) / `false`.
- `pageSize`: page size. Defaults to 100
......@@ -313,6 +530,9 @@ The full list of options is below. Simple options can be specified as `data-` at
- `args`: the URL query parameters passed to the FormHandler
- `options`: the options applicable to the FormHandler
- returns a dict with modified values of `data` and `meta`
- `icon`: if `table: 'grid'` is used, display an icon:
- `icon: 'fa fa-home fa-3x'` renders a FontAwesome home icon
- `icon: './path/to/image.png'` renders the image specified
**Advanced**. Each component can have a target which specifies a selector. For
e.g., to render the export button somewhere else, use
......@@ -338,12 +558,16 @@ with a simple input. Available template strings are:
- `exportTemplate`
- `filtersTemplate`
- `searchTemplate`
- `rowTemplate`, which can be a string template or function
- function accepts an object with these keys:
- `row`: row data
- `data`: the dataset from `src`
- string template can use the above variables
Features to be implemented:
- Loading indicator
- Full text search
- URL prefix / namespace, if there are multiple tables on the same page
- URL targets other than '#', e.g. pushState
### $.formhandler events
......@@ -356,21 +580,35 @@ Features to be implemented:
### $.formhandler examples
Add a simple table using the FormHandler at `./data` that shows specific columns
with a page size of 10 rows, and does not show the export filter.
Render a table using the FormHandler at `./data`:
```html
<div class="formhandler"
src="./data"
data-columns="id,country,state,sales"
data-page-size="10"
data-export="false"
></div>
<div class="formhandler" data-src="./data"></div>
<script>
$('.formhandler').formhandler()
</script>
```
Customize cell rendering to display a chart in a cell:
```html
<div class="formhandler" data-src="./data"></div>
<script>
$('.formhandler').formhandler({
columns: [
{name: '*'},
{
name: 'c1',
format: function (o) {
return '<svg height="10" width="10"><circle cx="5" cy="5" r="' + o.c1 / 10 + '" fill="red"></circle></svg>'
}
}
}
})
</script>
```
## $.template
......@@ -742,200 +980,6 @@ selection.call(
```
## url.parse
`g1.url` provides URL manipulation utilities.
```js
var url = g1.url.parse("https://username:password@example.com:80/~folder/subfolder/filename.html?a=1&a=2&b=3%2E&d#hash")
```
### url object attributes
This parses the URL and returns an object with the following attributes matching `window.location`:
| Attribute | Value |
|------------|------------------------------------|
| `href` | the original URL |
| `protocol` | `https` |
| `origin` | `username:password@example.com:80` |
| `username` | `username` |
| `password` | `password` |
| `hostname` | `example.com` |
| `port` | `80` |
| `pathname` | `folder/subfolder/filename.html` |
| `search` | `a=1&a=2&b=3%2E&d` |
| `hash` | `hash` |
... and additional attributes:
| Attribute | Value |
|--------------|--------------------------------------------------------|
| `userinfo` | `username:password` |
| `relative` | `folder/subfolder/filename.html?a=1&a=2&b=3%2E&d#hash` |
| `directory` | `folder/subfolder/` |
| `file` | `filename.html` |
| `searchKey` | `{'a:'2', b:'3.', d:''}` |
| `searchList` | `{'a:['1', '2'], b:['3.'], d:['']}` |
It can also parse URL query strings.
```js
var url = g1.url.parse('?a=1&a=2&b=3%2E&d#hash')
```
| Attribute | Value |
|--------------|----------------------------------|
| `search` | `a=1&a=2&b=3%2E&d` |
| `hash` | `hash` |
| `searchKey` | `{a:'2', b:'3.', d:''}` |
| `searchList` | `a:['1', '2'], b:['3.'], d:['']` |
These attributes are **not mutable**. To change the URL, use
[url.join](#urljoin) or [url.update](#urlupdate).
### url object methods
The url object has a `.toString()` method that converts the object back into a
string.
## url.join
```js
var url = url.join(another_url)
```
updates the `url` with the attributes from `another_url`. For example:
| url | joined with | gives |
|------------------------|----------------------|----------------------------|
| `/path/p` | `a/b/c` | `/path/a/b/c` |
| `/path/p/q/` | `../a/..` | `/path/p/` |
| `http://host1/p` | `http://host2/q` | `http://host2/q` |
| `https://a:b@host1/p` | `//c:d@host2/q?x=1` | `https://c:d@host2/q?x=1` |
| `/path/p?b=1` | `./?a=1#top` | `/path/?a=1#top` |
`.join()` updates the query parameters and hash fragment as well. To prevent this, use:
```js
url.join(another_url, {query: false, hash: false})
```
For example:
```js
g1.url.parse('/').join('/?x=1#y=1', {hash: false}).toString() == '/?x=1'
g1.url.parse('/').join('/?x=1#y=1', {query: false}).toString() == '/#y=1'
```
## url.update
```js
var url = url.update(object)
```
updates the `url` query parameters with the attributes from `object`. For example:
| url | updated with | gives |
|--------------|----------------------|-----------------------|
| `/` | `{a:1}` | `/?a=1` |
| `/?a=1&b=2` | `{b:3, a:4, c:''}` | `/?a=4&b=3&c=` |
| `/?a=1&b=2` | `{a:null}` | `/?b=2` |
| `/?a=1&b=2` | `{a:[3,4], b:[4,5]}` | `/?a=3&a=4&b=4&b=5` |
By default, it *updates* the query parameters. But:
- `url.update(object, 'add')` *adds* the query parameters instead of updating
- `url.update(object, 'del')` *deletes* the query parameters instead of updating
- `url.update(object, 'toggle')` *toggles* the query parameters (i.e. adds if missing, deletes if present)
For example:
| url | updated with | in mode | gives |
|---------------------|----------------------|---------------------|-----------------------|
| `/?a=1&a=2` | `{a:3, b:1}` | `add` | `/?a=1&a=2&a=3&b=1` |
| `/?a=1&a=2'` | `{a:[3,4]}` | `add` | `/?a=1&a=2&a=3&a=4` |
| `/?a=1&a=2&b=1` | `{a:2, b:2}` | `del` | `/?a=1&b=1` |
| `/?a=1&a=2&b=1` | `{a:[1,4]}` | `del` | `/?a=2&b=1` |
| `/?a=1&a=2` | `{a:1, b:1}` | `toggle` | `/?a=2&b=1` |
| `/?a=1&a=2&b=1&b=2` | `{a:[2,3], b:[1,3]}` | `toggle` | `/?a=1&a=3&b=2&b=3` |
You can specify different modes for different query parameters.
```js
g1.url.parse('/?a=1&b=2&c=3&d=4') // Update this URL
.update({a:1, b:[2,3], c:6, d:7}, // With this object
'a=del&b=toggle&c=add') // Delete ?a, Toggle ?b, add ?c, update ?d (default)
// Returns /?b=3&c=3&c=6&d=7
```
## types
`g1.types(data)` returns the column data types. For example:
```js
var data = [
{a: 1, b: 1.1, c: 'c', d: '2014-04-04', e: true},
{a: 2, b: 2},
]
g1.types(data) // Returns {"a": "number", "b": "number", "c": "string", "d": "date", "e": "boolean"}
```
### types options
`types()` accepts 2 parameters:
- `data`: a list of objects
- `options`: a dictionary that may contain these keys:
- `convert`: converts values to the right type. For example, "1" is converted to 1. default: `false`
- `limit`: number of rows to evaluate. default: 1000
- `ignore`: list of values that should be ignored. default: `[null, undefined]`
Rules:
- Evaluate up to `limit` rows
- Ignore values that are keys in the `ignore` option. Only consider the rest
- If `convert` is `false`, then for each column:
- If all values are Date objects -> `date`
- Else if all values are numbers -> `number`
- Else if all values are strings -> `string`
- Else if all values are bools -> `boolean`
- Else if there are no values or is undefined or null -> `null`
- Else -> `mixed`
- Else if `convert` is `true`, then for each column:
- If all values can be converted to Date -> `date`
- Else if all values can be converted to numbers -> `number`
- Else if all values are bools -> `boolean`
- Else if there are no values or is undefined or null -> `null`
- Else -> `string`
## Interaction conventions
All interaction components use this naming convention:
- Interactions are enabled on a *container*, typically `body`. For example,
`$('body').urlfilter()`. Containers have these common attributes:
- `data-selector`: selector to identify triggers. e.g. `.urlfilter`, `.highlight`
- `data-target`: selector that all triggers act on by default
- `data-mode`: mode of interaction for all triggers
- `data-attr`: attribute that contains the interaction data, e.g. `href` for `.urlfilter`
- Interactions are triggered on a *trigger*. For example, `.urlfilter` for `$().urlfilter()`.
Clicking / hovering on / typing in a trigger triggers the interaction.
- `data-target`: selector that this trigger acts on
- `data-mode`: mode of interaction for this trigger
- Interactions change a *target*. For example, `urlfilter` changes `location.href` by default. The
`data-target` on containers and triggers define this.
- Interactions data is contained in an attribute. This is applied to the target. For example,
`.urlfilter` applied `href` to the target. The attribute name is stored in `data-attr`.
- Interactions have *modes*. This can be controlled using `data-mode=`.
All container `data-` attributes can also be passed as an option to the
function. For example, `<body data-selector=".link">` is the same as
`$('body').urlfilter({selector: '.link'})`.
## Contributing
Contributions are welcome.
......
......@@ -6,3 +6,5 @@ url:
handler: FormHandler
kwargs:
url: test/formhandler.csv
xsrf_cookies: false # TODO: enable this and test
id: ID
{
"name": "g1",
"version": "0.6.0",
"version": "0.7.0",
"description": "Gramex 1.x interaction library",
"license": "UNLICENSED",
"author": "S Anand <s.anand@gramener.com>",
......
......@@ -24,7 +24,7 @@ export default [
},
{
input: "index-formhandler",
plugins: [htmlparts('src/formhandler.template.html'), uglify()],
plugins: [htmlparts('src/formhandler.template.html'), process.env.npm_lifecycle_event == 'dev' ? '' : uglify()],
output: { file: "dist/formhandler.min.js", format: "umd", name: "g1" }
},
{
......
......@@ -6,9 +6,11 @@ import { parse } from './url.js'
import { namespace } from './namespace_util.js'
// Render components in this order. The empty component is the root component.
var components = ['', 'table', 'page', 'size', 'count', 'export', 'filters', 'error']
var components = ['', 'table', 'edit', 'add', 'page', 'size', 'count', 'export', 'filters', 'error', 'table_grid']
var default_options = {
table: true,
edit: false,
add: false,
page: true,
pageSize: 100,
size: true,
......@@ -47,9 +49,13 @@ function col_defaults(colinfo) {
if (!('filters' in colinfo) || (colinfo.filters === true))
colinfo.filters = default_filters[colinfo.type]
// Hide defaults
// Hideable defaults
if (!('hideable' in colinfo))
colinfo.hideable = true
// Hide defaults
if (!('hide' in colinfo))
colinfo.hide = false
}
......@@ -68,6 +74,7 @@ export function formhandler(js_options) {
// Pre-process options
var options = $.extend({}, default_options, js_options, $this.data())
if (!options.columns)
options.columns = []
else if (typeof options.columns == 'string')
......@@ -81,44 +88,49 @@ export function formhandler(js_options) {
})
function draw_table(data, args, meta) {
// Add metadata
meta.rows = data.length,
meta.rows = data.length
meta.columns = data.length ? _.map(data[0], function (val, col) { return { name: col } }) : []
// Assumption: Column name will not be '*'
if (options.columns.some(function (o) { return o['name'] === '*' }))
options.columns = _.map(meta.columns, function (col) {
// If any column name is '*', show all columns
var star_col = _.find(options.columns, function (o) { return o['name'] === '*' })
if (star_col) {
var action_header_cols = _.cloneDeep(meta.columns)
_.map(options.columns, function(option_col) {
var found = _.find(meta.columns, function (o) { return o['name'] === option_col.name })
if (!found && option_col.name !== '*')
action_header_cols.push(option_col)
})
action_header_cols = _.map(action_header_cols, function (col) {
var options_col = _.find(options.columns, function (o) { return o['name'] === col.name })
return options_col ? options_col : col