<commit_before>"use strict";

class PopupText {
  constructor(options) {
    this.options = options;
    this.init = false;
    this.triggers = [];
    this.timers = [];

    this.kMaxRowsOfText = 2;
  }

  SetTimelineLoader(timelineLoader) {
    this.timelineLoader = timelineLoader;
  }

  OnPlayerChange(e) {
    if (!this.init) {
      this.init = true;
      this.infoText = document.getElementById('popup-text-info');
      this.alertText = document.getElementById('popup-text-alert');
      this.alarmText = document.getElementById('popup-text-alarm');
    }

    if (this.job != e.detail.job || this.me != e.detail.name)
      this.OnJobChange(e);
  }

  OnDataFilesRead(e) {
    this.triggerSets = Options.Triggers;
    for (var filename in e.detail.files) {
      // Reads from the data/triggers/ directory.
      if (!filename.startsWith('triggers/'))
        continue;

      var text = e.detail.files[filename];
      var json;
      try {
        json = eval(text);
      } catch (exception) {
        console.log('Error parsing JSON from ' + filename + ': ' + exception);
        continue;
      }
      if (typeof json != "object" || !(json.length >= 0)) {
        console.log('Unexpected JSON from ' + filename + ', expected an array');
        continue;
      }
      for (var i = 0; i < json.length; ++i) {
        if (!('zoneRegex' in json[i])) {
          console.log('Unexpected JSON from ' + filename + ', expected a zoneRegex');
          continue;
        }
        if (!('triggers' in json[i])) {
          console.log('Unexpected JSON from ' + filename + ', expected a triggers');
          continue;
        }
        if (typeof json[i].triggers != 'object' || !(json[i].triggers.length >= 0)) {
          console.log('Unexpected JSON from ' + filename + ', expected triggers to be an array');
          continue;
        }
      }
      Array.prototype.push.apply(this.triggerSets, json);
    }
  }

  OnZoneChange(e) {
    this.zoneName = e.detail.zoneName;
    this.ReloadTimelines();
  }

  ReloadTimelines() {
    // Datafiles, job, and zone must be loaded.
    if (!this.triggerSets || !this.me || !this.zoneName)
      return;

    this.Reset();

    // Drop the triggers and timelines from the previous zone, so we can add new ones.
    this.triggers = [];
    var timelineFiles = [];
    var timelines = [];

    // Recursively/iteratively process timeline entries for triggers.
    // Functions get called with data, arrays get iterated, strings get appended.
    var addTimeline = (function(obj) {
      if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; ++i)
          addTimeline(obj[i]);
      } else if (typeof(obj) == 'function') {
        addTimeline(obj(this.data));
      } else if (obj) {
        timelines.push(obj);
      }
    }).bind(this);

    for (var i = 0; i < this.triggerSets.length; ++i) {
      var set = this.triggerSets[i];
      if (this.zoneName.search(set.zoneRegex) >= 0) {
        // Save the triggers from each set that matches.
        Array.prototype.push.apply(this.triggers, set.triggers);
        // And set the timeline files/timelines from each set that matches.
        if (set.timelineFile)
          timelineFiles.push(set.timelineFile);
        if (set.timeline)
          addTimeline(set.timeline);
      }
    }

    this.timelineLoader.SetTimelines(timelineFiles, timelines);
  }

  OnJobChange(e) {
    this.me = e.detail.name;
    this.job = e.detail.job;
    if (this.job.search(/^(WAR|DRK|PLD|MRD|GLD)$/) >= 0)
      this.role = 'tank';
    else if (this.job.search(/^(WHM|SCH|AST|CNJ)$/) >= 0)
      this.role = 'healer';
    else if (this.job.search(/^(MNK|NIN|DRG|SAM|ROG|LNC|PUG)$/) >= 0)
      this.role = 'dps-melee';
    else if (this.job.search(/^(BLM|SMN|RDM|THM|ACN)$/) >= 0)
      this.role = 'dps-caster';
    else if (this.job.search(/^(BRD|MCH|ARC)$/) >= 0)
      this.role = 'dps-ranged';
    else if (this.job.search(/^(CRP|BSM|ARM|GSM|LTW|WVR|ALC|CUL)$/) >= 0)
      this.role = 'crafting';
    else if (this.job.search(/^(MIN|BOT|FSH)$/) >= 0)
      this.role = 'gathering';
    else {
      this.role = '';
      console.log("Unknown job role")
    }

    this.ReloadTimelines();
  }

  OnInCombat(e) {
    // If we're in a boss fight and combat ends, ignore that.
    // Otherwise consider it a fight reset.
    if (!e.detail.inCombat && !this.inBossFight)
      this.Reset();
  }

  OnBossFightStart(e) {
    this.inBossFight = true;
  }

  OnBossFightEnd(e) {
    this.inBossFight = false;
    this.Reset();
  }

  Reset() {
    this.data = {
      me: this.me,
      job: this.job,
      role: this.role,
      ParseLocaleFloat: function(s) { return Regexes.ParseLocaleFloat(s); },
    };
    for (var i = 0; i < this.timers.length; ++i)
      window.clearTimeout(this.timers[i]);
    this.timers = [];
  }

  OnLog(e) {
    if (!this.init)
      return;

    for (var i = 0; i < e.detail.logs.length; i++) {
      var log = e.detail.logs[i];

      for (var j = 0; j < this.triggers.length; ++j) {
        var trigger = this.triggers[j];
        var r = log.match(Regexes.Parse(trigger.regex));
        if (r != null)
          this.OnTrigger(trigger, r);
      }
    }
  }

  OnTrigger(trigger, matches) {
    if (!this.options.AlertsEnabled)
      return;
    if ('disabled' in trigger && trigger.disabled)
      return;
    if ('condition' in trigger) {
      if (!trigger.condition(this.data, matches))
        return;
    }

    var that = this;
    var ValueOrFunction = function(f) {
      return (typeof(f) == "function") ? f(that.data, matches) : f;
    }

    var showText = this.options.TextAlertsEnabled;
    var playSounds = this.options.SoundAlertsEnabled;
    var playSpeech = this.options.SpokenAlertsEnabled;
    var userDisabled = trigger.id && this.options.DisabledTriggers[trigger.id];
    var delay = 'delaySeconds' in trigger ? ValueOrFunction(trigger.delaySeconds) : 0;
    var duration = 'durationSeconds' in trigger ? ValueOrFunction(trigger.durationSeconds) : 3;

    var triggerOptions = trigger.id && this.options.PerTriggerOptions[trigger.id];
    if (triggerOptions) {
      if ('SpeechAlert' in triggerOptions)
        playSpeech = triggerOptions.SpeechAlert;
      if ('SoundAlert' in triggerOptions)
        playSounds = triggerOptions.SoundAlert;
      if ('TextAlert' in triggerOptions)
        showText = triggerOptions.TextAlert;
    }

    var f = function() {
      var soundUrl = '';
      var soundVol = 1;
      var ttsText = '';

      var addText = function(container, e) {
        container.appendChild(e);
        if (container.children.length > this.kMaxRowsOfText)
          container.removeChild(container.children[0]);
      }
      var removeText = function(container, e) {
        for (var i = 0; i < container.children.length; ++i) {
          if (container.children[i] == e) {
            container.removeChild(e);
            break;
          }
        }
      }
      var makeTextElement = function(text, className) {
        var div = document.createElement('div');
        div.classList.add(className);
        div.classList.add('animate-text');
        div.innerText = text;
        return div;
      }

      if ('infoText' in trigger) {
        var text = ValueOrFunction(trigger.infoText);
        if (text && !userDisabled && showText) {
          var holder = that.infoText.getElementsByClassName('holder')[0];
          var div = makeTextElement(text, 'info-text');
          addText.bind(that)(holder, div);
          window.setTimeout(removeText.bind(that, holder, div), duration * 1000);

          if (!('sound' in trigger)) {
            soundUrl = that.options.InfoSound;
            soundVol = that.options.InfoSoundVolume;
          }
        }
      }
      if ('alertText' in trigger) {
        var text = ValueOrFunction(trigger.alertText);
        if (text && !userDisabled && showText) {
          var holder = that.alertText.getElementsByClassName('holder')[0];
          var div = makeTextElement(text, 'alert-text');
          addText.bind(that)(holder, div);
          window.setTimeout(removeText.bind(that, holder, div), duration * 1000);

          if (!('sound' in trigger)) {
            soundUrl = that.options.AlertSound;
            soundVol = that.options.AlertSoundVolume;
          }
        }
      }
      if ('alarmText' in trigger) {
        var text = ValueOrFunction(trigger.alarmText);
        if (text && !userDisabled && showText) {
          var holder = that.alarmText.getElementsByClassName('holder')[0];
          var div = makeTextElement(text, 'alarm-text');
          addText.bind(that)(holder, div);
          window.setTimeout(removeText.bind(that, holder, div), duration * 1000);

          if (!('sound' in trigger)) {
            soundUrl = that.options.AlarmSound;
            soundVol = that.options.AlarmSoundVolume;
          }
        }
      }
      if ('tts' in trigger && playSpeech) {
        var text = ValueOrFunction(trigger.tts);
        if (text && !userDisabled)
          ttsText = text;
      }

      if (trigger.sound) {
        soundUrl = trigger.sound;

        var namedSound = trigger.sound + 'Sound';
        var namedSoundVolume = trigger.sound + 'SoundVolume';
        if (namedSound in that.options) {
          soundUrl = that.options[namedSound];
          if (namedSoundVolume in that.options)
            soundVol = that.options[namedSoundVolume];
        }
        if ('soundVolume' in trigger)
          soundVol = trigger.soundVolume;
      }

      if (triggerOptions) {
        soundUrl = triggerOptions.SoundOverride || soundUrl;
        soundVol = triggerOptions.VolumeOverride || soundVol;
      }

      // Text to speech overrides all other sounds.  This is so
      // that a user who prefers tts can still get the benefit
      // of infoText triggers without tts entries by turning
      // on (speech=true, text=true, sound=true) but this will
      // not cause tts to play over top of sounds or noises.
      if (soundUrl && playSounds && !userDisabled && !ttsText) {
        var audio = new Audio(soundUrl);
        audio.volume = soundVol;
        audio.play();
      }

      if (ttsText && !userDisabled) {
        var cmd = { 'say': ttsText };
        OverlayPluginApi.overlayMessage(OverlayPluginApi.overlayName, JSON.stringify(cmd));
      }

      if ('run' in trigger)
        trigger.run(that.data, matches);
    };
    if (!delay)
      f();
    else
      this.timers.push(window.setTimeout(f, delay * 1000));
  }

  Test(zone, log) {
    this.OnPlayerChange({ detail: { name : 'ME' } });
    this.OnZoneChange({ detail: { zoneName: zone } });
    this.OnLog({ detail: { logs : ['abcdefgh', log, 'hgfedcba']}});
  }
};

