Source: paferachooser.js

"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">
        &lt;&lt; ${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);
      }
    }
  }
}