"use strict";
// ********************************************************************
/** Control to display a screen where the user can choose items from a
* grid. Supports multiple selections, tree based navigation, and
* item reordering.
*
* @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>div</dt>
* <dd>A CSS selector given to the auto generated fullscreen layer.</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>fields</dt>
* <dd>An array containing the names of the fields that are shown
* in each card.</dd>
* <dt>enablestacks, enablereorder, multipleselect</dt>
* <dd>Set these to true to enable their various actions.</dd>
* <dt>extrabuttons</dt>
* <dd>HTML for extra buttons to show at the top toolbar.</dd>
* <dt>customclickfunc</dt>
* <dd>Enable to handle all of the click handling yourself
* instead of depending on the widget's default handling.</dd>
* <dt>onfinished</dt>
* <dd>Handler for when the user clicks on an item, or is
* done adding items in multiple select mode.</dd>
* <dt>title</dt>
* <dd>Text to use as the header.</dd>
* </dl>
*/
class PaferaChooser
{
// ------------------------------------------------------------------
constructor(config)
{
this.config = config;
this.div = config.div;
this.reference = config.reference;
this.fields = config.fields;
this.enablestacks = config.enablestacks;
this.enablereorder = config.enablereorder;
this.extrabuttons = config.extrabuttons || '';
this.title = config.title || '';
this.bgclass = config.bgclass || 'dpurpleg';
this.customclickfunc = config.customclickfunc || 0;
this.onfinished = config.onfinished || 0;
this.multipleselect = config.multipleselect;
this.available = [];
this.availablecount = 0;
this.chosen = [];
this.laststart = 0;
this.cardstack = [];
if (this.div[0] == '.')
{
this.div = this.div.substr(1);
}
}
// ------------------------------------------------------------------
/** Displays the widget inside the div specified at construction and
* setups all handlers.
*/
Display()
{
let self = this;
P.AddFullScreen(
self.bgclass,
'',
`${self.title
? self.title
: ''
}
<div class="${self.div} Grid ChooserGrid" style="${self.multipleselect ? 'grid-template-columns: 1fr 12em;' : ''}">
<div class="AvailableSection" style="border-right: 0.1em solid white;">
<h2 class="Center">${T.available}</h2>
<div class="ButtonBar">
<input type="text" class="MaxWidth600 AvailableSearchText">
<a class="Color1" onclick="${self.reference}.GetAvailable()">${T.search}</a>
${
self.multipleselect
? `<a class="Color2" onclick="${self.reference}.SelectAll()">${T.selectall}</a>
<a class="Color4" onclick="${self.reference}.SelectNone()">${T.selectnone}</a>
<a class="Color5" onclick="${self.reference}.AddSelected()">${T.add}</a>
<a class="Color6" onclick="${self.reference}.RemoveSelected()">${T.remove}</a>`
: ''
}
${self.extrabuttons}
<a class="Color3" onclick="${self.reference}.OnFinished()">${T.finished}</a>
</div>
<div class="AvailableCards FlexGrid16"></div>
<div class="AvailableCardsPageBar"></div>
</div>
${self.multipleselect
? `<div class="ChosenSection">
<h2 class="Center">${T.chosen}</h2>
<div class="ChosenCards"></div>
</div>`
: ''
}
</div>
<div class="${self.div}Results"></div>
<div class="ButtonBar">
<a class="Color1" onclick="P.RemoveFullScreen()">${T.back}</a>
<a class="Color3" onclick="${self.reference}.OnFinished()">${T.finished}</a>
</div>
`
);
P.DoneTyping(
'.' + self.div + ' .AvailableSearchText',
function(el)
{
self.GetAvailable();
}
);
if (!self.customclickfunc)
{
P.OnClick(
'.' + self.div + ' .AvailableCards',
P.LBUTTON,
function (e)
{
let card = P.TargetClass(e, '.AvailableCard');
if (!card)
{
return;
}
let dbid = card.dataset.dbid;
if (!dbid)
{
return;
}
let selected = Q('.' + self.div + ' .Selected');
if (selected.length && card)
{
if (card.classList.contains('Selected'))
{
card.classList.remove('Selected');
} else
{
self.AddSelected();
}
return;
}
self.OnAvailableClicked(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, '.AvailableCard');
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 + ' .AvailableCard');
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');
}
}
}
}
);
P.OnClick(
'.' + self.div + ' .ChosenCards',
P.LBUTTON,
function (e)
{
let card = P.TargetClass(e, '.ChosenCard');
if (!card)
return;
let dbid = card.dataset.dbid;
if (!dbid)
return;
let action = e.target.dataset['action'];
switch (action)
{
case 'MoveUpAll':
self.Move(dbid, -9999);
return;
case 'MoveUp5':
self.Move(dbid, -5);
return;
case 'MoveUp':
self.Move(dbid, -1);
return;
case 'MoveDown':
self.Move(dbid, 1);
return;
case 'MoveDown5':
self.Move(dbid, 5);
return;
case 'MoveDownAll':
self.Move(dbid, 9999);
return;
}
let selected = Q('.' + self.div + ' .Selected');
if (selected.length && card)
{
if (card.classList.contains('Selected'))
{
card.classList.remove('Selected');
} else
{
self.RemoveSelected();
}
return;
}
self.OnChosenClicked(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, '.ChosenCard');
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 + ' .ChosenCard');
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');
}
}
}
}
);
}
self.GetAvailable();
self.GetChosen();
}
// ------------------------------------------------------------------
/** Calls self.onfinished when the user clicks the finish button.
* The handler should have signature async func(dbid, chosenobject)
*/
async OnFinished(dbid, r)
{
let self = this;
if (self.onfinished)
{
await self.onfinished(dbid, r);
}
P.RemoveFullScreen();
}
// ------------------------------------------------------------------
/** Calls self.OnGetAvailable() with the desired position and search
* term. You should override OnGetAvailable() for custom handling
* instead of this function.
*
* @param {int} start - The desired starting position, or leave
* undefined to use the last position.
*/
async GetAvailable(start)
{
let self = this;
start = (start == undefined) ? self.laststart : start;
self.laststart = start;
let searchtext = E('.' + self.div + ' .AvailableSearchText');
let searchterm = searchtext ? searchtext.value : '';
let resultsdiv = E('.' + self.div + ' AvailableCards');
P.Loading(resultsdiv);
await self.OnGetAvailable(start, resultsdiv, searchterm);
self.DisplayAvailable();
}
// ------------------------------------------------------------------
/** Gets the available items and saves them in self.available to be
* displayed.
*
* @param {int} start - The desired starting position
* @param {DOMElement} resultsdiv - The div to display the searching
* progress within
* @param {string} searchterm - An optional text string to search for
*/
async OnGetAvailable(start, resultsdiv, searchterm = '')
{
}
// ------------------------------------------------------------------
/** Gets the chosen items in self.chosen and displays them.
*
* Most of the time, you don't need to override this function since
* all it does is to display the items in self.chosen.
*/
GetChosen()
{
let self = this;
self.DisplayChosen();
}
// ------------------------------------------------------------------
/** Renders the HTML of an item card. If not overridden, this will
* simply show all the fields in self.fields.
*
* @param {object} r - The object to render
*/
RenderItem(r)
{
let self = this;
let ls = [];
for (let f of self.fields)
{
ls.push(`<div class="${f}">${P.EscapeHTML(r[f])}</div>`);
}
return ls.join("\n");
}
// ------------------------------------------------------------------
/** Called by self.GetAvailable(), this displays the returned items
* from the server.
*/
DisplayAvailable()
{
let self = this;
let ls = [];
if (self.enablestacks && self.cardstack.length)
{
ls.push(`<div class="Color4 Pad25 Rounded Margin25 MinWidth800 AvailableCard" data-dbid="previouscards">
<< ${T.previous}
</div>`);
}
if (self.available.length)
{
for (let i = 0, l = self.available.length; i < l; i++)
{
let r = self.available[i];
let alreadychosen = 0;
for (let j = 0, m = self.chosen.length; j < m; j++)
{
if (self.chosen[j].idcode == r.idcode)
{
alreadychosen = 1;
break;
}
}
if (alreadychosen)
{
continue;
}
ls.push(`<div class="Color3 Pad25 Rounded Margin25 MinWidth800 AvailableCard" data-dbid="${r.idcode}">`);
ls.push(self.RenderItem(r));
ls.push('</div>');
}
} else
{
ls.push(T.nothingfound);
}
P.HTML('.' + self.div + ' .AvailableCards', ls);
P.HTML(
'.' + self.div + ' .AvailableCardsPageBar',
P.PageBar(
self.availablecount,
self.laststart,
100,
self.available.length,
self.reference + '.GetAvailable'
)
);
}
// ------------------------------------------------------------------
/** Called by self.GetChosen(), this displays the items in self.chosen.
*/
DisplayChosen()
{
let self = this;
if (!self.multipleselect)
{
return;
}
let ls = [];
if (self.chosen.length)
{
for (let i = 0, l = self.chosen.length; i < l; i++)
{
let r = self.chosen[i];
ls.push(`<div class="Color3 Pad25 Rounded Margin25 ChosenCard" data-dbid="${r.idcode}">`);
ls.push(self.RenderItem(r));
if (self.enablereorder)
{
ls.push(`
<div class="ButtonBar">
${i > 0
? `<a class="Color1" data-action="MoveUpAll">⮝⮝</a>`
: ''
}
${i > 5
? `<a class="Color1" data-action="MoveUp5">⮝5</a>`
: ''
}
${i > 0
? `<a class="Color2" data-action="MoveUp">⮝</a>`
: ''
}
${i < self.chosen.length - 1
? `<a class="Color5" data-action="MoveDown">⮟</a>`
: ''
}
${i < self.chosen.length - 5
? `<a class="Color6" data-action="MoveDown5">⮟5</a>`
: ''
}
${i < self.chosen.length - 1
? `<a class="Color5" data-action="MoveDownAll">⮟⮟</a>`
: ''
}
</div>
`);
}
ls.push('</div>');
}
} else
{
ls.push(T.nothingfound);
}
P.HTML('.' + self.div + ' .ChosenCards', ls);
}
// ------------------------------------------------------------------
/** Called when reordering is enabled. This moves the chosen object
* with idcode to another position as specified by diff.
*
* @param {value} idcode - ID of the item to be moved
* @param {int} diff - The difference between the current position
* and the new position
*/
Move(idcode, diff)
{
let self = this;
for (let i = 0, l = self.chosen.length; i < l; i++)
{
let r = self.chosen[i];
if (r.idcode == idcode)
{
let newpos = i + diff;
if (newpos < 0)
{
newpos = 0;
}
if (newpos > self.chosen.length - 1)
{
newpos = self.chosen.length - 1;
}
self.chosen.move(i, newpos);
self.DisplayChosen();
break;
}
}
}
// ------------------------------------------------------------------
/** Override this method for selecting from tree structures. It
* should return true when obj has children objects.
*
* @param {object} obj - Object to examine children for
*/
HasChildren(obj)
{
return 0;
}
// ------------------------------------------------------------------
/** Handles click events for available items, which normally results
* in calling self.onfinished(dbid, object) if the chooser is single
* selector or adding the item to self.chosen if the chooser is
* multiple select.
*
* @param {value} dbid - Database ID of the clicked item
*/
OnAvailableClicked(dbid)
{
let self = this;
if (self.enablestacks && dbid == 'previouscards')
{
self.cardstack.pop();
self.GetAvailable();
return;
}
for (let i = 0, l = self.available.length; i < l; i++)
{
let r = self.available[i];
if (r.idcode == dbid)
{
if (self.enablestacks && self.HasChildren(r))
{
self.cardstack.push(r.idcode);
self.GetAvailable();
} else
{
if (!self.multipleselect && self.onfinished)
{
self.onfinished(dbid, r);
P.RemoveFullScreen();
} else
{
self.chosen.push(r);
self.available.splice(i, 1);
self.DisplayAvailable();
self.DisplayChosen();
self.OnChosenAdded(dbid, r);
}
}
break;
}
}
}
// ------------------------------------------------------------------
/** This handler is here for derived classes which want to perform
* additional actions when a chosen item is added.
*
* @param {value} dbid - The database ID of the added object
* @param {object} obj - The added object
*/
OnChosenAdded(dbid, obj)
{
}
// ------------------------------------------------------------------
/** Handles click events for chosen items, which normally just sends
* them back to self.available
*
* @param {value} dbid - Database ID of the clicked item
*/
OnChosenClicked(dbid)
{
let self = this;
for (let i = 0, l = self.chosen.length; i < l; i++)
{
let r = self.chosen[i];
if (r.idcode == dbid)
{
self.chosen.splice(i, 1);
self.available.push(r);
self.DisplayAvailable();
self.DisplayChosen();
self.OnAvailableAdded(dbid, r);
break;
}
}
}
// ------------------------------------------------------------------
/** This handler is here for derived classes which want to perform
* additional actions when a chosen item is returned to the
* available pool.
*
* @param {value} dbid - The database ID of the returned object
* @param {object} obj - The returned object
*/
OnAvailableAdded(dbid, obj)
{
}
// ------------------------------------------------------------------
/** Selects every currently displayed available item.
*/
SelectAll()
{
let self = this;
let dbitems = Q('.' + self.div + ' .AvailableCard');
for (let dbitem of dbitems)
{
if (dbitem.dataset.dbid != 'previouscards')
{
dbitem.classList.add('Selected');
}
}
}
// ------------------------------------------------------------------
/** Deselects every currently selected available item.
*/
SelectNone()
{
let self = this;
let dbitems = Q('.' + self.div + ' .AvailableCard');
for (let dbitem of dbitems)
{
if (dbitem.dataset.dbid != 'previouscards')
{
dbitem.classList.remove('Selected');
}
}
}
// ------------------------------------------------------------------
/** Adds every selected item to the chosen list.
*/
AddSelected()
{
let self = this;
let dbitems = Q('.' + self.div + ' .AvailableCard');
for (let dbitem of dbitems)
{
if (dbitem.classList.contains('Selected'))
{
let dbid = dbitem.dataset.dbid;
self.OnAvailableClicked(dbid);
}
}
}
// ------------------------------------------------------------------
/** Removes every selected chosen item and moves them back into the
* available pool.
*/
RemoveSelected()
{
let self = this;
let dbitems = Q('.' + self.div + ' .ChosenCard');
for (let dbitem of dbitems)
{
if (dbitem.classList.contains('Selected'))
{
let dbid = dbitem.dataset.dbid;
self.OnChosenClicked(dbid);
}
}
}
}