class PopupTextGenerator {
  constructor(popupText) { this.popupText = popupText; }

  Info(text) {
    this.popupText.OnTrigger({
      infoText: text,
    });
  }

  Alert(text) {
    this.popupText.OnTrigger({
      alertText: text,
    });
  }

  Alarm(text) {
    this.popupText.OnTrigger({
      alarmText: text,
    });
  }

  TTS(text) {
    this.popupText.OnTrigger({
      tts: text,
    });
  }
}

var gPopupText;

document.addEventListener("onPlayerChangedEvent", function(e) {
  gPopupText.OnPlayerChange(e);
});
document.addEventListener("onZoneChangedEvent", function(e) {
  gPopupText.OnZoneChange(e);
});
document.addEventListener("onInCombatChangedEvent", function (e) {
  gPopupText.OnInCombat(e);
});
document.addEventListener("onBossFightStart", function(e) {
  gPopupText.OnBossFightStart(e);
});
document.addEventListener("onBossFightEnd", function(e) {
  gPopupText.OnBossFightEnd(e);
});
document.addEventListener("onLogEvent", function(e) {
  gPopupText.OnLog(e);
});
document.addEventListener("onDataFilesRead", function(e) {
  gPopupText.OnDataFilesRead(e);
});
<commit_msg>tts/infotext timeline interchange<commit_after>"use strict";

class PopupText {
  constructor(options) {
    this.options = options;
    this.init = false;
    this.triggers = [];
    this.timers = [];

    this.kMaxRowsOfText = 2;
  }

  SetTimelineLoader(timelineLoader) {
    this.timelineLoader = timelineLoader;
  }

  OnPlayerChange(e) {
    if (!this.init) {
      this.init = true;
      this.infoText = document.getElementById('popup-text-info');
      this.alertText = document.getElementById('popup-text-alert');
      this.alarmText = document.getElementById('popup-text-alarm');
    }

    if (this.job != e.detail.job || this.me != e.detail.name)
      this.OnJobChange(e);
  }

  OnDataFilesRead(e) {
    this.triggerSets = Options.Triggers;
    for (var filename in e.detail.files) {
      // Reads from the data/triggers/ directory.
      if (!filename.startsWith('triggers/'))
        continue;

      var text = e.detail.files[filename];
      var json;
      try {
        json = eval(text);
      } catch (exception) {
        console.log('Error parsing JSON from ' + filename + ': ' + exception);
        continue;
      }
      if (typeof json != "object" || !(json.length >= 0)) {
        console.log('Unexpected JSON from ' + filename + ', expected an array');
        continue;
      }
      for (var i = 0; i < json.length; ++i) {
        if (!('zoneRegex' in json[i])) {
          console.log('Unexpected JSON from ' + filename + ', expected a zoneRegex');
          continue;
        }
        if (!('triggers' in json[i])) {
          console.log('Unexpected JSON from ' + filename + ', expected a triggers');
          continue;
        }
        if (typeof json[i].triggers != 'object' || !(json[i].triggers.length >= 0)) {
          console.log('Unexpected JSON from ' + filename + ', expected triggers to be an array');
          continue;
        }
      }
      Array.prototype.push.apply(this.triggerSets, json);
    }
  }

  OnZoneChange(e) {
    this.zoneName = e.detail.zoneName;
    this.ReloadTimelines();
  }

  ReloadTimelines() {
    // Datafiles, job, and zone must be loaded.
    if (!this.triggerSets || !this.me || !this.zoneName)
      return;

    this.Reset();

    // Drop the triggers and timelines from the previous zone, so we can add new ones.
    this.triggers = [];
    var timelineFiles = [];
    var timelines = [];

    // Recursively/iteratively process timeline entries for triggers.
    // Functions get called with data, arrays get iterated, strings get appended.
    var addTimeline = (function(obj) {
      if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; ++i)
          addTimeline(obj[i]);
      } else if (typeof(obj) == 'function') {
        addTimeline(obj(this.data));
      } else if (obj) {
        timelines.push(obj);
      }
    }).bind(this);

    for (var i = 0; i < this.triggerSets.length; ++i) {
      var set = this.triggerSets[i];
      if (this.zoneName.search(set.zoneRegex) >= 0) {
        // Save the triggers from each set that matches.
        Array.prototype.push.apply(this.triggers, set.triggers);
        // And set the timeline files/timelines from each set that matches.
        if (set.timelineFile)
          timelineFiles.push(set.timelineFile);
        if (set.timeline)
          addTimeline(set.timeline);
      }
    }

    this.timelineLoader.SetTimelines(timelineFiles, timelines);
  }

  OnJobChange(e) {
    this.me = e.detail.name;
    this.job = e.detail.job;
    if (this.job.search(/^(WAR|DRK|PLD|MRD|GLD)$/) >= 0)
      this.role = 'tank';
    else if (this.job.search(/^(WHM|SCH|AST|CNJ)$/) >= 0)
      this.role = 'healer';
    else if (this.job.search(/^(MNK|NIN|DRG|SAM|ROG|LNC|PUG)$/) >= 0)
      this.role = 'dps-melee';
    else if (this.job.search(/^(BLM|SMN|RDM|THM|ACN)$/) >= 0)
      this.role = 'dps-caster';
    else if (this.job.search(/^(BRD|MCH|ARC)$/) >= 0)
      this.role = 'dps-ranged';
    else if (this.job.search(/^(CRP|BSM|ARM|GSM|LTW|WVR|ALC|CUL)$/) >= 0)
      this.role = 'crafting';
    else if (this.job.search(/^(MIN|BOT|FSH)$/) >= 0)
      this.role = 'gathering';
    else {
      this.role = '';
      console.log("Unknown job role")
    }

    this.ReloadTimelines();
  }

  OnInCombat(e) {
    // If we're in a boss fight and combat ends, ignore that.
    // Otherwise consider it a fight reset.
    if (!e.detail.inCombat && !this.inBossFight)
      this.Reset();
  }

  OnBossFightStart(e) {
    this.inBossFight = true;
  }

  OnBossFightEnd(e) {
    this.inBossFight = false;
    this.Reset();
  }

  Reset() {
    this.data = {
      me: this.me,
      job: this.job,
      role: this.role,
      ParseLocaleFloat: function(s) { return Regexes.ParseLocaleFloat(s); },
    };
    for (var i = 0; i < this.timers.length; ++i)
      window.clearTimeout(this.timers[i]);
    this.timers = [];
  }

  OnLog(e) {
    if (!this.init)
      return;

    for (var i = 0; i < e.detail.logs.length; i++) {
      var log = e.detail.logs[i];

      for (var j = 0; j < this.triggers.length; ++j) {
        var trigger = this.triggers[j];
        var r = log.match(Regexes.Parse(trigger.regex));
        if (r != null)
          this.OnTrigger(trigger, r);
      }
    }
  }

  OnTrigger(trigger, matches) {
    if (!this.options.AlertsEnabled)
      return;
    if ('disabled' in trigger && trigger.disabled)
      return;
    if ('condition' in trigger) {
      if (!trigger.condition(this.data, matches))
        return;
    }

    var that = this;
    var ValueOrFunction = function(f) {
      return (typeof(f) == "function") ? f(that.data, matches) : f;
    }

    var showText = this.options.TextAlertsEnabled;
    var playSounds = this.options.SoundAlertsEnabled;
    var playSpeech = this.options.SpokenAlertsEnabled;
    var userDisabled = trigger.id && this.options.DisabledTriggers[trigger.id];
    var delay = 'delaySeconds' in trigger ? ValueOrFunction(trigger.delaySeconds) : 0;
    var duration = 'durationSeconds' in trigger ? ValueOrFunction(trigger.durationSeconds) : 3;

    var triggerOptions = trigger.id && this.options.PerTriggerOptions[trigger.id];
    if (triggerOptions) {
      if ('SpeechAlert' in triggerOptions)
        playSpeech = triggerOptions.SpeechAlert;
      if ('SoundAlert' in triggerOptions)
        playSounds = triggerOptions.SoundAlert;
      if ('TextAlert' in triggerOptions)
        showText = triggerOptions.TextAlert;
    }

    var f = function() {
      var soundUrl = '';
      var soundVol = 1;
      var ttsText = '';

      var addText = function(container, e) {
        container.appendChild(e);
        if (container.children.length > this.kMaxRowsOfText)
          container.removeChild(container.children[0]);
      }
      var removeText = function(container, e) {
        for (var i = 0; i < container.children.length; ++i) {
          if (container.children[i] == e) {
            container.removeChild(e);
            break;
          }
        }
      }
      var makeTextElement = function(text, className) {
        var div = document.createElement('div');
        div.classList.add(className);
        div.classList.add('animate-text');
        div.innerText = text;
        return div;
      }

      if ('infoText' in trigger) {
        var text = ValueOrFunction(trigger.infoText);
        if (text && !userDisabled && showText) {
          var holder = that.infoText.getElementsByClassName('holder')[0];
          var div = makeTextElement(text, 'info-text');
          addText.bind(that)(holder, div);
          window.setTimeout(removeText.bind(that, holder, div), duration * 1000);

          if (!('sound' in trigger)) {
            soundUrl = that.options.InfoSound;
            soundVol = that.options.InfoSoundVolume;
          }
        }
      }
      if ('alertText' in trigger) {
        var text = ValueOrFunction(trigger.alertText);
        if (text && !userDisabled && showText) {
          var holder = that.alertText.getElementsByClassName('holder')[0];
          var div = makeTextElement(text, 'alert-text');
          addText.bind(that)(holder, div);
          window.setTimeout(removeText.bind(that, holder, div), duration * 1000);

          if (!('sound' in trigger)) {
            soundUrl = that.options.AlertSound;
            soundVol = that.options.AlertSoundVolume;
          }
        }
      }
      if ('alarmText' in trigger) {
        var text = ValueOrFunction(trigger.alarmText);
        if (text && !userDisabled && showText) {
          var holder = that.alarmText.getElementsByClassName('holder')[0];
          var div = makeTextElement(text, 'alarm-text');
          addText.bind(that)(holder, div);
          window.setTimeout(removeText.bind(that, holder, div), duration * 1000);

          if (!('sound' in trigger)) {
            soundUrl = that.options.AlarmSound;
            soundVol = that.options.AlarmSoundVolume;
          }
        }
      }
      if ('tts' in trigger && playSpeech) {
        var text = ValueOrFunction(trigger.tts);
        if (text && !userDisabled)
          ttsText = text;
      }

      if (trigger.sound) {
        soundUrl = trigger.sound;

        var namedSound = trigger.sound + 'Sound';
        var namedSoundVolume = trigger.sound + 'SoundVolume';
        if (namedSound in that.options) {
          soundUrl = that.options[namedSound];
          if (namedSoundVolume in that.options)
            soundVol = that.options[namedSoundVolume];
        }
        if ('soundVolume' in trigger)
          soundVol = trigger.soundVolume;
      }

      if (triggerOptions) {
        soundUrl = triggerOptions.SoundOverride || soundUrl;
        soundVol = triggerOptions.VolumeOverride || soundVol;
      }

      // Text to speech overrides all other sounds.  This is so
      // that a user who prefers tts can still get the benefit
      // of infoText triggers without tts entries by turning
      // on (speech=true, text=true, sound=true) but this will
      // not cause tts to play over top of sounds or noises.
      if (soundUrl && playSounds && !userDisabled && !ttsText) {
        var audio = new Audio(soundUrl);
        audio.volume = soundVol;
        audio.play();
      }

      if (ttsText && !userDisabled) {
        var cmd = { 'say': ttsText };
        OverlayPluginApi.overlayMessage(OverlayPluginApi.overlayName, JSON.stringify(cmd));
      }

      if ('run' in trigger)
        trigger.run(that.data, matches);
    };
    if (!delay)
      f();
    else
      this.timers.push(window.setTimeout(f, delay * 1000));
  }

  Test(zone, log) {
    this.OnPlayerChange({ detail: { name : 'ME' } });
    this.OnZoneChange({ detail: { zoneName: zone } });
    this.OnLog({ detail: { logs : ['abcdefgh', log, 'hgfedcba']}});
  }
};

