Commit 9928515e authored by S Anand's avatar S Anand
Browse files

ENH: urlfilter should handle forms, inputs, sliders. Fixes #21 @tejesh.p

parent c550e317
Pipeline #52745 passed with stage
in 3 minutes and 6 seconds
......@@ -253,6 +253,7 @@ All interaction components use this naming convention:
- `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`
- `data-event`: event that triggers urlfilter. Defaults to `'click'`
- 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
......
......@@ -8,7 +8,7 @@ var state_transitions = {
'boolean': { state: 'boolean' },
'string': { state: 'string' },
'object': { state: 'object' },
'mixed': { state: 'mixed', end: true },
'mixed': { state: 'mixed', end: true }
},
'date': {
'null': { state: 'date' },
......
import {parse} from './url.js'
import {hasdata} from './_util.js'
import { parse } from './url.js'
import { hasdata } from './_util.js'
export function urlfilter(options) {
options = options || {}
......@@ -9,69 +9,82 @@ export function urlfilter(options) {
return
var doc = $self[0].ownerDocument
var attr = options.attr || $self.data('attr') || 'href'
var selector = options.selector || $self.data('selector') || '.urlfilter'
var default_src = options.src || $self.data('src') || 'src'
var default_mode = options.mode || $self.data('mode')
var default_target = options.target || $self.data('target')
var default_remove = options.remove || hasdata($self, 'remove')
var off = options.off || hasdata($self, 'off')
var attr = options.attr || $self.data('attr') || 'href'
var event = options.event || $self.data('event') || 'click'
var selector = options.selector || $self.data('selector') || '.urlfilter'
var default_src = options.src || $self.data('src') || 'src'
var default_mode = options.mode || $self.data('mode')
var default_target = options.target || $self.data('target')
var default_remove = options.remove || hasdata($self, 'remove')
var off = options.off || hasdata($self, 'off')
// options.location and options.history are used purely for testing
var loc = options.location || (doc.defaultView || doc.parentWindow).location
var hist = options.history || (doc.defaultView || doc.parentWindow).history
if (off)
return $self.off('click.urlfilter')
return $self.off(event + '.urlfilter')
return $self.on('click.urlfilter', selector, function(e) {
e.preventDefault()
return $self
.on(event + '.urlfilter', selector, function (e) {
e.preventDefault()
var $this = $(this),
mode = $this.data('mode') || default_mode,
target = $this.data('target') || default_target,
src = $this.data('src') || default_src,
remove = hasdata($this, 'remove', default_remove),
href = $this.attr(attr),
url = parse(href),
q = url.searchList
var $this = $(this),
mode = $this.data('mode') || default_mode,
target = $this.data('target') || default_target,
src = $this.data('src') || default_src,
remove = hasdata($this, 'remove', default_remove)
function target_url(url) {
var result = parse(url)
.join(href, {query: false, hash: false})
.update(q, mode)
if (remove) {
var missing_keys = {}
for (var key in result.searchKey)
if (result.searchKey[key] === '')
missing_keys[key] = null
result.update(missing_keys)
var href
if (e.type == 'click')
href = $this.attr(attr)
else if (e.type == 'submit')
href = '?' + $this.serialize()
else if (e.type == 'input' || e.type == 'change') {
var key = encodeURIComponent($this.attr('id') || $this.attr('name'))
var val = encodeURIComponent($this.val())
href = '?' + key + '=' + val
}
return result.toString()
}
/*
If the target is... the URL is get/set at
------------------------ ---------------------
unspecified (=> window) location.href
'pushState' location.href
'#' location.hash
anything else $(target).data(src)
*/
if (!target)
loc.href = target_url(loc.href)
else if (target == '#')
loc.hash = target_url(loc.hash.replace(/^#/, ''))
else if (target.match(/^pushstate$/i))
hist.pushState({}, '', target_url(loc.href))
else {
$(target).each(function() {
var $target = $(this)
var url = target_url($target.attr(src))
$target.attr(src, url).load(url, function() {
$target.trigger({ type: 'load', url: url })
var url = parse(href),
q = url.searchList
function target_url(url) {
var result = parse(url)
.join(href, { query: false, hash: false })
.update(q, mode)
if (remove) {
var missing_keys = {}
for (var key in result.searchKey)
if (result.searchKey[key] === '')
missing_keys[key] = null
result.update(missing_keys)
}
return result.toString()
}
/*
If the target is... the URL is get/set at
------------------------ ---------------------
unspecified (=> window) location.href
'pushState' location.href
'#' location.hash
anything else $(target).data(src)
*/
if (!target)
loc.href = target_url(loc.href)
else if (target == '#')
loc.hash = target_url(loc.hash.replace(/^#/, ''))
else if (target.match(/^pushstate$/i))
hist.pushState({}, '', target_url(loc.href))
else {
$(target).each(function () {
var $target = $(this)
var url = target_url($target.attr(src))
$target.attr(src, url).load(url, function () {
$target.trigger({ type: 'load', url: url })
})
})
})
}
$this.trigger({ type: 'urlfilter', url: url })
})
}
$this.trigger({ type: 'urlfilter', url: url })
})
}
<!DOCTYPE html>
<html>
<head>
<title>urlfilter tests</title>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
......@@ -12,12 +13,12 @@
// emit a "<key>.get"/"<key>.set" event on emitter along with value
function proxylog(object, emitter) {
return new Proxy(object, {
get: function(obj, key) {
get: function (obj, key) {
var val = obj[key]
emitter.emit(key + '.get', val)
return val
},
set: function(obj, key, val) {
set: function (obj, key, val) {
obj[key] = val
emitter.emit(key + '.set', val)
return true
......@@ -25,28 +26,29 @@
})
}
var events = new Emitter()
var loc = proxylog({href: '', hash: ''}, events)
var hist = {pushState: function(obj, name, href) { loc.href = href }}
var loc = proxylog({ href: '', hash: '' }, events)
var hist = { pushState: function (obj, name, href) { loc.href = href } }
// Check if clicking "selector" sets location.href / location.hash "attr".
// The search key in location should initially be "init" and should become "result"
// A "urlfilter" event is triggered on the "selector"
function check(t, selector, attr, init, result) {
t.test('clicking ' + selector + ' sets location.' + attr + ' from ' + init + ' to ' + result, function(st) {
function check(t, selector, attr, init, result, event = 'click') {
t.test('Event: ' + event +' ' + selector + ' sets location.' + attr + ' from ' + init + ' to ' + result, function (st) {
st.plan(3)
loc[attr] = init
events.once(attr + '.set', function (val) {
st.equal(g1.url.parse(val).search, result, 'check.' + attr + '.set')
})
$(selector)
.one('urlfilter', function(e) {
.one('urlfilter', function (e) {
st.equal(e.type, 'urlfilter', 'event.type == urlfilter')
st.ok(e.url, 'event.url exists')
})
.dispatch('click')
.dispatch(event)
})
}
</script>
</head>
<body>
<script>
tape.onFinish(function () { window.renderComplete = true })
......@@ -54,10 +56,10 @@
<section class="default-section">
<script>
var $el = $('.default-section').urlfilter({location: loc, history: hist})
tape('$().urlfilter binds a live click event to the .urlfilter children', function(t) {
var $el = $('.default-section').urlfilter({ location: loc, history: hist })
tape('$().urlfilter binds a live click event to the .urlfilter children', function (t) {
t.plan(2 * $el.length)
$el.each(function() {
$el.each(function () {
var event = jQuery._data(this, 'events').click[0]
t.equal(event.namespace, 'urlfilter')
t.equal(event.selector, '.urlfilter')
......@@ -70,17 +72,17 @@
<div class="urlfilter base" href="?x=1">?x=1</div>
<button class="urlfilter base" href="?x=1">?x=1</button>
<script>
tape('clicking any element sets location.href to a page with an UPDATED URL', function(t) {
tape('clicking any element sets location.href to a page with an UPDATED URL', function (t) {
check(t, 'a.urlfilter.base', 'href', '?y=1', 'y=1&x=1')
check(t, 'i.urlfilter.base', 'href', '?y=1', 'y=1&x=1')
check(t, 'div.urlfilter.base', 'href', '?y=1', 'y=1&x=1')
check(t, 'button.urlfilter.base', 'href', '?y=1', 'y=1&x=1')
})
tape('the URL is updated, irrespective of the current URL', function(t) {
check(t, 'a.urlfilter.base', 'href', '', 'x=1')
check(t, 'a.urlfilter.base', 'href', '?x', 'x=1')
check(t, 'a.urlfilter.base', 'href', '?x=', 'x=1')
check(t, 'a.urlfilter.base', 'href', '?x=0', 'x=1')
tape('the URL is updated, irrespective of the current URL', function (t) {
check(t, 'a.urlfilter.base', 'href', '', 'x=1')
check(t, 'a.urlfilter.base', 'href', '?x', 'x=1')
check(t, 'a.urlfilter.base', 'href', '?x=', 'x=1')
check(t, 'a.urlfilter.base', 'href', '?x=0', 'x=1')
check(t, 'a.urlfilter.base', 'href', '?x=0&x=2', 'x=1')
})
</script>
......@@ -99,30 +101,33 @@
<a class="urlfilter del x2" href="?x=2" data-mode="del">del ?x=2</a>
<a class="urlfilter del x12" href="?x=1&amp;x=2" data-mode="del">del ?x=1&amp;x=2</a>
<script>
tape('setting data-mode as a child attribute works', function(t) {
check(t, '.def.x1', 'href', '', 'x=1')
check(t, '.add.x1', 'href', '', 'x=1')
check(t, '.tog.x1', 'href', '', 'x=1')
check(t, '.del.x1', 'href', '', '')
check(t, '.def.x1', 'href', '?x=1', 'x=1')
check(t, '.add.x1', 'href', '?x=1', 'x=1&x=1')
check(t, '.tog.x1', 'href', '?x=1', '')
check(t, '.del.x1', 'href', '?x=1', '')
check(t, '.def.y1', 'href', '?x=1', 'x=1&y=1')
check(t, '.add.y1', 'href', '?x=1', 'x=1&y=1')
check(t, '.tog.y1', 'href', '?x=1', 'x=1&y=1')
check(t, '.del.y1', 'href', '?x=1', 'x=1')
check(t, '.def.x2', 'href', '?x=1', 'x=2')
check(t, '.add.x2', 'href', '?x=1', 'x=1&x=2')
check(t, '.tog.x2', 'href', '?x=1', 'x=1&x=2')
check(t, '.del.x2', 'href', '?x=1', 'x=1')
check(t, '.del.x1', 'href', '?x=1&x=2&y=3', 'x=2&y=3')
check(t, '.del.x12', 'href', '?x=1&x=2', '')
check(t, '.del.x12', 'href', '?x=1&x=2&x=3&y=4', 'x=3&y=4')
tape('setting data-mode as a child attribute works', function (test) {
test.plan(1)
test.test('test for a tag click', function (t) {
check(t, 'a.def.x1', 'href', '', 'x=1')
check(t, 'a.add.x1', 'href', '', 'x=1')
check(t, 'a.tog.x1', 'href', '', 'x=1')
check(t, 'a.del.x1', 'href', '', '')
check(t, 'a.def.x1', 'href', '?x=1', 'x=1')
check(t, 'a.add.x1', 'href', '?x=1', 'x=1&x=1')
check(t, 'a.tog.x1', 'href', '?x=1', '')
check(t, 'a.del.x1', 'href', '?x=1', '')
check(t, 'a.def.y1', 'href', '?x=1', 'x=1&y=1')
check(t, 'a.add.y1', 'href', '?x=1', 'x=1&y=1')
check(t, 'a.tog.y1', 'href', '?x=1', 'x=1&y=1')
check(t, 'a.del.y1', 'href', '?x=1', 'x=1')
check(t, 'a.def.x2', 'href', '?x=1', 'x=2')
check(t, 'a.add.x2', 'href', '?x=1', 'x=1&x=2')
check(t, 'a.tog.x2', 'href', '?x=1', 'x=1&x=2')
check(t, 'a.del.x2', 'href', '?x=1', 'x=1')
check(t, 'a.del.x1', 'href', '?x=1&x=2&y=3', 'x=2&y=3')
check(t, 'a.del.x12', 'href', '?x=1&x=2', '')
check(t, 'a.del.x12', 'href', '?x=1&x=2&x=3&y=4', 'x=3&y=4')
})
})
</script>
......@@ -133,17 +138,17 @@
<iframe class="target-iframe" src="empty.html?y=1" width="10" height="10"></iframe>
<div class="target-div" src="empty.html?y=1"></div>
<script>
tape('setting data-target to pushState or hash works', function(t) {
check(t, '.pushstate', 'href', '?y=1', 'y=1&x=1')
check(t, '.hash', 'hash', '#?y=1', 'y=1&x=1')
tape('setting data-target to pushState or hash works', function (t) {
check(t, '.default-section .pushstate', 'href', '?y=1', 'y=1&x=1')
check(t, '.default-section .hash', 'hash', '#?y=1', 'y=1&x=1')
})
tape('setting data-target to selector changes src= and triggers load event', function(t) {
tape('setting data-target to selector changes src= and triggers load event', function (t) {
t.plan(2 * 5)
$('.selector-div, .selector-iframe').each(function() {
$('.selector-div, .selector-iframe').each(function () {
var $this = $(this)
var $target = $($this.data('target'))
.one('load', function(e) {
.one('load', function (e) {
t.equal(e.type, 'load', 'load event')
t.equal($target.attr('src'), 'empty.html?y=1&x=1')
t.ok(e.url, 'load event url exists')
......@@ -162,24 +167,158 @@
<a class="urlfilter remove" href="?x=&amp;y=" data-remove>?x=&amp;y= remove</a>
<a class="urlfilter remove-1" href="?x=&amp;y=" data-remove="1">?x=&amp;y= remove-1</a>
<script>
tape('data-remove removes empty values', function(t) {
tape('data-remove removes empty values', function (t) {
check(t, '.no-remove', 'href', '?x=1&y=2&z=3', 'x=&y=&z=3')
check(t, '.remove', 'href', '?x=1&y=2&z=3', 'z=3')
})
</script>
</section><!-- .default-section -->
</section>
<!-- .default-section -->
<section class="default-input-section" data-event="input">
<input type="range" class="urlfilter base" name="slider">
<script>
$('.default-input-section').urlfilter({ location: loc, history: hist, event: 'input' })
tape('clicking any element sets location.href to a page with an UPDATED URL', function (test) {
test.plan(3)
test.test('test slider no input', function (t) {
check(t, 'input.urlfilter.base', 'href', '?slider=50', 'slider=50', 'input')
})
// update slider
test.test('test slider with change in val', function (t) {
$('input.urlfilter.base').val(80)
check(t, 'input.urlfilter.base', 'href', '?slider=80', 'slider=80', 'input')
})
// update name attr and update slider
test.test('test slider with change in name of <input> element', function (t) {
$('input.urlfilter.base').attr('name', 'slider_renamed')
$('input.urlfilter.base').val(60)
check(t, 'input.urlfilter.base', 'href', '?slider_renamed=620', 'slider_renamed=60', 'input')
})
})
</script>
<input type="range" class="urlfilter def x1" name="x" value="50" data-mode="">
<input type="text" class="urlfilter unicode" id=" _&lt;&quote;高&quote;gt;%/=?;,@$+" value=" _&lt;&quote;高&quote;gt;%/=?;,@$+">
<input type="range" class="urlfilter" id="x" name="anything" value="50" data-mode="">
<input type="range" class="urlfilter add x1" name="x" value="50" data-mode="add">
<input type="range" class="urlfilter tog x1" name="x" value="50" data-mode="toggle">
<input type="range" class="urlfilter del x1" name="x" value="50" data-mode="del">
<input type="range" class="urlfilter def y1" name="y" value="50" data-mode="">
<input type="range" class="urlfilter add y1" name="y" value="50" data-mode="add">
<input type="range" class="urlfilter tog y1" name="y" value="50" data-mode="toggle">
<input type="range" class="urlfilter del y1" name="y" value="50" data-mode="del">
<input type="range" class="urlfilter def x2" name="x" value="20" data-mode="">
<input type="range" class="urlfilter add x2" name="x" value="20" data-mode="add">
<input type="range" class="urlfilter tog x2" name="x" value="20" data-mode="toggle">
<input type="range" class="urlfilter del x2" name="x" value="20" data-mode="del">
<script>
tape('setting data-mode as a child attribute works for input events on sliders', function (test) {
test.plan(1)
test.test('test for input filters', function (t) {
check(t, 'input.def.x1', 'href', '', 'x=50', 'input')
check(t, '#x', 'href', '', 'x=50', 'input')
check(t, 'input.unicode', 'href', '', '%20_%3C%26quote%3B%E9%AB%98%26quote%3Bgt%3B%25%2F%3D%3F%3B%2C%40%24%2B=%20_%3C%26quote%3B%E9%AB%98%26quote%3Bgt%3B%25%2F%3D%3F%3B%2C%40%24%2B', 'input')
check(t, 'input.add.x1', 'href', '', 'x=50', 'input')
check(t, 'input.tog.x1', 'href', '', 'x=50', 'input')
check(t, 'input.del.x1', 'href', '', '', 'input')
check(t, 'input.def.x1', 'href', '?x=50', 'x=50', 'input')
check(t, 'input.add.x1', 'href', '?x=50', 'x=50&x=50', 'input')
check(t, 'input.tog.x1', 'href', '?x=50', '', 'input')
check(t, 'input.del.x1', 'href', '?x=50', '', 'input')
check(t, 'input.def.y1', 'href', '?x=50', 'x=50&y=50', 'input')
check(t, 'input.add.y1', 'href', '?x=50', 'x=50&y=50', 'input')
check(t, 'input.tog.y1', 'href', '?x=50', 'x=50&y=50', 'input')
check(t, 'input.del.y1', 'href', '?x=50', 'x=50', 'input')
check(t, 'input.def.x2', 'href', '?x=1', 'x=20', 'input')
check(t, 'input.add.x2', 'href', '?x=1', 'x=1&x=20', 'input')
check(t, 'input.tog.x2', 'href', '?x=1', 'x=1&x=20', 'input')
check(t, 'input.del.x2', 'href', '?x=1', 'x=1', 'input')
check(t, 'input.del.x1', 'href', '?x=50&x=2&y=3', 'x=2&y=3', 'input')
})
})
</script>
<!--
TODO:
- Add test case for <input type="text" name=" _&lt;&quote;高&quote;gt;%/=?;,@$+" value=" _&lt;&quote;高&quote;gt;%/=?;,@$+">
- Add test case for <form><input name="k1"><input id="k2"></form>
-->
<input type="range" class="urlfilter ipushstate" name="x" value="1" data-target="pushState">
<input type="range" class="urlfilter ihash" name="x" value="1" data-target="#">
<script>
tape('setting data-target to pushState or hash works for input type events', function (t) {
check(t, '.default-input-section input.ipushstate', 'href', '?y=1', 'y=1&x=1', 'input')
check(t, '.default-input-section input.ihash', 'hash', '#?y=1', 'y=1&x=1', 'input')
})
</script>
</section>
<section class="remove-section" data-remove>
<a class="urlfilter remove-na" href="?x=&amp;y=">?x=&amp;y=</a>
<a class="urlfilter remove-no" href="?x=&amp;y=" data-remove="no">?x=&amp;y= remove=no</a>
<script>
var $el = $('.remove-section').urlfilter({ location: loc, history: hist })
tape('data-remove is inherited but can be over-ridden', function(t) {
check(t, '.remove-na', 'href', '?x=1&y=2&z=3', 'z=3')
check(t, '.remove-no', 'href', '?x=1&y=2&z=3', 'x=&y=&z=3')
$('.remove-section').urlfilter({ location: loc, history: hist })
tape('data-remove is inherited but can be over-ridden', function (t) {
check(t, 'a.remove-na', 'href', '?x=1&y=2&z=3', 'z=3')
check(t, 'a.remove-no', 'href', '?x=1&y=2&z=3', 'x=&y=&z=3')
})
</script>
</section>
<!-- .remove-section -->
<section class="default-form-section" data-event="submit">
<form class="urlfilter all_elm">
<!-- To SERIALIZE the element must have a name attribute. -->
<input name="k1"><input id="k2">
<input type="text" name=" _&lt;&quote;高&quote;gt;%/=?;,@$+" value=" _&lt;&quote;高&quote;gt;%/=?;,@$+">
<input type="text" id="textbox" name="textbox" value="dummy"><br>
<select name="cars">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="fiat">Fiat</option>
<option value="audi">Audi</option>
</select><br>
<input type="radio" name="gender" value="male" checked> Male<br>
<input type="radio" name="gender" value="female"> Female<br>
<input type="radio" name="gender" value="other"> Other<br>
<input type="range" name="slider" value="20"><br>
<input type="checkbox" name="vehicle" value="Bike"> I have a bike<br>
<input type="checkbox" name="vehicle" value="Car" checked="checked"> I have a car<br>
<button type="submit">Submit</button>
</form>
<script>
$('.default-form-section').urlfilter({ location: loc, history: hist })
tape('g1.urlfilter test forms with input text, range and dropdown', function (test) {
test.plan(2)
test.test('test form with all input elements in it', function (t) {
check(t, 'form.urlfilter.all_elm', 'href', '?', 'k1=&%20_%3C%26quote%3B%E9%AB%98%26quote%3Bgt%3B%25%2F%3D%3F%3B%2C%40%24%2B=%20_%3C%26quote%3B%E9%AB%98%26quote%3Bgt%3B%25%2F%3D%3F%3B%2C%40%24%2B&textbox=dummy&cars=volvo&gender=male&slider=20&vehicle=Car', 'submit')
})
// update textbox value
test.test('test form with all input elements and change slider', function (t) {
$('#textbox').val("tummy")
check(t, 'form.urlfilter.all_elm', 'href', '?', 'k1=&%20_%3C%26quote%3B%E9%AB%98%26quote%3Bgt%3B%25%2F%3D%3F%3B%2C%40%24%2B=%20_%3C%26quote%3B%E9%AB%98%26quote%3Bgt%3B%25%2F%3D%3F%3B%2C%40%24%2B&textbox=tummy&cars=volvo&gender=male&slider=20&vehicle=Car', 'submit')
})
})
</script>
</section><!-- .remove-section -->
<!--
TODO:
- [x] Add test case for <input type="text" name=" _&lt;&quote;高&quote;gt;%/=?;,@$+" value=" _&lt;&quote;高&quote;gt;%/=?;,@$+">
- [x] Add test case for <form><input name="k1"><input id="k2"></form>
-->
</section>
</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