Commit 05138536 authored by S Anand's avatar S Anand
Browse files

BLD: Initial release of g1 v0.1.0

parents
Pipeline #38008 failed with stage
in 48 seconds
# .editorconfig maintains consistent coding styles between different editors.
# Get plugins at http://editorconfig.org/
# - Sublime text: https://github.com/sindresorhus/editorconfig-sublime
# - Notepad++: https://github.com/editorconfig/editorconfig-notepad-plus-plus
root = true
# Apply common styles for most standard text files.
# Do not apply to * - that covers binary files as well
[*.{js,html,php,py,css,svg,json,less,yaml,yml,scss,xml,sh,java,bat,R}]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# Stick to 2-space indenting by default, to conserve space
indent_style = space
indent_size = 2
[*.py]
indent_size = 4
[Makefile]
indent_style = tab
indent_size = 4
# Markdown files require trailing whitespace
# http://robandlauren.com/2013/11/21/configuring-sublime-text-markdown/
[*.md]
trim_trailing_whitespace = false
src/package.js
module.exports = {
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
},
"env": {
"node": true, // Include node globals
"browser": true, // Include browser globals
"jquery": true, // Include jQuery and $
"mocha": true, // Include it(), assert(), etc
"es6": true, // Include ES6 features
},
"globals": {
"_": true, // underscore.js
"d3": true, // d3.js
},
"extends": "eslint:recommended",
"rules": {
/* Override default rules */
"indent": [2, 2, {"VariableDeclarator": 2}], // Force 2 space indentation
"linebreak-style": ["error", "unix"], // Force UNIX style line
"semi": ["error", "never"], // Force no-semicolon style
"no-cond-assign": ["off", "always"], // Allow this for loops
"quotes": ["off", "double"] // We may go for a double-quotes style
}
};
# Ignore generated files
dist/
src/package.js
test/tape.js
# Ignore node related items
node_modules/
npm-debug.log
# Ignore editor files
.vscode/
# Don't re-download node_modules every time. Cache it
cache:
paths:
- node_modules/
validate:
script:
- yarn install
- npm run test
`g1` is library for common interaction and layout patterns.
Install using `yarn install g1` or `npm install --save g1`. Then add this to your HTML:
<script src="node_modules/g1/dist/g1.min.js"></script>
### Interactions
- [$.urlfilter](#urlfilter) changes URL query parameters when clicked. Used to filter data.
### Utilities
- [g1.url.parse](#urlparse) parses a URL into a structured object
- [g1.url.join](#urljoin) joins two URLs
- [g1.url.update](#urlupdate) updates a URL's query parameters
- [$.dispatch](#dispatch) is like [trigger](https://api.jquery.com/trigger/) but sends a native event (triggers non-jQuery events too)
# Documentation
## urlfilter
Example:
```html
<a class="urlfilter" href="?name=John">Link</a>
<script>
$('body').urlfilter()
</script>
```
Let's say the page is `?city=NY`. Clicking on any `.urlfilter` in `body` opens
`?city=NY&name=John`. The `href=` in the `.urlfilter` link *updates* the current
page URL instead of replacing it.
`data-mode` controls the way the URL is updated by the `href`:
| URL | href | default | `data-mode="add"` | `data-mode="toggle"` | `data-mode="del"` |
|--------|-----------|------------|-------------------|----------------------|-------------------|
| `?` | `?x=1` | `?x=1` | `?x=1` | `?x=1` | `?` |
| `?x=1` | `?x=1` | `?x=1` | `?x=1&x=1` | `?` | `?` |
| `?x=1` | `?y=1` | `?x=1&y=1` | `?x=1&y=1` | `?x=1&y=1` | `?x=1` |
| `?x=1` | `?x=2` | `?x=2` | `?x=1&x=2` | `?x=1&x=2` | `?x=1` |
### urlfilter attributes
A `.urlfilter` class can use these attributes:
- `href=` updates the URL
- `data-target=` defines the target where the URL is updated:
- default: updates `window.location`
- `pushState`: updates the current page using pushState
- `#`: updates the `window.location.hash`
- `.class`: loads URL into `.class` by updating its `src=` attribute as the base URL
- `data-remove`: removes any URL query parameters without values. e.g. `?x&y=1` becomes `?`
The element on which `.urlfilter()` is called can have these attributes:
- `data-selector=` changes which nodes urlfilter applies to. Default: `.urlfilter`
- `data-attr=` changes which attribute updates the URL. Default: `href`
- `data-src=` changes which attribute holds the current URL when `data-target=` is a selector. Default: `src`
- `data-remove` is the same as specifying data-remove on every `.urlfilter`. Default: none
### urlfilter events
- `urlfilter` is fired on the source when the URL is changed. 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
### urlfilter examples
Just add this line to the page.
$('body').urlfilter()
This activates all `.urlfilter` classes as below:
```html
<a class="urlfilter" href="city=NY"> Change ?city= to NY</a>
<a class="urlfilter" href="city=NY" data-mode="add"> Add ?city= to NY</a>
<a class="urlfilter" href="city=NY" data-mode="del"> Remove NY from ?city=</a>
<a class="urlfilter" href="city=NY" data-mode="toggle"> Toggle NY in ?city=</a>
<a class="urlfilter" href="city=NY" data-target="pushState">Change ?city= to NY using pushState</a>
<a class="urlfilter" href="city=NY" data-target="#"> Change location.hash, i.e. #?city= to NY</a>
<a class="urlfilter" href="city=NY" data-target="iframe"> Change iframe URL ?city= NY</a>
Please register or sign in to reply
<iframe src="?country=US"></iframe>
<a class="urlfilter" href="city=NY" data-target=".block"> Use AJAX to load ?city=NY into .block</a>
<div class="block" src="?country=US"></div>
```
## 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` |
  • @s.anand -- would it help to have method to clear/reset the querylist without specifying keys. Would it be

    • g1.url.parse('path?a=1&b=2').update(null).toString() // I'm okay with this.
    • g1.url.parse('path?a=1&b=2').update({}).toString() // and this too.
    • Or g1.url.parse('path?a=1&b=2').clearQuery().toString() // Not keen on this.

    You can argue why not set location.hash = '' or location.search = '' or use data-remove.

    The question is -- What would be consistent with g1.url usage?

    Edited by Pratap Vardhan
  • @pratap.vardhan

    IF we do it (and let's discuss this), the approach should be your first option: url.update(null) to clear all values. But my suggestion would be to use url.searchKey = '' when using the g1.url library.

    When using $.urlfilter, I would suggest <a href="?"> and probably avoid data-remove.

    Do you think it's worth introducing url.update(null) and/or an alternate attribute-based way of clearing all keys?

Please register or sign in to reply
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
```
## dispatch
```js
$('a.action').dispatch('click')
```
mimics a user click action on `a.action`. Unlike [$.trigger](https://api.jquery.com/trigger/),
this executes non-jQuery event handlers as well.
You can add an optional dict as the second parameter. It can have any
[event properties](https://developer.mozilla.org/en-US/docs/Web/API/Event#Properties)
as attributes. For example:
```js
$('a.action').dispatch('click', {bubbles: true, cancelable: false})
```
# Change log
- `0.1.0`: Initial release with:
- [$.urlfilter](#urlfilter) changes URL query parameters when clicked. Used to filter data
- [g1.url.parse](#urlparse) parses a URL into a structured object
- [g1.url.join](#urljoin) joins two URLs
- [g1.url.update](#urlupdate) updates a URL's query parameters
- [$.dispatch](#dispatch) is like [trigger](https://api.jquery.com/trigger/) but sends a native event (triggers non-jQuery events too)
# Release
Clone the repo and run:
yarn install
To build locally, run:
npm run build
To test locally, run:
npm test
To publish a new version on npm:
# Run tests on dev branch
git checkout dev
npm test
# Update package.json version
# Update README.md change log
# Ensure that there are no build errors on the server
git commit -m"DOC: Release version x.x.x"
git push
# Merge into dev branch
git checkout master
git merge dev
git tag -a v0.x.x # Add a one-line summary
git push --follow-tags
npm publish # as sanand0
git checkout dev
export {version} from "./src/package.js"
import {parse, unparse, join, update} from "./src/url.js"
export var url = {
parse: parse,
unparse: unparse,
join: join,
update: update
}
import {dispatch} from './src/jquery.dispatch.js'
import {urlfilter} from './src/jquery.urlfilter.js'
if (typeof jQuery != 'undefined') {
jQuery.extend(jQuery.fn, {
dispatch: dispatch,
urlfilter: urlfilter
})
}
{
"name": "g1",
"version": "0.1.0",
"description": "Gramex 1.x interaction library",
"license": "UNLICENSED",
"author": "S Anand <s.anand@gramener.com>",
"main": "index.js",
"repository": {
"type": "git",
"url": "git@code.gramener.com:s.anand/g1.git"
},
"scripts": {
"lint": "eslint index.js src",
"build": "rimraf dist && json2module package.json > src/package.js && rollup -c && uglifyjs dist/g1.js -m -o dist/g1.min.js",
"pretest": "npm run build && browserify -s tape -r tape -o test/tape.js",
"server": "npm run pretest && npm run lint && node test/server.js",
"test": "npm run lint && tape test/test-*.js | faucet && node test/server.js test/jquery.urlfilter.html test/jquery.dispatch.html | tap-merge | faucet",
"prepublishOnly": "npm test"
},
"devDependencies": {
"browserify": "14",
"component-emitter": "1",
"eslint": "^4",
"express": "4",
"faucet": "^0.0.1",
"jquery": "3",
"json2module": "0.0",
"puppeteer": "0.13",
"rimraf": "2",
"rollup": "0.52",
"tap-merge": "0.3",
"tape": "4",
"uglify-js": "3"
}
}
export default {
input: "index",
extend: true,
output: {
file: "dist/g1.js",
format: "umd",
name: "g1"
}
}
/*
Usage: $(selector).dispatch(event, options)
Simulate the event on the selector.
$('a').dispatch('click')
$('input').dispatch('change')
Options
-------
- bubbles: whether the event bubbles or not. default: true
- cancelable: whether the event is cancelable or not. default: true
- All other `new Event()` options will also work
https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
*/
var _event
try {
new Event('click')
_event = function(name, options) {
// On Firefox, you needed to send the right event subclass. This is no longer a problem.
// See https://developer.mozilla.org/en-US/docs/Web/Reference/Events for the list
// if (name.match(/click$|^mouse|^menu$/)) return new MouseEvent(name, options)
// else if (name.match(/^key/)) return new KeyboardEvent(name, options)
// else if (name.match(/^focus|^blur$/)) return new FocusEvent(name, options)
return new Event(name, options)
}
} catch (e) {
// The old fashioned way, for IE
_event = function(name, options) {
var evt = document.createEvent('event')
evt.initEvent(name, options.bubbles, options.cancelable)
return evt
}
}
export function dispatch(name, options) {
return this.each(function() {
this.dispatchEvent(_event(name, $.extend({
bubbles: true,
cancelable: true
}, options)))
})
}
import {parse} from './url.js'
import {hasdata} from './jquery.util.js'
export function urlfilter(options) {
options = options || {}
var $self = this
// If there are no elements in the selection, exit silently
if ($self.length == 0)
return
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 doc = $self[0].ownerDocument
// 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.on('click.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.searchKey
if (remove)
for (var key in q)
if (q[key] === '')
q[key] = null
function target_url(url) {
return parse(url)
.join(href, {query: false, hash: false})
.update(q, mode)
.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 })
})
}
// jQuery utilities.
// These are NOT jQuery plugins. They accept a $node directly.
// These are NOT exposed as part of the API. Purely internal.
// Return all values that match the selector
// AMONG and UNDER the $node
export function findall($node, selector) {
return $node.filter(selector).add($node.find(selector))
}
// Return all values that DO NOT match the selector
// AMONG and UNDER the $node. Complement of findall
export function notall($node, selector) {
return $node.not(selector).add($node.not(selector))
}
// Returns true if data attribute is present
// But if the value is "false", "off", "n" or "no" in any case, returns false (like YAML)
// Returns default_value if data attribute is missing
export function hasdata($node, key, default_value) {
var val = $node.data(key)
if (typeof val == 'undefined' || val === false || val === null)
return default_value
if (typeof val == 'string' && val.match(/^(false|off|n|no)$/i))
return false
return true
}
// Return the {width:..., height:...} of the node
export function getSize($node) {
// Ideally, this is just one line:
// return $node.getBoundingClientRect()
// But see http://stackoverflow.com/q/18153989/100904
// and https://bugzilla.mozilla.org/show_bug.cgi?id=530985
// If the contents exceed the bounds of an element,
// getBoundingClientRect() failes in Firefox.
// So set display:block, get $().width(), and unset display:block
$node = $($node)
var old_display = $node.css('display'),
result = {}
if (old_display != 'block')
$node.css('display', 'block')
result.width = $node.width()
result.height = $node.height()
if (old_display != 'block')
$node.css('display', old_display)
return result
}
/*
url.parse() provides results consistent with window.location.
0 href Full URL source