class PopupTextGenerator {
  constructor(popupText) { this.popupText = popupText; }

  Info(text) {
    this.popupText.OnTrigger({
      infoText: text,
      tts: text,
    });
  }

  Alert(text) {
    this.popupText.OnTrigger({
      alertText: text,
      tts: text,
    });
  }

  Alarm(text) {
    this.popupText.OnTrigger({
      alarmText: text,
      tts: text,
    });
  }

  TTS(text) {
    this.popupText.OnTrigger({
      infoText: text,
      tts: text,
    });
  }
}

var gPopupText;

document.addEventListener("onPlayerChangedEvent", function(e) {
  gPopupText.OnPlayerChange(e);
});
document.addEventListener("onZoneChangedEvent", function(e) {
  gPopupText.OnZoneChange(e);
});
document.addEventListener("onInCombatChangedEvent", function (e) {
  gPopupText.OnInCombat(e);
});
document.addEventListener("onBossFightStart", function(e) {
  gPopupText.OnBossFightStart(e);
});
document.addEventListener("onBossFightEnd", function(e) {
  gPopupText.OnBossFightEnd(e);
});
document.addEventListener("onLogEvent", function(e) {
  gPopupText.OnLog(e);
});
document.addEventListener("onDataFilesRead", function(e) {
  gPopupText.OnDataFilesRead(e);
});
<|endoftext|>


<commit_before>// Text rendering style

import Builders from '../builders';
import Texture from '../../gl/texture';
import WorkerBroker from '../../utils/worker_broker';
import Utils from '../../utils/utils';
import {Sprites} from '../sprites/sprites';
import Label from './label';

export let TextStyle = Object.create(Sprites);

Object.assign(TextStyle, {
    name: 'text',
    super: Sprites,
    built_in: true,

    init() {

        this.super.init.apply(this);

        // Provide a hook for this object to be called from worker threads
        if (Utils.isMainThread) {
            WorkerBroker.addTarget('TextStyle', this);
        }

        this.texts = {}; // unique texts, keyed by tile
        this.texture = {};
        this.canvas = {};
        this.bboxes = {};
        this.geometries = {};

        this.font_style = {
            typeface: 'Helvetica',
            size: '12px',
            fill: 'white',
            stroke: 'black'
        };

        // default label style
        this.label_style = {
            lines: { exceed: 60 }
        };
    },

    // Set font style params for canvas drawing
    setFont (tile, { size, typeface, fill, stroke }) {
        this.size = parseInt(size);
        this.buffer = 6; // pixel padding around text
        let ctx = this.canvas[tile].context;

        ctx.font = size + ' ' + typeface;
        ctx.strokeStyle = stroke;
        ctx.fillStyle = fill;
        ctx.lineWidth = 4;
        ctx.miterLimit = 2;
    },

    // Width and height of text based on current font style
    textSize (text, tile) {
        return [
            Math.ceil(this.canvas[tile].context.measureText(text).width) + this.buffer * 2,
            this.size + this.buffer * 2
        ];
    },

    // Draw text at specified location, adjusting for buffer and baseline
    drawText (text, [x, y], tile) {
        // TODO: optional stroke
        this.canvas[tile].context.strokeText(text, x + this.buffer, y + this.buffer + this.size);
        this.canvas[tile].context.fillText(text, x + this.buffer, y + this.buffer + this.size);
    },

    setTextureTextPositions (texts) {
        // Find widest label and sum of all label heights
        let widest = 0, height = 0;

        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let text_info = text_infos[text];
                let size = text_info.size;

                text_info.position = [0, height];

                if (size[0] > widest) {
                    widest = size[0];
                }

                height += size[1];
            }
        }

        return [ widest, height ];
    },

    getTextSizes (tile, texts) {
        // create a canvas
        if(this.canvas[tile] === undefined) {
            let canvas = document.createElement('canvas');
            this.canvas[tile] = {
                canvas: canvas,
                context: canvas.getContext('2d')
            };
        }

        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                // update text sizes
                this.setFont(tile, text_infos[text].text_style);
                text_infos[text].size = this.textSize(text, tile);
            }
        }

        return Promise.resolve(texts);
    },

    rasterize (tile, texts, texture_size) {
        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let info = text_infos[text];

                this.setFont(tile, info.text_style);
                this.drawText(text, info.position, tile);

                info.texcoords = Builders.getTexcoordsForSprite(
                    info.position,
                    info.size,
                    texture_size
                );
            }
        }
    },

    // Called on main thread from worker, to create atlas of labels for a tile
    addTexts (tile, texts) {
        this.texts[tile] = texts;

        let texture_size = this.setTextureTextPositions(texts);
        let context = this.canvas[tile].context;

        console.log(`text summary for tile ${tile}: fits in ${texture_size[0]}x${texture_size[1]}px`);

        // update the canvas "context"
        this.canvas[tile].canvas.width = texture_size[0];
        this.canvas[tile].canvas.height = texture_size[1];
        context.clearRect(0, 0, texture_size[0], texture_size[1]);

        // create a texture
        this.texture[tile] = new Texture(this.gl, 'labels-' + tile, { filtering: 'linear' });
        this.texture[tile].setCanvas(this.canvas[tile].canvas);

        // ask for rasterization for the text set
        this.rasterize(tile, texts, texture_size);

        this.texture[tile].update();

        return Promise.resolve(this.texts[tile]);
    },

    // Override
    startData () {
        let tile_data = this.super.startData.apply(this);
        tile_data.queue = [];
        return tile_data;
    },

    // Override
    endData (tile_data) {
        // Count collected text
        let tile, count;
        if (tile_data.queue.length > 0) {
            tile = tile_data.queue[0][2].tile.key;
            count = Object.keys(this.texts[tile]||{}).length;
            console.log(`# texts for tile ${tile}: ${count}`);
        }
        if (!count) {
            return Promise.resolve();
        }

        // Attach tile-specific label atlas to mesh as a texture uniform
        tile_data.uniforms = { u_textures: ['labels-'+tile] };

        // first call to main thread, ask for text pixel sizes
        return WorkerBroker.postMessage('TextStyle', 'getTextSizes', tile, this.texts[tile]).then(texts => {
            this.bboxes[tile] = [];

            // cleanup of texts that should be removed after occlusion test
            for (let style in texts) {
                let text_infos = texts[style];

                for (let text in text_infos) {
                    let text_info = text_infos[text];
                    let label;
                    let keep_in_tile;
                    let move_in_tile;
                    let geometry = this.geometries[tile][style][text];
                    let exceed_heuristic = this.label_style.lines.exceed;

                    if (geometry.type === "LineString") {
                        let lines = geometry.coordinates;
                        let line = [lines[0]];

                        keep_in_tile = true;
                        move_in_tile = true;

                        label = new Label(text, line[0], text_info.size, lines);
                    } else if (geometry.type === "Point") {
                        let points = [geometry.coordinates];

                        keep_in_tile = true;
                        move_in_tile = false;

                        label = new Label(text, points[0], text_info.size);
                    }

                    if (label.discard(move_in_tile, keep_in_tile, this.bboxes[tile], exceed_heuristic)) {
                        // remove the text from the map
                        delete text_infos[text];
                    }

                    text_info.label = label;
                }

                // No labels for this style
                if (Object.keys(text_infos).length === 0) {
                    delete texts[style];
                }
            }

            // No labels for this tile
            if (Object.keys(texts).length === 0) {
                // early exit
                return;
            }

            // second call to main thread, for rasterizing the set of texts
            return WorkerBroker.postMessage('TextStyle', 'addTexts', tile, texts).then(texts => {
                this.texts[tile] = texts;
                // Build queued features
                tile_data.queue.forEach(q => this.super.addFeature.apply(this, q));
                tile_data.queue = [];
                return this.super.endData.call(this, tile_data);
            });
        });
    },

    // Override to queue features instead of processing immediately
    addFeature (feature, rule, context, tile_data) {
        // Collect text
        let text = feature.properties.name;

        if (text) {
            let tile = context.tile.key;
            if (!this.texts[tile]) {
                this.texts[tile] = {};
            }

            if (!this.geometries[tile]) {
                this.geometries[tile] = {};
            }

            let style = this.constructFontStyle(rule);
            let style_key = this.constructStyleKey(style);

            if (!this.texts[tile][style_key]) {
                this.texts[tile][style_key] = {};
            }

            if (!this.geometries[tile][style_key]) {
                this.geometries[tile][style_key] = feature.geometry;
            }

            this.texts[tile][style_key][text] = {
                text_style: style
            };

            this.geometries[tile][style_key][text] = feature.geometry;
        }

        tile_data.queue.push([feature, rule, context, tile_data]);
    },

    constructFontStyle (rule) {
        let style = this.font_style;

        if (rule.font) {
            style = {
                typeface: rule.font.typeface || this.font_style.typeface,
                size: rule.font.size || this.font_size.font_size,
                fill: rule.font.fill === undefined ? this.font_style.fill : Utils.toCanvasColor(rule.font.fill),
                stroke: rule.font.stroke === undefined ? this.font_style.stroke : Utils.toCanvasColor(rule.font.stroke)
            };
        }

        return style;
    },

    constructStyleKey ({ typeface, size, fill, stroke }) {
        return `${typeface}/${size}/${fill}/${stroke}`;
    },

    build (style, vertex_data) {
        let vertex_template = this.makeVertexTemplate(style);

        Builders.buildSpriteQuadsForPoints(
            [ style.label.position ],
            Utils.scaleInt16(style.label.size[0], 128), Utils.scaleInt16(style.label.size[1], 128),
            Utils.scaleInt16(Utils.radToDeg(style.label.angle), 360),
            Utils.scaleInt16(1, 256),
            vertex_data,
            vertex_template,
            this.vertex_layout.index.a_shape,
            { texcoord_index: this.vertex_layout.index.a_texcoord, texcoord_scale: this.texcoord_scale }
        );
    },

    buildLines (lines, style, vertex_data) {
        if (style.discarded) {
            return;
        }

        this.build(style, vertex_data);
    },

    buildPoints (points, style, vertex_data) {
        if (style.discarded) {
            return;
        }

        this.build(style, vertex_data);
    },

    _parseFeature (feature, rule_style, context) {
        let style = this.feature_style;
        let tile = context.tile.key;
        let text = feature.properties.name;

        let font_style = this.constructFontStyle(rule_style);
        let style_key = this.constructStyleKey(font_style);

        style.discarded  = (!this.texts[tile] || !this.texts[tile][style_key] || !this.texts[tile][style_key][text]);

        if (!style.discarded) {
            let text_info = this.texts[tile][style_key][text];
            this.texcoord_scale = text_info.texcoords;

            style.text = text;

            // to store bbox by tiles
            style.tile = tile;
            style.label = text_info.label;
        }

        return style;
    }

});

