Commit d546223f authored by S Anand's avatar S Anand

ENH: implement $.search(). Fixes #33

parent f3b648a1
Pipeline #72635 passed with stage
in 2 minutes and 8 seconds
......@@ -21,6 +21,7 @@ To use all features, add this to your HTML:
- [$.urlfilter](docs/urlfilter.md) changes URL query parameters when clicked. Used to filter data.
- [$.urlchange](docs/urlchange.md) listens to hash changes and routes events
- [$.search](docs/search.md) highlights elements as you type in a search box
- [$.highlight](docs/highlight.md) toggles classes on elements when clicked or hover
## Components
......@@ -51,6 +52,7 @@ or one of the individual libraries below:
- [urlfilter.min.js](dist/urlfilter.min.js)
- [urlchange.min.js](dist/urlchange.min.js)
- [highlight.min.js](dist/highlight.min.js)
- [search.min.js](dist/search.min.js)
- [formhandler.min.js](dist/formhandler.min.js)
- [sanddance.min.js](dist/sanddance.min.js)
- [template.min.js](dist/template.min.js)
......
......@@ -25,15 +25,13 @@ Highlight triggers support these attributes:
- `data-toggle="highlight"` indicates that the element acts as a highlighter
- `data-target=` selectors to highlight (required)
- `data-mode="click"` highlights on click. Use `data-mode="hover"` to higlight on hover (default)
- `data-classes=` space-separated class names to toggle on target elements
- `data-mode="click"` highlights on click. Default: `data-mode="hover"`
- `data-classes=` space-separated class names to toggle on target elements. Default: `active`
Highlight containers support these attributes:
- `data-selector=` defines the triggers, i.e. which nodes $.highlight applies to. Default: `[data-toggle="highlight"]`
- `data-mode` is the same as specifying data-mode on every trigger. Default: `hover`
- `data-attr` is the attribute that defines classes to toggle. Default: `data-classes`
- `data-classes=` is the same as specifiying the data-classes on every trigger. Default: `active`
- `data-selector=` is a selector that picks the triggers for highlight. Default: `[data-toggle="highlight"]`
- Any other `data-*` attribute acts as a default `data-*` attribute for the trigger.
## $.highlight events
......
# $.search
`$.search()` hides or shows matching elements as the user types in a search box.
In this example, searching in the text box highlights the matching elements in the list.
```html
<input type="search" data-search="@text" data-target=".list .item" data-hide-class="d-none">
<ul class="list">
<li class="item">First item</li>
<li class="item">Second item</li>
<li class="item">Third item</li>
</ul>
<script>
$('body').search()
</script>
```
## $.search attributes
When we run `$('body').search()`, the `body` is called a "container". It
listens to events on "triggers", like `<... data-search="...">`
Search triggers support these attributes:
- `data-search=` identifies the attribute that will be searched.
- `data-search="@text"` searches the text inside target elements
- `data-search="title"` searches the `title` attribute of target elements
- `data-search="data-text"` searches the `data-title` attribute of target elements
- ... and so on
- `data-target=` is a selector that picks target elements. Example:
`data-target=".item"` searches within all `item` classes
- `data-hide-class=` adds classes elements not matching the search. Example:
`data-hide-class="d-none"` adds a `d-none` class to all non-matching items.
This is useful to hide non-matches.
- `data-show-class=` adds classes elements matching elements. Example:
`data-hide-class="bg-success"` adds a `bg-success` class to all matching items.
This is useful to highlight matches.
<!--
TODO: Document
- `data-transform="strip"`
- `data-change="words"`
-->
Search containers support these attributes:
- `data-selector=` is a selector that picks the triggers for search. Default: `[data-search]`
- Any other `data-*` attribute acts as a default `data-*` attribute for the trigger.
## $.search events
- `shown` is fired on the trigger when activated. Attributes:
- `searchText`: the original search keyword
- `search`: the transformed search used
- `matches`: the number of shown nodes (same as `results.length`)
- `results`: the list of target nodes searched. (Some are shown, some hidden)
- `refresh` can be fired on the trigger to refresh search cache. The DOM is
slow. We cache search text. Run `$(input_to_be_refreshed).trigger('refresh')`
to refresh the cache.
- `search` can be fired on the trigger if the DOM changes, and you need to
re-run the same search. Run `$(input_to_be_refreshed).trigger('search')`.
Note: `.trigger('change')` and `.dispatch('change')` won't work because the
code will refuse to search again since the query has not changed.
export { version } from './src/package.js'
import { search } from './src/search.js'
if (typeof jQuery != 'undefined') {
jQuery.extend(jQuery.fn, {
search: search
})
}
......@@ -15,5 +15,6 @@ import './index-event.js'
import './index-formhandler.js'
import './index-highlight.js'
import './index-leaflet.js'
import './index-search.js'
import './index-template.js'
import './index-urlchange.js'
......@@ -126,5 +126,17 @@ export default [
format: "umd",
name: "g1"
}
},
{
input: "index-search",
plugins: [
resolve(),
commonjs()
],
output: {
file: "dist/search.min.js",
format: "umd",
name: "g1"
}
}
]
......@@ -4,7 +4,6 @@ var container_options = {
selector: '[data-toggle="highlight"]',
target: '.highlight-target',
mode: 'hover',
attr: '.data-classes',
classes: 'active'
}
......
// data- attribute to store the last performed search
var _lastsearch_attr = 'search-last'
// data- attribute to store granular search results
var _search_results = 'search-results'
var container_options = {
selector: '[data-search]',
hideClass: '',
showClass: '',
transform: 'strip',
change: 'words',
}
export function search(options) {
var settings = $.extend({}, container_options, options, this.data())
this
.off('.g.search')
.on('keyup.g.search change.g.search', settings.selector, run_search)
.on('refresh.g.search', refresh)
.on('search.g.search', function (e) {
refresh(e)
run_search(e)
})
// If the container *IS* the trigger, run search
this.filter(settings.selector)
.on('keyup.g.search change.g.search', run_search)
return this
// Extract & transform search strings. Cache in input's dataset.search_results
// Return the search strings.
function refresh(e) {
var opts = $.extend({}, settings, e.target.dataset)
var search_text = opts.search == '@text' ?
function (el) { return el.textContent } :
function (el) { return el.getAttribute(opts.search) }
var transform = search.transforms[opts.transform]
var result = $(opts.target).map(function() {
var s = search_text(this)
return { el: $(this), original: s, text: transform(s), show: true }
}).get()
$(e.target).data(_search_results, result)
.removeData(_lastsearch_attr)
return result
}
function run_search(e) {
var opts = $.extend({}, settings, e.target.dataset)
var $el = $(e.target)
var out = {
type: 'shown.g.search',
searchText: $el.val()
}
out.search = search.changes[opts.change](search.transforms[opts.transform](out.searchText))
var lastsearch = $el.data(_lastsearch_attr)
if (lastsearch == out.search)
return
$el.data(_lastsearch_attr, out.search)
var hidecls = opts.hideClass,
showcls = opts.showClass,
re = new RegExp(out.search || '.*')
out.results = $el.data(_search_results) || refresh(e)
out.matches = out.results.length
out.results.forEach(function (cell) {
var show = cell.text.match(re)
if (show !== cell.show) {
if (hidecls) cell.el[!show ? 'addClass' : 'removeClass'](hidecls)
if (showcls) cell.el[show ? 'addClass' : 'removeClass'](showcls)
cell.show = show
}
if (!show) out.matches--
})
$el.trigger(out)
}
}
search.transforms = {
strip: function (s) { return (s || '').toLowerCase().replace(/\s+/g, ' ').replace(/^ /, '').replace(/ $/, '') }
}
search.changes = {
words: function (s) { return s.replace(/\s+/g, '.*') }
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>search tests</title>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script src="../dist/search.min.js"></script>
<script src="tape.js"></script>
<style>
.select { stroke-width: 5; stroke: #000; }
.d-none { display: none; }
</style>
</head>
<body>
<script>
tape.onFinish(function () { window.renderComplete = true })
</script>
<input id="search-1" type="search" data-search="@text" data-target=".list1 .item" data-hide-class="d-none">
<ul class="list1">
<li class="item">First item</li>
<li class="item">Second item</li>
<li class="item">Third item</li>
</ul>
<script>
function match(selector, is) {
return $(selector).map(function () { return $(this).is(is)}).get()
}
function check(t, $input, list) {
// "rst" matches First
$input.val('rst').trigger('keyup')
t.deepEquals(match(list + ' .item', '.d-none'), [false, true, true])
// "ir" matches First and Third
$input.val('ir').trigger('change')
t.deepEquals(match(list + ' .item', '.d-none'), [false, true, false])
// Clearing the search matches all items
$input.val('').trigger('keyup')
t.deepEquals(match(list + ' .item', '.d-none'), [false, false, false])
t.end()
}
tape('$().search() on trigger hides list items', function(t) {
check(t, $('#search-1').search(), '.list1')
})
</script>
<div id="search-2" data-search="@text" data-target=".list2 .item" data-hide-class="na">
<input type="search" data-hide-class="d-none">
</div>
<ul class="list2">
<li class="item">First item</li>
<li class="item">Second item</li>
<li class="item">Third item</li>
</ul>
<script>
var $input = $('#search-2').search().find('input')
tape('$().search() on container hides list items', function(t) {
check(t, $input, '.list2')
})
tape('$().search() caches results', function(t) {
$('.list2').append('<li class="item">Fourth item</li>')
$input.val('xxx').trigger('change')
t.deepEquals(match('.list2 .item', '.d-none'), [true, true, true, false])
t.end()
})
tape('$().search(): refresh refreshes the cache', function (t) {
$input.trigger('refresh').val('xxx').trigger('change')
t.deepEquals(match('.list2 .item', '.d-none'), [true, true, true, true])
t.end()
})
tape('$().search(): search event refreshes the cache', function (t) {
$('.list2').append('<li class="item">Fifth item</li>')
$input.val('xxx').trigger('search')
t.deepEquals(match('.list2 .item', '.d-none'), [true, true, true, true, true])
t.end()
})
</script>
<form id="search-form" data-search="fill" data-target=".result circle">
<input type="search" id="search-3" data-search="title" data-target=".result rect" data-show-class="select">
<input type="search" id="search-4" data-search="@text" data-target=".result rect" data-hide-class="d-none">
</form>
<svg width="880" height="40" class="result">
<rect x="000" y="5" width="90" height="30" fill="#00f" title="Blue">Blue</rect>
<rect x="100" y="5" width="90" height="30" fill="#0f0" title="Green">Green</rect>
<rect x="200" y="5" width="90" height="30" fill="#f00" title="Red">Red</rect>
<rect x="300" y="5" width="90" height="30" fill="#ff0" title="Yellow">Yellow</rect>
<rect x="400" y="5" width="90" height="30" fill="#0ff" title="Cyan">Cyan</rect>
<rect x="500" y="5" width="90" height="30" fill="#f0f" title="Purple">Purple</rect>
<rect x="600" y="5" width="90" height="30" fill="#888" title="Grey">Grey</rect>
<rect x="700" y="5" width="90" height="30" fill="#000" title="Black">Black</rect>
<text x="810" y="15" dy=".35em"></text>
</svg>
<script>$('#search-form').search()</script>
<script>
$('body').on('shown.g.search', function (e) {
$('.result text').text(e.matches + ' matches')
})
</script>
<script>
tape('$().search() show-class adds classes to matches', function (t) {
// When we type "blue", all blues should be selected. No non-blue should be selected
$('.d-none, .select').removeClass('d-none select')
$('#search-3').val('blue').trigger('change')
t.equals($('rect[title="Blue"].select').length, 1)
t.equals($('rect[title!="Blue"].select').length, 0)
t.equals($('.d-none').length, 0)
t.equals($('.result text').text(), '1 matches')
t.end()
})
tape('$().search() hide-class adds classes to non-matches', function (t) {
// When we type "green", all greens should visible. No non-green should be visible
$('.d-none, .select').removeClass('d-none select')
$('#search-4').val('green').change()
t.equals($('rect[title="Green"]:not(.d-none)').length, 1)
t.equals($('rect[title!="Green"]:not(.d-none)').length, 0)
t.equals($('.select').length, 0)
t.equals($('.result text').text(), '1 matches')
t.end()
})
</script>
</body>
</html>
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