Mithril.js 中大量元素的性能问题
Perfomance issues with large number of elements in Mithril.js
到目前为止,我的应用程序有一个排序和可过滤列表以及一些输入和复选框。
如果列表中的项目超过 500 个,那么问题就会出现,然后每个带有用户输入的元素(复选框、输入字段、菜单)开始有大约半秒的滞后,随着列表中项目的数量增加。列表的排序和过滤速度足够快,但输入元素的滞后时间太长。
问题是:列表和输入元素如何解耦?
列表代码如下:
var list = {}
list.controller = function(args) {
var model = args.model;
var vm = args.vm;
var vmc = args.vmc;
var appCtrl = args.appCtrl;
this.items = vm.filteredList;
this.onContextMenu = vmc.onContextMenu;
this.isSelected = function(guid) {
return utils.getState(vm.listState, guid, "isSelected");
}
this.setSelected = function(guid) {
utils.setState(vm.listState, guid, "isSelected", true);
}
this.toggleSelected = function(guid) {
utils.toggleState(vm.listState, guid, "isSelected");
}
this.selectAll = function() {
utils.setStateBatch(vm.listState, "GUID", "isSelected", true, this.items());
}.bind(this);
this.deselectAll = function() {
utils.setStateBatch(vm.listState, "GUID", "isSelected", false, this.items());
}.bind(this);
this.invertSelection = function() {
utils.toggleStateBatch(vm.listState, "GUID", "isSelected", this.items());
}.bind(this);
this.id = "201505062224";
this.contextMenuId = "201505062225";
this.initRow = function(item, idx) {
if (item.online) {
return {
id : item.guid,
filePath : (item.FilePath + item.FileName).replace(/\/g, "\\"),
class : idx % 2 !== 0 ? "online odd" : "online even",
}
} else {
return {
class : idx % 2 !== 0 ? "odd" : "even"
}
}
};
// sort helper function
this.sorts = function(list) {
return {
onclick : function(e) {
var prop = e.target.getAttribute("data-sort-by")
//console.log("100")
if (prop) {
var first = list[0]
if(prop === "selection") {
list.sort(function(a, b) {
return this.isSelected(b.GUID) - this.isSelected(a.GUID)
}.bind(this));
} else {
list.sort(function(a, b) {
return a[prop] > b[prop] ? 1 : a[prop] < b[prop] ? -1 : 0
})
}
if (first === list[0])
list.reverse()
}
}.bind(this)
}
};
// text inside the table can be selected with the mouse and will be stored for
// later retrieval
this.getSelected = function() {
//console.log(utils.getSelText());
vmc.lastSelectedText(utils.getSelText());
};
};
list.view = function(ctrl) {
var contextMenuSelection = m("div", {
id : ctrl.contextMenuId,
class : "hide"
}, [
m(".menu-item.allow-hover", {
onclick : ctrl.selectAll
}, "Select all"),
m(".menu-item.allow-hover", {
onclick : ctrl.deselectAll
}, "Deselect all"),
m(".menu-item.allow-hover", {
onclick : ctrl.invertSelection
}, "Invert selection") ]);
var table = m("table", ctrl.sorts(ctrl.items()), [
m("tr", [
m("th[data-sort-by=selection]", {
oncontextmenu : ctrl.onContextMenu(ctrl.contextMenuId, "context-menu context-menu-bkg", "hide" )
}, "S"),
m("th[data-sort-by=FileName]", "Name"),
m("th[data-sort-by=FileSize]", "Size"),
m("th[data-sort-by=FilePath]", "Path"),
m("th[data-sort-by=MediumName]", "Media") ]),
ctrl.items().map(function(item, idx) {
return m("tr", ctrl.initRow(item, idx), {
key : item.GUID
},
[ m("td", [m("input[type=checkbox]", {
id : item.GUID,
checked : ctrl.isSelected(item.GUID),
onclick : function(e) {ctrl.toggleSelected(this.id);}
}) ]),
m("td", {
onmouseup: function(e) {ctrl.getSelected();}
}, item.FileName),
m("td", utils.numberWithDots(item.FileSize)),
m("td", item.FilePath),
m("td", item.MediumName) ])
}) ])
return m("div", [contextMenuSelection, table])
}
这是从应用程序主视图初始化列表和所有其他组件的方式:
// the main view which assembles all components
var mainCompView = function(ctrl, args) {
// TODO do we really need him there?
// add the main controller for this page to the arguments for all
// added components
var myArgs = args;
myArgs.appCtrl = ctrl;
// create all needed components
var filterComp = m.component(filter, myArgs);
var part_filter = m(".row", [ m(".col-md-2", [ filterComp ]) ]);
var listComp = m.component(list, myArgs);
var part_list = m(".col-md-10", [ listComp ]);
var optionsComp = m.component(options, myArgs);
var part_options = m(".col-md-10", [ optionsComp ]);
var menuComp = m.component(menu, myArgs);
var part_menu = m(".menu-0", [ menuComp ]);
var outputComp = m.component(output, myArgs);
var part_output = m(".col-md-10", [ outputComp ]);
var part1 = m("[id='1']", {
class : 'optionsContainer'
}, "", [ part_options ]);
var part2 = m("[id='2']", {
class : 'menuContainer'
}, "", [ part_menu ]);
var part3 = m("[id='3']", {
class : 'commandContainer'
}, "", [ part_filter ]);
var part4 = m("[id='4']", {
class : 'outputContainer'
}, "", [ part_output ]);
var part5 = m("[id='5']", {
class : 'listContainer'
}, "", [ part_list ]);
return [ part1, part2, part3, part4, part5 ];
}
// run
m.mount(document.body, m.component({
controller : MainCompCtrl,
view : mainCompView
}, {
model : modelMain,
vm : modelMain.getVM(),
vmc : viewModelCommon
}));
我开始通过向单击事件添加 m.redraw.strategy("none") 和 m.startComputation/endComputation 来解决问题,这解决了问题,但这是正确的解决方案吗?例如,如果我将来自第 3 方的 Mithril 组件与我的列表组件一起使用,我应该如何在不更改其代码的情况下对外部组件执行此操作?
另一方面,我的列表组件可以使用类似 'retain' 标志的东西吗?所以默认情况下列表不会重绘,除非它被告知这样做?但是第 3 方组件的问题也会持续存在。
我知道还有其他策略可以解决这个问题,例如列表分页,但我想知道 Mithril 方面的最佳实践是什么。
提前致谢,
斯特凡
文档中有一些关于何时更改重绘策略的好例子:http://mithril.js.org/mithril.redraw.html#changing-redraw-strategy
但一般来说,如果应用程序状态存储在某处以便 Mithril 可以在不触及 DOM 的情况下访问和计算差异,则很少使用更改重绘策略。看起来你的数据在别处,所以你的 sorts
方法在一定大小后会变得昂贵到 运行 吗?
您只能在修改列表的事件发生后对列表进行排序。否则它会在 Mithril 每次重绘时进行排序,这可能很常见。
m.start/endComputation
对于第 3 方代码很有用,尤其是在 DOM 上运行时。如果库存储了一些状态,您也应该将其用于应用程序状态,这样就不会有任何冗余和可能不匹配的数据。
感谢 Barney 的评论,我找到了一个解决方案:遮挡剔除。可以在此处找到原始示例 http://jsfiddle.net/7JNUy/1/ 。
我根据自己的需要调整了代码,尤其是需要限制触发的滚动事件,以便重绘次数足以平滑滚动。看函数obj.onScroll.
var list = {}
list.controller = function(args) {
var obj = {};
var model = args.model;
var vm = args.vm;
var vmc = args.vmc;
var appCtrl = args.appCtrl;
obj.vm = vm;
obj.items = vm.filteredList;
obj.onContextMenu = vmc.onContextMenu;
obj.isSelected = function(guid) {
return utils.getState(vm.listState, guid, "isSelected");
}
obj.setSelected = function(guid) {
utils.setState(vm.listState, guid, "isSelected", true);
}
obj.toggleSelected = function(guid) {
utils.toggleState(vm.listState, guid, "isSelected");
m.redraw.strategy("none");
}
obj.selectAll = function() {
utils.setStateBatch(vm.listState, "GUID", "isSelected", true, obj.items());
};
obj.deselectAll = function() {
utils.setStateBatch(vm.listState, "GUID", "isSelected", false, obj.items());
};
obj.invertSelection = function() {
utils.toggleStateBatch(vm.listState, "GUID", "isSelected", obj.items());
};
obj.id = "201505062224";
obj.contextMenuId = "201505062225";
obj.initRow = function(item, idx) {
if (item.online) {
return {
id : item.GUID,
filePath : (item.FilePath + item.FileName).replace(/\/g, "\\"),
class : idx % 2 !== 0 ? "online odd" : "online even",
onclick: console.log(item.GUID)
}
} else {
return {
id : item.GUID,
// class : idx % 2 !== 0 ? "odd" : "even",
onclick: function(e) { obj.selectRow(e, this, item.GUID);
m.redraw.strategy("none");
e.stopPropagation();
}
}
}
};
// sort helper function
obj.sorts = function(list) {
return {
onclick : function(e) {
var prop = e.target.getAttribute("data-sort-by")
// console.log("100")
if (prop) {
var first = list[0]
if(prop === "selection") {
list.sort(function(a, b) {
return obj.isSelected(b.GUID) - obj.isSelected(a.GUID)
});
} else {
list.sort(function(a, b) {
return a[prop] > b[prop] ? 1 : a[prop] < b[prop] ? -1 : 0
})
}
if (first === list[0])
list.reverse()
} else {
e.stopPropagation();
m.redraw.strategy("none");
}
}
}
};
// text inside the table can be selected with the mouse and will be stored
// for
// later retrieval
obj.getSelected = function(e) {
// console.log("getSelected");
var sel = utils.getSelText();
if(sel.length != 0) {
vmc.lastSelectedText(utils.getSelText());
e.stopPropagation();
// console.log("1000");
}
m.redraw.strategy("none");
// console.log("1001");
};
var selectedRow, selectedId;
var eventHandlerAdded = false;
// Row callback; reset the previously selected row and select the new one
obj.selectRow = function (e, row, id) {
console.log("selectRow " + id);
unSelectRow();
selectedRow = row;
selectedId = id;
selectedRow.style.background = "#FDFF47";
if(!eventHandlerAdded) {
console.log("eventListener added");
document.addEventListener("click", keyHandler, false);
document.addEventListener("keypress", keyHandler, false);
eventHandlerAdded = true;
}
};
var unSelectRow = function () {
if (selectedRow !== undefined) {
selectedRow.removeAttribute("style");
selectedRow = undefined;
selectedId = undefined;
}
};
var keyHandler = function(e) {
var num = parseInt(utils.getKeyChar(e), 10);
if(constants.RATING_NUMS.indexOf(num) != -1) {
console.log("number typed: " + num);
// TODO replace with the real table name and the real column name
// $___{<request>res:/tables/catalogItem</request>}
model.newValue("item_update_values", selectedId, {"Rating": num});
m.redraw.strategy("diff");
m.redraw();
} else if((e.keyCode && (e.keyCode === constants.ESCAPE_KEY))
|| e.type === "click") {
console.log("eventListener removed");
document.removeEventListener("click", keyHandler, false);
document.removeEventListener("keypress", keyHandler, false);
eventHandlerAdded = false;
unSelectRow();
}
};
// window seizes for adjusting lists, tables etc
vm.state = {
pageY : 0,
pageHeight : 400
};
vm.scrollWatchUpdateStateId = null;
obj.onScroll = function() {
return function(e) {
console.log("scroll event found");
vm.state.pageY = e.target.scrollTop;
m.redraw.strategy("none");
if (!vm.scrollWatchUpdateStateId) {
vm.scrollWatchUpdateStateId = setTimeout(function() {
// update pages
m.redraw();
vm.scrollWatchUpdateStateId = null;
}, 50);
}
}
};
// clean up on unload
obj.onunload = function() {
delete vm.state;
delete vm.scrollWatchUpdateStateId;
};
return obj;
};
list.view = function(ctrl) {
var pageY = ctrl.vm.state.pageY;
var pageHeight = ctrl.vm.state.pageHeight;
var begin = pageY / 41 | 0
// Add 2 so that the top and bottom of the page are filled with
// next/prev item, not just whitespace if item not in full view
var end = begin + (pageHeight / 41 | 0 + 2)
var offset = pageY % 41
var heightCalc = ctrl.items().length * 41;
var contextMenuSelection = m("div", {
id : ctrl.contextMenuId,
class : "hide"
}, [
m(".menu-item.allow-hover", {
onclick : ctrl.selectAll
}, "Select all"),
m(".menu-item.allow-hover", {
onclick : ctrl.deselectAll
}, "Deselect all"),
m(".menu-item.allow-hover", {
onclick : ctrl.invertSelection
}, "Invert selection") ]);
var header = m("table.listHeader", ctrl.sorts(ctrl.items()), m("tr", [
m("th.select_col[data-sort-by=selection]", {
oncontextmenu : ctrl.onContextMenu(ctrl.contextMenuId, "context-menu context-menu-bkg", "hide" )
}, "S"),
m("th.name_col[data-sort-by=FileName]", "Name"),
${ <request>
# add other column headers as configured
<identifier>active:jsPreprocess</identifier>
<argument name="id">list:table01:header</argument>
</request>
} ]), contextMenuSelection);
var table = m("table", ctrl.items().slice(begin, end).map(function(item, idx) {
return m("tr", ctrl.initRow(item, idx), {
key : item.GUID
},
[ m("td.select_col", [m("input[type=checkbox]", {
id : item.GUID,
checked : ctrl.isSelected(item.GUID),
onclick : function(e) {ctrl.toggleSelected(this.id);}
}) ]),
m("td.nameT_col", {
onmouseup: function(e) {ctrl.getSelected(e);}
}, item.FileName),
${ <request>
# add other columns as configured
<identifier>active:jsPreprocess</identifier>
<argument name="id">list:table01:row</argument>
</request>
} ])
}) );
var table_container = m("div[id=l04]",
{style: {position: "relative", top: pageY + "px"}}, table);
var scrollable = m("div[id=l03]",
{style: {height: heightCalc + "px", position: "relative",
top: -offset + "px"}}, table_container);
var scrollable_container = m("div.scrollableContainer[id=l02]",
{onscroll: ctrl.onScroll()}, scrollable );
var list = m("div[id=l01]", [header, scrollable_container]);
return list;
}
感谢评论!
到目前为止,我的应用程序有一个排序和可过滤列表以及一些输入和复选框。 如果列表中的项目超过 500 个,那么问题就会出现,然后每个带有用户输入的元素(复选框、输入字段、菜单)开始有大约半秒的滞后,随着列表中项目的数量增加。列表的排序和过滤速度足够快,但输入元素的滞后时间太长。
问题是:列表和输入元素如何解耦?
列表代码如下:
var list = {}
list.controller = function(args) {
var model = args.model;
var vm = args.vm;
var vmc = args.vmc;
var appCtrl = args.appCtrl;
this.items = vm.filteredList;
this.onContextMenu = vmc.onContextMenu;
this.isSelected = function(guid) {
return utils.getState(vm.listState, guid, "isSelected");
}
this.setSelected = function(guid) {
utils.setState(vm.listState, guid, "isSelected", true);
}
this.toggleSelected = function(guid) {
utils.toggleState(vm.listState, guid, "isSelected");
}
this.selectAll = function() {
utils.setStateBatch(vm.listState, "GUID", "isSelected", true, this.items());
}.bind(this);
this.deselectAll = function() {
utils.setStateBatch(vm.listState, "GUID", "isSelected", false, this.items());
}.bind(this);
this.invertSelection = function() {
utils.toggleStateBatch(vm.listState, "GUID", "isSelected", this.items());
}.bind(this);
this.id = "201505062224";
this.contextMenuId = "201505062225";
this.initRow = function(item, idx) {
if (item.online) {
return {
id : item.guid,
filePath : (item.FilePath + item.FileName).replace(/\/g, "\\"),
class : idx % 2 !== 0 ? "online odd" : "online even",
}
} else {
return {
class : idx % 2 !== 0 ? "odd" : "even"
}
}
};
// sort helper function
this.sorts = function(list) {
return {
onclick : function(e) {
var prop = e.target.getAttribute("data-sort-by")
//console.log("100")
if (prop) {
var first = list[0]
if(prop === "selection") {
list.sort(function(a, b) {
return this.isSelected(b.GUID) - this.isSelected(a.GUID)
}.bind(this));
} else {
list.sort(function(a, b) {
return a[prop] > b[prop] ? 1 : a[prop] < b[prop] ? -1 : 0
})
}
if (first === list[0])
list.reverse()
}
}.bind(this)
}
};
// text inside the table can be selected with the mouse and will be stored for
// later retrieval
this.getSelected = function() {
//console.log(utils.getSelText());
vmc.lastSelectedText(utils.getSelText());
};
};
list.view = function(ctrl) {
var contextMenuSelection = m("div", {
id : ctrl.contextMenuId,
class : "hide"
}, [
m(".menu-item.allow-hover", {
onclick : ctrl.selectAll
}, "Select all"),
m(".menu-item.allow-hover", {
onclick : ctrl.deselectAll
}, "Deselect all"),
m(".menu-item.allow-hover", {
onclick : ctrl.invertSelection
}, "Invert selection") ]);
var table = m("table", ctrl.sorts(ctrl.items()), [
m("tr", [
m("th[data-sort-by=selection]", {
oncontextmenu : ctrl.onContextMenu(ctrl.contextMenuId, "context-menu context-menu-bkg", "hide" )
}, "S"),
m("th[data-sort-by=FileName]", "Name"),
m("th[data-sort-by=FileSize]", "Size"),
m("th[data-sort-by=FilePath]", "Path"),
m("th[data-sort-by=MediumName]", "Media") ]),
ctrl.items().map(function(item, idx) {
return m("tr", ctrl.initRow(item, idx), {
key : item.GUID
},
[ m("td", [m("input[type=checkbox]", {
id : item.GUID,
checked : ctrl.isSelected(item.GUID),
onclick : function(e) {ctrl.toggleSelected(this.id);}
}) ]),
m("td", {
onmouseup: function(e) {ctrl.getSelected();}
}, item.FileName),
m("td", utils.numberWithDots(item.FileSize)),
m("td", item.FilePath),
m("td", item.MediumName) ])
}) ])
return m("div", [contextMenuSelection, table])
}
这是从应用程序主视图初始化列表和所有其他组件的方式:
// the main view which assembles all components
var mainCompView = function(ctrl, args) {
// TODO do we really need him there?
// add the main controller for this page to the arguments for all
// added components
var myArgs = args;
myArgs.appCtrl = ctrl;
// create all needed components
var filterComp = m.component(filter, myArgs);
var part_filter = m(".row", [ m(".col-md-2", [ filterComp ]) ]);
var listComp = m.component(list, myArgs);
var part_list = m(".col-md-10", [ listComp ]);
var optionsComp = m.component(options, myArgs);
var part_options = m(".col-md-10", [ optionsComp ]);
var menuComp = m.component(menu, myArgs);
var part_menu = m(".menu-0", [ menuComp ]);
var outputComp = m.component(output, myArgs);
var part_output = m(".col-md-10", [ outputComp ]);
var part1 = m("[id='1']", {
class : 'optionsContainer'
}, "", [ part_options ]);
var part2 = m("[id='2']", {
class : 'menuContainer'
}, "", [ part_menu ]);
var part3 = m("[id='3']", {
class : 'commandContainer'
}, "", [ part_filter ]);
var part4 = m("[id='4']", {
class : 'outputContainer'
}, "", [ part_output ]);
var part5 = m("[id='5']", {
class : 'listContainer'
}, "", [ part_list ]);
return [ part1, part2, part3, part4, part5 ];
}
// run
m.mount(document.body, m.component({
controller : MainCompCtrl,
view : mainCompView
}, {
model : modelMain,
vm : modelMain.getVM(),
vmc : viewModelCommon
}));
我开始通过向单击事件添加 m.redraw.strategy("none") 和 m.startComputation/endComputation 来解决问题,这解决了问题,但这是正确的解决方案吗?例如,如果我将来自第 3 方的 Mithril 组件与我的列表组件一起使用,我应该如何在不更改其代码的情况下对外部组件执行此操作?
另一方面,我的列表组件可以使用类似 'retain' 标志的东西吗?所以默认情况下列表不会重绘,除非它被告知这样做?但是第 3 方组件的问题也会持续存在。
我知道还有其他策略可以解决这个问题,例如列表分页,但我想知道 Mithril 方面的最佳实践是什么。
提前致谢, 斯特凡
文档中有一些关于何时更改重绘策略的好例子:http://mithril.js.org/mithril.redraw.html#changing-redraw-strategy
但一般来说,如果应用程序状态存储在某处以便 Mithril 可以在不触及 DOM 的情况下访问和计算差异,则很少使用更改重绘策略。看起来你的数据在别处,所以你的 sorts
方法在一定大小后会变得昂贵到 运行 吗?
您只能在修改列表的事件发生后对列表进行排序。否则它会在 Mithril 每次重绘时进行排序,这可能很常见。
m.start/endComputation
对于第 3 方代码很有用,尤其是在 DOM 上运行时。如果库存储了一些状态,您也应该将其用于应用程序状态,这样就不会有任何冗余和可能不匹配的数据。
感谢 Barney 的评论,我找到了一个解决方案:遮挡剔除。可以在此处找到原始示例 http://jsfiddle.net/7JNUy/1/ 。 我根据自己的需要调整了代码,尤其是需要限制触发的滚动事件,以便重绘次数足以平滑滚动。看函数obj.onScroll.
var list = {}
list.controller = function(args) {
var obj = {};
var model = args.model;
var vm = args.vm;
var vmc = args.vmc;
var appCtrl = args.appCtrl;
obj.vm = vm;
obj.items = vm.filteredList;
obj.onContextMenu = vmc.onContextMenu;
obj.isSelected = function(guid) {
return utils.getState(vm.listState, guid, "isSelected");
}
obj.setSelected = function(guid) {
utils.setState(vm.listState, guid, "isSelected", true);
}
obj.toggleSelected = function(guid) {
utils.toggleState(vm.listState, guid, "isSelected");
m.redraw.strategy("none");
}
obj.selectAll = function() {
utils.setStateBatch(vm.listState, "GUID", "isSelected", true, obj.items());
};
obj.deselectAll = function() {
utils.setStateBatch(vm.listState, "GUID", "isSelected", false, obj.items());
};
obj.invertSelection = function() {
utils.toggleStateBatch(vm.listState, "GUID", "isSelected", obj.items());
};
obj.id = "201505062224";
obj.contextMenuId = "201505062225";
obj.initRow = function(item, idx) {
if (item.online) {
return {
id : item.GUID,
filePath : (item.FilePath + item.FileName).replace(/\/g, "\\"),
class : idx % 2 !== 0 ? "online odd" : "online even",
onclick: console.log(item.GUID)
}
} else {
return {
id : item.GUID,
// class : idx % 2 !== 0 ? "odd" : "even",
onclick: function(e) { obj.selectRow(e, this, item.GUID);
m.redraw.strategy("none");
e.stopPropagation();
}
}
}
};
// sort helper function
obj.sorts = function(list) {
return {
onclick : function(e) {
var prop = e.target.getAttribute("data-sort-by")
// console.log("100")
if (prop) {
var first = list[0]
if(prop === "selection") {
list.sort(function(a, b) {
return obj.isSelected(b.GUID) - obj.isSelected(a.GUID)
});
} else {
list.sort(function(a, b) {
return a[prop] > b[prop] ? 1 : a[prop] < b[prop] ? -1 : 0
})
}
if (first === list[0])
list.reverse()
} else {
e.stopPropagation();
m.redraw.strategy("none");
}
}
}
};
// text inside the table can be selected with the mouse and will be stored
// for
// later retrieval
obj.getSelected = function(e) {
// console.log("getSelected");
var sel = utils.getSelText();
if(sel.length != 0) {
vmc.lastSelectedText(utils.getSelText());
e.stopPropagation();
// console.log("1000");
}
m.redraw.strategy("none");
// console.log("1001");
};
var selectedRow, selectedId;
var eventHandlerAdded = false;
// Row callback; reset the previously selected row and select the new one
obj.selectRow = function (e, row, id) {
console.log("selectRow " + id);
unSelectRow();
selectedRow = row;
selectedId = id;
selectedRow.style.background = "#FDFF47";
if(!eventHandlerAdded) {
console.log("eventListener added");
document.addEventListener("click", keyHandler, false);
document.addEventListener("keypress", keyHandler, false);
eventHandlerAdded = true;
}
};
var unSelectRow = function () {
if (selectedRow !== undefined) {
selectedRow.removeAttribute("style");
selectedRow = undefined;
selectedId = undefined;
}
};
var keyHandler = function(e) {
var num = parseInt(utils.getKeyChar(e), 10);
if(constants.RATING_NUMS.indexOf(num) != -1) {
console.log("number typed: " + num);
// TODO replace with the real table name and the real column name
// $___{<request>res:/tables/catalogItem</request>}
model.newValue("item_update_values", selectedId, {"Rating": num});
m.redraw.strategy("diff");
m.redraw();
} else if((e.keyCode && (e.keyCode === constants.ESCAPE_KEY))
|| e.type === "click") {
console.log("eventListener removed");
document.removeEventListener("click", keyHandler, false);
document.removeEventListener("keypress", keyHandler, false);
eventHandlerAdded = false;
unSelectRow();
}
};
// window seizes for adjusting lists, tables etc
vm.state = {
pageY : 0,
pageHeight : 400
};
vm.scrollWatchUpdateStateId = null;
obj.onScroll = function() {
return function(e) {
console.log("scroll event found");
vm.state.pageY = e.target.scrollTop;
m.redraw.strategy("none");
if (!vm.scrollWatchUpdateStateId) {
vm.scrollWatchUpdateStateId = setTimeout(function() {
// update pages
m.redraw();
vm.scrollWatchUpdateStateId = null;
}, 50);
}
}
};
// clean up on unload
obj.onunload = function() {
delete vm.state;
delete vm.scrollWatchUpdateStateId;
};
return obj;
};
list.view = function(ctrl) {
var pageY = ctrl.vm.state.pageY;
var pageHeight = ctrl.vm.state.pageHeight;
var begin = pageY / 41 | 0
// Add 2 so that the top and bottom of the page are filled with
// next/prev item, not just whitespace if item not in full view
var end = begin + (pageHeight / 41 | 0 + 2)
var offset = pageY % 41
var heightCalc = ctrl.items().length * 41;
var contextMenuSelection = m("div", {
id : ctrl.contextMenuId,
class : "hide"
}, [
m(".menu-item.allow-hover", {
onclick : ctrl.selectAll
}, "Select all"),
m(".menu-item.allow-hover", {
onclick : ctrl.deselectAll
}, "Deselect all"),
m(".menu-item.allow-hover", {
onclick : ctrl.invertSelection
}, "Invert selection") ]);
var header = m("table.listHeader", ctrl.sorts(ctrl.items()), m("tr", [
m("th.select_col[data-sort-by=selection]", {
oncontextmenu : ctrl.onContextMenu(ctrl.contextMenuId, "context-menu context-menu-bkg", "hide" )
}, "S"),
m("th.name_col[data-sort-by=FileName]", "Name"),
${ <request>
# add other column headers as configured
<identifier>active:jsPreprocess</identifier>
<argument name="id">list:table01:header</argument>
</request>
} ]), contextMenuSelection);
var table = m("table", ctrl.items().slice(begin, end).map(function(item, idx) {
return m("tr", ctrl.initRow(item, idx), {
key : item.GUID
},
[ m("td.select_col", [m("input[type=checkbox]", {
id : item.GUID,
checked : ctrl.isSelected(item.GUID),
onclick : function(e) {ctrl.toggleSelected(this.id);}
}) ]),
m("td.nameT_col", {
onmouseup: function(e) {ctrl.getSelected(e);}
}, item.FileName),
${ <request>
# add other columns as configured
<identifier>active:jsPreprocess</identifier>
<argument name="id">list:table01:row</argument>
</request>
} ])
}) );
var table_container = m("div[id=l04]",
{style: {position: "relative", top: pageY + "px"}}, table);
var scrollable = m("div[id=l03]",
{style: {height: heightCalc + "px", position: "relative",
top: -offset + "px"}}, table_container);
var scrollable_container = m("div.scrollableContainer[id=l02]",
{onscroll: ctrl.onScroll()}, scrollable );
var list = m("div[id=l01]", [header, scrollable_container]);
return list;
}
感谢评论!