Skip to content

Commit

Permalink
added multi-select
Browse files Browse the repository at this point in the history
  • Loading branch information
crisward committed Dec 19, 2015
1 parent 4afc681 commit 01b7b8f
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 29 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ npm install riot-grid

See demo on [codepen](http://codepen.io/crisward/pen/rxepMX?editors=101)
This includes a bit of code to generate 100,000 random users to show off the performance of the grid.
I tried increasing this to 10,000,000 but codepen thought I had an infinite loop.
I tried increasing this to 10,000,000 but codepen thought I had an infinite loop.

Note: the keyboard interaction doesn't seem to work in codepen


## Api Changes
Expand All @@ -42,7 +44,7 @@ you hold back for a few days I may have the update in place, along with the v1.0

## Todo

* Add multi Select
* ~~Add multi Select~~
* ~~Add keyboard interaction~~
* Add demo of reorder by column
* Add demo of change column width
Expand Down
61 changes: 49 additions & 12 deletions lib/grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ riot.tag2('grid', '<yield></yield>', 'grid { position: relative; display: block;
});
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;
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:isActive(row)}"><yield></yield></div> </div> </div>', '', '', function(opts) {
this.active = [];

this.hasFocus = false;

Expand All @@ -18,10 +18,6 @@ this.downKey = 40;

this.upKey = 38;

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

this.on('mount', function() {
var ref;
this.rowheight = ((ref = this.parent.opts) != null ? ref.rowheight : void 0) || 30;
Expand Down Expand Up @@ -85,16 +81,25 @@ this.focused = (function(_this) {
};
})(this);

this.isActive = (function(_this) {
return function(row) {
return _this.active.indexOf(row) > -1;
};
})(this);

this.keydown = (function(_this) {
return function(e) {
var index;
var idx, index, row;
if (_this.parent.root !== document.activeElement) {
return _this.update({
hasFocus: false
});
}
if (e.keyCode !== _this.downKey && e.keyCode !== _this.upKey) {
return;
}
_this.hasFocus = true;
index = _this.parent.opts.data.indexOf(_this.active);
index = _this.parent.opts.data.indexOf(_this.active[_this.active.length - 1]);
if (e.keyCode === _this.downKey) {
index++;
}
Expand All @@ -108,7 +113,17 @@ this.keydown = (function(_this) {
index = _this.parent.opts.data.length - 1;
}
_this.keyPressed = index;
_this.active = _this.parent.opts.data[index];
row = _this.parent.opts.data[index];
if (e.shiftKey) {
idx = _this.active.indexOf(row);
if (idx > -1) {
_this.active.splice(idx, 1);
} else {
_this.active.push(row);
}
} else {
_this.active = [row];
}
_this.parent.opts.onchange(_this.active);
if (e.keyCode === _this.downKey || e.keyCode === _this.upKey) {
e.preventDefault();
Expand All @@ -125,12 +140,34 @@ this.scrolling = (function(_this) {

this.handleClick = (function(_this) {
return function(e) {
var idx, idx1, idx2, j, ref, ref1, results;
if (!_this.parent.opts.click) {
return;
}
_this.active = e.item.row;
if (e.shiftKey && (_this.firstSelectedIndex != null)) {
idx1 = _this.firstSelectedIndex;
idx2 = _this.parent.opts.data.indexOf(e.item.row);
_this.active = (function() {
results = [];
for (var j = ref = Math.min(idx1, idx2), ref1 = Math.max(idx1, idx2); ref <= ref1 ? j <= ref1 : j >= ref1; ref <= ref1 ? j++ : j--){ results.push(j); }
return results;
}).apply(this).map(function(i) {
return _this.parent.opts.data[i];
});
} else if (e.metaKey) {
idx = _this.active.indexOf(e.item.row);
if (idx > -1) {
_this.active.splice(idx, 1);
} else {
_this.active.push(e.item.row);
}
} else {
_this.active = [e.item.row];
_this.firstSelectedIndex = _this.parent.opts.data.indexOf(e.item.row);
}
window.getSelection().removeAllRanges();
if ((_this.parent.opts.click != null) && typeof _this.parent.opts.click === "function") {
_this.parent.opts.click(_this.active);
_this.parent.opts.click(e.item.row);
}
if ((_this.parent.opts.onchange != null) && typeof _this.parent.opts.onchange === "function") {
return _this.parent.opts.onchange(_this.active);
Expand All @@ -143,7 +180,7 @@ this.handleDblClick = (function(_this) {
if (!_this.parent.opts.dblclick) {
return;
}
_this.active = e.item.row;
_this.active = [e.item.row];
if ((_this.parent.opts.dblclick != null) && typeof _this.parent.opts.dblclick === "function") {
return _this.parent.opts.dblclick(e.item.row);
}
Expand Down
35 changes: 27 additions & 8 deletions src/grid.tag
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,20 @@ gridhead
gridbody
.gridbody(onscroll='{scrolling}', style='height:{parseInt(parent.opts.height,10)-30}px')
.scrollblock(style='position:relative;height:{rowheight*parent.opts.data.length}px;background:white')
.gridrow(each='{row, i in visibleRows}', class='{active:active==row}', style='top:{rowheight*(i+scrollTop)}px',ondblclick='{handleDblClick}', onclick='{handleClick}')
.gridrow(each='{row, i in visibleRows}', class='{active:isActive(row)}', style='top:{rowheight*(i+scrollTop)}px',ondblclick='{handleDblClick}', onclick='{handleClick}')
<yield></yield>

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
script(type='text/coffee').
@active = false
@active = []
@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 'error',(err)-> console.error err.message

@on 'mount',->
@rowheight = @parent.opts?.rowheight || 30
Expand Down Expand Up @@ -90,16 +90,25 @@ gridbody
@focused = =>
if @parent.root == document.activeElement then @update(hasFocus:true) else @update(hasFocus:false)

@isActive = (row)=>
@active.indexOf(row)>-1

@keydown = (e)=>
return @update(hasFocus:false) if @parent.root != document.activeElement
return if e.keyCode != @downKey && e.keyCode != @upKey
@hasFocus = true
index = @parent.opts.data.indexOf(@active)
index = @parent.opts.data.indexOf(@active[@active.length-1])
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]
row = @parent.opts.data[index]
if e.shiftKey
idx = @active.indexOf(row)
if idx>-1 then @active.splice(idx,1) else @active.push(row)
else
@active = [row]
@parent.opts.onchange(@active) #if @parent.opts.onchange? && typeof @parent.opts.onchange == "function"
e.preventDefault() if e.keyCode == @downKey || e.keyCode == @upKey
@update()
Expand All @@ -109,12 +118,22 @@ gridbody

@handleClick = (e)=>
return if !@parent.opts.click
@active = e.item.row
@parent.opts.click(@active) if @parent.opts.click? && typeof @parent.opts.click == "function"
if e.shiftKey && @firstSelectedIndex?
idx1 = @firstSelectedIndex
idx2 = @parent.opts.data.indexOf(e.item.row)
@active = [Math.min(idx1,idx2)..Math.max(idx1,idx2)].map (i)=> @parent.opts.data[i]
else if e.metaKey
idx = @active.indexOf(e.item.row)
if idx>-1 then @active.splice(idx,1) else @active.push(e.item.row)
else
@active = [e.item.row]
@firstSelectedIndex = @parent.opts.data.indexOf(e.item.row)
window.getSelection().removeAllRanges()
@parent.opts.click(e.item.row) 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
@active = e.item.row
@active = [e.item.row]
@parent.opts.dblclick(e.item.row) if @parent.opts.dblclick? && typeof @parent.opts.dblclick == "function"

40 changes: 33 additions & 7 deletions test/grid.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ spyclick = null
spyclick2 = null
spyChange = null
test = {}
rows = null


describe 'grid',->
Expand All @@ -26,6 +27,7 @@ describe 'grid',->
spyChange = sinon.spy()
@tag = riot.mount('testtag',{griddata:griddata,gridheight:gridheight,testclick:spyclick,testclick2:spyclick2,testchange:spyChange})[0]
riot.update()
rows = document.querySelectorAll('.gridrow')

afterEach ->
@tag.unmount()
Expand Down Expand Up @@ -70,23 +72,20 @@ describe 'grid',->
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])
Expand All @@ -98,21 +97,43 @@ describe 'grid',->
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

it "should select multiple and all in between with shift click",->
simulant.fire(rows[3],'click')
simulant.fire(rows[5],'click',{shiftKey:true})
expect(spyChange.calledTwice).to.be.true
expect(@domnode.querySelector('.active')).to.equal(rows[3])
expect(spyChange.args[1][0].length).to.equal(3)

it "should add one at a time if meta-clicked",->
simulant.fire(rows[3],'click')
simulant.fire(rows[5],'click',{metaKey:true})
expect(@domnode.querySelectorAll('.active').length).to.equal(2)
expect(spyChange.args[1][0]).to.eql([griddata[3],griddata[5]])

it "should deselect row if meta-clicked",->
simulant.fire(rows[20],'click')
expect(@domnode.querySelectorAll('.active').length).to.equal(1)
simulant.fire(rows[20],'click',{metaKey:true})
expect(@domnode.querySelectorAll('.active').length).to.equal(0)

it "should select multiple with shift+arrow keys",->
simulant.fire(rows[3],'click')
document.querySelector('grid').focus()
expect(@domnode.querySelectorAll('.active').length).to.equal(1)
simulant.fire(document,'keydown',{keyCode:38,shiftKey:true})
expect(@domnode.querySelectorAll('.active').length).to.equal(2)

describe 'grid without data',->

Expand Down Expand Up @@ -149,7 +170,12 @@ describe 'grid without data',->
simulant.fire(document.querySelector('.gridrow'),'dblclick')
expect(@domnode.querySelectorAll('.active').length).to.equal(0)

it "should set active row if passed in",->
@tag = riot.mount('testtag2',{griddata:griddata,gridheight:gridheight,activerow:griddata[1]})[0]
it "should set active rows if passed in",->
@tag = riot.mount('testtag2',{griddata:griddata,gridheight:gridheight,activerow:[griddata[1]]})[0]
riot.update()
expect(@domnode.querySelectorAll('.active').length).to.equal(1)

it "should set multiple active rows if passed in",->
@tag = riot.mount('testtag2',{griddata:griddata,gridheight:gridheight,activerow:[griddata[1],griddata[3],griddata[5]]})[0]
riot.update()
expect(@domnode.querySelectorAll('.active').length).to.equal(3)

0 comments on commit 01b7b8f

Please sign in to comment.