src/components/ListView.js
/*
* OS.js - JavaScript Cloud/Web Desktop Platform
*
* Copyright (c) Anders Evenrud <andersevenrud@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Anders Evenrud <andersevenrud@gmail.com>
* @licence Simplified BSD License
*/
import {h} from 'hyperapp';
import {filteredProps, doubleTap} from '../utils';
import {Element} from './Element';
import {Icon} from './Icon';
const tapper = doubleTap();
const createView = props => {
let debounceScroll;
const cols = (paneIndex) => (row, rowIndex) => {
const col = row.columns[paneIndex] || {};
const colIcon = col.icon ? h(Icon, col.icon) : null;
const children = [h('span', {}, [typeof col === 'object' ? col.label : col])];
const selected = props.multiselect
? props.selectedIndex.indexOf(rowIndex) !== -1
: props.selectedIndex === rowIndex;
if (colIcon) {
children.unshift(colIcon);
}
return h('div', {
key: row.key,
'data-has-icon': col.icon ? true : undefined,
class: 'osjs-gui-list-view-cell' + (selected ? ' osjs__active' : ''),
ontouchstart: (ev) => tapper(ev, () => props.onactivate({index: rowIndex, ev})),
ondblclick: (ev) => props.onactivate({index: rowIndex, ev}),
onclick: (ev) => props.onselect({index: rowIndex, ev}),
oncontextmenu: (ev) => props.oncontextmenu({index: rowIndex, ev}),
oncreate: (el) => props.oncreate({index: rowIndex, el})
}, children);
};
const pane = (index, col) => h('div', {
class: 'osjs-gui-list-view-pane',
style: col.style || {}
}, [
h('div', {
class: 'osjs-gui-list-view-header',
style: {
display: props.hideColumns ? 'none' : undefined
}
}, h('span', {}, typeof col === 'object' ? col.label : col)),
h('div', {
class: 'rows',
'data-zebra': String(props.zebra)
}, props.rows.map(cols(index)))
]);
return h('div', {
class: 'osjs-gui-list-view-wrapper',
onscroll: ev => {
debounceScroll = clearTimeout(debounceScroll);
debounceScroll = setTimeout(() => {
props.onscroll(ev);
}, 100);
},
oncreate: el => (el.scrollTop = props.scrollTop),
onupdate: el => {
const notSelected = props.multiselect
? props.selectedIndex.length === 0
: props.selectedIndex < 0;
if (notSelected) {
el.scrollTop = props.scrollTop;
}
}
}, props.columns.map((c, i) => pane(i, c)));
};
export const ListView = props => h(Element, Object.assign({
class: 'osjs-gui-list-view'
}, props.box || {}), createView(filteredProps(props, ['box'])));
export const listView = ({
component: (state, actions) => {
const createSelection = index => {
if (state.multiselect) {
const foundIndex = state.selectedIndex.indexOf(index);
const newSelection = [...state.selectedIndex];
if (foundIndex === -1) {
newSelection.push(index);
} else {
newSelection.splice(foundIndex, 1);
}
return newSelection;
}
return state.selectedIndex;
};
/**
* Creates a range of indexes from start to end
* @param {Number} start
* @param {Number} end
* @return {Array}
*/
const createSelectionRange = (start, end) => {
// Swaps start and end if start is greater than end
if (start > end) {
[start, end] = [end, start];
}
const indices = [
...state.selectedIndex,
// Generates a range of indexes from start to end
...Array.from({length: end - start + 1}, (_, i) => i + start)
];
// Remove duplicates from the array
return [...new Set(indices)];
};
const getSelection = (index, ev) => {
const selected = state.multiselect
? (ev.shiftKey
? createSelectionRange(state.previousSelectedIndex, index)
: ev.ctrlKey
? createSelection(index)
: [index])
: index;
const data = state.multiselect
? selected.map((item) => state.rows[item].data)
: state.rows[selected].data;
// Store the previous index in the state to use for calculating the
// range if the shift key is pressed
if (state.multiselect) {
actions.setPreviousSelectedIndex(index);
}
return {selected, data};
};
const clearCurrentSelection = (index) => {
const selected = state.multiselect ? [] : -1;
const data = state.multiselect
? state.selectedIndex.map((item) => state.rows[item].data)
: state.rows[index].data;
return {selected, data};
};
const newProps = Object.assign({
multiselect: false,
zebra: true,
columns: [],
rows: [],
onselect: ({index, ev}) => {
const {selected, data} = getSelection(index, ev);
actions.select({data, index, ev, selected});
actions.setSelectedIndex(selected);
},
onactivate: ({index, ev}) => {
const {selected, data} = clearCurrentSelection(index);
actions.activate({data, index, ev, selected});
actions.setSelectedIndex(selected);
},
oncontextmenu: ({index, ev}) => {
const {selected, data} = getSelection(index, ev);
actions.select({data, index, ev});
actions.contextmenu({data, index, ev, selected});
actions.setSelectedIndex(selected);
},
oncreate: ({index, el}) => {
const data = state.rows[index].data;
actions.created({index, el, data});
},
onscroll: (ev) => {
actions.scroll(ev);
}
}, state);
return (props = {}) => ListView(Object.assign(newProps, props));
},
state: state => Object.assign({
selectedIndex: state.multiselect ? [] : -1,
scrollTop: 0
}, state),
actions: actions => Object.assign({
select: () => () => ({}),
activate: () => () => ({}),
contextmenu: () => () => ({}),
created: () => () => ({}),
scroll: () => state => state,
setRows: rows => ({rows}),
setColumns: columns => ({columns}),
setScrollTop: scrollTop => state => ({scrollTop}),
setSelectedIndex: selectedIndex => ({selectedIndex}),
setPreviousSelectedIndex: previousSelectedIndex => ({previousSelectedIndex}),
}, actions || {})
});