Skip to content

Commit

Permalink
added keyboard interaction
Browse files Browse the repository at this point in the history
  • Loading branch information
crisward committed Dec 18, 2015
1 parent c91c3e7 commit 28ba152
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 28 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ you hold back for a few days I may have the update in place, along with the v1.0
## Todo

* Add multi Select
* Work out if I can highjack onclick and ondblclick, instead of select and edit
* ~~Add keyboard interaction~~
* Add demo of reorder by column
* Add demo of change column width
* Take column widths from head and apply to body ? Pehaps make optional?
Expand Down
107 changes: 91 additions & 16 deletions lib/grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,45 @@ riot.tag2('gridhead', '<yield></yield>', '', '', function(opts) {
riot.tag2('gridbody', '<div onscroll="{scrolling}" riot-style="height:{parseInt(parent.opts.height,10)-30}px" class="gridbody"> <div riot-style="position:relative;height:{rowheight*parent.opts.data.length}px;background:white" class="scrollblock"> <div each="{row, i in visibleRows}" riot-style="top:{rowheight*(i+scrollTop)}px" ondblclick="{handleDblClick}" onclick="{handleClick}" class="gridrow {active:active==row}"><yield></yield></div> </div> </div>', '', '', function(opts) {
this.active = false;

this.hasFocus = false;

this.scrollTop = 0;

this.scrollBottom = 10;

this.prevScrollTop = -1;

this.downKey = 40;

this.upKey = 38;

this.on('error', function(err) {
return console.error(err.message);
});

this.on('mount', (function(_this) {
return function() {
var ref;
_this.rowheight = ((ref = _this.parent.opts) != null ? ref.rowheight : void 0) || 30;
if (_this.parent.opts.active != null) {
return _this.active = _this.parent.opts.active;
}
};
})(this));
this.on('mount', function() {
var ref;
this.rowheight = ((ref = this.parent.opts) != null ? ref.rowheight : void 0) || 30;
if (this.parent.opts.active != null) {
this.active = this.parent.opts.active;
}
document.addEventListener('keydown', this.keydown);
['click', 'focus', 'blur'].forEach((function(_this) {
return function(ev) {
return _this.root.addEventListener(ev, _this.focused);
};
})(this));
return this.update();
});

this.on('unmount', function() {
document.removeEventListener('keydown', this.keydown);
return ['click', 'focus', 'blur'].forEach((function(_this) {
return function(ev) {
return _this.root.removeEventListener(ev, _this.focused);
};
})(this));
});

this.on('update', function() {
var oldScrolltop;
Expand All @@ -34,16 +54,69 @@ this.on('update', function() {
}
oldScrolltop = this.scrollTop;
this.scrollTop = Math.round((this.gridbody.scrollTop / this.rowheight) / 2) * 2 - 10;
if (this.scrollTop !== oldScrolltop) {
setTimeout(this.update, 100);
}
if (this.scrollTop < 0) {
this.scrollTop = 0;
}
this.scrollBottom = this.scrollTop + Math.round((this.gridbody.offsetHeight / this.rowheight) / 2) * 2 + 20;
return this.visibleRows = this.parent.opts.data.slice(this.scrollTop, this.scrollBottom);
this.visibleRows = this.parent.opts.data.slice(this.scrollTop, this.scrollBottom);
if (this.keyPressed) {
this.activePos = (this.keyPressed * this.rowheight) - (this.gridbody.offsetHeight / 2) + this.rowheight / 2;
if (this.activePos > 0) {
this.gridbody.scrollTop = this.activePos;
}
if (this.activePos < 0) {
this.gridbody.scrollTop = 0;
}
return this.keyPressed = false;
}
});

this.focused = (function(_this) {
return function() {
if (_this.parent.root === document.activeElement) {
return _this.update({
hasFocus: true
});
} else {
return _this.update({
hasFocus: false
});
}
};
})(this);

this.keydown = (function(_this) {
return function(e) {
var index;
if (_this.parent.root !== document.activeElement) {
return _this.update({
hasFocus: false
});
}
_this.hasFocus = true;
index = _this.parent.opts.data.indexOf(_this.active);
if (e.keyCode === _this.downKey) {
index++;
}
if (e.keyCode === _this.upKey) {
index--;
}
if (index < 0) {
index = 0;
}
if (index >= _this.parent.opts.data.length) {
index = _this.parent.opts.data.length - 1;
}
_this.keyPressed = index;
_this.active = _this.parent.opts.data[index];
_this.parent.opts.onchange(_this.active);
if (e.keyCode === _this.downKey || e.keyCode === _this.upKey) {
e.preventDefault();
}
return _this.update();
};
})(this);

this.scrolling = (function(_this) {
return function(e) {
return _this.update();
Expand All @@ -56,10 +129,12 @@ this.handleClick = (function(_this) {
return;
}
_this.active = e.item.row;
if (typeof _this.parent.opts.click !== "function") {
return;
if ((_this.parent.opts.click != null) && typeof _this.parent.opts.click === "function") {
_this.parent.opts.click(_this.active);
}
if ((_this.parent.opts.onchange != null) && typeof _this.parent.opts.onchange === "function") {
return _this.parent.opts.onchange(_this.active);
}
return _this.parent.opts.click(e.item.row);
};
})(this);

Expand Down
39 changes: 35 additions & 4 deletions src/grid.tag
Original file line number Diff line number Diff line change
Expand Up @@ -53,34 +53,65 @@ gridbody
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
script(type='text/coffee').
@active = false
@hasFocus = false
@scrollTop = 0
@scrollBottom = 10 #default to rendering 10 rows
@prevScrollTop = -1
@downKey = 40
@upKey = 38

@on 'error',(err)-> console.error err.message

@on 'mount',=>
@on 'mount',->
@rowheight = @parent.opts?.rowheight || 30
@active = @parent.opts.active if @parent.opts.active?
document.addEventListener 'keydown',@keydown
['click','focus','blur'].forEach (ev)=> @root.addEventListener ev,@focused
@update()

@on 'unmount',->
document.removeEventListener 'keydown',@keydown
['click','focus','blur'].forEach (ev)=> @root.removeEventListener ev,@focused

@on 'update',->
@gridbody = @root.querySelector(".gridbody")
return if !@parent.opts.data
oldScrolltop = @scrollTop
@scrollTop = Math.round((@gridbody.scrollTop / @rowheight)/2)*2 -10
setTimeout(@update,100) if @scrollTop!=oldScrolltop #reupdate if scroll has changed
@scrollTop = 0 if @scrollTop < 0
@scrollBottom = @scrollTop+Math.round((@gridbody.offsetHeight / @rowheight)/2)*2 +20
@visibleRows = @parent.opts.data.slice(@scrollTop,@scrollBottom)
if @keyPressed
@activePos = (@keyPressed*@rowheight)-(@gridbody.offsetHeight/2)+@rowheight/2
@gridbody.scrollTop = @activePos if @activePos > 0
@gridbody.scrollTop = 0 if @activePos < 0
@keyPressed = false

@focused = =>
if @parent.root == document.activeElement then @update(hasFocus:true) else @update(hasFocus:false)

@keydown = (e)=>
return @update(hasFocus:false) if @parent.root != document.activeElement
@hasFocus = true
index = @parent.opts.data.indexOf(@active)
index++ if e.keyCode == @downKey
index-- if e.keyCode == @upKey
index = 0 if index < 0
index = @parent.opts.data.length-1 if index >= @parent.opts.data.length
@keyPressed = index
@active = @parent.opts.data[index]
@parent.opts.onchange(@active) #if @parent.opts.onchange? && typeof @parent.opts.onchange == "function"
e.preventDefault() if e.keyCode == @downKey || e.keyCode == @upKey
@update()

@scrolling = (e)=>
@update()

@handleClick = (e)=>
return if !@parent.opts.click
@active = e.item.row
return if typeof @parent.opts.click != "function"
@parent.opts.click(e.item.row)
@parent.opts.click(@active) if @parent.opts.click? && typeof @parent.opts.click == "function"
@parent.opts.onchange(@active) if @parent.opts.onchange? && typeof @parent.opts.onchange == "function"

@handleDblClick = (e)=>
return if !@parent.opts.dblclick
Expand Down
50 changes: 48 additions & 2 deletions test/grid.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ simulant = require 'simulant'

spyclick = null
spyclick2 = null
spyChange = null
test = {}



describe 'grid',->

beforeEach ->
Expand All @@ -23,7 +23,8 @@ describe 'grid',->
@node = document.body.appendChild(@domnode)
spyclick = sinon.spy()
spyclick2 = sinon.spy()
@tag = riot.mount('testtag',{griddata:griddata,gridheight:gridheight,testclick:spyclick,testclick2:spyclick2})[0]
spyChange = sinon.spy()
@tag = riot.mount('testtag',{griddata:griddata,gridheight:gridheight,testclick:spyclick,testclick2:spyclick2,testchange:spyChange})[0]
riot.update()

afterEach ->
Expand Down Expand Up @@ -68,6 +69,51 @@ describe 'grid',->
expect(spyclick2.calledOnce).to.be.true
expect(spyclick2.args[0][0]).to.eql(griddata[2])

it "should select next item when down key is pressed",->
rows = document.querySelectorAll('.gridrow')
simulant.fire(rows[0],'click')
document.querySelector('grid').focus()
expect(@domnode.querySelector('.active')).to.equal(rows[0])
simulant.fire(document,'keydown',{keyCode:40})
expect(@domnode.querySelector('.active')).to.equal(rows[1])

it "should select next item when down key is pressed",->
rows = document.querySelectorAll('.gridrow')
simulant.fire(rows[0],'click')
document.querySelector('grid').focus()
expect(@domnode.querySelector('.active')).to.equal(rows[0])
simulant.fire(document,'keydown',{keyCode:40})
expect(@domnode.querySelector('.active')).to.equal(rows[1])

it "should select previous item when up key is pressed",->
rows = document.querySelectorAll('.gridrow')
simulant.fire(rows[3],'click')
document.querySelector('grid').focus()
expect(@domnode.querySelector('.active')).to.equal(rows[3])
simulant.fire(document,'keydown',{keyCode:38})
expect(@domnode.querySelector('.active')).to.equal(rows[2])
simulant.fire(document,'keydown',{keyCode:38})
expect(@domnode.querySelector('.active')).to.equal(rows[1])
simulant.fire(document,'keydown',{keyCode:38})
expect(@domnode.querySelector('.active')).to.equal(rows[0])

it "should not change on keypress if not focused",->
rows = document.querySelectorAll('.gridrow')
simulant.fire(rows[2],'click')
expect(@domnode.querySelector('.active')).to.equal(rows[2])
simulant.fire(document,'keydown',{keyCode:38})
expect(@domnode.querySelector('.active')).to.equal(rows[2])

it "should fire onchange on keypress",->
rows = document.querySelectorAll('.gridrow')
simulant.fire(rows[3],'click')
document.querySelector('grid').focus()
expect(@domnode.querySelector('.active')).to.equal(rows[3])
simulant.fire(document,'keydown',{keyCode:38})
expect(spyChange.calledTwice).to.be.true



describe 'grid without data',->

beforeEach ->
Expand Down
64 changes: 64 additions & 0 deletions test/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<title>Riot Grid</title>
</head>
<body>

<script type="riot/tag">
<sample>
<grid data="{collection}" tabindex="1" height="{300}" click="{handleSelect}" onchange="{handleSelect}" dblclick="{handleDoubleClick}">
<gridhead>
<span style="width:70%">Name</span>
<span style="width:30%">Age</span>
</gridhead>

<gridbody>
<span style="width:70%">{row.name}</span>
<span style="width:30%">{row.age}</span>
</gridbody>
</grid>
<div style="background:#aaa;padding:10px;margin:2px 0;">
{collection.length} Rows
</div>
<p if="{sel.name}">You have selected {sel.name}</p>
var self = this
this.handleSelect = function(row){
self.update({sel:row})
}
this.handleDoubleClick = function(row){
self.update({sel:row})
console.log(row,'double clicked')
}
</sample>

</script>

<sample></sample>


<script src="../node_modules/riot/riot+compiler.min.js"></script>
<script src="../lib/grid.js"></script>
<script>
var randAge = function() { return Math.round(Math.random() * 110) + 1;};
var randFirstname = function() {
var names = ["Chloe", "Emily", "Megan", "Charlotte", "Jessica", "Lauren", "Sophie", "Olivia", "Hannah", "Lucy", "Georgia", "Rebecca", "Bethany", "Amy", "Ellie", "Katie", "Emma", "Abigail", "Molly", "Grace", "Courtney", "Shannon", "Caitlin", "Eleanor", "Jade", "Ella", "Leah", "Alice", "Holly", "Laura", "Anna", "Jasmine", "Sarah", "Elizabeth", "Amelia", "Rachel", "Amber", "Phoebe", "Natasha", "Niamh", "Zoe", "Paige", "Nicole", "Abbie", "Mia", "Imogen", "Lily", "Alexandra", "Chelsea", "Daisy", "Jack", "Thomas", "James", "Joshua", "Daniel", "Harry", "Samuel", "Joseph", "Matthew", "Callum", "Luke", "William", "Lewis", "Oliver", "Ryan", "Benjamin", "George", "Liam", "Jordan", "Adam", "Alexander", "Jake", "Connor", "Cameron", "Nathan", "Kieran", "Mohammed", "Jamie", "Jacob", "Michael", "Ben", "Ethan", "Charlie", "Bradley", "Brandon", "Aaron", "Max", "Dylan", "Kyle", "Robert", "Christopher", "David", "Edward", "Charles", "Owen", "Louis", "Alex", "Joe", "Rhyce"];
return names[Math.round(Math.random() * (names.length - 1))];
};
var randSurname = function() {
var names = ["Smith", "Jones", "Taylor", "Williams", "Brown", "Davies", "Evans", "Wilson", "Thomas", "Roberts", "Johnson", "Lewis", "Walker", "Robinson", "Wood", "Thompson", "White", "Watson", "Jackson", "Wright", "Green", "Harris", "Cooper", "King", "Lee", "Martin", "Clarke", "James", "Morgan", "Hughes", "Edwards", "Hill", "Moore", "Clark", "Harrison", "Scott", "Young", "Morris", "Hall", "Ward", "Turner", "Carter", "Phillips", "Mitchell", "Patel", "Adams", "Campbell", "Anderson", "Allen", "Cook", "Bailey", "Parker", "Miller", "Davis", "Murphy", "Price", "Bell", "Baker", "Griffiths", "Kelly", "Simpson", "Marshall", "Collins", "Bennett", "Cox", "Richardson", "Fox", "Gray", "Rose", "Chapman", "Hunt", "Robertson", "Shaw", "Reynolds", "Lloyd", "Ellis", "Richards", "Russell", "Wilkinson", "Khan", "Graham", "Stewart", "Reid", "Murray", "Powell", "Palmer", "Holmes", "Rogers", "Stevens", "Walsh", "Hunter", "Thomson", "Matthews", "Ross", "Owen", "Mason", "Knight", "Kennedy", "Butler", "Saunders"];
return names[Math.round(Math.random() * (names.length - 1))];
};
var collection=[]
var count=0
while(count<100000){
collection.push({id:count,name:randFirstname()+" "+randSurname(),age:randAge()})
count++;
}
riot.mount('sample',{collection:collection})

</script>


</body>
</html>
13 changes: 8 additions & 5 deletions test/testtag.tag
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
testtag

grid(data="{data}",height="{height}",click="{handleSelect}",dblclick="{handleEdit}")
grid(data="{data}",tabindex="1",height="{height}",click="{handleSelect}",dblclick="{handleEdit}",onchange="{handleChange}")
gridhead
span(style="width:40%") First Name
span(style="width:40%") Surname
Expand All @@ -17,11 +17,14 @@ testtag
@data = opts.griddata
@height = opts.gridheight

@handleSelect = (rows)=>
opts.testclick(rows)
@handleSelect = (row)=>
opts.testclick(row)

@handleEdit = (rows)=>
opts.testclick2(rows)
@handleEdit = (row)=>
opts.testclick2(row)

@handleChange = (rows)=>
opts.testchange(rows)


testtag2
Expand Down

0 comments on commit 28ba152

Please sign in to comment.