"use strict";
// ********************************************************************
/** The main widget for interfacing with the database, this displays
* a grid of cards or a table of database rows.
*
* To use this widget, create an instance of it under the temporary
* global object G, call paferalist.Display() to setup the controls,
* and then paferalist.List() to display the contents from the database.
*
* For a nicer looking display, you should override
* paferalist.OnRenderItem() to suit your own purposes.
*
* For hooks in derived classes, look at OnList, OnListParams(),
* OnSaveData(), and other such functions.
*
* @param {object} config - With so many options, please see the source
* to see what settings are available. The most commonly used ones are
*
* <dl>
* <dt>apiurl</dt>
* <dd>The URL for the JSON API. It should support the standard
* Pafera search, save, and delete commands.</dd>
* <dt>div</dt>
* <dd>a CSS selector to display the controls in</dd>
* <dt>reference</dt>
* <dd>a global reference to this object such as G.listctrl.
* It is used for buttons and actions to refer to itself.</dd>
* <dt>displayfields</dt>
* <dd>A list of arrays containing the fields that are shown
* in each card and the table columns with format
* [fieldname, fieldtype, fieldtitle]</dd>
* <dt>editfields</dt>
* <dd>The fields to pass to P.EditPopup(), with the same format.
* If you need specific fields or values for each object, override
* paferalist.GetEditFields()</dd>
* <dt>limit</dt>
* <dd>The number of items per page to display</dd>
* <dt>cardstyles</dt>
* <dd>Extra CSS styles for the cards displayed</dd>
* <dt>tablestyles</dt>
* <dd>Extra CSS styles for the table element</dd>
* <dt>extrabuttons</dt>
* <dd>A string containing HTML controls which are shown at the
* control bar at the top of the widget.</dd>
* <dt>extraactions</dt>
* <dd>A list of [displayname, funcname, colornum]
* shown on every row or card. You should define a method with funcname on
* a derived object. It will be passed the card element and the dbid of
* the row, thus ['Edit', 'editfunc', 3] will result in
* paferalist.editfunc(cardelement, dbid) being called.
*
* Note that if you want to support actions on multiple selected
* objects, use the GetSelectedIDs() function instead of just using
* dbid.
* </dd>
* <dt>enableadd, enableedit, enablesearch, enableselect</dt>
* <dd>Set these to true to enable their various actions.</dd>
* </dl>
*/
class PaferaList
{
// ------------------------------------------------------------------
constructor(config)
{
this.apiurl = config.apiurl;
this.div = config.div;
this.reference = config.reference;
this.displayfields = config.displayfields;
this.editfields = config.editfields || config.displayfields;
this.extrabuttons = config.extrabuttons || '';
this.extraactions = config.extraactions || [];
this.extraparams = config.extraparams || {};
this.extrasaveparams = config.extrasaveparams || {};
this.liststyles = config.liststyles || 'FlexGrid16';
this.cardstyles = config.cardstyles || 'whiteb Rounded Margin25 Pad25 HoverHighlight';
this.tablestyles = config.tablestyles || 'Styled Width100P';
this.trstyles = config.trstyles || 'HoverHighlight';
this.colors = config.colors || ['red', 'green', 'blue', 'purple', 'brown', 'black'];
this.controlclasses = config.controlclasses || '';
this.title = config.title || '';
this.orderby = config.orderby || 0;
this.listonchangeselectors = config.listonchangeselectors || '';
this.usetable = config.usetable;
this.enableadd = config.enableadd;
this.enableedit = config.enableedit;
this.enabledelete = config.enabledelete;
this.enablesearch = config.enablesearch;
this.enableselect = config.enableselect;
this.enabletable = config.enabletable;
this.limitoptions = config.limitoptions || [20, 50, 100, 200, 500, 1000];
this.actions = {};
this.oncardclick = config.oncardclick;
this.saveandcontinue = config.saveandcontinue || 0;
this.headertext = config.headertext || '';
if (config.enableedit)
{
this.extraactions.push([T.edit, 'Edit', 3]);
}
if (config.enabledelete)
{
this.extraactions.push([T.delete, 'Delete', 1]);
}
this.items = [];
this.itemscount = 0;
this.laststart = 0;
this.limit = config.limit || 100;
if (this.div[0] == '.')
{
this.div = this.div.substr(1);
}
if (this.orderby)
{
let orderby = {};
for (let k in this.orderby)
{
let v = this.orderby[k];
orderby[k] = v + ' ⬆️';
orderby[k + ' DESC'] = v + ' ⬇️';
}
this.orderby = orderby;
}
}
// ------------------------------------------------------------------
/** Returns the edit fields for use in P.EditPopup(). If you want to
* have different fields for different objects, then override this
* function.
*
* @param {object} obj - The original object to edit
*/
GetEditFields(obj)
{
let self = this;
return self.editfields;
}
// ------------------------------------------------------------------
/** Returns the item in self.items that matches dbid.
*
* @param {value} dbid
*/
GetItemByID(dbid)
{
let self = this;
for (let r of self.items)
{
if (r.idcode == dbid)
{
return r;
}
}
return 0;
}
// ------------------------------------------------------------------
/** Returns the displayed name of an item, which by default is its
* first field. You may want to override this in a derived class
* if you want to make the name more meaningful.
*
* @param {object} obj
*/
GetItemDisplayName(obj)
{
let self = this;
let firstfield = self.displayfields[0];
if (firstfield[1] == 'translation')
{
return P.BestTranslation(obj[firstfield[0]])
}
return obj[firstfield[0]];
}
// ------------------------------------------------------------------
/** Returns a list of all of the currently selected objects.
*/
GetSelectedIDs()
{
let ids = [];
for (let r of Q('.' + self.div + ' .Selected'))
{
ids.push(r.dataset.dbid);
}
return ids;
}
// ------------------------------------------------------------------
/** Displays the widget inside the div specified at construction and
* setups all handlers.
*/
Display()
{
let self = this;
let numperpage = ['<select class="NumPerPage" name="NumPerPage">'];
for (let num of self.limitoptions)
{
numperpage.push(`<option ${num == self.limit ? 'selected' : ''}>${num}</option>`)
}
numperpage.push('</select>');
P.HTML(
'.' + self.div,
`<div class="ListControl ${self.controlclasses}" data-reference="${self.reference}">
${
self.title
? self.title
: ''
}
<div class="ButtonBar">
${numperpage}
${self.orderby
? P.ObjectToSelect(self.orderby, 'OrderBy', 'OrderBy')
: ''
}
${self.enablesearch
? `<input type="text" name="SearchText" class="SearchText">
<a class="Color6" onclick="${this.reference}.List()">${T.search}</a>`
: ''
}
${self.enableselect
? `<a class="Color2" onclick="${this.reference}.SelectAll()">${T.selectall}</a>
<a class="Color4" onclick="${this.reference}.SelectNone()">${T.selectnone}</a>`
: ''
}
${self.enabletable
? `<a class="Color5" onclick="${this.reference}.ToggleTable()">${T.listtable}</a>`
: ''
}
${self.enableadd
? `<a class="Color3" onclick="${this.reference}.Add()">${T.add}</a>`
: ''
}
${self.enabledelete
? `<a class="Color1 DeleteButton Disabled" onclick="${this.reference}.DeleteSelected()">${T.delete}</a>`
: ''
}
${self.extrabuttons
? self.extrabuttons
: ''
}
</div>
<div class="ItemsList"></div>
<div class="ItemsPageBar"></div>
</div>
`
);
P.DoneTyping(
'.' + self.div + ' .SearchText',
function(el)
{
self.List();
}
);
P.OnClick(
'.' + self.div + ' .ItemsList',
P.LBUTTON,
function (e)
{
let action = e.target.dataset.action;
let card = P.TargetClass(e, '.ItemCard');
let dbid = card ? card.dataset.dbid : '';
if (self[action])
{
self[action](card, dbid);
return;
}
if (!dbid)
{
return;
}
let selected = Q('.' + self.div + ' .Selected');
if (selected.length && card)
{
if (card.classList.contains('Selected'))
{
card.classList.remove('Selected');
} else
{
card.classList.add('Selected');
}
return;
}
selected = Q('.' + self.div + ' .Selected');
if (self.enabledelete)
{
if (selected.length)
{
E('.' + self.div + ' .DeleteButton').classList.remove('Disabled');
} else
{
E('.' + self.div + ' .DeleteButton').classList.add('Disabled');
}
}
self.OnItemClicked(card, dbid);
},
function (e)
{
// No default function for double clicks since it's not commonly
// used for mobile devices
},
function (e)
{
let card = P.TargetClass(e, '.ItemCard');
let dbid = card ? card.dataset.dbid : '';
if (!dbid)
return;
if (card)
{
let selected = Q('.' + self.div + ' .Selected');
if (selected.length)
{
let dbitems = Q('.' + self.div + ' .ItemCard');
let foundselected = 0;
for (let i = 0, l = dbitems.length; i < l; i++)
{
let dbitem = dbitems[i];
if (dbitem.dataset.dbid == dbid)
{
dbitem.classList.add('Selected');
break;
}
if (foundselected)
{
dbitem.classList.add('Selected');
} else if (dbitem.classList.contains('Selected'))
{
foundselected = 1;
}
}
foundselected = 0;
for (let i = dbitems.length - 1; i >= 0; i--)
{
let dbitem = dbitems[i];
if (dbitem.dataset.dbid == dbid)
{
dbitem.classList.add('Selected');
break;
}
if (foundselected)
{
dbitem.classList.add('Selected');
} else if (dbitem.classList.contains('Selected'))
{
foundselected = 1;
}
}
} else
{
if (card.classList.contains('Selected'))
{
card.classList.remove('Selected');
} else
{
card.classList.add('Selected');
}
}
selected = Q('.' + self.div + ' .Selected');
if (self.enabledelete)
{
if (selected.length)
{
E('.' + self.div + ' .DeleteButton').classList.remove('Disabled');
} else
{
E('.' + self.div + ' .DeleteButton').classList.add('Disabled');
}
}
}
}
);
if (self.listonchangeselectors)
{
P.On(
self.listonchangeselectors,
'change',
function()
{
self.List();
}
)
}
if (self.orderby)
{
P.On(
'.' + self.div + ' .OrderBy',
'change',
function()
{
self.List(0);
}
);
}
}
// ------------------------------------------------------------------
/** Called when the user clicks the finished button to signal that
* all changes have been made. Derived classes should override this
* for their own handling.
*/
OnFinished()
{
}
// ------------------------------------------------------------------
/** Called before a list request is made to the server. Derived
* classes can add their own data to be sent.
*/
OnListParams(params)
{
}
// ------------------------------------------------------------------
/** Calls the server to get the list of items and displays them.
*
* @param {int} start - Leave undefined to use the last position,
* otherwise, specifies the starting position to display.
*/
async List(start)
{
let self = this;
start = (start == undefined) ? self.laststart : start;
self.laststart = start;
let searchel = E('.' + self.div + ' .SearchText');
let searchterm = searchel ? searchel.value : '';
let orderbyel = E('.' + self.div + ' .OrderBy');
let orderby = orderbyel ? orderbyel.value : '';
let numperpage = E('.' + self.div + ' .NumPerPage');
self.limit = numperpage ? parseInt(numperpage.value) : self.limit;
let params = {
command: 'search',
keyword: searchterm,
start: start,
limit: self.limit,
orderby: orderby
};
if (self.extraparams)
{
for (let k in self.extraparams)
{
params[k] = self.extraparams[k];
}
}
self.OnListParams(params)
let resultsdiv = E('.' + self.div + ' .ItemsList');
if (self.apiurl)
{
let json = (await P.LoadingAPI(
resultsdiv,
self.apiurl,
params,
{
dontshowsuccess: 1
}
)).json;
self.items = json.data;
self.numitems = json.count;
for (let i = 0, l = self.items.length; i < l; i++)
{
let r = self.items[i];
if (r.rid && !r.idcode)
{
r.idcode = ToShortCode(r.rid);
}
}
self.DisplayItems();
} else
{
await self.OnList(resultsdiv, start, searchterm);
self.DisplayItems();
}
}
// ------------------------------------------------------------------
/** Called to return the HTML inside of a card or tr element. By
* default, it just loops through self.displayfields, but if you
* want a nicer display, feel free to customize this in a derived
* class.
*
* @param {object} r - The object to display
*/
OnRenderItem(r)
{
let self = this;
let ls = [];
let colornum = 0;
for (let f of self.displayfields)
{
let key = f[0];
let valuetype = f[1];
let value = r[key];
switch (valuetype)
{
case 'translation':
// Only show the first 32 characters of long strings
value = P.EscapeHTML(P.BestTranslation(value).toString().trim().substr(0, 32));
break;
case 'imagefile':
value = `<div class="Center">
${P.ImgFile(value, 'Height400')}
</div>
`
break;
case 'soundfile':
value = `<div class="Center">
${P.SoundFile(value, '', 'controls=1')}
</div>
`;
break;
case 'videofile':
value = `<div class="Center">
${P.VideoFile(value, '', 'controls=1')}
</div>
`;
break;
case 'headshot':
value = `<div class="Center">
${P.HeadShotImg(value, 'Square400')}
</div>
`;
case 'filelist':
value = `<div class="Center FileList">
</div>
`;
break;
case 'datetime':
case 'timestamp':
value = UTCToLocal(value * 1000);
break;
default:
// Only show the first 32 characters of long strings
value = value ? P.EscapeHTML(value.toString().trim().substr(0, 32)) : '';
}
if (self.usetable)
{
ls.push(`<td class="Pad25 ${self.colors[colornum]} ${key}">
${value}
</td>`);
} else
{
ls.push(`<div class="Pad25 ${self.colors[colornum]} ${key}">
${value}
</div>`);
}
colornum++;
if (colornum >= self.colors.length)
{
colornum = 0;
}
}
return ls.join('\n');
}
// ------------------------------------------------------------------
/** Returns the full HTML for a card or table row, calling
* OnRenderItem() to handle the inner portion.
*
* Derived classes normally should not override this method since
* this only takes care of the outer container and action buttons.
* Instead, override OnRenderItem() to customize your display.
*
* @param {object} r - The object to display
*/
RenderItem(r)
{
let self = this;
let ls = [];
let colornum = 0;
if (self.usetable)
{
ls.push(`<tr class="${self.trstyles} ItemCard" data-dbid="${r.idcode}">`);
if (self.showidcode)
{
ls.push(`<td class="dblueg Pad25 Center">${r.idcode}</td>`);
}
ls.push(self.OnRenderItem(r));
for (let r of self.extraactions)
{
ls.push(`<td class="Color${r[2]}" data-action="${r[1]}">${r[0]}</td>`);
}
ls.push('</tr>');
} else
{
ls.push(`<div class="ItemCard ${self.cardstyles}" data-dbid="${r.idcode}">`);
if (self.showidcode)
{
ls.push(`<div class="blueg Pad25 Center">${r.idcode}</div>`);
}
ls.push(self.OnRenderItem(r));
ls.push('<div class="ButtonBar Flex FlexCenter">');
for (let r of self.extraactions)
{
ls.push(`<a class="Color${r[2]}" data-action="${r[1]}">${r[0]}</a>`);
}
ls.push('</div></div>');
}
return ls.join("\n");
}
// ------------------------------------------------------------------
/** Called by List() to display the returned items from the server.
*/
DisplayItems()
{
let self = this;
let ls = [];
if (self.items.length)
{
if (self.usetable)
{
ls.push(`<table class="${self.tablestyles}">
<tr class="Pad25 Rounded Margin25 ItemCard" data-dbid="">`);
if (self.showidcode)
{
ls.push(`<th class="lgrayb Pad25 Center">ID</th>`);
}
let colornum = 0;
for (let f of self.displayfields)
{
let key = f[0];
let translation = f[2];
ls.push(`<th class="lgrayb Pad25 ${self.colors[colornum]}">${translation}</th>`);
colornum++;
if (colornum >= self.colors.length)
{
colornum = 0;
}
}
for (let r of self.extraactions)
{
ls.push(`<th class=""></th>`);
}
ls.push(`
</tr>
`);
for (let r of self.items)
{
ls.push(self.RenderItem(r));
}
ls.push('</table>');
} else
{
ls.push(`<div class="${self.liststyles}">`);
for (let r of self.items)
{
ls.push(self.RenderItem(r));
}
ls.push('</div>');
}
} else
{
ls.push(`<div class="Error">${T.nothingfound}</div>`);
}
P.HTML('.' + self.div + ' .ItemsList', ls);
P.HTML(
'.' + self.div + ' .ItemsPageBar',
P.PageBar(
self.numitems,
self.laststart,
self.limit,
self.items.length,
self.reference + '.List'
)
);
}
// ------------------------------------------------------------------
/** Handler for when a card is clicked. Derived classes should not
* override this function, but the oncardclick(cardelement, dbid)
* handler instad.
*
* @param {object} card - The object clicked
* @param {value} dbid - The database ID of the object
*/
OnItemClicked(card, dbid)
{
let self = this;
if (self.oncardclick)
{
self.oncardclick(card, dbid);
} else
{
self.Edit(card, dbid);
}
}
// ------------------------------------------------------------------
/** Called to add a new object. Normally just calls Edit() with a
* blank object.
*/
Add()
{
this.Edit();
}
// ------------------------------------------------------------------
/** Called after the editing form is displayed for custom fields.
*
* @param {object} obj - The options passed to P.EditPopup()
*/
async OnEditForm(obj, formselector)
{
}
// ------------------------------------------------------------------
/** Custom save handler if you don't have an apiurl. This function
* should call await OnSaveComplete() and return true if it succeeds.
* If it does not succeed, then it should return false.
*
* @param {string} formdiv - The selector for the whole form
* @param {object} data - The data to save to the server
* @param {string} resultsdiv - The div to display progress in
* @param {event} e - The event that trigged the save
* @param {bool} saveandcontinue - A flag indicating whether to
* return to the previous screen or to stay on this screen and
* allow further changes.
*/
async OnSave(formdiv, data, resultsdiv, e, saveandcontinue)
{
}
// ------------------------------------------------------------------
/** Called before the changed object is sent to the server for last
* minute data changes.
*
* @param {object} obj - The data to send to the server
*/
OnSaveData(obj)
{
}
// ------------------------------------------------------------------
/** Called after an object has been saved with the returned data
* from the server.
*
* The normal behavior is to list new changes and remove the editing
* layer, so make sure that if you override this function, your
* own function must do the same.
*
* @param {object} data - The data to save to the server
* @param {string} resultsdiv - The div to display progress in
* @param {bool} saveandcontinue - A flag indicating whether to
* return to the previous screen or to stay on this screen and
* allow further changes.
*/
async OnSaveComplete(data, resultsdiv, saveandcontinue)
{
let self = this;
if (!saveandcontinue)
{
self.OnFinished();
self.List();
return 1;
}
return 0;
}
// ------------------------------------------------------------------
/** Loads all of the information for a particular object from the
* database and returns it as a JavaScript object.
*
* @param {value} dbid - The database ID of the existing object.
*/
async LoadItem(dbid)
{
let self = this;
if (self.apiurl)
{
let params = {
command: 'load',
idcode: dbid
};
params = Merge(params, self.extraparams);
let json = (await P.DialogAPI(self.apiurl, params)).json;
self.extraloadinfo = json.extrainfo;
return json.data;
}
return await self.OnLoadItem(dbid);
}
// ------------------------------------------------------------------
/**
*
* @param {value} dbid - The database ID of the existing object.
*/
async OnLoadItem(dbid)
{
}
// ------------------------------------------------------------------
/** Shows an P.EditPopup() to edit an existing object or add a new
* object.
*
* @param {object} card - The object to change, or an empty object
* if adding a new object.
* @param {value} dbid - The database ID of the existing object, or
* a blank id if adding a new object.
*/
async Edit(card, dbid)
{
let self = this;
if (!self.enableedit)
{
return;
}
let obj = dbid ? await self.LoadItem(dbid) : {};
self.editdiv = await P.EditPopup(
self.GetEditFields(obj),
async function(formdiv, data, resultsdiv, e, saveandcontinue)
{
self.OnSaveData(data);
if (self.apiurl)
{
let params = {
command: 'save',
data: data
};
if (self.extraparams)
{
for (let k in self.extraparams)
{
params[k] = self.extraparams[k];
}
}
if (self.extrasaveparams)
{
for (let k in self.extrasaveparams)
{
params.data[k] = self.extrasaveparams[k];
}
}
let returned = await P.LoadingAPI(
resultsdiv,
self.apiurl,
params
);
if (returned.json.error)
{
return 0;
}
await self.OnSaveComplete(returned.json, resultsdiv, saveandcontinue);
} else
{
return await self.OnSave(formdiv, data, resultsdiv, e, saveandcontinue);
}
return 1;
},
{
enabletranslation: 1,
fullscreen: 1,
obj: obj,
saveandcontinue: self.saveandcontinue,
headertext: self.headertext,
formdiv: self.div + 'Edit'
}
);
await self.OnEditForm(obj, '.' + self.div + 'Edit');
}
// ------------------------------------------------------------------
/** Deletes the item from this widget and the database. Will show a
* confirm delete popup for one chance to cancel with ConfirmDelete()
*
* If multiple cards are selected, this function will delete all of them.
*
* @param {object} card - The object clicked
* @param {value} dbid - The database ID of the object
*/
Delete(card, dbid)
{
let self = this;
let selected = self.GetSelectedIDs();
if (selected.length)
{
self.ConfirmDelete(selected);
return;
}
self.ConfirmDelete([dbid]);
}
// ------------------------------------------------------------------
/** Calls P.ConfirmDeletePopup() before actually deleting the items
* from the database.
*
* Note that this will delete *all* selected cards
*
* @param {list} dbids - The database IDs to be deleted
*/
ConfirmDelete(dbids)
{
let self = this;
let objnames = [];
for (let r of dbids)
{
objnames.push(self.GetItemDisplayName(self.GetItemByID(r)));
}
P.ConfirmDeletePopup(
objnames.join(', '),
async function(resultsdiv)
{
if (self.apiurl)
{
let params = {
command: 'delete',
idcodes: dbids
};
if (self.extraparams)
{
for (let k in self.extraparams)
{
params[k] = self.extraparams[k];
}
}
await P.LoadingAPI(
resultsdiv,
self.apiurl,
params
);
self.List();
} else
{
self.OnDelete(dbids, resultsdiv);
}
}
);
}
// ------------------------------------------------------------------
/** Toggles between table and card displays.
*/
ToggleTable()
{
let self = this;
self.usetable = !self.usetable;
self.DisplayItems();
}
// ------------------------------------------------------------------
/** Selects every currently displayed card or table row.
*/
SelectAll()
{
let self = this;
let dbitems = Q('.' + self.div + ' .ItemCard');
for (let i = 0, l = dbitems.length; i < l; i++)
{
let r = dbitems[i];
if (r.dataset.dbid)
{
r.classList.add('Selected');
}
}
E('.' + self.div + ' .DeleteButton').classList.remove('Disabled');
}
// ------------------------------------------------------------------
/** Deelects every currently selected card or table row.
*/
SelectNone()
{
let self = this;
let dbitems = Q('.' + self.div + ' .ItemCard');
for (let i = 0, l = dbitems.length; i < l; i++)
{
let r = dbitems[i];
r.classList.remove('Selected');
}
E('.' + self.div + ' .DeleteButton').classList.add('Disabled');
}
// ------------------------------------------------------------------
/** Deletes every currently displayed card or table row.
*/
DeleteSelected()
{
let self = this;
let selected = Q('.' + self.div + ' .ItemCard.Selected');
let selectedids = [];
for (let i = 0, l = selected.length; i < l; i++)
{
let r = selected[i];
selectedids.push(r.dataset.dbid)
}
self.ConfirmDelete(selectedids);
}
}