Commit 2af532f1 authored by Tejesh's avatar Tejesh 🖖

merge dev

parent 59900777
Pipeline #78626 failed with stage
in 61 minutes and 11 seconds
# Change log
- `0.15.0`:
- [$.formhandler](docs/formhandler.md) now supports client-side validation
and sorting by multiple columns. A bug related to encoding special
characters is also fixed.
- `0.14.0`:
- [$.formhandler](docs/formhandler.md) uses a `onhashchange: false` to
disable changing the URL when elements are selected. This is useful when
......
......@@ -39,6 +39,7 @@ To use all features, add this to your HTML:
- [g1.url.parse](docs/url.md) parses a URL into a structured object
- [url.join](docs/url.md#urljoin) joins two URLs
- [url.update](docs/url.md#urlupdate) updates a URL's query parameters
- [g1.fuzzysearch](docs/fuzzysearch.md) searches for text with fuzzy matching
- [$.ajaxchain](docs/ajaxchain.md) chains AJAX requests, loading multiple items in sequence
- [L.TopoJSON](docs/topojson.md) loads TopoJSON files just like GeoJSON. Requires [topojson](https://github.com/topojson/topojson)
- [$.dispatch](docs/dispatch.md) is like [trigger](https://api.jquery.com/trigger/) but sends a native event (triggers non-jQuery events too)
......@@ -72,14 +73,6 @@ For debugging, use [dist/g1.js](dist/g1.js) -- an un-minified version.
[CHANGELOG](CHANGELOG.md) mentions all release changes.
Brief notes with examples are described in Gramex releases. For example:
- [v0.12](https://learn.gramener.com/guide/release/1.49/#g1-animated-templates)
- [v0.11](https://learn.gramener.com/guide/release/1.47/#g1)
- [v0.10.1](https://learn.gramener.com/guide/release/1.45/#g1)
- [v0.10.0](https://learn.gramener.com/guide/release/1.44/#g1)
- [v0.9.0](https://learn.gramener.com/guide/release/1.41/#g1)
## Browser support
Every release is tested on the current versions of Chrome, Edge and Firefox.
......
This diff is collapsed.
# g1.fuzzysearch
`g1.fuzzysearch(data, options)` returns a fuzzy search function that filteres
the data based on the text.
For example:
```js
var data = [
{"product": "Cider Apple Vinegar"},
{"product": "JBL In-Ear Headphones"},
{"product": "Vaseline Body Lotion"},
{"product": "Redux Men's Watch"},
{"product": "Omega3 Fish Oil"},
]
var search = g1.fuzzysearch(data, {
keys: ['product'], // Search within these keys
limit: 2, // Return only the top 2 results
})
search('omega')
// Returns {product: "Omega3 Fish Oil", ...} since it's the only one
search('red')
// Returns {product: "Redux Men's Watch"} and {product: "JBL In-Ear Headphones"}
// The second matches r (in "Ear"), followed by e then d in "Headphones"
```
It matches with the following priority. For example, if the string is "alpha
beta", then:
1. Match the exact phrase ("alpha beta")
2. Match all words in the same order ("alp bet")
3. Match words in any order ("bet alp")
4. Match partial words in any order ("ba aph")
5. Match letters in order ("abt")
It accepts an `options` dict with these keys:
- `keys`: a list of keys to search in. The keys are calculated and joined with a
space. (Default: assumes that data is a string list.) Each key can be either:
- a string (e.g. `"name"`, `"title"`) picks keys from the objects in the
`data` list.
- a function (e.g. `function (v) { return v['key'] })`) runs the function on
each element in the `data` list
- `limit`: the maximum number of results to return. (Default: `100`)
- `case`: `true` for case-sensitive comparisons. (Default: `false`)
export { version } from './src/package.js'
export { fuzzysearch } from './src/fuzzysearch.js'
// export item into the g1.* namespace
export { version } from './src/package.js'
export { types } from './index-types.js'
export { url } from './index-urlfilter.js'
export { scale } from './index-scale.js'
export { datafilter } from './index-datafilter.js'
export { fuzzysearch } from './index-fuzzysearch.js'
export { sanddance } from './index-sanddance.js'
export { scale } from './index-scale.js'
export { types } from './index-types.js'
export { url } from './index-urlfilter.js'
// Mapviewer is not part of g1
// import './index-mapviewer.js'
......
{
"name": "g1",
"version": "0.14.0",
"version": "0.15.0",
"description": "Gramex 1.x interaction library",
"license": "MIT",
"author": "S Anand <s.anand@gramener.com>",
......@@ -13,7 +13,7 @@
"lint": "eslint index*.js src && eclint check '**/*.html' '**/*.js' '**/*.css' '**/*.yaml' '**/*.md'",
"build": "rimraf dist && json2module package.json > src/package.js && rollup -c",
"dev": "rimraf dist && json2module package.json > src/package.js && rollup -c -w",
"pretest": "npm run build && browserify -s tape -r tape -o test/tape.js",
"pretest": "npm run lint && npm run build && browserify -s tape -r tape -o test/tape.js",
"server": "npm run pretest && npm run lint && node test/server.js",
"test": "tape test/test-*.js | faucet && node test/server.js puppeteer | tap-merge | faucet",
"test-chrome": "node test/server.js chrome | tap-merge | faucet",
......
......@@ -148,6 +148,11 @@ export default [
extend: true
}
},
{
input: "index-fuzzysearch",
output: { file: "dist/fuzzysearch.min.js", format: "umd", name: "g1", extend: true },
plugins: [uglify()]
},
{
input: "index-types",
output: { file: "dist/types.min.js", format: "umd", name: "g1", extend: true },
......
......@@ -368,23 +368,28 @@ function editHandler($this, template_data, options, template) {
var edit_btn = $('.edit button', $this)
var add_btn = $('.add button', $this)
if (edit_btn.html().toLowerCase() == 'save') {
edit_btn.html('Edit') // TODO: remove hardcoding of name Edit
add_btn.prop('disabled', false)
var edited_rows = $('.edited-row')
if (edited_rows.length > 0)
$('.loader', $this).removeClass('d-none')
var all_ajax = []
var allRowsValid = true
$.each(edited_rows, function (key, edited_row) {
var data = JSON.parse(edited_row.getAttribute('data-val'))
var rowIndex = edited_row.getAttribute('data-row')
for (key in data) {
// TODO: refactor to identify editable columns other than using data-key attrs on <td> tag
var editable_element = $('td[data-key="' + (remove_quotes(key)) + '"] :input', $(edited_row))
if (editable_element.length) {
data[key] = template_data['data'][rowIndex][key] = editable_element.val()
}
$('td[data-key="' + (remove_quotes(key)) + '"] :input', edited_row).each(function() {
if (this.checkValidity()) {
$(this).removeClass('is-invalid')
data[key] = template_data['data'][rowIndex][key] = $(this).val()
} else {
$(this).addClass('is-invalid')
allRowsValid = false
}
})
}
all_ajax.push(
$.ajax(options.src, {
method: 'PUT',
......@@ -397,10 +402,15 @@ function editHandler($this, template_data, options, template) {
})
)
})
if (!allRowsValid) return
$.when.apply($, all_ajax).then(function () {
$('.loader', $this).addClass('d-none')
edit_btn.html('Edit') // TODO: remove hardcoding of name Edit
add_btn.prop('disabled', false)
if (options.edit.done) options.edit.done()
})
template_data.isEdit = false
render_template('table', template_data, options, $this, template)
} else if (edit_btn.html().toLowerCase() == 'edit') {
......
......@@ -95,16 +95,21 @@ Each template receives these variables:
col_defaults(colinfo, data)
var menu_item = false
var col_id = idcount++
var qsort = parse('?')
var isSorted = _.includes(args['_sort'], colinfo.name) ? {op: '', cls: 'table-primary'} : _.includes(args['_sort'], '-' + colinfo.name) ? {op: '-', cls: 'table-danger'} : {}
%>
<th class="<%- args['_sort'] == colinfo.name ? 'table-primary' : args['_sort'] == '-' + colinfo.name ? 'table-danger' : '' %>" data-col="<%- colinfo.name %>">
<th class="<%- isSorted.cls %>" data-col="<%- colinfo.name %>">
<div class="dropdown">
<a href="#" class="dropdown-toggle text-nowrap" id="fh-dd-<%- col_id %>" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<%- colinfo.title || colinfo.name %>
</a>
<div class="dropdown-menu" aria-labelledby="fh-dd-<%- col_id %>">
<% _.each(colinfo.sort, function(title, op) { menu_item = true
var active = args['_sort'] == op + colinfo.name %>
<a class="dropdown-item urlfilter <%- active ? 'active': '' %>" href="?_sort=<%- active ? '' : op + colinfo.name %>">
qsort = qsort.update({_sort: args['_sort'] || []})
if (!_.isEmpty(isSorted))
qsort = qsort.update({_sort: [colinfo.name, '-' + colinfo.name]}, 'del')
var active = _.includes(args['_sort'], op + colinfo.name) %>
<a class="dropdown-item urlfilter <%- active ? 'active': '' %>" href="<%- qsort.update({_sort: [op + colinfo.name]}, active ? 'del': 'add').toString() %>">
<%- title %>
</a>
<% }) %>
......@@ -130,7 +135,7 @@ Each template receives these variables:
<div class="dropdown-divider"></div>
<% } %>
<% if (colinfo.hideable) { %>
<a class="dropdown-item urlfilter" href="?_c=-<%- colinfo.name %>" data-mode="add">Hide</a>
<a class="dropdown-item urlfilter" href="?_c=-<%- encodeURIComponent(colinfo.name) %>" data-mode="add">Hide</a>
<% } %>
</div><!-- .dropdown-menu -->
</div><!-- .dropdown -->
......@@ -189,7 +194,7 @@ Each template receives these variables:
</td>
<% } else { %>
<td>
<a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<a class="urlfilter" href="?<%- encodeURIComponent(colinfo.name) %>=<%- encodeURIComponent(val) %>&amp;_offset=">
<%= disp %>
</a>
</td>
......@@ -241,6 +246,11 @@ Each template receives these variables:
/>
<% } %>
<% if (isEditable.validationMessage) { %>
<div class="invalid-feedback">
<%- isEditable.validationMessage %>
</div>
<% } %>
<!-- end -->
<!-- var template_page -->
......@@ -350,7 +360,7 @@ Each template receives these variables:
qparts.update({_c: col_name}, 'add')
var hide_col = col_name[0] == '-'
var display_name = hide_col ? col_name.slice(1) : col_name %>
<a href="?_c=<%- col_name %>" data-mode="del" class="badge badge-pill <%- hide_col ? 'badge-dark' : 'badge-danger' %> urlfilter"
<a href="?_c=<%- encodeURIComponent(col_name) %>" data-mode="del" class="badge badge-pill <%- hide_col ? 'badge-dark' : 'badge-danger' %> urlfilter"
title="<%- hide_col ? 'Show' : 'Hide' %> column <%- display_name %>">
<%- display_name %>
</a>
......@@ -361,7 +371,7 @@ Each template receives these variables:
var update = {}
update[key] = col_name
qparts.update(update, 'add') %>
<a href="?<%- key %>=<%- col_name %>" data-mode="del" class="badge badge-pill badge-dark urlfilter" title="Clear <%- key %> filter">
<a href="?<%- encodeURIComponent(key) %>=<%- encodeURIComponent(col_name) %>" data-mode="del" class="badge badge-pill badge-dark urlfilter" title="Clear <%- key %> filter">
<%- key %> = <%- col_name %>
</a>
<% })
......@@ -419,7 +429,7 @@ Each template receives these variables:
var col_link = typeof colinfo.link == 'function' ? colinfo.link({row: row, value: val, index: rowIndex, name: colinfo.name, data: data, format: disp}) : _.template(colinfo.link)({row: row, value: val, index: rowIndex, name: colinfo.name, data: data, format: disp}) %>
<a href="<%- col_link %>" target="_blank"><%= disp %></a>
<% } else { %>
<a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<a class="urlfilter" href="?<%- encodeURIComponent(colinfo.name) %>=<%- encodeURIComponent(val) %>&amp;_offset=">
<%= disp %>
</a>
<% } %>
......
/*
var search = g1.fuzzysearch(data, options)
// Specify keys in data as a list of column names or functions
options.keys = [ 'col1', function(v) { return v.info.name } ]
// Specify max results to return. Default: 100
options.limit = 10
// Case sensitive search. Default: false
options.case = True
*/
export function fuzzysearch (data, options) {
options = options || {}
// "values" is the string array with the text to search
var values
// If no options.keys are provided, use the raw data as-is
if (!options.keys)
values = data
// Else, join the provided keys
else
values = data.map(function(row) {
return options.keys.map(function(v) {
return typeof v == 'function' ? v(row) : row[v]
}).join(' ')
})
var limit = options.limit || 100
var flags = options.case ? '' : 'i'
// TODO: document these options once stabilized
var depth = options.depth || 10
var escape = options.escape || true
return function(text) {
var results = [], // Final results
vals = values.slice(), // Values to search. Crosses off matches to avoid duplication
re
// Trim the search text
text = text.replace(/^\s/, '').replace(/\s$/, '')
if (escape)
text = text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
// 1. Match full phrase
re = new RegExp(text.replace(/\s+/, '\\s+'), flags)
vals.forEach(function (v, i) {
if (v && re.test(v)) { results.push(data[i]); vals[i] = '' }
})
if (depth <= 1 || results.length >= limit) return results.slice(0, limit)
// 2. Match words in order
re = new RegExp(text.replace(/\s+/, '.*'), flags)
vals.forEach(function (v, i) {
if (v && re.test(v)) { results.push(data[i]); vals[i] = '' }
})
if (depth <= 2 || results.length >= limit) return results.slice(0, limit)
// 3. Match words in any order
re = text.split(/\s+/).map(function (word) { return new RegExp(word, flags) })
vals.forEach(function (v, i) {
if (v && re.every(function (word) { return word.test(v) })) { results.push(data[i]); vals[i] = '' }
})
if (depth <= 3 || results.length >= limit) return results.slice(0, limit)
// 4. Match partial words in any order
re = text.split(/\s+/).map(function (word) { return new RegExp(word.replace(/(.)/g, '$&[\\S]*'), flags) })
vals.forEach(function (v, i) {
if (v && re.every(function (word) { return word.test(v) })) { results.push(data[i]); vals[i] = '' }
})
// 5. Match characters in order
re = new RegExp(text.replace(/(.)/g, '$&.*'), flags)
vals.forEach(function (v, i) {
if (v && re.test(v)) { results.push(data[i]); vals[i] = '' }
})
if (depth <= 4 || results.length >= limit) return results.slice(0, limit)
return results.slice(0, limit)
}
}
Continent,Cross,ID,Name,Shapes,Stripes,Symbols,Text,Union-Flag,c1,c2,c3,c4,c5,c6,c7,c8,date col
Continent,Cross,ID,Name,Shapes,Stripes,Symbols,Text&,Union-Flag,c1,c2,c3,c4,c5,c6,c7,c8,date col
Europe,,AND,Andorra,,Horizontal,,,,35,1,26,0,32,0,0,4,16-01-2013
Asia,,ARE,United Arab Emirates,,Horizontal,,,,24.0,0.0,0.0,25.0,0.0,0.0,25.0,24.0,17-02-2013
Asia,,AFG,Afghanistan,,Vertical,,Country,,28.0,1.0,0.0,33.0,0.0,0.0,33.0,3.0,06-02-2013
Asia,,AFG,Afghanistan,,Vertical,,Country&,,28.0,1.0,0.0,33.0,0.0,0.0,33.0,3.0,06-02-2013
North America,,ATG,Antigua Barbuda,,,,,,50.0,0.0,5.0,0.0,0.0,10.0,25.0,7.0,11-02-2013
Europe,,ALB,Albania,,,Bird,,,87.0,0.0,0.0,0.0,0.0,0.0,12.0,0.0,18-01-2013
Asia,,ARM,Armenia,,Horizontal,,,,33.0,33.0,0.0,0.0,33.0,0.0,0.0,0.0,12-01-2013
......
This diff is collapsed.
......@@ -71,8 +71,10 @@
attrs: {
min: 10,
max: 100,
required: '',
placeholder: '0 - 100'
}
},
validationMessage: 'Number must be between 0-100 and is mandatory'
}
},
{
......@@ -112,9 +114,9 @@
}
},
{
name: 'delete',
name: 'Actions',
template: function(row) {
return "<td><button data-action='delete'><i class='fa fa-trash'></i></button></td>"
return "<td><button data-action='edit'><i class='fa fa-trash'></i></button></td>"
},
}
],
......@@ -144,20 +146,46 @@
$('.edit button').click()
// make sure the initial value is Europe
test.equals($(".edit-fh table > tbody > tr:nth-child(1) > td:nth-child(2) > input").val().trim(), init_cell_value)
$(".edit-fh table > tbody > tr:nth-child(1) > td:nth-child(3) > input").val('300000').trigger('change')
$(".edit-fh table > tbody > tr:nth-child(3) > td:nth-child(3) > input").val('-90').trigger('change')
// save row
$('.edit button').click()
test.equals($('.is-invalid').val(), '300000')
$(".edit-fh table > tbody > tr:nth-child(1) > td:nth-child(3) > input").val('35').trigger('change')
// modify cell value inside <input>
$(".edit-fh table > tbody > tr:nth-child(1) > td:nth-child(2) > input").val('Edited')
$(".edit-fh table > tbody > tr:nth-child(1) > td:nth-child(2) > input").trigger('change')
$(".edit-fh table > tbody > tr:nth-child(1) > td:nth-child(2) > input").val('Edited').trigger('change')
test.equals($(".edit-fh table > tbody > tr:nth-child(1) > td:nth-child(2) > input").val().trim(), 'Edited')
// // save row
$('.edit button').click()
test.equals($('.is-invalid').val(), '-90')
$(".edit-fh table > tbody > tr:nth-child(3) > td:nth-child(3) > input").val('28').trigger('change')
// save row
$('.edit button').click()
setTimeout(function() {
// COMMENTING NEXT TEST CASE: Gramex required
// test.notOk($('.add button').prop('disabled'))
// test.equals($('.edit-btn').text(), 'Edit')
$('.add button').click()
test.ok($('.edit button').prop('disabled'))
test.equals($('div.edit-fh tr.new-row td:nth-child(4) select').length, 1)
// all other columns must be input textbox and editable, overriding isEditable: false option also
test.equals($('div.edit-fh tr.new-row td:nth-child(1) input').length, 1)
test.ok($('.edit button').prop('disabled'))
test.end()
$('.add button').click()
test.equals($('div.edit-fh tr.new-row td:nth-child(4) select').length, 1)
// all other columns must be input textbox and editable, overriding isEditable: false option also
test.equals($('div.edit-fh tr.new-row td:nth-child(1) input').length, 1)
test.end()
}, 500)
/*
......
......@@ -93,9 +93,11 @@
e.which = 13; //choose the one you want
e.keyCode = 13;
$("#city_table table tbody tr:nth-child(1) td:nth-child(1) input").trigger(e);
t.equals($("#city_table .edit button").text(), 'Edit')
$("#city_table tr:nth-child(1) td:nth-child(3) div").click()
t.equals($("#city_table .note").text(), " NOTFIED ×")
setTimeout(function() {
t.equals($("#city_table .edit button").text(), 'Edit')
$("#city_table tr:nth-child(1) td:nth-child(3) div").click()
t.equals($("#city_table .note").text(), " NOTFIED ×")
})
//close notification
// $("#city_table .note span").click()
......
......@@ -80,6 +80,8 @@
edit: true
})
.on('load', function () {
// Test if column name and value filtered are URIEncoded
t.equals($('.delete_btn tr:nth-child(3) td:nth-child(8) a').attr('href'), '?Text%26=Country%26&_offset=')
$('.edit-btn').click()
t.ok($('.add-btn').prop('disabled'))
$('.edit-btn').click()
......
const test = require('tape')
const g1 = require('../dist/fuzzysearch.min')
test('fuzzy search', function (t) {
t.test('Example in docs', function (t) {
var data = [
{ "product": "Cider Apple Vinegar" },
{ "product": "JBL In-Ear Headphones" },
{ "product": "Vaseline Body Lotion" },
{ "product": "Redux Men's Watch" },
{ "product": "Omega3 Fish Oil" },
]
var search = g1.fuzzysearch(data, {
keys: ['product'], // Search within these keys
limit: 2, // Return only the top 2 results
})
t.deepEquals(search('omega'), [
{ "product": "Omega3 Fish Oil" }
])
t.deepEquals(search('red'), [
{ "product": "Redux Men's Watch" }, // Returns highest priority match first
{ "product": "JBL In-Ear Headphones" }, // Then the second priority (no duplicates)
])
t.end()
})
t.test('Test specs', function (t) {
var search = g1.fuzzysearch(['alpha beta'])
// 1. Match the exact phrase("alpha beta")
t.deepEquals(search('alpha beta'), ['alpha beta'])
// 2. Match all words in the same order("alp bet")
t.deepEquals(search('alp bet'), ['alpha beta'])
// 3. Match words in any order("bet alp")
t.deepEquals(search('bet alp'), ['alpha beta'])
// 4. Match partial words in any order("ba aph")
t.deepEquals(search('ba aph'), ['alpha beta'])
// 5. Match letters in order("abt")
t.deepEquals(search('abt'), ['alpha beta'])
t.end()
})
t.test('Options', function (t) {
var search
search = g1.fuzzysearch(['X1', 'x2'], { case: true })
t.deepEquals(search('x'), ['x2'])
search = g1.fuzzysearch(['X1', 'x2'], { limit: 1 })
t.deepEquals(search('x'), ['X1'])
search = g1.fuzzysearch([{ x: 'X1' }, { x: 'x2' }], {
keys: [
'x',
function (v) { return 'a' + v.x },
function (v) { return 'b' + v.x },
]
})
t.deepEquals(search('x ax bx'), [{ x: 'X1' }, { x: 'x2' }])
t.end()
})
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment