I am making a custom datetime filter, because the datetime filter of Ext is not very nice. I have a datetime field: a datefield and timefield in a fieldcontainer that has value... it works fine.
I extend the Ext.ux.grid.filter.Filter and build my filter like the date filter.
fieldCfg = Ext.apply(me.fieldOpts, {
xtype: 'datetimefield',
overflowX: 'visible',
format: me.format,
});
fields = {};
for (i = 0, len = me.menuItems.length; i < len; i++) {
item = me.menuItems[i];
if (item !== '-') {
cfg = {
itemId: 'range-' + item,
text: me[item + 'Text'],
menu: Ext.create('Ext.menu.Menu', {
plain: true,
items: [
Ext.apply(fieldCfg, {
itemId: item,
margin: '2 10 2 2',
overflowX: 'visible'
})
],
}),
listeners: {
scope: me,
checkchange: me.onCheckChange,
render: function (t){
var dtf = t.menu.items.items[0];
var df = dtf.getDateField();
var tf = dtf.getTimeField();
df.on("select", me.selectDateEvent, me)
tf.on("select", me.selectDateEvent, me)
},
}
};
item = me.fields[item] = Ext.create('Ext.menu.CheckItem', cfg);
}
me.menu.add(item);
}
me.values = {};
Here is the function to make value of filter:
selectDateEvent: function (t) {
if(t.ownerCt.getValue() !== undefined){
var date = Ext.Date.format(t.ownerCt.getValue(), format);
this.values[t.ownerCt.itemId] = date;
this.fireEvent('update', this);
this.onMenuSelect(t.ownerCt, date);
}
},
Here's the code of the other handlers:
onCheckChange: function(item, checked) {
var me = this,
picker = item.menu.items.items[0],
itemId = picker.itemId,
values = me.values;
if (checked) {
values[itemId] = picker.getValue();
} else {
delete values[itemId]
}
me.setActive(me.isActivatable());
me.fireEvent('update', me);
}
onMenuSelect: function(picker, date) {
var fields = this.fields,
field = this.fields[picker.itemId];
field.setChecked(true);
if (field == fields.on) {
fields.before.setChecked(false, true);
fields.after.setChecked(false, true);
} else {
fields.on.setChecked(false, true);
if (field == fields.after && this.getFieldValue('before') < date) {
fields.before.setChecked(false, true);
} else if (field == fields.before && this.getFieldValue('after') > date) {
fields.after.setChecked(false, true);
}
}
this.fireEvent('update', this);
picker.up('menu').hide();
}
But it doesn't filter the data of grid. How can I make it to work? What did I wrong?
If your store is filtered on the client side, you need to implement the validateRecord method.
Related
Using a script to sync google sheets with google calendar. It was originally posted on GitHub by DavePar - the original can be seen here - GitHub project
Changed a couple of titles and it works fine.
However, added a column title Stand and getting the following error message. When tried to run the script now
TypeError: Cannot find function getStand in object CalendarEvent.
Have looked through several times and can't see where the problem lies. Can anyone have a look though and point me in the right direction, please?
Here is the slightly modified version:
// Script to synchronize a calendar to a spreadsheet and vice versa.
//
// See https://github.com/Davepar/gcalendarsync for instructions on setting this up.
//
// Set this value to match your calendar!!!
// Calendar ID can be found in the "Calendar Address" section of the Calendar Settings.
var calendarId = 'dtsmike#googlemail.com';
// Set the beginning and end dates that should be synced. beginDate can be set to Date() to use
// today. The numbers are year, month, date, where month is 0 for Jan through 11 for Dec.
var beginDate = new Date(1970, 0, 1); // Default to Jan 1, 1970
var endDate = new Date(2500, 0, 1); // Default to Jan 1, 2500
// Date format to use in the spreadsheet.
var dateFormat = 'd/m/yyyy H:mm';
var titleRowMap = {
'title': 'Title',
'stand': 'Stand',
'description': 'Kit Prep',
'location': 'Location',
'starttime': 'Install',
'endtime': 'Show Finish',
'guests': 'Guests',
'color': 'Color',
'id': 'Id'
};
var titleRowKeys = ['title', 'stand', 'description', 'location', 'starttime', 'endtime', 'guests', 'color', 'id'];
var requiredFields = ['id', 'title', 'starttime', 'endtime'];
// This controls whether email invites are sent to guests when the event is created in the
// calendar. Note that any changes to the event will cause email invites to be resent.
var SEND_EMAIL_INVITES = false;
// Setting this to true will silently skip rows that have a blank start and end time
// instead of popping up an error dialog.
var SKIP_BLANK_ROWS = true;
// Updating too many events in a short time period triggers an error. These values
// were tested for updating 40 events. Modify these values if you're still seeing errors.
var THROTTLE_THRESHOLD = 10;
var THROTTLE_SLEEP_TIME = 75;
// Adds the custom menu to the active spreadsheet.
function onOpen() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var menuEntries = [
{
name: "Update from Calendar",
functionName: "syncFromCalendar"
}, {
name: "Update to Calendar",
functionName: "syncToCalendar"
}
];
spreadsheet.addMenu('Calendar Sync', menuEntries);
}
// Creates a mapping array between spreadsheet column and event field name
function createIdxMap(row) {
var idxMap = [];
for (var idx = 0; idx < row.length; idx++) {
var fieldFromHdr = row[idx];
for (var titleKey in titleRowMap) {
if (titleRowMap[titleKey] == fieldFromHdr) {
idxMap.push(titleKey);
break;
}
}
if (idxMap.length <= idx) {
// Header field not in map, so add null
idxMap.push(null);
}
}
return idxMap;
}
// Converts a spreadsheet row into an object containing event-related fields
function reformatEvent(row, idxMap, keysToAdd) {
var reformatted = row.reduce(function(event, value, idx) {
if (idxMap[idx] != null) {
event[idxMap[idx]] = value;
}
return event;
}, {});
for (var k in keysToAdd) {
reformatted[keysToAdd[k]] = '';
}
return reformatted;
}
// Converts a calendar event to a psuedo-sheet event.
function convertCalEvent(calEvent) {
convertedEvent = {
'id': calEvent.getId(),
'title': calEvent.getTitle(),
'stand': calEvent.getStand(),
'description': calEvent.getDescription(),
'location': calEvent.getLocation(),
'guests': calEvent.getGuestList().map(function(x) {return x.getEmail();}).join(','),
'color': calEvent.getColor()
};
if (calEvent.isAllDayEvent()) {
convertedEvent.starttime = calEvent.getAllDayStartDate();
var endtime = calEvent.getAllDayEndDate();
if (endtime - convertedEvent.starttime === 24 * 3600 * 1000) {
convertedEvent.endtime = '';
} else {
convertedEvent.endtime = endtime;
if (endtime.getHours() === 0 && endtime.getMinutes() == 0) {
convertedEvent.endtime.setSeconds(endtime.getSeconds() - 1);
}
}
} else {
convertedEvent.starttime = calEvent.getStartTime();
convertedEvent.endtime = calEvent.getEndTime();
}
return convertedEvent;
}
// Converts calendar event into spreadsheet data row
function calEventToSheet(calEvent, idxMap, dataRow) {
convertedEvent = convertCalEvent(calEvent);
for (var idx = 0; idx < idxMap.length; idx++) {
if (idxMap[idx] !== null) {
dataRow[idx] = convertedEvent[idxMap[idx]];
}
}
}
// Returns empty string or time in milliseconds for Date object
function getEndTime(ev) {
return ev.endtime === '' ? '' : ev.endtime.getTime();
}
// Tests whether calendar event matches spreadsheet event
function eventMatches(cev, sev) {
var convertedCalEvent = convertCalEvent(cev);
return convertedCalEvent.title == sev.title &&
convertedCalEvent.description == sev.description &&
convertedCalEvent.stand == sev.stand &&
convertedCalEvent.location == sev.location &&
convertedCalEvent.starttime.toString() == sev.starttime.toString() &&
getEndTime(convertedCalEvent) === getEndTime(sev) &&
convertedCalEvent.guests == sev.guests &&
convertedCalEvent.color == ('' + sev.color);
}
// Determine whether required fields are missing
function areRequiredFieldsMissing(idxMap) {
return requiredFields.some(function(val) {
return idxMap.indexOf(val) < 0;
});
}
// Returns list of fields that aren't in spreadsheet
function missingFields(idxMap) {
return titleRowKeys.filter(function(val) {
return idxMap.indexOf(val) < 0;
});
}
// Set up formats and hide ID column for empty spreadsheet
function setUpSheet(sheet, fieldKeys) {
sheet.getRange(1, fieldKeys.indexOf('starttime') + 1, 999).setNumberFormat(dateFormat);
sheet.getRange(1, fieldKeys.indexOf('endtime') + 1, 999).setNumberFormat(dateFormat);
sheet.hideColumns(fieldKeys.indexOf('id') + 1);
}
// Display error alert
function errorAlert(msg, evt, ridx) {
var ui = SpreadsheetApp.getUi();
if (evt) {
ui.alert('Skipping row: ' + msg + ' in event "' + evt.title + '", row ' + (ridx + 1));
} else {
ui.alert(msg);
}
}
// Updates a calendar event from a sheet event.
function updateEvent(calEvent, sheetEvent){
sheetEvent.sendInvites = SEND_EMAIL_INVITES;
if (sheetEvent.endtime === '') {
calEvent.setAllDayDate(sheetEvent.starttime);
} else {
calEvent.setTime(sheetEvent.starttime, sheetEvent.endtime);
}
calEvent.setTitle(sheetEvent.title);
calEvent.setDescription(sheetEvent.description);
calEvent.setStand(sheetEvent.stand);
calEvent.setLocation(sheetEvent.location);
// Set event color
if (sheetEvent.color > 0 && sheetEvent.color < 12) {
calEvent.setColor('' + sheetEvent.color);
}
var guestCal = calEvent.getGuestList().map(function (x) {
return {
email: x.getEmail(),
added: false
};
});
var sheetGuests = sheetEvent.guests || '';
var guests = sheetGuests.split(',').map(function (x) {
return x ? x.trim() : '';
});
// Check guests that are already invited.
for (var gIx = 0; gIx < guestCal.length; gIx++) {
var index = guests.indexOf(guestCal[gIx].email);
if (index >= 0) {
guestCal[gIx].added = true;
guests.splice(index, 1);
}
}
guests.forEach(function (x) {
if (x) calEvent.addGuest(x);
});
guestCal.forEach(function (x) {
if (!x.added) {
calEvent.removeGuest(x.email);
}
});
}
// Synchronize from calendar to spreadsheet.
function syncFromCalendar() {
// Get calendar and events
var calendar = CalendarApp.getCalendarById(calendarId);
var calEvents = calendar.getEvents(beginDate, endDate);
// Get spreadsheet and data
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getActiveSheet();
var range = sheet.getDataRange();
var data = range.getValues();
var eventFound = new Array(data.length);
// Check if spreadsheet is empty and add a title row
var titleRow = [];
for (var idx = 0; idx < titleRowKeys.length; idx++) {
titleRow.push(titleRowMap[titleRowKeys[idx]]);
}
if (data.length < 1) {
data.push(titleRow);
range = sheet.getRange(1, 1, data.length, data[0].length);
range.setValues(data);
setUpSheet(sheet, titleRowKeys);
}
if (data.length == 1 && data[0].length == 1 && data[0][0] === '') {
data[0] = titleRow;
range = sheet.getRange(1, 1, data.length, data[0].length);
range.setValues(data);
setUpSheet(sheet, titleRowKeys);
}
// Map spreadsheet headers to indices
var idxMap = createIdxMap(data[0]);
var idIdx = idxMap.indexOf('id');
// Verify header has all required fields
if (areRequiredFieldsMissing(idxMap)) {
var reqFieldNames = requiredFields.map(function(x) {return titleRowMap[x];}).join(', ');
errorAlert('Spreadsheet must have ' + reqFieldNames + ' columns');
return;
}
// Array of IDs in the spreadsheet
var sheetEventIds = data.slice(1).map(function(row) {return row[idIdx];});
// Loop through calendar events
for (var cidx = 0; cidx < calEvents.length; cidx++) {
var calEvent = calEvents[cidx];
var calEventId = calEvent.getId();
var ridx = sheetEventIds.indexOf(calEventId) + 1;
if (ridx < 1) {
// Event not found, create it
ridx = data.length;
var newRow = [];
var rowSize = idxMap.length;
while (rowSize--) newRow.push('');
data.push(newRow);
} else {
eventFound[ridx] = true;
}
// Update event in spreadsheet data
calEventToSheet(calEvent, idxMap, data[ridx]);
}
// Remove any data rows not found in the calendar
var rowsDeleted = 0;
for (var idx = eventFound.length - 1; idx > 0; idx--) {
//event doesn't exists and has an event id
if (!eventFound[idx] && sheetEventIds[idx - 1]) {
data.splice(idx, 1);
rowsDeleted++;
}
}
// Save spreadsheet changes
range = sheet.getRange(1, 1, data.length, data[0].length);
range.setValues(data);
if (rowsDeleted > 0) {
sheet.deleteRows(data.length + 1, rowsDeleted);
}
}
// Synchronize from spreadsheet to calendar.
function syncToCalendar() {
// Get calendar and events
var calendar = CalendarApp.getCalendarById(calendarId);
if (!calendar) {
errorAlert('Cannot find calendar. Check instructions for set up.');
}
var calEvents = calendar.getEvents(beginDate, endDate);
var calEventIds = calEvents.map(function(val) {return val.getId();});
// Get spreadsheet and data
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getActiveSheet();
var range = sheet.getDataRange();
var data = range.getValues();
if (data.length < 2) {
errorAlert('Spreadsheet must have a title row and at least one data row');
return;
}
// Map headers to indices
var idxMap = createIdxMap(data[0]);
var idIdx = idxMap.indexOf('id');
var idRange = range.offset(0, idIdx, data.length, 1);
var idData = idRange.getValues()
// Verify header has all required fields
if (areRequiredFieldsMissing(idxMap)) {
var reqFieldNames = requiredFields.map(function(x) {return titleRowMap[x];}).join(', ');
errorAlert('Spreadsheet must have ' + reqFieldNames + ' columns');
return;
}
var keysToAdd = missingFields(idxMap);
// Loop through spreadsheet rows
var numChanges = 0;
var numUpdated = 0;
var changesMade = false;
for (var ridx = 1; ridx < data.length; ridx++) {
var sheetEvent = reformatEvent(data[ridx], idxMap, keysToAdd);
// If enabled, skip rows with blank/invalid start and end times
if (SKIP_BLANK_ROWS && !(sheetEvent.starttime instanceof Date) &&
!(sheetEvent.endtime instanceof Date)) {
continue;
}
// Do some error checking first
if (!sheetEvent.title) {
errorAlert('must have title', sheetEvent, ridx);
continue;
}
if (!(sheetEvent.starttime instanceof Date)) {
errorAlert('start time must be a date/time', sheetEvent, ridx);
continue;
}
if (sheetEvent.endtime !== '') {
if (!(sheetEvent.endtime instanceof Date)) {
errorAlert('end time must be empty or a date/time', sheetEvent, ridx);
continue;
}
if (sheetEvent.endtime < sheetEvent.starttime) {
errorAlert('end time must be after start time for event', sheetEvent, ridx);
continue;
}
}
// Ignore events outside of the begin/end range desired.
if (sheetEvent.starttime > endDate) {
continue;
}
if (sheetEvent.endtime === '') {
if (sheetEvent.starttime < beginDate) {
continue;
}
} else {
if (sheetEvent.endtime < beginDate) {
continue;
}
}
// Determine if spreadsheet event is already in calendar and matches
var addEvent = true;
if (sheetEvent.id) {
var eventIdx = calEventIds.indexOf(sheetEvent.id);
if (eventIdx >= 0) {
calEventIds[eventIdx] = null; // Prevents removing event below
addEvent = false;
var calEvent = calEvents[eventIdx];
if (!eventMatches(calEvent, sheetEvent)) {
// Update the event
updateEvent(calEvent, sheetEvent);
// Maybe throttle updates.
numChanges++;
if (numChanges > THROTTLE_THRESHOLD) {
Utilities.sleep(THROTTLE_SLEEP_TIME);
}
}
}
}
if (addEvent) {
var newEvent;
sheetEvent.sendInvites = SEND_EMAIL_INVITES;
if (sheetEvent.endtime === '') {
newEvent = calendar.createAllDayEvent(sheetEvent.title, sheetEvent.starttime, sheetEvent);
} else {
newEvent = calendar.createEvent(sheetEvent.title, sheetEvent.starttime, sheetEvent.endtime, sheetEvent);
}
// Put event ID back into spreadsheet
idData[ridx][0] = newEvent.getId();
changesMade = true;
// Set event color
if (sheetEvent.color > 0 && sheetEvent.color < 12) {
newEvent.setColor('' + sheetEvent.color);
}
// Maybe throttle updates.
numChanges++;
if (numChanges > THROTTLE_THRESHOLD) {
Utilities.sleep(THROTTLE_SLEEP_TIME);
}
}
}
// Save spreadsheet changes
if (changesMade) {
idRange.setValues(idData);
}
// Remove any calendar events not found in the spreadsheet
var numToRemove = calEventIds.reduce(function(prevVal, curVal) {
if (curVal !== null) {
prevVal++;
}
return prevVal;
}, 0);
if (numToRemove > 0) {
var ui = SpreadsheetApp.getUi();
var response = ui.Button.YES;
if (numToRemove > numUpdated) {
response = ui.alert('Delete ' + numToRemove + ' calendar event(s) not found in spreadsheet?',
ui.ButtonSet.YES_NO);
}
if (response == ui.Button.YES) {
calEventIds.forEach(function(id, idx) {
if (id != null) {
calEvents[idx].deleteEvent();
Utilities.sleep(20);
}
});
}
}
Logger.log('Updated %s calendar events', numChanges);
}
// Set up a trigger to automatically update the calendar when the spreadsheet is
// modified. See the instructions for how to use this.
function createSpreadsheetEditTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('syncToCalendar')
.forSpreadsheet(ss)
.onEdit()
.create();
}
// Delete the trigger. Use this to stop automatically updating the calendar.
function deleteTrigger() {
// Loop over all triggers.
var allTriggers = ScriptApp.getProjectTriggers();
for (var idx = 0; idx < allTriggers.length; idx++) {
if (allTriggers[idx].getHandlerFunction() === 'syncToCalendar') {
ScriptApp.deleteTrigger(allTriggers[idx]);
}
}
}
Please check the following image:
Right now we have this array of objects defined in the schema as follows:
`destinationCountry: {
type: Array,
label: () => {return TAPi18n.__('destinationCountry.label')},
minCount:1,
maxCount:5,
},
'destinationCountry.$': {
type: Object,
label:()=>{ return " "},
},
'destinationCountry.$.country':{
type:String,
autoform:{
type: "select2",
'data-placeholder': () => {return TAPi18n.__('destinationCountry.placeholder')},
options: () => {return TAPi18n.__('countryList', {returnObjectTrees:true, joinArrays:false})},
afFieldInput: {
multiple: false,
width: 'resolve',
},
afQuickField: {
'panelClass': 'col'
},
}
},
'destinationCountry.$.days':{
type:String,
autoform:{
type:"select",
options:function (){
// here is where the suggested code would go
},
}
},`
Now, is there any way to make it so that the options in days are reactive to the ones selected in the previous array index object for $.days? We would like so that if the maximum of days in all the $.days fields is 20, as soon as you select 13 in one of them, there will only be 7 available for all the other $.days select fields. How could we achieve this reactively?
update 1:
I tried the following:
'destinationCountry.$.days': {
type: String,
autoform: {
type: "select",
options: function () {
let arr = [...Array(days.get())].map((v, i) => i);
let finalArr = [{
label: "Connection Flight",
value: 0
}]
arr.map((v, i) => {
finalArr.push({
label: (v + 1).toString(),
value: i + 1
})
});
if (this.name == 'destinationCountry.0.days') {
var usedDays = 0;
var usedArr;
if (AutoForm.getFormValues('step2') == null) {
usedArr = []
console.log("usedArr", usedArr)
} else {
usedArr = AutoForm.getFormValues('step2').insertDoc.destinationCountry
console.log("usedArr", usedArr)
}
for (let index = 0; index < usedArr.length; index++) {
const element = Number(usedArr[index].days);
usedDays += element
}
console.log("finalArr", finalArr)
if (usedDays == 0) {
console.log('usedDays', usedDays)
return finalArr
} else if (usedDays > 0) {
newFinalArr = finalArr.slice(0, "-" + (usedDays));
return newFinalArr
}
}
if (this.name == 'destinationCountry.1.days') {
var usedDays = 0;
var usedArr;
if (AutoForm.getFormValues('step2') == null) {
usedArr = []
console.log("usedArr", usedArr)
} else {
usedArr = AutoForm.getFormValues('step2').insertDoc.destinationCountry
console.log("usedArr", usedArr)
}
for (let index = 0; index < usedArr.length; index++) {
const element = Number(usedArr[index].days);
usedDays += element
}
console.log("finalArr", finalArr)
if (usedDays == 0) {
console.log('usedDays', usedDays)
return finalArr
} else if (usedDays > 0) {
newFinalArr = finalArr.slice(0, "-" + (usedDays));
return newFinalArr
}
}
},
}
},
First days option array gets erased when adding the second array object...
Now every time you select an option in the first destinationCountry.0.days, the 1.days select gets correctly reduced depending on the number you select in the 0.days field. but it re-renders and removes the option selected in the first 0.days. is there any way to set and keep the value selected for the first 0.days field?
My app has a sort- and filterable list and a few inputs and checkboxes so far.
The problem appears if the list has more than 500 items, then every every element with user input (checkboxes, input fields, menus) start to have a lag around half a second increasing with the number of items in the list. The sorting and filtering of the list is done fast enough but the lag on the input elements is too long.
The question is: how can the list and the input elements be decoupled?
Here is the list code:
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])
}
And this is how the list and all other components are initialized from the apps main view:
// 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
}));
I started to workaround the problem by adding m.redraw.strategy("none") and m.startComputation/endComputation to click events and this solves the problem but is this the right solution? As an example, if I use a Mithril component from a 3rd party together with my list component, how should I do this for the foreign component without changing its code?
On the other side, could my list component use something like the 'retain' flag? So the list doesn't redraw by default unless it's told to do? But also the problem with a 3rd party component would persist.
I know there are other strategies to solve this problem like pagination for the list but I would like to know what are best practices from the Mithril side.
Thanks in advance,
Stefan
Thanks to the comment from Barney I found a solution: Occlusion culling. The original example can be found here http://jsfiddle.net/7JNUy/1/ .
I adapted the code for my needs, especially there was the need to throttle the scroll events fired so the number of redraws are good enough for smooth scrolling. Look at the function 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;
}
Thanks for the comments!
There are some good examples of when to change redraw strategy in the docs: http://mithril.js.org/mithril.redraw.html#changing-redraw-strategy
But in general, changing redraw strategy is rarely used if the application state is stored somewhere so Mithril can access and calculate the diff without touching DOM. It seems like your data is elsewhere, so could it be that your sorts method is getting expensive to run after a certain size?
You could sort the list only after events that modifies it. Otherwise it will be sorted on every redraw Mithril does, which can be quite often.
m.start/endComputation is useful for 3rd party code, especially if it operates on DOM. If the library stores some state, you should use that for the application state as well, so there aren't any redundant and possibly mismatching data.
Although I have been searching high and low to find the answer to this question, I have not been able to find it. Should there be a well-hidden corner of the internet that I have not searched that does contain the answer to this question, please let me know.
Anyway, on the Dynatable website there is an example to query a table for a certain value. If your input html is indeed a table, this seems to work, but if you're using a stylized listthis produces the following error in my case:
Uncaught TypeError: Cannot read property 'length' of null
Which traces back to line 1582 of the dynatable.jquery.js file:
// Find an object in an array of objects by attributes.
// E.g. find object with {id: 'hi', name: 'there'} in an array of objects
findObjectInArray: function(array, objectAttr) {
var _this = this,
foundObject;
for (var i = 0, len = array.length; i < len; i++) {
var item = array[i];
// For each object in array, test to make sure all attributes in objectAttr match
if (_this.allMatch(item, objectAttr, function(item, key, value) { return item[key] == value; })) {
foundObject = item;
break;
}
}
The following it my select element:
<select id="filter-prop" name="prop">
<option>All</option>
<option>First Run</option>
<option>Rejected</option>
</select>
And the dynatable conversion of my list:
// Function that renders the list items from our records
function ulWriter(rowIndex, record, columns, cellWriter) {
var cssClass = record.prop, li;
if (rowIndex % 3 === 0) { cssClass += ' first'; }
li = '<li class="' + cssClass + '"><div class="thumbnail"><div class="thumbnail-image">' + record.thumbnail + '</div><div class="caption">' + record.caption + '</div></div></li>';
return li;
}
// Function that creates our records from the DOM when the page is loaded
function ulReader(index, li, record) {
var $li = $(li),
$caption = $li.find('.caption');
record.thumbnail = $li.find('.thumbnail-image').html();
record.caption = $caption.html();
record.label = $caption.find('h3').text();
record.description = $caption.find('p').text();
record.color = $li.data('color');
record.prop = $li.attr('class');
}
var pictable = $('#picturelist').dynatable({
table: {
bodyRowSelector: 'li'
},
features :{
recordCount: false,
sorting: false,
},
dataset: {
perPageDefault: 10,
perPageOptions: [5, 10, 15, 20]
},
writers: {
_rowWriter: ulWriter
},
readers: {
_rowReader: ulReader
},
params: {
records: 'objects'
}
}).data('dynatable');
$('#filter-prop').change( function() {
var value = $(this).val();
if (value === "All") {
pictable.queries.remove("Prop");
} else if (value === "First Run") {
pictable.queries.add("Prop","span4 firstrun rejected");
pictable.queries.add("Prop","span4 firstrun");
} else if (value === "Rejected") {
pictable.queries.add("Prop","span4 firstrun rejected");
pictable.queries.add("Prop","span4 rejected");
};
pictable.process();
});
I have not changed much from the examples of the website. The most I did was merge the list example with the query example. Again, if I apply a similar method to a table, it seems to work, but for my list it doesn't. What is going wrong?
The docs weren't very clear here, but I solved the same problem for myself earlier today. Here is your code in working order:
var pictable = $('#picturelist')
.bind('dynatable:init', function(e, dynatable) {
dynatable.queries.functions['prop'] = function(record, queryValue) {
if ( record.prop == queryValue )
{
return true;
}
else
{
return false;
}
};
})
.dynatable({
table: {
bodyRowSelector: 'li'
},
features :{
recordCount: false,
sorting: false
},
dataset: {
perPageDefault: 10,
perPageOptions: [5, 10, 15, 20]
},
writers: {
_rowWriter: ulWriter
},
readers: {
_rowReader: ulReader
},
params: {
records: 'objects'
}
}).data('dynatable');
$('#filter-prop').change( function() {
var value = $(this).val();
if (value === "All")
{
pictable.queries.remove("prop");
}
else
{
pictable.queries.add("prop", value)
}
pictable.process();
});
Cheers
Following code example provided by Telerik I am trying to preserve Kendo Grid state. Everything seems to be working but whenever I use date column filter and reload page the date in filter is decreased by one day... I pick 5th Oct., refresh and BAM! filter says 4th Oct. :) Any clues?
$(document).ready(function () {
var grid = $("#ProjectsGrid").data("kendoGrid");
var state = JSON.parse($.cookie("projectsState"));
if (state) {
if (state.filter) {
parseFilterDates(state.filter, grid.dataSource.options.schema.model.fields);
}
grid.dataSource.query(state);
}
else {
grid.dataSource.read();
}
});
function dataBound(e) {
var grid = this;
var dataSource = this.dataSource;
var state = kendo.stringify({
page: dataSource.page(),
pageSize: dataSource.pageSize(),
sort: dataSource.sort(),
group: dataSource.group(),
filter: dataSource.filter()
});
$.cookie("projectsState", state);
if ($.cookie('empRows')) {
$.each(JSON.parse($.cookie('empRows')), function() {
var item = dataSource.get(this);
var row = grid.tbody.find('[data-uid=' + item.uid + ']');
row.addClass('k-state-selected');
})
}
}
function parseFilterDates(filter, fields) {
if (filter.filters) {
for (var i = 0; i < filter.filters.length; i++) {
parseFilterDates(filter.filters[i], fields);
}
}
else {
if (fields[filter.field].type == "date") {
filter.value = kendo.parseDate(filter.value);
}
}
}