<commit_msg>labels: delete canvas if no labels to render<commit_after>// Text rendering style

import Builders from '../builders';
import Texture from '../../gl/texture';
import WorkerBroker from '../../utils/worker_broker';
import Utils from '../../utils/utils';
import {Sprites} from '../sprites/sprites';
import Label from './label';

export let TextStyle = Object.create(Sprites);

Object.assign(TextStyle, {
    name: 'text',
    super: Sprites,
    built_in: true,

    init() {

        this.super.init.apply(this);

        // Provide a hook for this object to be called from worker threads
        if (Utils.isMainThread) {
            WorkerBroker.addTarget('TextStyle', this);
        }

        this.texts = {}; // unique texts, keyed by tile
        this.texture = {};
        this.canvas = {};
        this.bboxes = {};
        this.geometries = {};

        this.font_style = {
            typeface: 'Helvetica',
            size: '12px',
            fill: 'white',
            stroke: 'black'
        };

        // default label style
        this.label_style = {
            lines: { exceed: 60 }
        };
    },

    // Set font style params for canvas drawing
    setFont (tile, { size, typeface, fill, stroke }) {
        this.size = parseInt(size);
        this.buffer = 6; // pixel padding around text
        let ctx = this.canvas[tile].context;

        ctx.font = size + ' ' + typeface;
        ctx.strokeStyle = stroke;
        ctx.fillStyle = fill;
        ctx.lineWidth = 4;
        ctx.miterLimit = 2;
    },

    // Width and height of text based on current font style
    textSize (text, tile) {
        return [
            Math.ceil(this.canvas[tile].context.measureText(text).width) + this.buffer * 2,
            this.size + this.buffer * 2
        ];
    },

    // Draw text at specified location, adjusting for buffer and baseline
    drawText (text, [x, y], tile) {
        // TODO: optional stroke
        this.canvas[tile].context.strokeText(text, x + this.buffer, y + this.buffer + this.size);
        this.canvas[tile].context.fillText(text, x + this.buffer, y + this.buffer + this.size);
    },

    setTextureTextPositions (texts) {
        // Find widest label and sum of all label heights
        let widest = 0, height = 0;

        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let text_info = text_infos[text];
                let size = text_info.size;

                text_info.position = [0, height];

                if (size[0] > widest) {
                    widest = size[0];
                }

                height += size[1];
            }
        }

        return [ widest, height ];
    },

    getTextSizes (tile, texts) {
        // create a canvas
        if(!this.canvas[tile]) {
            let canvas = document.createElement('canvas');
            this.canvas[tile] = {
                canvas: canvas,
                context: canvas.getContext('2d')
            };
        }

        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                // update text sizes
                this.setFont(tile, text_infos[text].text_style);
                text_infos[text].size = this.textSize(text, tile);
            }
        }

        return Promise.resolve(texts);
    },

    // Called on main thread to release tile-specific resources
    freeTile (tile) {
        this.canvas[tile] = null;
    },

    rasterize (tile, texts, texture_size) {
        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let info = text_infos[text];

                this.setFont(tile, info.text_style);
                this.drawText(text, info.position, tile);

                info.texcoords = Builders.getTexcoordsForSprite(
                    info.position,
                    info.size,
                    texture_size
                );
            }
        }
    },

    // Called on main thread from worker, to create atlas of labels for a tile
    addTexts (tile, texts) {
        this.texts[tile] = texts;

        let texture_size = this.setTextureTextPositions(texts);
        let context = this.canvas[tile].context;

        console.log(`text summary for tile ${tile}: fits in ${texture_size[0]}x${texture_size[1]}px`);

        // update the canvas "context"
        this.canvas[tile].canvas.width = texture_size[0];
        this.canvas[tile].canvas.height = texture_size[1];
        context.clearRect(0, 0, texture_size[0], texture_size[1]);

        // create a texture
        this.texture[tile] = new Texture(this.gl, 'labels-' + tile, { filtering: 'linear' });
        this.texture[tile].setCanvas(this.canvas[tile].canvas);

        // ask for rasterization for the text set
        this.rasterize(tile, texts, texture_size);

        this.texture[tile].update();

        return Promise.resolve(this.texts[tile]);
    },

    // Override
    startData () {
        let tile_data = this.super.startData.apply(this);
        tile_data.queue = [];
        return tile_data;
    },

    // Override
    endData (tile_data) {
        // Count collected text
        let tile, count;
        if (tile_data.queue.length > 0) {
            tile = tile_data.queue[0][2].tile.key;
            count = Object.keys(this.texts[tile]||{}).length;
            console.log(`# texts for tile ${tile}: ${count}`);
        }
        if (!count) {
            return Promise.resolve();
        }

        // Attach tile-specific label atlas to mesh as a texture uniform
        tile_data.uniforms = { u_textures: ['labels-'+tile] };

        // first call to main thread, ask for text pixel sizes
        return WorkerBroker.postMessage('TextStyle', 'getTextSizes', tile, this.texts[tile]).then(texts => {
            this.bboxes[tile] = [];

            // cleanup of texts that should be removed after occlusion test
            for (let style in texts) {
                let text_infos = texts[style];

                for (let text in text_infos) {
                    let text_info = text_infos[text];
                    let label;
                    let keep_in_tile;
                    let move_in_tile;
                    let geometry = this.geometries[tile][style][text];
                    let exceed_heuristic = this.label_style.lines.exceed;

                    if (geometry.type === "LineString") {
                        let lines = geometry.coordinates;
                        let line = [lines[0]];

                        keep_in_tile = true;
                        move_in_tile = true;

                        label = new Label(text, line[0], text_info.size, lines);
                    } else if (geometry.type === "Point") {
                        let points = [geometry.coordinates];

                        keep_in_tile = true;
                        move_in_tile = false;

                        label = new Label(text, points[0], text_info.size);
                    }

                    if (label.discard(move_in_tile, keep_in_tile, this.bboxes[tile], exceed_heuristic)) {
                        // remove the text from the map
                        delete text_infos[text];
                    }

                    text_info.label = label;
                }

                // No labels for this style
                if (Object.keys(text_infos).length === 0) {
                    delete texts[style];
                }
            }

            // No labels for this tile
            if (Object.keys(texts).length === 0) {
                WorkerBroker.postMessage('TextStyle', 'freeTile', tile);
                // early exit
                return;
            }

            // second call to main thread, for rasterizing the set of texts
            return WorkerBroker.postMessage('TextStyle', 'addTexts', tile, texts).then(texts => {
                this.texts[tile] = texts;
                // Build queued features
                tile_data.queue.forEach(q => this.super.addFeature.apply(this, q));
                tile_data.queue = [];
                return this.super.endData.call(this, tile_data);
            });
        });
    },

    // Override to queue features instead of processing immediately
    addFeature (feature, rule, context, tile_data) {
        // Collect text
        let text = feature.properties.name;

        if (text) {
            let tile = context.tile.key;
            if (!this.texts[tile]) {
                this.texts[tile] = {};
            }

            if (!this.geometries[tile]) {
                this.geometries[tile] = {};
            }

            let style = this.constructFontStyle(rule);
            let style_key = this.constructStyleKey(style);

            if (!this.texts[tile][style_key]) {
                this.texts[tile][style_key] = {};
            }

            if (!this.geometries[tile][style_key]) {
                this.geometries[tile][style_key] = feature.geometry;
            }

            this.texts[tile][style_key][text] = {
                text_style: style
            };

            this.geometries[tile][style_key][text] = feature.geometry;
        }

        tile_data.queue.push([feature, rule, context, tile_data]);
    },

    constructFontStyle (rule) {
        let style = this.font_style;

        if (rule.font) {
            style = {
                typeface: rule.font.typeface || this.font_style.typeface,
                size: rule.font.size || this.font_size.font_size,
                fill: rule.font.fill === undefined ? this.font_style.fill : Utils.toCanvasColor(rule.font.fill),
                stroke: rule.font.stroke === undefined ? this.font_style.stroke : Utils.toCanvasColor(rule.font.stroke)
            };
        }

        return style;
    },

    constructStyleKey ({ typeface, size, fill, stroke }) {
        return `${typeface}/${size}/${fill}/${stroke}`;
    },

    build (style, vertex_data) {
        let vertex_template = this.makeVertexTemplate(style);

        Builders.buildSpriteQuadsForPoints(
            [ style.label.position ],
            Utils.scaleInt16(style.label.size[0], 128), Utils.scaleInt16(style.label.size[1], 128),
            Utils.scaleInt16(Utils.radToDeg(style.label.angle), 360),
            Utils.scaleInt16(1, 256),
            vertex_data,
            vertex_template,
            this.vertex_layout.index.a_shape,
            { texcoord_index: this.vertex_layout.index.a_texcoord, texcoord_scale: this.texcoord_scale }
        );
    },

    buildLines (lines, style, vertex_data) {
        if (style.discarded) {
            return;
        }

        this.build(style, vertex_data);
    },

    buildPoints (points, style, vertex_data) {
        if (style.discarded) {
            return;
        }

        this.build(style, vertex_data);
    },

    _parseFeature (feature, rule_style, context) {
        let style = this.feature_style;
        let tile = context.tile.key;
        let text = feature.properties.name;

        let font_style = this.constructFontStyle(rule_style);
        let style_key = this.constructStyleKey(font_style);

        style.discarded  = (!this.texts[tile] || !this.texts[tile][style_key] || !this.texts[tile][style_key][text]);

        if (!style.discarded) {
            let text_info = this.texts[tile][style_key][text];
            this.texcoord_scale = text_info.texcoords;

            style.text = text;

            // to store bbox by tiles
            style.tile = tile;
            style.label = text_info.label;
        }

        return style;
    }

});

<|endoftext|>


<commit_before>exports = module.exports = TestHelpers = {
  getFfmpegCheck: function() {
    var platform = require('os').platform();

    if (!platform.match(/win(32|64)/)) {
      // linux/mac, use which
      return 'which ffmpeg';
    } else {
      // windows, use where (> windows server 2003 / windows 7)
      return 'where /Q ffmpeg';
    }
  },

  logger: {
    debug: function(arg) { console.log('[DEBUG] ' + arg); },
    info: function(arg) { console.log('[INFO] ' + arg); },
    warn: function(arg) { console.log('[WARN] ' + arg); },
    error: function(arg) { console.log('[ERROR] ' + arg); }
  },

  logArgError: function(err) {
    if (err) {
      console.log('got error: ' + (err.stack || err));
      if (err.ffmpegOut) {
        console.log('---stdout---');
        console.log(err.ffmpegOut);
      }
      if (err.ffmpegErr) {
        console.log('---stderr---');
        console.log(err.ffmpegErr);
      }
      if (err.spawnErr) {
        console.log('---spawn error---');
        console.log(err.spawnErr.stack || err.spawnErr);
      }
    }
  },

  logError: function(err, stdout, stderr) {
    if (err) {
      console.log('got error: ' + (err.stack || err));
      if (err.ffmpegOut) {
        console.log('---metadata stdout---');
        console.log(err.ffmpegOut);
      }
      if (err.ffmpegErr) {
        console.log('---metadata stderr---');
        console.log(err.ffmpegErr);
      }
      if (err.spawnErr) {
        console.log('---metadata spawn error---');
        console.log(err.spawnErr.stack || err.spawnErr);
      }
      if (stdout) {
        console.log('---stdout---');
        console.log(stdout);
      }
      if (stderr) {
        console.log('---stderr---');
        console.log(stderr);
      }
    }
  }
}<commit_msg>Index logger messages<commit_after>exports = module.exports = TestHelpers = {
  getFfmpegCheck: function() {
    var platform = require('os').platform();

    if (!platform.match(/win(32|64)/)) {
      // linux/mac, use which
      return 'which ffmpeg';
    } else {
      // windows, use where (> windows server 2003 / windows 7)
      return 'where /Q ffmpeg';
    }
  },

  logger: {
    debug: function(arg) { console.log('          [DEBUG] ' + arg); },
    info: function(arg) { console.log('          [INFO] ' + arg); },
    warn: function(arg) { console.log('          [WARN] ' + arg); },
    error: function(arg) { console.log('          [ERROR] ' + arg); }
  },

  logArgError: function(err) {
    if (err) {
      console.log('got error: ' + (err.stack || err));
      if (err.ffmpegOut) {
        console.log('---stdout---');
        console.log(err.ffmpegOut);
      }
      if (err.ffmpegErr) {
        console.log('---stderr---');
        console.log(err.ffmpegErr);
      }
      if (err.spawnErr) {
        console.log('---spawn error---');
        console.log(err.spawnErr.stack || err.spawnErr);
      }
    }
  },

  logError: function(err, stdout, stderr) {
    if (err) {
      console.log('got error: ' + (err.stack || err));
      if (err.ffmpegOut) {
        console.log('---metadata stdout---');
        console.log(err.ffmpegOut);
      }
      if (err.ffmpegErr) {
        console.log('---metadata stderr---');
        console.log(err.ffmpegErr);
      }
      if (err.spawnErr) {
        console.log('---metadata spawn error---');
        console.log(err.spawnErr.stack || err.spawnErr);
      }
      if (stdout) {
        console.log('---stdout---');
        console.log(stdout);
      }
      if (stderr) {
        console.log('---stderr---');
        console.log(stderr);
      }
    }
  }
}<|endoftext|>


<commit_before><commit_msg>remove useless file<commit_after><|endoftext|>


<commit_before>(function($) {
  module('List view', {
    setup: function() {
      $.fx.off = true;
    },
    teardown: function() {
      // Cleanup notification box
      $('.notification-box').remove();
    }
  });

  test('Basic', function() {
    var listView = {
      listView: {
        section: 'test123',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' }
        },
        dataProvider: function(args) {
          args.response.success({
            data: []
          });
        }
      }
    };
    var $listView;

    ok($listView = $('<div>').listView(listView), 'Initialize list view');
    equal($listView.find('.list-view').size(), 1, 'List view has container div');
    equal($listView.find('.list-view.test123').size(), 1, 'Container div has section ID as CSS class');
    equal($listView.find('.list-view table').size(), 2, 'List view has split tables');
    equal($listView.find('.list-view .fixed-header table thead tr').size(), 1, 'List view has fixed table header');
    equal($listView.find('.list-view .fixed-header table thead th').size(), 2, 'List view has correct column headers');
    equal($listView.find('.list-view .fixed-header table thead th:first').html(), 'testFieldA', 'First header has correct label');
    ok($listView.find('.list-view .fixed-header table thead th:first').hasClass('fieldA'), 'First header has correct class');
    ok($listView.find('.list-view .fixed-header table thead th:last').hasClass('fieldB'), 'First header has correct class');
    equal($listView.find('.list-view .fixed-header table thead th:last').html(), 'testFieldB', 'First header has correct label');
    equal($listView.find('.list-view table tbody tr').size(), 1, 'List view has table body');
    equal($listView.find('.toolbar').size(), 1, 'List view has toolbar');
    equal($listView.find('.toolbar .text-search .search-bar input[type=text]').size(), 1, 'Toolbar has search box');
    equal($listView.find('.toolbar .text-search .search-bar input[type=text]').size(), 1, 'Toolbar has search box');
    equal($listView.find('.toolbar .text-search .button.search').size(), 1, 'Toolbar has search button');
    ok(!$listView.find('.toolbar .filters').size(), 'Toolbar doesn\'t have filters');
  });

  test('Data provider', function() {
    var $listView = $('<div>');

    stop();
    $listView.listView({
      context: {
        tests: []
      },
      listView: {
        section: 'test',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' }
        },
        dataProvider: function(args) {
          start();
          equal(args.page, 1, 'Correct page # passed');
          equal(args.filterBy.search.value, '', 'No search params specified');
          ok($.isArray(args.context.tests), 'Context passed');
          args.response.success({
            data: [
              {
                fieldA: '1A',
                fieldB: '1B'
              },
              {
                fieldA: '2A',
                fieldB: '2B'
              },
              {
                fieldA: '3A',
                fieldB: '3B'
              }
            ]
          });
          stop();
          setTimeout(function() {
            start();
            equal($listView.find('tbody tr').size(), 3, 'Data row count is correct');
            equal($listView.find('tbody tr:first td.fieldA span').html(), '1A', 'Correct table item value for first row');
            equal($listView.find('tbody tr:first td.fieldB span').html(), '1B', 'Correct table item value for first row');
            equal($listView.find('tbody tr:last td.fieldA span').html(), '3A', 'Correct table item value for last row');
            equal($listView.find('tbody tr:last td.fieldB span').html(), '3B', 'Correct table item value for last row');
          });
        }
      }
    });
  });

  test('Pre-filter', function() {
    var $listView = $('<div>');

    stop();
    $listView.listView({
      listView: {
        section: 'test',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' },
          fieldC: { label: 'testFieldC' }
        },
        preFilter: function(args) {
          return ['fieldC'];
        },
        dataProvider: function(args) {
          args.response.success({
            data: [
              {
                fieldA: '1A',
                fieldB: '1B',
                fieldC: '1C'
              }
            ]
          });
          setTimeout(function() {
            start();
            equal($listView.find('thead th').size(), 2, 'Correct # of filtered columns present');
            equal($listView.find('tbody tr:first td').size(), 2, 'Correct # of body columns present');
            equal($listView.find('tbody tr:first td.fieldA span').html(), '1A', 'Correct table item value for data row');
            equal($listView.find('tbody tr:first td.fieldB span').html(), '1B', 'Correct table item value for data row');
          });
        }
      }
    });
  });

  test('Section select', function() {
    var $listView = $('<div>');

    ok($listView.listView({
      sectionSelect: {
        label: 'testSectionSelect'
      },
      sections: {
        sectionA: {
          type: 'select',
          title: 'sectionA',
          listView: {
            fields: {
              fieldA: { label: 'testFieldA' },
              fieldB: { label: 'testFieldB' }
            },
            dataProvider: function(args) {
              args.response.success({
                data: [
                  {
                    fieldA: '1A',
                    fieldB: '1B'
                  },
                  {
                    fieldA: '2A',
                    fieldB: '2B'
                  }
                ]
              });
            }
          }
        },

        sectionB: {
          type: 'select',
          title: 'sectionB',
          listView: {
            fields: {
              fieldC: { label: 'testFieldC' },
              fieldD: { label: 'testFieldD' },
              fieldE: { label: 'testFieldE' }
            },
            dataProvider: function(args) {
              args.response.success({
                data: [
                  {
                    fieldC: '1C',
                    fieldD: '1D',
                    fieldE: '1E'
                  },
                  {
                    fieldC: '2C',
                    fieldD: '2D',
                    fieldE: '2E'
                  },
                  {
                    fieldC: '3C',
                    fieldD: '3D',
                    fieldE: '3E'
                  },
                  {
                    fieldC: '4C',
                    fieldD: '4D',
                    fieldE: '4E'
                  }
                ]
              });
            }
          }
        }
      }
    }));

    equal($listView.find('.toolbar .section-switcher').size(), 1,
          'Section switcher present in toolbar');
    equal($listView.find('.toolbar .section-switcher .section-select select').size(), 1,
          'Section select present');
    equal($listView.find('.toolbar .section-switcher .section-select label').html(),
          'testSectionSelect' + ':',
          'Section select label is correct');
    equal($listView.find('.toolbar .section-switcher .section-select select option').size(), 2,
          'Selectable sections present as options');
    equal($listView.find('.toolbar .section-switcher .section-select select option:first').html(), 'sectionA',
          'First select has correct title');
    equal($listView.find('.toolbar .section-switcher .section-select select option:selected')[0],
          $listView.find('.toolbar .section-switcher .section-select select option:first')[0],
          'First section option is selected by default');
    equal($listView.find('.toolbar .section-switcher .section-select select option:last').html(), 'sectionB', 'Last select has correct title');
    equal($listView.find('.list-view thead th').size(), 2, 'Correct list view column count present');
    equal($listView.find('.list-view thead th:first').html(), 'testFieldA', 'Column 1 is labeled correctly');
    equal($listView.find('.list-view thead th:last').html(), 'testFieldB', 'Column 2 is labeled correctly');
    equal($listView.find('.list-view tbody tr').size(), 2, 'Correct # of data rows present');
    equal($listView.find('.list-view tbody tr:first td').size(), 2, 'Correct # of data columns present');

    $listView.find('.toolbar .section-switcher select').val('sectionB');
    stop();
    ok($listView.find('.toolbar .section-switcher select').change(), 'Change section');
    start();

    equal($listView.find('.list-view thead th').size(), 3, 'Correct list view column count present');
    equal($listView.find('.list-view thead th:first').html(), 'testFieldC', 'Column 1 is labeled correctly');
    equal($($listView.find('.list-view thead th')[1]).html(), 'testFieldD', 'Column 2 is labeled correctly');
    equal($listView.find('.list-view thead th:last').html(), 'testFieldE', 'Column 3 is labeled correctly');
    equal($listView.find('.list-view tbody tr').size(), 4, 'Correct # of data rows present');
    equal($listView.find('.list-view tbody tr:first td').size(), 3, 'Correct # of data columns present');
    equal($listView.find('.list-view tbody tr:first td:first span').html(), '1C', 'First table cell has correct data');
    equal($listView.find('.list-view tbody tr:last td:last span').html(), '4E', 'Last table cell has correct data');

    $listView.find('.toolbar .section-switcher select').val('sectionA');
    stop();
    ok($listView.find('.toolbar .section-switcher select').change(), 'Change section');
    start();
    equal($listView.find('.toolbar .section-switcher .section-select select option:last').html(), 'sectionB', 'Last select has correct title');
    equal($listView.find('.list-view thead th').size(), 2, 'Correct list view column count present');
    equal($listView.find('.list-view thead th:first').html(), 'testFieldA', 'Column 1 is labeled correctly');
    equal($listView.find('.list-view thead th:last').html(), 'testFieldB', 'Column 2 is labeled correctly');
    equal($listView.find('.list-view tbody tr').size(), 2, 'Correct # of data rows present');
    equal($listView.find('.list-view tbody tr:first td').size(), 2, 'Correct # of data columns present');
    equal($listView.find('.list-view tbody tr:first td:first span').html(), '1A', 'First table cell has correct data');
    equal($listView.find('.list-view tbody tr:last td:last span').html(), '2B', 'Last table cell has correct data');
  });

  test('Basic sync action', function() {
    var $cloudStack = $('<div>').appendTo('#qunit-fixture');
    var listView = {
      listView: {
        section: 'test123',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' }
        },
        actions: {
          basicSync: {
            label: 'basicAction',
            messages: {
              confirm: function() {
                return 'basicActionConfirm';
              },
              notification: function() {
                return 'basicActionNotification';
              }
            },
            action: function(args) {
              args.response.success();
            }
          }
        },
        dataProvider: function(args) {
          args.response.success({
            data: [
              {
                fieldA: '1A',
                fieldB: '1B',
                fieldC: '1C'
              },
              {
                fieldA: '2A',
                fieldB: '2B',
                fieldC: '2C'
              }
            ]
          });
        }
      }
    };

    // CloudStack object is needed for notification handling for actions
    var cloudStack = {
      sections: {
        testActions: listView
      },

      home: 'testActions'
    };

    ok($cloudStack.cloudStack(cloudStack), 'Initialize cloudStack widget w/ list view');

    var $listView = $cloudStack.find('.list-view');

    equal($listView.find('table thead th').size(), 3, 'Correct header column count');
    equal($listView.find('table thead th.actions').size(), 1, 'Action header column present');
    equal($listView.find('table tbody tr:first td').size(), 3, 'Correct data column count');
    equal($listView.find('table tbody tr:first td.actions').size(), 1, 'Action data column present');
    equal($listView.find('table tbody tr:first td.actions .action').size(), 1, 'Correct action count');
    equal($listView.find('table tbody tr:first td.actions .action:first .icon').size(), 1, 'Action has icon');
    ok($listView.find('table tbody tr:first td.actions .action:first').hasClass('basicSync'),
       'First action has ID as CSS class');
    ok($listView.find('td.actions .action:first').click(), 'Click first action');
    equal($('.ui-dialog.confirm .message').html(), 'basicActionConfirm', 'Confirmation message present');

    // Make sure dialog is cleaned up -- always put below all confirm tests
    $(':ui-dialog, .overlay').appendTo('#qunit-fixture');

    ok($('.ui-dialog.confirm .ui-button.ok').click(), 'Confirm action');
    equal($cloudStack.find('.notifications .total span').html(), '1', 'Notification total increased');
    equal($('.notification-box ul li').size(), 1, 'Notification list has 1 item');
    equal($('.notification-box ul li span').html(), 'basicActionNotification', 'Notification list item has correct label');
    ok($('.notification-box ul li').hasClass('pending'), 'Notification has pending state');
    stop();
    setTimeout(function() {
      start();
      ok(!$('.notification-box ul li').hasClass('pending'), 'Notification has completed state');
    });
  });

  test('Async action', function() {
    var $cloudStack = $('<div>').appendTo('#qunit-fixture');
    var listView = {
      listView: {
        section: 'test123',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' }
        },
        actions: {
          asyncTest: {
            label: 'asyncAction',
            messages: {
              confirm: function() {
                return 'asyncActionConfirm';
              },
              notification: function() {
                return 'asyncActionNotification';
              }
            },
            action: function(args) {
              args.response.success();
            }
          }
        },
        dataProvider: function(args) {
          args.response.success({
            data: [
              {
                fieldA: '1A',
                fieldB: '1B',
                fieldC: '1C'
              },
              {
                fieldA: '2A',
                fieldB: '2B',
                fieldC: '2C'
              }
            ]
          });
        }
      }
    };

    // CloudStack object is needed for notification handling for actions
    var cloudStack = {
      sections: {
        testActions: listView
      },

      home: 'testActions'
    };

    ok($cloudStack.cloudStack(cloudStack), 'Initialize cloudStack widget w/ list view');

    var $listView = $cloudStack.find('.list-view');

    equal($listView.find('table thead th').size(), 3, 'Correct header column count');
    equal($listView.find('table thead th.actions').size(), 1, 'Action header column present');
    equal($listView.find('table tbody tr:first td').size(), 3, 'Correct data column count');
    equal($listView.find('table tbody tr:first td.actions').size(), 1, 'Action data column present');
    equal($listView.find('table tbody tr:first td.actions .action').size(), 1, 'Correct action count');
    equal($listView.find('table tbody tr:first td.actions .action:first .icon').size(), 1, 'Action has icon');
    ok($listView.find('table tbody tr:first td.actions .action:first').hasClass('asyncTest'),
       'First action has ID as CSS class');
    ok($listView.find('td.actions .action:first').click(), 'Click first action');
    equal($('.ui-dialog.confirm .message').html(), 'asyncActionConfirm', 'Confirmation message present');

    // Make sure dialog is cleaned up -- always put below all confirm tests
    $(':ui-dialog, .overlay').appendTo('#qunit-fixture');

    ok($('.ui-dialog.confirm .ui-button.ok').click(), 'Confirm action');
    equal($cloudStack.find('.notifications .total span').html(), '1', 'Notification total increased');
    equal($('.notification-box ul li').size(), 1, 'Notification list has 1 item');
    equal($('.notification-box ul li span').html(), 'asyncActionNotification', 'Notification list item has correct label');
    ok($('.notification-box ul li').hasClass('pending'), 'Notification has pending state');
    stop();
  });
}(jQuery));
<commit_msg>Add list view async action test<commit_after>(function($) {
  module('List view', {
    setup: function() {
      $.fx.off = true;
      cloudStack.debug = true;
    },
    teardown: function() {
      // Cleanup notification box
      $('.notification-box').remove();
    }
  });

  test('Basic', function() {
    var listView = {
      listView: {
        section: 'test123',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' }
        },
        dataProvider: function(args) {
          args.response.success({
            data: []
          });
        }
      }
    };
    var $listView;

    ok($listView = $('<div>').listView(listView), 'Initialize list view');
    equal($listView.find('.list-view').size(), 1, 'List view has container div');
    equal($listView.find('.list-view.test123').size(), 1, 'Container div has section ID as CSS class');
    equal($listView.find('.list-view table').size(), 2, 'List view has split tables');
    equal($listView.find('.list-view .fixed-header table thead tr').size(), 1, 'List view has fixed table header');
    equal($listView.find('.list-view .fixed-header table thead th').size(), 2, 'List view has correct column headers');
    equal($listView.find('.list-view .fixed-header table thead th:first').html(), 'testFieldA', 'First header has correct label');
    ok($listView.find('.list-view .fixed-header table thead th:first').hasClass('fieldA'), 'First header has correct class');
    ok($listView.find('.list-view .fixed-header table thead th:last').hasClass('fieldB'), 'First header has correct class');
    equal($listView.find('.list-view .fixed-header table thead th:last').html(), 'testFieldB', 'First header has correct label');
    equal($listView.find('.list-view table tbody tr').size(), 1, 'List view has table body');
    equal($listView.find('.toolbar').size(), 1, 'List view has toolbar');
    equal($listView.find('.toolbar .text-search .search-bar input[type=text]').size(), 1, 'Toolbar has search box');
    equal($listView.find('.toolbar .text-search .search-bar input[type=text]').size(), 1, 'Toolbar has search box');
    equal($listView.find('.toolbar .text-search .button.search').size(), 1, 'Toolbar has search button');
    ok(!$listView.find('.toolbar .filters').size(), 'Toolbar doesn\'t have filters');
  });

  test('Data provider', function() {
    var $listView = $('<div>');

    stop();
    $listView.listView({
      context: {
        tests: []
      },
      listView: {
        section: 'test',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' }
        },
        dataProvider: function(args) {
          start();
          equal(args.page, 1, 'Correct page # passed');
          equal(args.filterBy.search.value, '', 'No search params specified');
          ok($.isArray(args.context.tests), 'Context passed');
          args.response.success({
            data: [
              {
                fieldA: '1A',
                fieldB: '1B'
              },
              {
                fieldA: '2A',
                fieldB: '2B'
              },
              {
                fieldA: '3A',
                fieldB: '3B'
              }
            ]
          });
          stop();
          setTimeout(function() {
            start();
            equal($listView.find('tbody tr').size(), 3, 'Data row count is correct');
            equal($listView.find('tbody tr:first td.fieldA span').html(), '1A', 'Correct table item value for first row');
            equal($listView.find('tbody tr:first td.fieldB span').html(), '1B', 'Correct table item value for first row');
            equal($listView.find('tbody tr:last td.fieldA span').html(), '3A', 'Correct table item value for last row');
            equal($listView.find('tbody tr:last td.fieldB span').html(), '3B', 'Correct table item value for last row');
          });
        }
      }
    });
  });

  test('Pre-filter', function() {
    var $listView = $('<div>');

    stop();
    $listView.listView({
      listView: {
        section: 'test',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' },
          fieldC: { label: 'testFieldC' }
        },
        preFilter: function(args) {
          return ['fieldC'];
        },
        dataProvider: function(args) {
          args.response.success({
            data: [
              {
                fieldA: '1A',
                fieldB: '1B',
                fieldC: '1C'
              }
            ]
          });
          setTimeout(function() {
            start();
            equal($listView.find('thead th').size(), 2, 'Correct # of filtered columns present');
            equal($listView.find('tbody tr:first td').size(), 2, 'Correct # of body columns present');
            equal($listView.find('tbody tr:first td.fieldA span').html(), '1A', 'Correct table item value for data row');
            equal($listView.find('tbody tr:first td.fieldB span').html(), '1B', 'Correct table item value for data row');
          });
        }
      }
    });
  });

  test('Section select', function() {
    var $listView = $('<div>');

    ok($listView.listView({
      sectionSelect: {
        label: 'testSectionSelect'
      },
      sections: {
        sectionA: {
          type: 'select',
          title: 'sectionA',
          listView: {
            fields: {
              fieldA: { label: 'testFieldA' },
              fieldB: { label: 'testFieldB' }
            },
            dataProvider: function(args) {
              args.response.success({
                data: [
                  {
                    fieldA: '1A',
                    fieldB: '1B'
                  },
                  {
                    fieldA: '2A',
                    fieldB: '2B'
                  }
                ]
              });
            }
          }
        },

        sectionB: {
          type: 'select',
          title: 'sectionB',
          listView: {
            fields: {
              fieldC: { label: 'testFieldC' },
              fieldD: { label: 'testFieldD' },
              fieldE: { label: 'testFieldE' }
            },
            dataProvider: function(args) {
              args.response.success({
                data: [
                  {
                    fieldC: '1C',
                    fieldD: '1D',
                    fieldE: '1E'
                  },
                  {
                    fieldC: '2C',
                    fieldD: '2D',
                    fieldE: '2E'
                  },
                  {
                    fieldC: '3C',
                    fieldD: '3D',
                    fieldE: '3E'
                  },
                  {
                    fieldC: '4C',
                    fieldD: '4D',
                    fieldE: '4E'
                  }
                ]
              });
            }
          }
        }
      }
    }));

    equal($listView.find('.toolbar .section-switcher').size(), 1,
          'Section switcher present in toolbar');
    equal($listView.find('.toolbar .section-switcher .section-select select').size(), 1,
          'Section select present');
    equal($listView.find('.toolbar .section-switcher .section-select label').html(),
          'testSectionSelect' + ':',
          'Section select label is correct');
    equal($listView.find('.toolbar .section-switcher .section-select select option').size(), 2,
          'Selectable sections present as options');
    equal($listView.find('.toolbar .section-switcher .section-select select option:first').html(), 'sectionA',
          'First select has correct title');
    equal($listView.find('.toolbar .section-switcher .section-select select option:selected')[0],
          $listView.find('.toolbar .section-switcher .section-select select option:first')[0],
          'First section option is selected by default');
    equal($listView.find('.toolbar .section-switcher .section-select select option:last').html(), 'sectionB', 'Last select has correct title');
    equal($listView.find('.list-view thead th').size(), 2, 'Correct list view column count present');
    equal($listView.find('.list-view thead th:first').html(), 'testFieldA', 'Column 1 is labeled correctly');
    equal($listView.find('.list-view thead th:last').html(), 'testFieldB', 'Column 2 is labeled correctly');
    equal($listView.find('.list-view tbody tr').size(), 2, 'Correct # of data rows present');
    equal($listView.find('.list-view tbody tr:first td').size(), 2, 'Correct # of data columns present');

    $listView.find('.toolbar .section-switcher select').val('sectionB');
    stop();
    ok($listView.find('.toolbar .section-switcher select').change(), 'Change section');
    start();

    equal($listView.find('.list-view thead th').size(), 3, 'Correct list view column count present');
    equal($listView.find('.list-view thead th:first').html(), 'testFieldC', 'Column 1 is labeled correctly');
    equal($($listView.find('.list-view thead th')[1]).html(), 'testFieldD', 'Column 2 is labeled correctly');
    equal($listView.find('.list-view thead th:last').html(), 'testFieldE', 'Column 3 is labeled correctly');
    equal($listView.find('.list-view tbody tr').size(), 4, 'Correct # of data rows present');
    equal($listView.find('.list-view tbody tr:first td').size(), 3, 'Correct # of data columns present');
    equal($listView.find('.list-view tbody tr:first td:first span').html(), '1C', 'First table cell has correct data');
    equal($listView.find('.list-view tbody tr:last td:last span').html(), '4E', 'Last table cell has correct data');

    $listView.find('.toolbar .section-switcher select').val('sectionA');
    stop();
    ok($listView.find('.toolbar .section-switcher select').change(), 'Change section');
    start();
    equal($listView.find('.toolbar .section-switcher .section-select select option:last').html(), 'sectionB', 'Last select has correct title');
    equal($listView.find('.list-view thead th').size(), 2, 'Correct list view column count present');
    equal($listView.find('.list-view thead th:first').html(), 'testFieldA', 'Column 1 is labeled correctly');
    equal($listView.find('.list-view thead th:last').html(), 'testFieldB', 'Column 2 is labeled correctly');
    equal($listView.find('.list-view tbody tr').size(), 2, 'Correct # of data rows present');
    equal($listView.find('.list-view tbody tr:first td').size(), 2, 'Correct # of data columns present');
    equal($listView.find('.list-view tbody tr:first td:first span').html(), '1A', 'First table cell has correct data');
    equal($listView.find('.list-view tbody tr:last td:last span').html(), '2B', 'Last table cell has correct data');
  });

  test('Basic sync action', function() {
    var $cloudStack = $('<div>').appendTo('#qunit-fixture');
    var listView = {
      listView: {
        section: 'test123',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' }
        },
        actions: {
          basicSync: {
            label: 'basicAction',
            messages: {
              confirm: function() {
                return 'basicActionConfirm';
              },
              notification: function() {
                return 'basicActionNotification';
              }
            },
            action: function(args) {
              args.response.success();
            }
          }
        },
        dataProvider: function(args) {
          args.response.success({
            data: [
              {
                fieldA: '1A',
                fieldB: '1B',
                fieldC: '1C'
              },
              {
                fieldA: '2A',
                fieldB: '2B',
                fieldC: '2C'
              }
            ]
          });
        }
      }
    };

    // CloudStack object is needed for notification handling for actions
    var cloudStack = {
      sections: {
        testActions: listView
      },

      home: 'testActions'
    };

    ok($cloudStack.cloudStack(cloudStack), 'Initialize cloudStack widget w/ list view');

    var $listView = $cloudStack.find('.list-view');

    equal($listView.find('table thead th').size(), 3, 'Correct header column count');
    equal($listView.find('table thead th.actions').size(), 1, 'Action header column present');
    equal($listView.find('table tbody tr:first td').size(), 3, 'Correct data column count');
    equal($listView.find('table tbody tr:first td.actions').size(), 1, 'Action data column present');
    equal($listView.find('table tbody tr:first td.actions .action').size(), 1, 'Correct action count');
    equal($listView.find('table tbody tr:first td.actions .action:first .icon').size(), 1, 'Action has icon');
    ok($listView.find('table tbody tr:first td.actions .action:first').hasClass('basicSync'),
       'First action has ID as CSS class');
    ok($listView.find('td.actions .action:first').click(), 'Click first action');
    equal($('.ui-dialog.confirm .message').html(), 'basicActionConfirm', 'Confirmation message present');

    // Make sure dialog is cleaned up -- always put below all confirm tests
    $(':ui-dialog, .overlay').appendTo('#qunit-fixture');

    ok($('.ui-dialog.confirm .ui-button.ok').click(), 'Confirm action');
    equal($cloudStack.find('.notifications .total span').html(), '1', 'Notification total increased');
    equal($('.notification-box ul li').size(), 1, 'Notification list has 1 item');
    equal($('.notification-box ul li span').html(), 'basicActionNotification', 'Notification list item has correct label');
    ok($('.notification-box ul li').hasClass('pending'), 'Notification has pending state');
    stop();
    setTimeout(function() {
      start();
      ok(!$('.notification-box ul li').hasClass('pending'), 'Notification has completed state');
    });
  });

  test('Async action', function() {
    var $cloudStack = $('<div>').appendTo('#qunit-fixture');
    var listView = {
      listView: {
        id: 'job123',
        fields: {
          fieldA: { label: 'testFieldA' },
          fieldB: { label: 'testFieldB' }
        },
        actions: {
          asyncTest: {
            label: 'asyncAction',
            messages: {
              confirm: function() {
                return 'asyncActionConfirm';
              },
              notification: function() {
                return 'asyncActionNotification';
              }
            },
            action: function(args) {
              args.response.success({
                _custom: {
                  jobId: 'job123'
                }
              });
            },
            notification: {
              interval: 0,
              poll: function(args) {
                start();
                equal(args._custom.jobId, 'job123', 'Job ID passed correctly');
                args.complete();
              }
            }
          }
        },
        dataProvider: function(args) {
          args.response.success({
            data: [
              {
                fieldA: '1A',
                fieldB: '1B',
                fieldC: '1C'
              },
              {
                fieldA: '2A',
                fieldB: '2B',
                fieldC: '2C'
              }
            ]
          });
        }
      }
    };

    // CloudStack object is needed for notification handling for actions
    var cloudStack = {
      sections: {
        testActions: listView
      },

      home: 'testActions'
    };

    ok($cloudStack.cloudStack(cloudStack), 'Initialize cloudStack widget w/ list view');

    var $listView = $cloudStack.find('.list-view');

    equal($listView.find('table thead th').size(), 3, 'Correct header column count');
    equal($listView.find('table thead th.actions').size(), 1, 'Action header column present');
    equal($listView.find('table tbody tr:first td').size(), 3, 'Correct data column count');
    equal($listView.find('table tbody tr:first td.actions').size(), 1, 'Action data column present');
    equal($listView.find('table tbody tr:first td.actions .action').size(), 1, 'Correct action count');
    equal($listView.find('table tbody tr:first td.actions .action:first .icon').size(), 1, 'Action has icon');
    ok($listView.find('table tbody tr:first td.actions .action:first').hasClass('asyncTest'),
       'First action has ID as CSS class');
    ok($listView.find('td.actions .action:first').click(), 'Click first action');
    equal($('.ui-dialog.confirm .message').html(), 'asyncActionConfirm', 'Confirmation message present');

    // Make sure dialog is cleaned up -- always put below all confirm tests
    $(':ui-dialog, .overlay').appendTo('#qunit-fixture');

    ok($('.ui-dialog.confirm .ui-button.ok').click(), 'Confirm action');
    equal($cloudStack.find('.notifications .total span').html(), '1', 'Notification total increased');
    equal($('.notification-box ul li').size(), 1, 'Notification list has 1 item');
    equal($('.notification-box ul li span').html(), 'asyncActionNotification', 'Notification list item has correct label');
    ok($('.notification-box ul li').hasClass('pending'), 'Notification has pending state');
    stop();
  });
}(jQuery));
<|endoftext|>


<commit_before>//By Rajeshwar Patlolla
//https://github.com/rajeshwarpatlolla

'use strict';
angular.module('ionic-datepicker', ['ionic', 'ionic-datepicker.template'])

  .directive('ionicDatepicker', ['$ionicPopup', function ($ionicPopup) {
    return {
      restrict: 'AE',
      replace: true,
      scope: {
        value: '=',
        weekBeginsOnMonday: '=',
        min: '=',
        max: '='
      },
      link: function (scope, element, attrs) {
        var currentDate = angular.copy(scope.value) || new Date(); // Date for the UI calendar display
        scope.highlightedDate = angular.copy(currentDate); // Temporary selected date before the 'Set' or 'Today' is pressed

        scope.dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
        if (scope.weekBeginsOnMonday || false) {
          scope.dayInitials.push(scope.dayInitials.shift());
        }

        scope.calendarRows = new Array(6);
        scope.calendarCols = new Array(7);

        var refreshCalendar = function (currentDate) {
          scope.dayList = [];
          var firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDate();
          var lastDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
          for (var i = firstDay; i <= lastDay; i++) {
            var tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), i);
            scope.dayList.push({
              date: tempDate,
              day: tempDate.getDate()
            });
          }
          // Offset the first of the month to match the week day
          firstDay = scope.dayList[0].date.getDay();
          if (scope.weekBeginsOnMonday || false) {
            firstDay--;
          }
          for (var j = 0; j < firstDay; j++) {
            scope.dayList.unshift(undefined);
          }
          scope.currentMonth = currentDate.getMonth();
          scope.currentYear = currentDate.getFullYear();
        };

        // Go to previous month
        scope.prevMonth = function () {
          var previousLastDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 0);
          if (undefined !== scope.min && previousLastDay < scope.min) {
            return;
          }
          currentDate.setMonth(currentDate.getMonth() - 1);
          scope.currentMonth = currentDate.getMonth();
          scope.currentYear = currentDate.getFullYear();
          refreshCalendar(currentDate)
        };

        // Go to next month
        scope.nextMonth = function () {
          var nextFirstDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
          if (undefined !== scope.max && nextFirstDay > scope.max) {
            return;
          }
          currentDate.setMonth(currentDate.getMonth() + 1);
          scope.currentMonth = currentDate.getMonth();
          scope.currentYear = currentDate.getFullYear();
          refreshCalendar(currentDate)
        };

        scope.highlightDate = function (date) {
          if (typeof date !== 'undefined') {
            // Minimum date
            if (undefined !== scope.min && date < scope.min) {
              return;
            }
            // Maximum date
            if (undefined !== scope.max && date > scope.max) {
              return;
            }
            scope.highlightedDate = angular.copy(date);
            currentDate = date;
            refreshCalendar(currentDate);
          }
        };

        // Check if the day is valid (for CSS class)
        scope.isValidDay = function (rowIndex, colIndex) {
          var day = scope.dayList[rowIndex * 7 + colIndex];
          return (typeof day !== 'undefined');
        };

        // Check if the day should be selected (for CSS class)
        scope.isSelected = function (rowIndex, colIndex) {
          if (scope.isValidDay(rowIndex, colIndex)) {
            var day = scope.dayList[rowIndex * 7 + colIndex];
            return (day.date.toDateString() === scope.highlightedDate.toDateString());
          }
          return false;
        };

        // Check if the day is today (for CSS class)
        scope.isToday = function (rowIndex, colIndex) {
          if (scope.isValidDay(rowIndex, colIndex)) {
            return (scope.dayList[rowIndex * 7 + colIndex].date.toDateString() === new Date().toDateString());
          }
          return false;
        }

        // Check if the day should be disabled (for CSS class)
        scope.isDisabled = function (rowIndex, colIndex) {
          if (scope.isValidDay(rowIndex, colIndex)) {
            // Minimum date
            if (scope.dayList[rowIndex * 7 + colIndex].date < scope.min) {
              return true;
            }
            // Maximum date
            if (scope.dayList[rowIndex * 7 + colIndex].date > scope.max) {
              return true;
            }
          }
          return false;
        }

        element.on('click', function () {
          refreshCalendar(currentDate);
          $ionicPopup.show({
            templateUrl: 'date-picker-modal.html',
            title: 'Select Date',
            subTitle: '',
            scope: scope,
            buttons: [
              { text: 'Close' },
              {
                text: 'Today',
                onTap: function (e) {
                  var now = new Date();
                  // Minimum date
                  if (undefined !== scope.min && now < scope.min) {
                    e.preventDefault();
                    return;
                  }
                  // Maxumum date
                  if (undefined !== scope.max && now > scope.max) {
                    e.preventDefault();
                    return;
                  }
                  scope.highlightDate(now);
                  scope.value = now;
                }
              },
              {
                text: 'Set',
                type: 'button-positive',
                onTap: function () {
                  scope.highlightDate(scope.highlightedDate);
                  scope.value = scope.highlightedDate;
                }
              }
            ]
          })
        })
      }
    }
  }]);
