Source: paferalist.js

"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);
  }
}