<commit_msg>Fix wrong template name<commit_after>//By Rajeshwar Patlolla
//https://github.com/rajeshwarpatlolla

'use strict';
angular.module('ionic-datepicker', ['ionic', 'ionic-datepicker.template'])

  .directive('ionicDatepicker', ['$ionicPopup', function ($ionicPopup) {
    return {
      restrict: 'AE',
      replace: true,
      scope: {
        value: '=',
        weekBeginsOnMonday: '=',
        min: '=',
        max: '='
      },
      link: function (scope, element, attrs) {
        var currentDate = angular.copy(scope.value) || new Date(); // Date for the UI calendar display
        scope.highlightedDate = angular.copy(currentDate); // Temporary selected date before the 'Set' or 'Today' is pressed

        scope.dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
        if (scope.weekBeginsOnMonday || false) {
          scope.dayInitials.push(scope.dayInitials.shift());
        }

        scope.calendarRows = new Array(6);
        scope.calendarCols = new Array(7);

        var refreshCalendar = function (currentDate) {
          scope.dayList = [];
          var firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDate();
          var lastDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
          for (var i = firstDay; i <= lastDay; i++) {
            var tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), i);
            scope.dayList.push({
              date: tempDate,
              day: tempDate.getDate()
            });
          }
          // Offset the first of the month to match the week day
          firstDay = scope.dayList[0].date.getDay();
          if (scope.weekBeginsOnMonday || false) {
            firstDay--;
          }
          for (var j = 0; j < firstDay; j++) {
            scope.dayList.unshift(undefined);
          }
          scope.currentMonth = currentDate.getMonth();
          scope.currentYear = currentDate.getFullYear();
        };

        // Go to previous month
        scope.prevMonth = function () {
          var previousLastDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 0);
          if (undefined !== scope.min && previousLastDay < scope.min) {
            return;
          }
          currentDate.setMonth(currentDate.getMonth() - 1);
          scope.currentMonth = currentDate.getMonth();
          scope.currentYear = currentDate.getFullYear();
          refreshCalendar(currentDate)
        };

        // Go to next month
        scope.nextMonth = function () {
          var nextFirstDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
          if (undefined !== scope.max && nextFirstDay > scope.max) {
            return;
          }
          currentDate.setMonth(currentDate.getMonth() + 1);
          scope.currentMonth = currentDate.getMonth();
          scope.currentYear = currentDate.getFullYear();
          refreshCalendar(currentDate)
        };

        scope.highlightDate = function (date) {
          if (typeof date !== 'undefined') {
            // Minimum date
            if (undefined !== scope.min && date < scope.min) {
              return;
            }
            // Maximum date
            if (undefined !== scope.max && date > scope.max) {
              return;
            }
            scope.highlightedDate = angular.copy(date);
            currentDate = date;
            refreshCalendar(currentDate);
          }
        };

        // Check if the day is valid (for CSS class)
        scope.isValidDay = function (rowIndex, colIndex) {
          var day = scope.dayList[rowIndex * 7 + colIndex];
          return (typeof day !== 'undefined');
        };

        // Check if the day should be selected (for CSS class)
        scope.isSelected = function (rowIndex, colIndex) {
          if (scope.isValidDay(rowIndex, colIndex)) {
            var day = scope.dayList[rowIndex * 7 + colIndex];
            return (day.date.toDateString() === scope.highlightedDate.toDateString());
          }
          return false;
        };

        // Check if the day is today (for CSS class)
        scope.isToday = function (rowIndex, colIndex) {
          if (scope.isValidDay(rowIndex, colIndex)) {
            return (scope.dayList[rowIndex * 7 + colIndex].date.toDateString() === new Date().toDateString());
          }
          return false;
        }

        // Check if the day should be disabled (for CSS class)
        scope.isDisabled = function (rowIndex, colIndex) {
          if (scope.isValidDay(rowIndex, colIndex)) {
            // Minimum date
            if (scope.dayList[rowIndex * 7 + colIndex].date < scope.min) {
              return true;
            }
            // Maximum date
            if (scope.dayList[rowIndex * 7 + colIndex].date > scope.max) {
              return true;
            }
          }
          return false;
        }

        element.on('click', function () {
          refreshCalendar(currentDate);
          $ionicPopup.show({
            templateUrl: 'ionic-datepicker.template.html',
            title: 'Select Date',
            subTitle: '',
            scope: scope,
            buttons: [
              { text: 'Close' },
              {
                text: 'Today',
                onTap: function (e) {
                  var now = new Date();
                  // Minimum date
                  if (undefined !== scope.min && now < scope.min) {
                    e.preventDefault();
                    return;
                  }
                  // Maxumum date
                  if (undefined !== scope.max && now > scope.max) {
                    e.preventDefault();
                    return;
                  }
                  scope.highlightDate(now);
                  scope.value = now;
                }
              },
              {
                text: 'Set',
                type: 'button-positive',
                onTap: function () {
                  scope.highlightDate(scope.highlightedDate);
                  scope.value = scope.highlightedDate;
                }
              }
            ]
          })
        })
      }
    }
  }]);
<|endoftext|>