<commit_before>google.charts.load('current', {packages: ['corechart', 'line']});

// Render a chart visual on dashboard page for tracking class visits by date
function drawBasic() {

  // Set up the data table to have a class name and visits associated w/ that specific class
  var data = new google.visualization.DataTable();
  data.addColumn('date', 'Date');
  data.addColumn('number', 'Visits');

  // Organize visit data through visit-by-date servlet
  fetch(`/visit-date?classCode=` + getParam("classCode"))
    .then(response => response.json()).then(visits=> {

    var dates = visits.dates;

    // Convert JSON date format to Date type
    for (var k = 0; k < dates.length; k++) {
      var dateStr = dates[k];
      var realDate = new Date(dateStr);
      dates[k] = realDate;
    }

    var numVisits = visits.classVisits;

    var tempDataHolder = []; // To be pushed into datatable after updating

    // Loop through both lists and add info sets for each class
    for (var i = 0; i < dates.length; i++) {
      tempDataHolder.push([dates[i], numVisits[i]]);
    }

    data.addRows(tempDataHolder); // Populate datatable with final data

    var options = {
      title: 'Number of Student Office Hour Visits',
      hAxis: {
        format: 'M/d/yy',
        title: 'Date',
        textStyle: {
          bold:true
        },
      },
      vAxis: {
        title: 'Number of Visits',
        textStyle: {
          bold:true
        },
      },
      backgroundColor: {
        fill: '#D6EBFF',
        stroke: '#031430',
        strokeWidth: 5
      },
    };

    var chart = new google.visualization.LineChart(
    document.getElementById("visit-chart"));

    chart.draw(data, options);

  });
}

// Render a chart visual on dashboard page for tracking average wait time by date
function drawTime() {

  // Set up the data table to have a class name and wait average associated w/ that specific class
  var data = new google.visualization.DataTable();
  data.addColumn('date', 'Date');
  data.addColumn('number', 'Wait');

  // Organize wait data through wait-time servlet
  fetch(`/wait-time?classCode=` + getParam("classCode"))
    .then(response => response.json()).then(waits=> {

    var dates = waits.dates;

    // Convert JSON date format to Date type
    for (var k = 0; k < dates.length; k++) {
      var dateStr = dates[k];
      var realDate = new Date(dateStr);
      dates[k] = realDate;
    }

    var waitAverages = waits.waitTimes;

    // Convert all times to minutes
    for (var j = 0; j < waitAverages.length; j++) {
      var timeInSeconds = waitAverages[j];
      var timeInMinutes = timeInSeconds / 60.0;
      waitAverages[j] = timeInMinutes.toFixed(2);  // Round to 2 decimal places
    }

    var tempDataHolder = []; // To be pushed into datatable after updating

    // Loop through both lists and add info sets for each class
    for (var i = 0; i < dates.length; i++) {
      tempDataHolder.push([dates[i], Number(waitAverages[i])]);
    }

    data.addRows(tempDataHolder); // Populate datatable with final data

    var options = {
      title: 'Average Wait Time by Date',
      hAxis: {
        format: 'M/d/yy',
        title: 'Date',
        textStyle: {
          bold:true
        },
      },
      vAxis: {
        title: 'Wait Time (minutes)',
        format: '0.00',
        textStyle: {
          bold:true
        },
      },
      backgroundColor: {
        fill: '#D6EBFF',
        stroke: '#031430',
        strokeWidth: 5
      },
    };

    const waitChart = document.getElementById("wait-chart");
    waitChart.classList.remove("hidden");
    var chart = new google.visualization.LineChart(waitChart);

    chart.draw(data, options);
    waitChart.classList.add("hidden");
  });
}

google.charts.setOnLoadCallback(drawBasic);
google.charts.setOnLoadCallback(drawTime);

/* Creates a <li> element for every item in json */
function createListElement(text) {
  const liElement = document.createElement('li');
  liElement.innerText = text;
  return liElement;
}

// Provide a link to the TA queue and display class code
function setRedirect(){
  var params = window.location.search;
  document.getElementById("redirect").onclick = () => window.location.href = "/queue/ta.html" + params;
  document.getElementById("classCode").innerText =  params.slice(11);

  // Get TA participants
  fetch(`/ta-participants?classCode=` + getParam("classCode")).then(response => response.json()).then((list) => {
    const listElement = document.getElementById('classTAList');
    listElement.innerHTML = '';

    // Use HTML to display each message
    for (var i = 0; i < list.length; i++) {
      listElement.appendChild(
        createListElement(list[i]));
    }
  });

  // Get student participants
  fetch(`/student-participants?classCode=` + getParam("classCode")).then(response => response.json()).then((list) => {
    const listElement = document.getElementById('classStudentList');
    listElement.innerHTML = '';

    // Use HTML to display each message
    for (var i = 0; i < list.length; i++) {
      listElement.appendChild(
        createListElement(list[i]));
    }
  });
}

// Obtain the class's specific code from URL parameter
function getClassCode() {
  document.getElementById("hiddenClassCode").value = getParam("classCode");
  return true;
}

// Obtain the class's specific code from URL parameter
function getRosterClassCode() {
  document.getElementById("hiddenRosterClassCode").value = getParam("classCode");
  return true;
}

function addEnvRow(name, status) {

  const template = document.getElementById("envRowTemplate");
  const copy = template.content.cloneNode(true).querySelector("tr");

  copy.querySelector(".envName").innerText = name;
  copy.querySelector(".envStatus").innerText = status;

  const deleteButton = copy.querySelector(".envDelete");
  deleteButton.disabled = status !== "ready" && status !== "failed";

  document.getElementById("envTable").appendChild(copy);

  return copy;
}

function checkDeletionStatus(envID, row) {
  getToken().then(tok => fetch(`/environment?envID=${envID}&idToken=${tok}`).then(resp => {
    if (resp.status === 404) {
      row.remove();
    } else {
      setTimeout(() => checkDeletionStatus(envID, row), 1000);
    }
  }));
}

function checkEnvStatus(envID, row) {
  getToken().then(tok => {
    fetch(`/environment?envID=${envID}&idToken=${tok}`).then(resp => resp.ok ? resp.json() : "failed").then(env => {
      row.querySelector(".envStatus").innerText = env.status;

      if (env.status === "pulling") {
        setTimeout(() => checkEnvStatus(envID, row), 1000);
      } else {
        const deleteButton = row.querySelector(".envDelete");
        deleteButton.disabled = false;
        deleteButton.onclick = () => {
          row.querySelector(".envStatus").innerText = "deleting";
          getToken().then(tok => fetch(`/environment?envID=${envID}&idToken=${tok}`, {method: 'DELETE'}));
          checkDeletionStatus(envID, row);
        };
      }
    });
  });
}

function pullImage() {
  const name = document.getElementById("envName").value;
  const image = document.getElementById("envImage").value;
  const tag = document.getElementById("envTag").value;
  const row = addEnvRow(name, "queueing");

  getToken().then(tok => {
    fetch(`/queueEnvPull?classID=${getParam("classCode")}&name=${name}&image=${image}&tag=${tag}&idToken=${tok}`)
      .then(resp => resp.text()).then(envID => {
        checkEnvStatus(envID, row);
    });
  });
}

function getEnvs() {
  getToken().then(tok => {
    fetch(`/getEnvironments?classID=${getParam("classCode")}&idToken=${tok}`).then(resp => resp.json()).then(envs => {

      for (var env of envs) {
       const row = addEnvRow(env.name, env.status);

       row.querySelector(".envDelete").onclick = () => {
        row.querySelector(".envStatus").innerText = "deleting";
        fetch(`/environment?envID=${env.id}`, {method: 'DELETE'});
        checkDeletionStatus(env.id, row);
       };
      }
    });
  });
}

function displayClass(){
  getToken().then((token) => {
    var params = window.location.search + "&idToken=" + token;

    const nameRequest = new Request("/get-class" + params, {method: "GET"});
    fetch(nameRequest).then(response => response.json()).then((name) => {
      document.getElementById("className").innerText = name;
    });
  });
}

function displayDelete(){
  getToken().then((token) => {
    var params = window.location.search + "&idToken=" + token;

    const displayRequest = new Request("/get-role" + params, {method: "GET"});
    fetch(displayRequest).then(response => response.json()).then((role) => {
      var elem = document.getElementById("delete");
      if (role !== "owner"){
        elem.style.display = "none";
      }
    });
  });
}

function deleteClass(){
  getToken().then((token) => {
    var params = window.location.search + "&idToken=" + token;
    const request = new Request("/delete-class" + params, {method: "POST"});
    fetch(request).then(response => {
      window.location.assign("/userDash.html");
    });
  });
}

function onload() {
  setRedirect();

  firebase.auth().onAuthStateChanged(function(user) {
    displayClass();
    displayDelete();
    getEnvs();
  });
}

function switchTab(tabName) {
  const tabs = document.getElementsByClassName("tab");

  for (let tab of tabs) {
    tab.classList.remove("active-tab");
    tab.classList.add("inactive-tab");
  }

  const charts = document.getElementsByClassName("chart");

  for (let chart of charts) {
    chart.classList.add("hidden");
  }

  const tab = document.getElementById(tabName + "-tab");
  tab.classList.remove("inactive-tab");
  tab.classList.add("active-tab");

  document.getElementById(tabName + "-chart").classList.remove("hidden");
}
<commit_msg>Update src/main/webapp/dashboard.js<commit_after>google.charts.load('current', {packages: ['corechart', 'line']});

// Render a chart visual on dashboard page for tracking class visits by date
function drawBasic() {

  // Set up the data table to have a class name and visits associated w/ that specific class
  var data = new google.visualization.DataTable();
  data.addColumn('date', 'Date');
  data.addColumn('number', 'Visits');

  // Organize visit data through visit-by-date servlet
  fetch(`/visit-date?classCode=` + getParam("classCode"))
    .then(response => response.json()).then(visits=> {

    var dates = visits.dates;

    // Convert JSON date format to Date type
    for (var k = 0; k < dates.length; k++) {
      var dateStr = dates[k];
      var realDate = new Date(dateStr);
      dates[k] = realDate;
    }

    var numVisits = visits.classVisits;

    var tempDataHolder = []; // To be pushed into datatable after updating

    // Loop through both lists and add info sets for each class
    for (var i = 0; i < dates.length; i++) {
      tempDataHolder.push([dates[i], numVisits[i]]);
    }

    data.addRows(tempDataHolder); // Populate datatable with final data

    var options = {
      title: 'Number of Student Office Hour Visits',
      hAxis: {
        format: 'M/d/yy',
        title: 'Date',
        textStyle: {
          bold:true
        },
      },
      vAxis: {
        title: 'Number of Visits',
        textStyle: {
          bold:true
        },
      },
      backgroundColor: {
        fill: '#D6EBFF',
        stroke: '#031430',
        strokeWidth: 5
      },
    };

    var chart = new google.visualization.LineChart(
    document.getElementById("visit-chart"));

    chart.draw(data, options);

  });
}

// Render a chart visual on dashboard page for tracking average wait time by date
function drawTime() {

  // Set up the data table to have a class name and wait average associated w/ that specific class
  var data = new google.visualization.DataTable();
  data.addColumn('date', 'Date');
  data.addColumn('number', 'Wait');

  // Organize wait data through wait-time servlet
  fetch(`/wait-time?classCode=` + getParam("classCode"))
    .then(response => response.json()).then(waits=> {

    var dates = waits.dates;

    // Convert JSON date format to Date type
    for (var k = 0; k < dates.length; k++) {
      var dateStr = dates[k];
      var realDate = new Date(dateStr);
      dates[k] = realDate;
    }

    var waitAverages = waits.waitTimes;

    // Convert all times to minutes
    for (var j = 0; j < waitAverages.length; j++) {
      var timeInSeconds = waitAverages[j];
      var timeInMinutes = timeInSeconds / 60.0;
      waitAverages[j] = timeInMinutes.toFixed(2);  // Round to 2 decimal places
    }

    var tempDataHolder = []; // To be pushed into datatable after updating

    // Loop through both lists and add info sets for each class
    for (var i = 0; i < dates.length; i++) {
      tempDataHolder.push([dates[i], Number(waitAverages[i])]);
    }

    data.addRows(tempDataHolder); // Populate datatable with final data

    var options = {
      title: 'Average Wait Time by Date',
      hAxis: {
        format: 'M/d/yy',
        title: 'Date',
        textStyle: {
          bold:true
        },
      },
      vAxis: {
        title: 'Wait Time (minutes)',
        format: '0.00',
        textStyle: {
          bold:true
        },
      },
      backgroundColor: {
        fill: '#D6EBFF',
        stroke: '#031430',
        strokeWidth: 5
      },
    };

    const waitChart = document.getElementById("wait-chart");
    waitChart.classList.remove("hidden");
    var chart = new google.visualization.LineChart(waitChart);

    chart.draw(data, options);
    waitChart.classList.add("hidden");
  });
}

google.charts.setOnLoadCallback(drawBasic);
google.charts.setOnLoadCallback(drawTime);

/* Creates a <li> element for every item in json */
function createListElement(text) {
  const liElement = document.createElement('li');
  liElement.innerText = text;
  return liElement;
}

// Provide a link to the TA queue and display class code
function setRedirect(){
  var params = window.location.search;
  document.getElementById("redirect").onclick = () => window.location.href = "/queue/ta.html" + params;
  document.getElementById("classCode").innerText =  params.slice(11);

  // Get TA participants
  fetch(`/ta-participants?classCode=` + getParam("classCode")).then(response => response.json()).then((list) => {
    const listElement = document.getElementById('classTAList');
    listElement.innerHTML = '';

    // Use HTML to display each message
    for (var i = 0; i < list.length; i++) {
      listElement.appendChild(
        createListElement(list[i]));
    }
  });

  // Get student participants
  fetch(`/student-participants?classCode=` + getParam("classCode")).then(response => response.json()).then((list) => {
    const listElement = document.getElementById('classStudentList');
    listElement.innerHTML = '';

    // Use HTML to display each user
    for (var i = 0; i < list.length; i++) {
      listElement.appendChild(
        createListElement(list[i]));
    }
  });
}

// Obtain the class's specific code from URL parameter
function getClassCode() {
  document.getElementById("hiddenClassCode").value = getParam("classCode");
  return true;
}

// Obtain the class's specific code from URL parameter
function getRosterClassCode() {
  document.getElementById("hiddenRosterClassCode").value = getParam("classCode");
  return true;
}

function addEnvRow(name, status) {

  const template = document.getElementById("envRowTemplate");
  const copy = template.content.cloneNode(true).querySelector("tr");

  copy.querySelector(".envName").innerText = name;
  copy.querySelector(".envStatus").innerText = status;

  const deleteButton = copy.querySelector(".envDelete");
  deleteButton.disabled = status !== "ready" && status !== "failed";

  document.getElementById("envTable").appendChild(copy);

  return copy;
}

function checkDeletionStatus(envID, row) {
  getToken().then(tok => fetch(`/environment?envID=${envID}&idToken=${tok}`).then(resp => {
    if (resp.status === 404) {
      row.remove();
    } else {
      setTimeout(() => checkDeletionStatus(envID, row), 1000);
    }
  }));
}

function checkEnvStatus(envID, row) {
  getToken().then(tok => {
    fetch(`/environment?envID=${envID}&idToken=${tok}`).then(resp => resp.ok ? resp.json() : "failed").then(env => {
      row.querySelector(".envStatus").innerText = env.status;

      if (env.status === "pulling") {
        setTimeout(() => checkEnvStatus(envID, row), 1000);
      } else {
        const deleteButton = row.querySelector(".envDelete");
        deleteButton.disabled = false;
        deleteButton.onclick = () => {
          row.querySelector(".envStatus").innerText = "deleting";
          getToken().then(tok => fetch(`/environment?envID=${envID}&idToken=${tok}`, {method: 'DELETE'}));
          checkDeletionStatus(envID, row);
        };
      }
    });
  });
}

function pullImage() {
  const name = document.getElementById("envName").value;
  const image = document.getElementById("envImage").value;
  const tag = document.getElementById("envTag").value;
  const row = addEnvRow(name, "queueing");

  getToken().then(tok => {
    fetch(`/queueEnvPull?classID=${getParam("classCode")}&name=${name}&image=${image}&tag=${tag}&idToken=${tok}`)
      .then(resp => resp.text()).then(envID => {
        checkEnvStatus(envID, row);
    });
  });
}

function getEnvs() {
  getToken().then(tok => {
    fetch(`/getEnvironments?classID=${getParam("classCode")}&idToken=${tok}`).then(resp => resp.json()).then(envs => {

      for (var env of envs) {
       const row = addEnvRow(env.name, env.status);

       row.querySelector(".envDelete").onclick = () => {
        row.querySelector(".envStatus").innerText = "deleting";
        fetch(`/environment?envID=${env.id}`, {method: 'DELETE'});
        checkDeletionStatus(env.id, row);
       };
      }
    });
  });
}

function displayClass(){
  getToken().then((token) => {
    var params = window.location.search + "&idToken=" + token;

    const nameRequest = new Request("/get-class" + params, {method: "GET"});
    fetch(nameRequest).then(response => response.json()).then((name) => {
      document.getElementById("className").innerText = name;
    });
  });
}

function displayDelete(){
  getToken().then((token) => {
    var params = window.location.search + "&idToken=" + token;

    const displayRequest = new Request("/get-role" + params, {method: "GET"});
    fetch(displayRequest).then(response => response.json()).then((role) => {
      var elem = document.getElementById("delete");
      if (role !== "owner"){
        elem.style.display = "none";
      }
    });
  });
}

function deleteClass(){
  getToken().then((token) => {
    var params = window.location.search + "&idToken=" + token;
    const request = new Request("/delete-class" + params, {method: "POST"});
    fetch(request).then(response => {
      window.location.assign("/userDash.html");
    });
  });
}

function onload() {
  setRedirect();

  firebase.auth().onAuthStateChanged(function(user) {
    displayClass();
    displayDelete();
    getEnvs();
  });
}

function switchTab(tabName) {
  const tabs = document.getElementsByClassName("tab");

  for (let tab of tabs) {
    tab.classList.remove("active-tab");
    tab.classList.add("inactive-tab");
  }

  const charts = document.getElementsByClassName("chart");

  for (let chart of charts) {
    chart.classList.add("hidden");
  }

  const tab = document.getElementById(tabName + "-tab");
  tab.classList.remove("inactive-tab");
  tab.classList.add("active-tab");

  document.getElementById(tabName + "-chart").classList.remove("hidden");
}
<|endoftext|>


<commit_before>/**
 * GridView
 *
 * Created by GUY on 2016/1/11.
 * @class BI.GridView
 * @extends BI.Widget
 */
BI.GridView = BI.inherit(BI.Widget, {
    _defaultConfig: function () {
        return BI.extend(BI.GridView.superclass._defaultConfig.apply(this, arguments), {
            baseCls: "bi-grid-view",
            // width: 400, //必设
            // height: 300, //必设
            overflowX: true,
            overflowY: true,
            overscanColumnCount: 0,
            overscanRowCount: 0,
            rowHeightGetter: BI.emptyFn, // number类型或function类型
            columnWidthGetter: BI.emptyFn, // number类型或function类型
            // estimatedColumnSize: 100, //columnWidthGetter为function时必设
            // estimatedRowSize: 30, //rowHeightGetter为function时必设
            scrollLeft: 0,
            scrollTop: 0,
            items: []
        });
    },

    render: function () {
        var self = this, o = this.options;
        this.renderedCells = [];
        this.renderedKeys = [];
        this.renderRange = {};
        this._scrollLock = false;
        this._debounceRelease = BI.debounce(function () {
            self._scrollLock = false;
        }, 1000 / 60);
        this.container = BI._lazyCreateWidget({
            type: "bi.absolute"
        });
        this.element.scroll(function () {
            if (self._scrollLock === true) {
                return;
            }
            o.scrollLeft = self.element.scrollLeft();
            o.scrollTop = self.element.scrollTop();
            self._calculateChildrenToRender();
            self.fireEvent(BI.GridView.EVENT_SCROLL, {
                scrollLeft: o.scrollLeft,
                scrollTop: o.scrollTop
            });
        });
        BI._lazyCreateWidget({
            type: "bi.vertical",
            element: this,
            scrollable: o.overflowX === true && o.overflowY === true,
            scrolly: o.overflowX === false && o.overflowY === true,
            scrollx: o.overflowX === true && o.overflowY === false,
            items: [this.container]
        });
        if (o.items.length > 0) {
            this._calculateSizeAndPositionData();
            this._populate();
        }
    },

    // mounted之后绑定事件
    mounted: function () {
        var o = this.options;
        if (o.scrollLeft !== 0 || o.scrollTop !== 0) {
            this.element.scrollTop(o.scrollTop);
            this.element.scrollLeft(o.scrollLeft);
        }
    },

    _calculateSizeAndPositionData: function () {
        var o = this.options;
        this.columnCount = 0;
        this.rowCount = 0;
        if (BI.isNumber(o.columnCount)) {
            this.columnCount = o.columnCount;
        } else if (o.items.length > 0) {
            this.columnCount = o.items[0].length;
        }
        if (BI.isNumber(o.rowCount)) {
            this.rowCount = o.rowCount;
        } else {
            this.rowCount = o.items.length;
        }
        this._columnSizeAndPositionManager = new BI.ScalingCellSizeAndPositionManager(this.columnCount, o.columnWidthGetter, o.estimatedColumnSize);
        this._rowSizeAndPositionManager = new BI.ScalingCellSizeAndPositionManager(this.rowCount, o.rowHeightGetter, o.estimatedRowSize);
    },

    _getOverscanIndices: function (cellCount, overscanCellsCount, startIndex, stopIndex) {
        return {
            overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
            overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount)
        };
    },

    _calculateChildrenToRender: function () {
        var self = this, o = this.options;

        var width = o.width, height = o.height, scrollLeft = BI.clamp(o.scrollLeft, 0, this._getMaxScrollLeft()),
            scrollTop = BI.clamp(o.scrollTop, 0, this._getMaxScrollTop()),
            overscanColumnCount = o.overscanColumnCount, overscanRowCount = o.overscanRowCount;

        if (height > 0 && width > 0) {
            var visibleColumnIndices = this._columnSizeAndPositionManager.getVisibleCellRange(width, scrollLeft);
            var visibleRowIndices = this._rowSizeAndPositionManager.getVisibleCellRange(height, scrollTop);

            if (BI.isEmpty(visibleColumnIndices) || BI.isEmpty(visibleRowIndices)) {
                return;
            }
            var horizontalOffsetAdjustment = this._columnSizeAndPositionManager.getOffsetAdjustment(width, scrollLeft);
            var verticalOffsetAdjustment = this._rowSizeAndPositionManager.getOffsetAdjustment(height, scrollTop);

            this._renderedColumnStartIndex = visibleColumnIndices.start;
            this._renderedColumnStopIndex = visibleColumnIndices.stop;
            this._renderedRowStartIndex = visibleRowIndices.start;
            this._renderedRowStopIndex = visibleRowIndices.stop;

            var overscanColumnIndices = this._getOverscanIndices(this.columnCount, overscanColumnCount, this._renderedColumnStartIndex, this._renderedColumnStopIndex);

            var overscanRowIndices = this._getOverscanIndices(this.rowCount, overscanRowCount, this._renderedRowStartIndex, this._renderedRowStopIndex);

            var columnStartIndex = overscanColumnIndices.overscanStartIndex;
            var columnStopIndex = overscanColumnIndices.overscanStopIndex;
            var rowStartIndex = overscanRowIndices.overscanStartIndex;
            var rowStopIndex = overscanRowIndices.overscanStopIndex;

            // 算区间size
            var minRowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowStartIndex);
            var minColumnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnStartIndex);
            var maxRowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowStopIndex);
            var maxColumnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnStopIndex);
            var top = minRowDatum.offset + verticalOffsetAdjustment;
            var left = minColumnDatum.offset + horizontalOffsetAdjustment;
            var bottom = maxRowDatum.offset + verticalOffsetAdjustment + maxRowDatum.size;
            var right = maxColumnDatum.offset + horizontalOffsetAdjustment + maxColumnDatum.size;
            // 如果滚动的区间并没有超出渲染的范围
            if (top >= this.renderRange.minY && bottom <= this.renderRange.maxY && left >= this.renderRange.minX && right <= this.renderRange.maxX) {
                return;
            }

            var renderedCells = [], renderedKeys = {}, renderedWidgets = {};
            var minX = this._getMaxScrollLeft(), minY = this._getMaxScrollTop(), maxX = 0, maxY = 0;
            var count = 0;
            for (var rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
                var rowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex);

                for (var columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
                    var key = rowIndex + "-" + columnIndex;
                    var columnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex);

                    var index = this.renderedKeys[key] && this.renderedKeys[key][2];
                    var child;
                    if (index >= 0) {
                        // if (columnDatum.size !== this.renderedCells[index]._width) {
                        // this.renderedCells[index]._width = columnDatum.size;
                        this.renderedCells[index].el.setWidth(columnDatum.size);
                        // }
                        // if (rowDatum.size !== this.renderedCells[index]._height) {
                        // this.renderedCells[index]._height = rowDatum.size;
                        this.renderedCells[index].el.setHeight(rowDatum.size);
                        // }
                        // if (this.renderedCells[index]._left !== columnDatum.offset + horizontalOffsetAdjustment) {
                        this.renderedCells[index].el.element.css("left", (columnDatum.offset + horizontalOffsetAdjustment) / BI.pixRatio + BI.pixUnit);
                        // }
                        // if (this.renderedCells[index]._top !== rowDatum.offset + verticalOffsetAdjustment) {
                        this.renderedCells[index].el.element.css("top", (rowDatum.offset + verticalOffsetAdjustment) / BI.pixRatio + BI.pixUnit);
                        // }
                        child = this.renderedCells[index].el;
                        renderedCells.push(this.renderedCells[index]);
                    } else {
                        child = BI._lazyCreateWidget(BI.extend({
                            type: "bi.label",
                            width: columnDatum.size,
                            height: rowDatum.size
                        }, o.items[rowIndex][columnIndex], {
                            cls: (o.items[rowIndex][columnIndex].cls || "") + " grid-cell" + (rowIndex === 0 ? " first-row" : "") + (columnIndex === 0 ? " first-col" : ""),
                            _rowIndex: rowIndex,
                            _columnIndex: columnIndex,
                            _left: columnDatum.offset + horizontalOffsetAdjustment,
                            _top: rowDatum.offset + verticalOffsetAdjustment
                        }), this);
                        renderedCells.push({
                            el: child,
                            left: columnDatum.offset + horizontalOffsetAdjustment,
                            top: rowDatum.offset + verticalOffsetAdjustment,
                            _left: columnDatum.offset + horizontalOffsetAdjustment,
                            _top: rowDatum.offset + verticalOffsetAdjustment
                            // _width: columnDatum.size,
                            // _height: rowDatum.size
                        });
                    }
                    minX = Math.min(minX, columnDatum.offset + horizontalOffsetAdjustment);
                    maxX = Math.max(maxX, columnDatum.offset + horizontalOffsetAdjustment + columnDatum.size);
                    minY = Math.min(minY, rowDatum.offset + verticalOffsetAdjustment);
                    maxY = Math.max(maxY, rowDatum.offset + verticalOffsetAdjustment + rowDatum.size);
                    renderedKeys[key] = [rowIndex, columnIndex, count];
                    renderedWidgets[count] = child;
                    count++;
                }
            }
            // 已存在的， 需要添加的和需要删除的
            var existSet = {}, addSet = {}, deleteArray = [];
            BI.each(renderedKeys, function (i, key) {
                if (self.renderedKeys[i]) {
                    existSet[i] = key;
                } else {
                    addSet[i] = key;
                }
            });
            BI.each(this.renderedKeys, function (i, key) {
                if (existSet[i]) {
                    return;
                }
                if (addSet[i]) {
                    return;
                }
                deleteArray.push(key[2]);
            });
            BI.each(deleteArray, function (i, index) {
                // 性能优化，不调用destroy方法防止触发destroy事件
                self.renderedCells[index].el._destroy();
            });
            var addedItems = [];
            BI.each(addSet, function (index, key) {
                addedItems.push(renderedCells[key[2]]);
            });
            // 与listview一样, 给上下文
            this.container.addItems(addedItems, this);
            // 拦截父子级关系
            this.container._children = renderedWidgets;
            this.container.attr("items", renderedCells);
            this.renderedCells = renderedCells;
            this.renderedKeys = renderedKeys;
            this.renderRange = {minX: minX, minY: minY, maxX: maxX, maxY: maxY};
        }
    },

    /**
     * 获取真实的可滚动的最大宽度
     * 对于grid_view如果没有全部渲染过，this._columnSizeAndPositionManager.getTotalSize获取的宽度是不准确的
     * 因此在调用setScrollLeft等函数时会造成没法移动到最右端(预估可移动距离太短)
     */
    // _getRealMaxScrollLeft: function () {
    //     var o = this.options;
    //     var totalWidth = 0;
    //     BI.count(0, this.columnCount, function (index) {
    //         totalWidth += o.columnWidthGetter(index);
    //     });
    //     return Math.max(0, totalWidth - this.options.width + (this.options.overflowX ? BI.DOM.getScrollWidth() : 0));
    // },

    _getMaxScrollLeft: function () {
        return Math.max(0, this._columnSizeAndPositionManager.getTotalSize() - this.options.width + (this.options.overflowX ? BI.DOM.getScrollWidth() : 0));
    },

    _getMaxScrollTop: function () {
        return Math.max(0, this._rowSizeAndPositionManager.getTotalSize() - this.options.height + (this.options.overflowY ? BI.DOM.getScrollWidth() : 0));
    },

    _populate: function (items) {
        var self = this, o = this.options;
        this._reRange();
        if (items && items !== this.options.items) {
            this.options.items = items;
            this._calculateSizeAndPositionData();
        }
        if (o.items.length > 0) {
            this.container.setWidth(this.columnCount * o.estimatedColumnSize);
            this.container.setHeight(this.rowCount * o.estimatedRowSize);

            this._debounceRelease();
            this._calculateChildrenToRender();
            // 元素未挂载时不能设置scrollTop
            try {
                this.element.scrollTop(o.scrollTop);
                this.element.scrollLeft(o.scrollLeft);
            } catch (e) {
            }
        }
    },

    setScrollLeft: function (scrollLeft) {
        if (this.options.scrollLeft === scrollLeft) {
            return;
        }
        this._scrollLock = true;
        this.options.scrollLeft = scrollLeft || 0;
        this._debounceRelease();
        this._calculateChildrenToRender();
        this.element.scrollLeft(this.options.scrollLeft);
    },

    setScrollTop: function (scrollTop) {
        if (this.options.scrollTop === scrollTop) {
            return;
        }
        this._scrollLock = true;
        this.options.scrollTop = scrollTop || 0;
        this._debounceRelease();
        this._calculateChildrenToRender();
        this.element.scrollTop(this.options.scrollTop);
    },

    setColumnCount: function (columnCount) {
        this.options.columnCount = columnCount;
    },

    setRowCount: function (rowCount) {
        this.options.rowCount = rowCount;
    },

    setOverflowX: function (b) {
        var self = this;
        if (this.options.overflowX !== !!b) {
            this.options.overflowX = !!b;
            BI.nextTick(function () {
                self.element.css({overflowX: b ? "auto" : "hidden"});
            });
        }
    },

    setOverflowY: function (b) {
        var self = this;
        if (this.options.overflowY !== !!b) {
            this.options.overflowY = !!b;
            BI.nextTick(function () {
                self.element.css({overflowY: b ? "auto" : "hidden"});
            });
        }
    },

    getScrollLeft: function () {
        return this.options.scrollLeft;
    },

    getScrollTop: function () {
        return this.options.scrollTop;
    },

    getMaxScrollLeft: function () {
        return this._getMaxScrollLeft();
    },

    getMaxScrollTop: function () {
        return this._getMaxScrollTop();
    },

    setEstimatedColumnSize: function (width) {
        this.options.estimatedColumnSize = width;
    },

    setEstimatedRowSize: function (height) {
        this.options.estimatedRowSize = height;
    },

    // 重新计算children
    _reRange: function () {
        this.renderRange = {};
    },

    _clearChildren: function () {
        this.container._children = {};
        this.container.attr("items", []);
    },

    restore: function () {
        BI.each(this.renderedCells, function (i, cell) {
            cell.el._destroy();
        });
        this._clearChildren();
        this.renderedCells = [];
        this.renderedKeys = [];
        this.renderRange = {};
        this._scrollLock = false;
    },

    populate: function (items) {
        if (items && items !== this.options.items) {
            this.restore();
        }
        this._populate(items);
    }
});
BI.GridView.EVENT_SCROLL = "EVENT_SCROLL";
BI.shortcut("bi.grid_view", BI.GridView);
<commit_msg>Pull request #2195: 无JIRA任务 grid优化<commit_after>/**
 * GridView
 *
 * Created by GUY on 2016/1/11.
 * @class BI.GridView
 * @extends BI.Widget
 */
BI.GridView = BI.inherit(BI.Widget, {
    _defaultConfig: function () {
        return BI.extend(BI.GridView.superclass._defaultConfig.apply(this, arguments), {
            baseCls: "bi-grid-view",
            // width: 400, //必设
            // height: 300, //必设
            overflowX: true,
            overflowY: true,
            overscanColumnCount: 0,
            overscanRowCount: 0,
            rowHeightGetter: BI.emptyFn, // number类型或function类型
            columnWidthGetter: BI.emptyFn, // number类型或function类型
            // estimatedColumnSize: 100, //columnWidthGetter为function时必设
            // estimatedRowSize: 30, //rowHeightGetter为function时必设
            scrollLeft: 0,
            scrollTop: 0,
            items: []
        });
    },

    render: function () {
        var self = this, o = this.options;
        this.renderedCells = [];
        this.renderedKeys = [];
        this.renderRange = {};
        this._scrollLock = false;
        this._debounceRelease = BI.debounce(function () {
            self._scrollLock = false;
        }, 1000 / 60);
        this.container = BI._lazyCreateWidget({
            type: "bi.absolute"
        });
        this.element.scroll(function () {
            if (self._scrollLock === true) {
                return;
            }
            o.scrollLeft = self.element.scrollLeft();
            o.scrollTop = self.element.scrollTop();
            self._calculateChildrenToRender();
            self.fireEvent(BI.GridView.EVENT_SCROLL, {
                scrollLeft: o.scrollLeft,
                scrollTop: o.scrollTop
            });
        });
        BI._lazyCreateWidget({
            type: "bi.vertical",
            element: this,
            scrollable: o.overflowX === true && o.overflowY === true,
            scrolly: o.overflowX === false && o.overflowY === true,
            scrollx: o.overflowX === true && o.overflowY === false,
            items: [this.container]
        });
        if (o.items.length > 0) {
            this._calculateSizeAndPositionData();
            this._populate();
        }
    },

    // mounted之后绑定事件
    mounted: function () {
        var o = this.options;
        if (o.scrollLeft !== 0 || o.scrollTop !== 0) {
            this.element.scrollTop(o.scrollTop);
            this.element.scrollLeft(o.scrollLeft);
        }
    },

    _calculateSizeAndPositionData: function () {
        var o = this.options;
        this.columnCount = 0;
        this.rowCount = 0;
        if (BI.isNumber(o.columnCount)) {
            this.columnCount = o.columnCount;
        } else if (o.items.length > 0) {
            this.columnCount = o.items[0].length;
        }
        if (BI.isNumber(o.rowCount)) {
            this.rowCount = o.rowCount;
        } else {
            this.rowCount = o.items.length;
        }
        this._columnSizeAndPositionManager = new BI.ScalingCellSizeAndPositionManager(this.columnCount, o.columnWidthGetter, o.estimatedColumnSize);
        this._rowSizeAndPositionManager = new BI.ScalingCellSizeAndPositionManager(this.rowCount, o.rowHeightGetter, o.estimatedRowSize);
    },

    _getOverscanIndices: function (cellCount, overscanCellsCount, startIndex, stopIndex) {
        return {
            overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
            overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount)
        };
    },

    _calculateChildrenToRender: function () {
        var self = this, o = this.options;

        var width = o.width, height = o.height, scrollLeft = BI.clamp(o.scrollLeft, 0, this._getMaxScrollLeft()),
            scrollTop = BI.clamp(o.scrollTop, 0, this._getMaxScrollTop()),
            overscanColumnCount = o.overscanColumnCount, overscanRowCount = o.overscanRowCount;

        if (height > 0 && width > 0) {
            var visibleColumnIndices = this._columnSizeAndPositionManager.getVisibleCellRange(width, scrollLeft);
            var visibleRowIndices = this._rowSizeAndPositionManager.getVisibleCellRange(height, scrollTop);

            if (BI.isEmpty(visibleColumnIndices) || BI.isEmpty(visibleRowIndices)) {
                return;
            }
            var horizontalOffsetAdjustment = this._columnSizeAndPositionManager.getOffsetAdjustment(width, scrollLeft);
            var verticalOffsetAdjustment = this._rowSizeAndPositionManager.getOffsetAdjustment(height, scrollTop);

            this._renderedColumnStartIndex = visibleColumnIndices.start;
            this._renderedColumnStopIndex = visibleColumnIndices.stop;
            this._renderedRowStartIndex = visibleRowIndices.start;
            this._renderedRowStopIndex = visibleRowIndices.stop;

            var overscanColumnIndices = this._getOverscanIndices(this.columnCount, overscanColumnCount, this._renderedColumnStartIndex, this._renderedColumnStopIndex);

            var overscanRowIndices = this._getOverscanIndices(this.rowCount, overscanRowCount, this._renderedRowStartIndex, this._renderedRowStopIndex);

            var columnStartIndex = overscanColumnIndices.overscanStartIndex;
            var columnStopIndex = overscanColumnIndices.overscanStopIndex;
            var rowStartIndex = overscanRowIndices.overscanStartIndex;
            var rowStopIndex = overscanRowIndices.overscanStopIndex;

            // 算区间size
            var minRowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowStartIndex);
            var minColumnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnStartIndex);
            var maxRowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowStopIndex);
            var maxColumnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnStopIndex);
            var top = minRowDatum.offset + verticalOffsetAdjustment;
            var left = minColumnDatum.offset + horizontalOffsetAdjustment;
            var bottom = maxRowDatum.offset + verticalOffsetAdjustment + maxRowDatum.size;
            var right = maxColumnDatum.offset + horizontalOffsetAdjustment + maxColumnDatum.size;
            // 如果滚动的区间并没有超出渲染的范围
            if (top >= this.renderRange.minY && bottom <= this.renderRange.maxY && left >= this.renderRange.minX && right <= this.renderRange.maxX) {
                return;
            }

            var renderedCells = [], renderedKeys = {}, renderedWidgets = {};
            var minX = this._getMaxScrollLeft(), minY = this._getMaxScrollTop(), maxX = 0, maxY = 0;
            var count = 0;
            for (var rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
                var rowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex);

                for (var columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
                    var key = rowIndex + "-" + columnIndex;
                    var columnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex);

                    var index = this.renderedKeys[key] && this.renderedKeys[key][2];
                    var child;
                    if (index >= 0) {
                        // if (columnDatum.size !== this.renderedCells[index]._width) {
                        // this.renderedCells[index]._width = columnDatum.size;
                        this.renderedCells[index].el.setWidth(columnDatum.size);
                        // }
                        // if (rowDatum.size !== this.renderedCells[index]._height) {
                        // this.renderedCells[index]._height = rowDatum.size;
                        this.renderedCells[index].el.setHeight(rowDatum.size);
                        // }
                        // if (this.renderedCells[index]._left !== columnDatum.offset + horizontalOffsetAdjustment) {
                        this.renderedCells[index].el.element.css("left", (columnDatum.offset + horizontalOffsetAdjustment) / BI.pixRatio + BI.pixUnit);
                        // }
                        // if (this.renderedCells[index]._top !== rowDatum.offset + verticalOffsetAdjustment) {
                        this.renderedCells[index].el.element.css("top", (rowDatum.offset + verticalOffsetAdjustment) / BI.pixRatio + BI.pixUnit);
                        // }
                        child = this.renderedCells[index].el;
                        renderedCells.push(this.renderedCells[index]);
                    } else {
                        child = BI._lazyCreateWidget(BI.extend({
                            type: "bi.label",
                            width: columnDatum.size,
                            height: rowDatum.size
                        }, o.items[rowIndex][columnIndex], {
                            cls: (o.items[rowIndex][columnIndex].cls || "") + " grid-cell" + (rowIndex === 0 ? " first-row" : "") + (columnIndex === 0 ? " first-col" : ""),
                            _rowIndex: rowIndex,
                            _columnIndex: columnIndex,
                            _left: columnDatum.offset + horizontalOffsetAdjustment,
                            _top: rowDatum.offset + verticalOffsetAdjustment
                        }), this);
                        renderedCells.push({
                            el: child,
                            left: columnDatum.offset + horizontalOffsetAdjustment,
                            top: rowDatum.offset + verticalOffsetAdjustment,
                            _left: columnDatum.offset + horizontalOffsetAdjustment,
                            _top: rowDatum.offset + verticalOffsetAdjustment
                            // _width: columnDatum.size,
                            // _height: rowDatum.size
                        });
                    }
                    minX = Math.min(minX, columnDatum.offset + horizontalOffsetAdjustment);
                    maxX = Math.max(maxX, columnDatum.offset + horizontalOffsetAdjustment + columnDatum.size);
                    minY = Math.min(minY, rowDatum.offset + verticalOffsetAdjustment);
                    maxY = Math.max(maxY, rowDatum.offset + verticalOffsetAdjustment + rowDatum.size);
                    renderedKeys[key] = [rowIndex, columnIndex, count];
                    renderedWidgets[count] = child;
                    count++;
                }
            }
            // 已存在的， 需要添加的和需要删除的
            var existSet = {}, addSet = {}, deleteArray = [];
            BI.each(renderedKeys, function (i, key) {
                if (self.renderedKeys[i]) {
                    existSet[i] = key;
                } else {
                    addSet[i] = key;
                }
            });
            BI.each(this.renderedKeys, function (i, key) {
                if (existSet[i]) {
                    return;
                }
                if (addSet[i]) {
                    return;
                }
                deleteArray.push(key[2]);
            });
            BI.each(deleteArray, function (i, index) {
                // 性能优化，不调用destroy方法防止触发destroy事件
                self.renderedCells[index].el._destroy();
            });
            var addedItems = [];
            BI.each(addSet, function (index, key) {
                addedItems.push(renderedCells[key[2]]);
            });
            // 与listview一样, 给上下文
            this.container.addItems(addedItems, this);
            // 拦截父子级关系
            this.container._children = renderedWidgets;
            this.container.attr("items", renderedCells);
            this.renderedCells = renderedCells;
            this.renderedKeys = renderedKeys;
            this.renderRange = {minX: minX, minY: minY, maxX: maxX, maxY: maxY};
        }
    },

    _getMaxScrollLeft: function () {
        return Math.max(0, this._getContainerWidth() - this.options.width + (this.options.overflowX ? BI.DOM.getScrollWidth() : 0));
    },

    _getMaxScrollTop: function () {
        return Math.max(0, this._getContainerHeight() - this.options.height + (this.options.overflowY ? BI.DOM.getScrollWidth() : 0));
    },

    _getContainerWidth: function () {
        return this.columnCount * this.options.estimatedColumnSize;
    },

    _getContainerHeight: function () {
        return this.rowCount * this.options.estimatedRowSize;
    },

    _populate: function (items) {
        var self = this, o = this.options;
        this._reRange();
        if (items && items !== this.options.items) {
            this.options.items = items;
            this._calculateSizeAndPositionData();
        }
        if (o.items.length > 0) {
            this.container.setWidth(this._getContainerWidth());
            this.container.setHeight(this._getContainerHeight());

            this._debounceRelease();
            this._calculateChildrenToRender();
            // 元素未挂载时不能设置scrollTop
            try {
                this.element.scrollTop(o.scrollTop);
                this.element.scrollLeft(o.scrollLeft);
            } catch (e) {
            }
        }
    },

    setScrollLeft: function (scrollLeft) {
        if (this.options.scrollLeft === scrollLeft) {
            return;
        }
        this._scrollLock = true;
        this.options.scrollLeft = BI.clamp(scrollLeft || 0, 0, this._getMaxScrollLeft());
        this._debounceRelease();
        this._calculateChildrenToRender();
        this.element.scrollLeft(this.options.scrollLeft);
    },

    setScrollTop: function (scrollTop) {
        if (this.options.scrollTop === scrollTop) {
            return;
        }
        this._scrollLock = true;
        this.options.scrollLeft = BI.clamp(scrollTop || 0, 0, this._getMaxScrollTop());
        this._debounceRelease();
        this._calculateChildrenToRender();
        this.element.scrollTop(this.options.scrollTop);
    },

    setColumnCount: function (columnCount) {
        this.options.columnCount = columnCount;
    },

    setRowCount: function (rowCount) {
        this.options.rowCount = rowCount;
    },

    setOverflowX: function (b) {
        var self = this;
        if (this.options.overflowX !== !!b) {
            this.options.overflowX = !!b;
            BI.nextTick(function () {
                self.element.css({overflowX: b ? "auto" : "hidden"});
            });
        }
    },

    setOverflowY: function (b) {
        var self = this;
        if (this.options.overflowY !== !!b) {
            this.options.overflowY = !!b;
            BI.nextTick(function () {
                self.element.css({overflowY: b ? "auto" : "hidden"});
            });
        }
    },

    getScrollLeft: function () {
        return this.options.scrollLeft;
    },

    getScrollTop: function () {
        return this.options.scrollTop;
    },

    getMaxScrollLeft: function () {
        return this._getMaxScrollLeft();
    },

    getMaxScrollTop: function () {
        return this._getMaxScrollTop();
    },

    setEstimatedColumnSize: function (width) {
        this.options.estimatedColumnSize = width;
    },

    setEstimatedRowSize: function (height) {
        this.options.estimatedRowSize = height;
    },

    // 重新计算children
    _reRange: function () {
        this.renderRange = {};
    },

    _clearChildren: function () {
        this.container._children = {};
        this.container.attr("items", []);
    },

    restore: function () {
        BI.each(this.renderedCells, function (i, cell) {
            cell.el._destroy();
        });
        this._clearChildren();
        this.renderedCells = [];
        this.renderedKeys = [];
        this.renderRange = {};
        this._scrollLock = false;
    },

    populate: function (items) {
        if (items && items !== this.options.items) {
            this.restore();
        }
        this._populate(items);
    }
});
BI.GridView.EVENT_SCROLL = "EVENT_SCROLL";
BI.shortcut("bi.grid_view", BI.GridView);
<|endoftext|>


<commit_before>var ChannelModule = require("./module");
var Flags = require("../flags");
var Account = require("../account");
var db = require("../database");

const TYPE_SET_CHANNEL_RANK = {
    name: "string",
    rank: "number"
};

function RankModule(channel) {
    ChannelModule.apply(this, arguments);

    if (this.channel.modules.chat) {
        this.channel.modules.chat.registerCommand("/rank", this.handleCmdRank.bind(this));
    }
}

RankModule.prototype = Object.create(ChannelModule.prototype);

RankModule.prototype.onUserPostJoin = function (user) {
    user.socket.typecheckedOn("setChannelRank", TYPE_SET_CHANNEL_RANK, this.handleRankChange.bind(this, user));
    var self = this;
    user.socket.on("requestChannelRanks", function () {
        self.sendChannelRanks([user]);
    });
};

RankModule.prototype.sendChannelRanks = function (users) {
    if (!this.channel.is(Flags.C_REGISTERED)) {
        return;
    }

    db.channels.allRanks(this.channel.name, function (err, ranks) {
        if (err) {
            return;
        }

        users.forEach(function (u) {
            if (u.account.effectiveRank >= 3) {
                u.socket.emit("channelRanks", ranks);
            }
        });
    });
};

RankModule.prototype.handleCmdRank = function (user, msg, meta) {
    var args = msg.split(" ");
    args.shift(); /* shift off /rank */
    var name = args.shift();
    var rank = parseInt(args.shift());

    if (!name || isNaN(rank)) {
        user.socket.emit("noflood", {
            action: "/rank",
            msg: "Syntax: /rank <username> <rank>.  <rank> must be a positive integer > 1"
        });
        return;
    }

    this.handleRankChange(user, { name: name, rank: rank });
};

RankModule.prototype.handleRankChange = function (user, data) {
    if (user.account.effectiveRank < 3) {
        return;
    }

    var rank = data.rank;
    var userrank = user.account.effectiveRank;
    var name = data.name.substring(0, 20).toLowerCase();

    if (isNaN(rank) || rank < 1 || (rank >= userrank && !(userrank === 4 && rank === 4))) {
        user.socket.emit("channelRankFail", {
            msg: "Updating user rank failed: You can't promote someone to a rank equal " +
                 "or higher than yourself, or demote them to below rank 1."
        });
        return;
    }

    var receiver;
    var lowerName = name.toLowerCase();
    for (var i = 0; i < this.channel.users.length; i++) {
        if (this.channel.users[i].getLowerName() === lowerName) {
            receiver = this.channel.users[i];
            break;
        }
    }

    if (name === user.getLowerName()) {
        user.socket.emit("channelRankFail", {
            msg: "Updating user rank failed: You can't promote or demote yourself."
        });
        return;
    }

    if (!this.channel.is(Flags.C_REGISTERED)) {
        user.socket.emit("channelRankFail", {
            msg: "Updating user rank failed: in an unregistered channel, a user must " +
                 "be online in the channel in order to have their rank changed."
        });
        return;
    }

    if (receiver) {
        var current = Math.max(receiver.account.globalRank, receiver.account.channelRank);
        if (current >= userrank && !(userrank === 4 && current === 4)) {
            user.socket.emit("channelRankFail", {
                msg: "Updating user rank failed: You can't promote or demote "+
                     "someone who has equal or higher rank than yourself"
            });
            return;
        }

        receiver.account.channelRank = rank;
        receiver.account.effectiveRank = Math.max(receiver.account.globalRank, rank);
        receiver.socket.emit("rank", receiver.account.effectiveRank);
        this.channel.logger.log("[mod] " + user.getName() + " set " + name + "'s rank " +
                                "to " + rank);
        this.channel.broadcastAll("setUserRank", data);

        if (!this.channel.is(Flags.C_REGISTERED)) {
            user.socket.emit("channelRankFail", {
                msg: "This channel is not registered.  Any rank changes are temporary " +
                     "and not stored in the database."
            });
            return;
        }

        if (!receiver.is(Flags.U_REGISTERED)) {
            user.socket.emit("channelRankFail", {
                msg: "The user you promoted is not a registered account.  " +
                     "Any rank changes are temporary and not stored in the database."
            });
            return;
        }

        data.userrank = userrank;

        this.updateDatabase(data, function (err) {
            if (err) {
                user.socket.emit("channelRankFail", {
                    msg: "Database failure when updating rank"
                });
            }
        });
    } else {
        data.userrank = userrank;
        var self = this;
        this.updateDatabase(data, function (err) {
            if (err) {
                user.socket.emit("channelRankFail", {
                    msg: "Updating user rank failed: " + err
                });
            }
            self.channel.logger.log("[mod] " + user.getName() + " set " + data.name +
                                    "'s rank to " + rank);
            self.channel.broadcastAll("setUserRank", data);
            if (self.channel.modules.chat) {
                self.channel.modules.chat.sendModMessage(
                    user.getName() + " set " + data.name + "'s rank to " + rank,
                    3
                );
            }
        });
    }
};

RankModule.prototype.updateDatabase = function (data, cb) {
    var chan = this.channel;
    Account.rankForName(data.name, { channel: this.channel.name }, function (err, rank) {
        if (err) {
            return cb(err);
        }

        if (rank >= data.userrank && !(rank === 4 && data.userrank === 4)) {
            cb("You can't promote or demote someone with equal or higher rank than you.");
            return;
        }

        db.channels.setRank(chan.name, data.name, data.rank, cb);
    });
};

module.exports = RankModule;
<commit_msg>Give up early if username is invalid for rank change<commit_after>var ChannelModule = require("./module");
var Flags = require("../flags");
var Account = require("../account");
var db = require("../database");

const TYPE_SET_CHANNEL_RANK = {
    name: "string",
    rank: "number"
};

function RankModule(channel) {
    ChannelModule.apply(this, arguments);

    if (this.channel.modules.chat) {
        this.channel.modules.chat.registerCommand("/rank", this.handleCmdRank.bind(this));
    }
}

RankModule.prototype = Object.create(ChannelModule.prototype);

RankModule.prototype.onUserPostJoin = function (user) {
    user.socket.typecheckedOn("setChannelRank", TYPE_SET_CHANNEL_RANK, this.handleRankChange.bind(this, user));
    var self = this;
    user.socket.on("requestChannelRanks", function () {
        self.sendChannelRanks([user]);
    });
};

RankModule.prototype.sendChannelRanks = function (users) {
    if (!this.channel.is(Flags.C_REGISTERED)) {
        return;
    }

    db.channels.allRanks(this.channel.name, function (err, ranks) {
        if (err) {
            return;
        }

        users.forEach(function (u) {
            if (u.account.effectiveRank >= 3) {
                u.socket.emit("channelRanks", ranks);
            }
        });
    });
};

RankModule.prototype.handleCmdRank = function (user, msg, meta) {
    var args = msg.split(" ");
    args.shift(); /* shift off /rank */
    var name = args.shift();
    var rank = parseInt(args.shift());

    if (!name || isNaN(rank)) {
        user.socket.emit("noflood", {
            action: "/rank",
            msg: "Syntax: /rank <username> <rank>.  <rank> must be a positive integer > 1"
        });
        return;
    }

    this.handleRankChange(user, { name: name, rank: rank });
};

RankModule.prototype.handleRankChange = function (user, data) {
    if (user.account.effectiveRank < 3) {
        return;
    }

    var rank = data.rank;
    var userrank = user.account.effectiveRank;
    var name = data.name.substring(0, 20).toLowerCase();

    if (!name.match(/^[a-zA-Z0-9_-]{1,20}$/)) {
        user.socket.emit("channelRankFail", {
            msg: "Invalid target name " + data.name
        });
        return;
    }

    if (isNaN(rank) || rank < 1 || (rank >= userrank && !(userrank === 4 && rank === 4))) {
        user.socket.emit("channelRankFail", {
            msg: "Updating user rank failed: You can't promote someone to a rank equal " +
                 "or higher than yourself, or demote them to below rank 1."
        });
        return;
    }

    var receiver;
    var lowerName = name.toLowerCase();
    for (var i = 0; i < this.channel.users.length; i++) {
        if (this.channel.users[i].getLowerName() === lowerName) {
            receiver = this.channel.users[i];
            break;
        }
    }

    if (name === user.getLowerName()) {
        user.socket.emit("channelRankFail", {
            msg: "Updating user rank failed: You can't promote or demote yourself."
        });
        return;
    }

    if (!this.channel.is(Flags.C_REGISTERED)) {
        user.socket.emit("channelRankFail", {
            msg: "Updating user rank failed: in an unregistered channel, a user must " +
                 "be online in the channel in order to have their rank changed."
        });
        return;
    }

    if (receiver) {
        var current = Math.max(receiver.account.globalRank, receiver.account.channelRank);
        if (current >= userrank && !(userrank === 4 && current === 4)) {
            user.socket.emit("channelRankFail", {
                msg: "Updating user rank failed: You can't promote or demote "+
                     "someone who has equal or higher rank than yourself"
            });
            return;
        }

        receiver.account.channelRank = rank;
        receiver.account.effectiveRank = Math.max(receiver.account.globalRank, rank);
        receiver.socket.emit("rank", receiver.account.effectiveRank);
        this.channel.logger.log("[mod] " + user.getName() + " set " + name + "'s rank " +
                                "to " + rank);
        this.channel.broadcastAll("setUserRank", data);

        if (!this.channel.is(Flags.C_REGISTERED)) {
            user.socket.emit("channelRankFail", {
                msg: "This channel is not registered.  Any rank changes are temporary " +
                     "and not stored in the database."
            });
            return;
        }

        if (!receiver.is(Flags.U_REGISTERED)) {
            user.socket.emit("channelRankFail", {
                msg: "The user you promoted is not a registered account.  " +
                     "Any rank changes are temporary and not stored in the database."
            });
            return;
        }

        data.userrank = userrank;

        this.updateDatabase(data, function (err) {
            if (err) {
                user.socket.emit("channelRankFail", {
                    msg: "Database failure when updating rank"
                });
            }
        });
    } else {
        data.userrank = userrank;
        var self = this;
        this.updateDatabase(data, function (err) {
            if (err) {
                user.socket.emit("channelRankFail", {
                    msg: "Updating user rank failed: " + err
                });
            }
            self.channel.logger.log("[mod] " + user.getName() + " set " + data.name +
                                    "'s rank to " + rank);
            self.channel.broadcastAll("setUserRank", data);
            if (self.channel.modules.chat) {
                self.channel.modules.chat.sendModMessage(
                    user.getName() + " set " + data.name + "'s rank to " + rank,
                    3
                );
            }
        });
    }
};

RankModule.prototype.updateDatabase = function (data, cb) {
    var chan = this.channel;
    Account.rankForName(data.name, { channel: this.channel.name }, function (err, rank) {
        if (err) {
            return cb(err);
        }

        if (rank >= data.userrank && !(rank === 4 && data.userrank === 4)) {
            cb("You can't promote or demote someone with equal or higher rank than you.");
            return;
        }

        db.channels.setRank(chan.name, data.name, data.rank, cb);
    });
};

module.exports = RankModule;
<|endoftext|>


<commit_before>(function() {
  'use strict';

  angular.module('foundation.notification', ['foundation.core'])
    .controller('ZfNotificationController', ZfNotificationController)
    .directive('zfNotificationSet', zfNotificationSet)
    .directive('zfNotification', zfNotification)
    .directive('zfNotificationStatic', zfNotificationStatic)
    .directive('zfNotify', zfNotify)
    .factory('NotificationFactory', NotificationFactory)
    .service('FoundationNotification', FoundationNotification)
  ;

  FoundationNotification.$inject = ['FoundationApi', 'NotificationFactory'];

  function FoundationNotification(foundationApi, NotificationFactory) {
    var service    = {};

    service.activate = activate;
    service.deactivate = deactivate;

    return service;

    //target should be element ID
    function activate(target) {
      foundationApi.publish(target, 'show');
    }

    //target should be element ID
    function deactivate(target) {
      foundationApi.publish(target, 'hide');
    }

    function toggle(target) {
      foundationApi.publish(target, 'toggle');
    }

    function createNotificationSet(config) {
      return new NotificationFactory(config);
    }
  }

  ZfNotificationController.$inject = ['$scope', 'FoundationApi'];

  function ZfNotificationController($scope, foundationApi) {
    var controller    = this;
    controller.notifications = $scope.notifications = $scope.notifications || [];

    controller.addNotification = function(info) {
      var id  = foundationApi.generateUuid();
      info.id = id;
      $scope.notifications.push(info);
    };

    controller.removeNotification = function(id) {
      $scope.notifications.forEach(function(notification) {
        if(notification.id === id) {
          var ind = $scope.notifications.indexOf(notification);
          $scope.notifications.splice(ind, 1);
        }
      });
    };

    controller.clearAll = function() {
      while($scope.notifications.length > 0) {
        $scope.notifications.pop();
      }
    };
  }

  zfNotificationSet.$inject = ['FoundationApi'];

  function zfNotificationSet(foundationApi) {
    var directive = {
      restrict: 'EA',
      templateUrl: 'components/notification/notification-set.html',
      controller: 'ZfNotificationController',
      replace: true,
      scope: {
        position: '@'
      },
      link: link
    };

    return directive;

    function link(scope, element, attrs, controller) {
      scope.position = scope.position ? scope.position.split(' ').join('-') : 'top-right';

      foundationApi.subscribe(attrs.id, function(msg) {
        if(msg === 'clearall') {
          controller.clearAll();
        }
        else {
          controller.addNotification(msg);
          if (!scope.$root.$$phase) {
            scope.$apply();
          }
        }
      });
    }
  }

  zfNotification.$inject = ['FoundationApi'];

  function zfNotification(foundationApi) {
    var directive = {
      restrict: 'EA',
      templateUrl: 'components/notification/notification.html',
      replace: true,
      transclude: true,
      require: '^zfNotificationSet',
      controller: function() { },
      scope: {
        title: '=?',
        content: '=?',
        image: '=?',
        notifId: '=',
        color: '=?',
        autoclose: '=?'
      },
      compile: compile
    };

    return directive;

    function compile() {

      return {
        pre: preLink,
        post: postLink
      };

      function preLink(scope, iElement, iAttrs) {
        iAttrs.$set('zf-closable', 'notification');
      }

      function postLink(scope, element, attrs, controller) {
        scope.active = false;
        var animationIn  = attrs.animationIn || 'fadeIn';
        var animationOut = attrs.animationOut || 'fadeOut';
        var hammerElem;

        //due to dynamic insertion of DOM, we need to wait for it to show up and get working!
        setTimeout(function() {
          scope.active = true;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
        }, 50);

        scope.hide = function() {
          scope.active = false;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
          setTimeout(function() {
            controller.removeNotification(scope.notifId);
          }, 50);
        };

        // close if autoclose
        if (scope.autoclose && scope.active) {
          setTimeout(function() {
            scope.hide();
          }, parseInt(scope.autoclose));
        };

        // close on swipe
        if (Hammer) {
          hammerElem = new Hammer(element[0]);
          // set the options for swipe (to make them a bit more forgiving in detection)
          hammerElem.get('swipe').set({
            direction: Hammer.DIRECTION_ALL,
            threshold: 5, // this is how far the swipe has to travel
            velocity: 0.5 // and this is how fast the swipe must travel
          });
        }

        hammerElem.on('swipe', function() {
          if (scope.active) {
            scope.hide();
          }
        });
      }
    }
  }

  zfNotificationStatic.$inject = ['FoundationApi'];

  function zfNotificationStatic(foundationApi) {
    var directive = {
      restrict: 'EA',
      templateUrl: 'components/notification/notification-static.html',
      replace: true,
      transclude: true,
      scope: {
        title: '@?',
        content: '@?',
        image: '@?',
        color: '@?',
        autoclose: '@?'
      },
      compile: compile
    };

    return directive;

    function compile() {
      var type = 'notification';

      return {
        pre: preLink,
        post: postLink
      };

      function preLink(scope, iElement, iAttrs, controller) {
        iAttrs.$set('zf-closable', type);
      }

      function postLink(scope, element, attrs, controller) {
        scope.position = attrs.position ? attrs.position.split(' ').join('-') : 'top-right';

        var animationIn = attrs.animationIn || 'fadeIn';
        var animationOut = attrs.animationOut || 'fadeOut';

        //setup
        foundationApi.subscribe(attrs.id, function(msg) {
          if(msg == 'show' || msg == 'open') {
            scope.show();
            // close if autoclose
            if (scope.autoclose) {
              setTimeout(function() {
                scope.hide();
              }, parseInt(scope.autoclose));
            };
          } else if (msg == 'close' || msg == 'hide') {
            scope.hide();
          } else if (msg == 'toggle') {
            scope.toggle();
            // close if autoclose
            if (scope.autoclose) {
              setTimeout(function() {
                scope.toggle();
              }, parseInt(scope.autoclose));
            };
          }

          foundationApi.animate(element, scope.active, animationIn, animationOut);
          scope.$apply();

          return;
        });

        scope.hide = function() {
          scope.active = false;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
          return;
        };

        scope.show = function() {
          scope.active = true;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
          return;
        };

        scope.toggle = function() {
          scope.active = !scope.active;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
          return;
        };

      }
    }
  }

  zfNotify.$inject = ['FoundationApi'];

  function zfNotify(foundationApi) {
    var directive = {
      restrict: 'A',
      scope: {
        title: '@?',
        content: '@?',
        color: '@?',
        image: '@?',
        autoclose: '@?'
      },
      link: link
    };

    return directive;

    function link(scope, element, attrs, controller) {
      element.on('click', function(e) {
        foundationApi.publish(attrs.zfNotify, {
          title: scope.title,
          content: scope.content,
          color: scope.color,
          image: scope.image,
          autoclose: scope.autoclose
        });
        e.preventDefault();
      });
    }
  }

  NotificationFactory.$inject = ['$http', '$templateCache', '$rootScope', '$compile', '$timeout', 'FoundationApi'];

  function NotificationFactory($http, $templateCache, $rootScope, $compile, $timeout, foundationApi) {
    return notificationFactory;

    function notificationFactory(config) {
      var self = this, //for prototype functions
          container = angular.element(config.container || document.body),
          id = config.id || foundationApi.generateUuid(),
          attached = false,
          destroyed = false,
          html,
          element,
          scope
      ;

      var props = [
        'position'
      ];

      assembleDirective();

      self.addNotification = addNotification;
      self.clearAll = clearAll;
      self.destroy = destroy;

      return {
        addNotification: addNotification,
        clearAll: clearAll,
        destroy: destroy
      };

      function checkStatus() {
        if(destroyed) {
          throw "Error: Notification Set was destroyed. Delete the object and create a new NotificationFactory instance."
        }
      }

      function addNotification(notification) {
        checkStatus();
        $timeout(function() {
          init(true, notification);
          foundationApi.publish(id, notification);
        }, 0, false);
      }

      function clearAll() {
        checkStatus();
        $timeout(function() {
          foundationApi.publish(id, 'clearall');
        }, 0, false);
      }

      function init(state, notification) {
        if(!attached && html.length > 0) {
          var modalEl = container.append(element);

          scope.notifications = [ notification ];
          $compile(element)(scope);
          attached = true;
        }
      }

      function assembleDirective() {
        html = '<zf-notification-set id="' + id + '"></zf-notification-set>';

        element = angular.element(html);

        scope = $rootScope.$new();

        for(var prop in props) {
          if(config[prop]) {
            element.attr(prop, config[prop]);
          }
        }
      }

      function destroy() {
        self.clearAll();
        setTimeout(function() {
          scope.$destroy();
          element.remove();
          destroyed = true;
        }, 3000);
      }

    }

  }
})();
<commit_msg>Fixed issue with autoclose for dynamically added notifications<commit_after>(function() {
  'use strict';

  angular.module('foundation.notification', ['foundation.core'])
    .controller('ZfNotificationController', ZfNotificationController)
    .directive('zfNotificationSet', zfNotificationSet)
    .directive('zfNotification', zfNotification)
    .directive('zfNotificationStatic', zfNotificationStatic)
    .directive('zfNotify', zfNotify)
    .factory('NotificationFactory', NotificationFactory)
    .service('FoundationNotification', FoundationNotification)
  ;

  FoundationNotification.$inject = ['FoundationApi', 'NotificationFactory'];

  function FoundationNotification(foundationApi, NotificationFactory) {
    var service    = {};

    service.activate = activate;
    service.deactivate = deactivate;

    return service;

    //target should be element ID
    function activate(target) {
      foundationApi.publish(target, 'show');
    }

    //target should be element ID
    function deactivate(target) {
      foundationApi.publish(target, 'hide');
    }

    function toggle(target) {
      foundationApi.publish(target, 'toggle');
    }

    function createNotificationSet(config) {
      return new NotificationFactory(config);
    }
  }

  ZfNotificationController.$inject = ['$scope', 'FoundationApi'];

  function ZfNotificationController($scope, foundationApi) {
    var controller    = this;
    controller.notifications = $scope.notifications = $scope.notifications || [];

    controller.addNotification = function(info) {
      var id  = foundationApi.generateUuid();
      info.id = id;
      $scope.notifications.push(info);
    };

    controller.removeNotification = function(id) {
      $scope.notifications.forEach(function(notification) {
        if(notification.id === id) {
          var ind = $scope.notifications.indexOf(notification);
          $scope.notifications.splice(ind, 1);
        }
      });
    };

    controller.clearAll = function() {
      while($scope.notifications.length > 0) {
        $scope.notifications.pop();
      }
    };
  }

  zfNotificationSet.$inject = ['FoundationApi'];

  function zfNotificationSet(foundationApi) {
    var directive = {
      restrict: 'EA',
      templateUrl: 'components/notification/notification-set.html',
      controller: 'ZfNotificationController',
      replace: true,
      scope: {
        position: '@'
      },
      link: link
    };

    return directive;

    function link(scope, element, attrs, controller) {
      scope.position = scope.position ? scope.position.split(' ').join('-') : 'top-right';

      foundationApi.subscribe(attrs.id, function(msg) {
        if(msg === 'clearall') {
          controller.clearAll();
        }
        else {
          controller.addNotification(msg);
          if (!scope.$root.$$phase) {
            scope.$apply();
          }
        }
      });
    }
  }

  zfNotification.$inject = ['FoundationApi'];

  function zfNotification(foundationApi) {
    var directive = {
      restrict: 'EA',
      templateUrl: 'components/notification/notification.html',
      replace: true,
      transclude: true,
      require: '^zfNotificationSet',
      controller: function() { },
      scope: {
        title: '=?',
        content: '=?',
        image: '=?',
        notifId: '=',
        color: '=?',
        autoclose: '=?'
      },
      compile: compile
    };

    return directive;

    function compile() {

      return {
        pre: preLink,
        post: postLink
      };

      function preLink(scope, iElement, iAttrs) {
        iAttrs.$set('zf-closable', 'notification');
      }

      function postLink(scope, element, attrs, controller) {
        scope.active = false;
        var animationIn  = attrs.animationIn || 'fadeIn';
        var animationOut = attrs.animationOut || 'fadeOut';
        var hammerElem;

        //due to dynamic insertion of DOM, we need to wait for it to show up and get working!
        setTimeout(function() {
          scope.active = true;

          // close if autoclose
          if (scope.autoclose) {
            setTimeout(function() {
              scope.hide();
            }, parseInt(scope.autoclose));
          };

          foundationApi.animate(element, scope.active, animationIn, animationOut);
        }, 50);

        scope.hide = function() {
          scope.active = false;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
          setTimeout(function() {
            controller.removeNotification(scope.notifId);
          }, 50);
        };

        // close if autoclose
        if (scope.autoclose && scope.active) {
          setTimeout(function() {
            scope.hide();
          }, parseInt(scope.autoclose));
        };

        // close on swipe
        if (Hammer) {
          hammerElem = new Hammer(element[0]);
          // set the options for swipe (to make them a bit more forgiving in detection)
          hammerElem.get('swipe').set({
            direction: Hammer.DIRECTION_ALL,
            threshold: 5, // this is how far the swipe has to travel
            velocity: 0.5 // and this is how fast the swipe must travel
          });
        }

        hammerElem.on('swipe', function() {
          if (scope.active) {
            scope.hide();
          }
        });
      }
    }
  }

  zfNotificationStatic.$inject = ['FoundationApi'];

  function zfNotificationStatic(foundationApi) {
    var directive = {
      restrict: 'EA',
      templateUrl: 'components/notification/notification-static.html',
      replace: true,
      transclude: true,
      scope: {
        title: '@?',
        content: '@?',
        image: '@?',
        color: '@?',
        autoclose: '@?'
      },
      compile: compile
    };

    return directive;

    function compile() {
      var type = 'notification';

      return {
        pre: preLink,
        post: postLink
      };

      function preLink(scope, iElement, iAttrs, controller) {
        iAttrs.$set('zf-closable', type);
      }

      function postLink(scope, element, attrs, controller) {
        scope.position = attrs.position ? attrs.position.split(' ').join('-') : 'top-right';

        var animationIn = attrs.animationIn || 'fadeIn';
        var animationOut = attrs.animationOut || 'fadeOut';

        //setup
        foundationApi.subscribe(attrs.id, function(msg) {
          if(msg == 'show' || msg == 'open') {
            scope.show();
            // close if autoclose
            if (scope.autoclose) {
              setTimeout(function() {
                scope.hide();
              }, parseInt(scope.autoclose));
            };
          } else if (msg == 'close' || msg == 'hide') {
            scope.hide();
          } else if (msg == 'toggle') {
            scope.toggle();
            // close if autoclose
            if (scope.autoclose) {
              setTimeout(function() {
                scope.toggle();
              }, parseInt(scope.autoclose));
            };
          }

          foundationApi.animate(element, scope.active, animationIn, animationOut);
          scope.$apply();

          return;
        });

        scope.hide = function() {
          scope.active = false;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
          return;
        };

        scope.show = function() {
          scope.active = true;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
          return;
        };

        scope.toggle = function() {
          scope.active = !scope.active;
          foundationApi.animate(element, scope.active, animationIn, animationOut);
          return;
        };

      }
    }
  }

  zfNotify.$inject = ['FoundationApi'];

  function zfNotify(foundationApi) {
    var directive = {
      restrict: 'A',
      scope: {
        title: '@?',
        content: '@?',
        color: '@?',
        image: '@?',
        autoclose: '@?'
      },
      link: link
    };

    return directive;

    function link(scope, element, attrs, controller) {
      element.on('click', function(e) {
        foundationApi.publish(attrs.zfNotify, {
          title: scope.title,
          content: scope.content,
          color: scope.color,
          image: scope.image,
          autoclose: scope.autoclose
        });
        e.preventDefault();
      });
    }
  }

  NotificationFactory.$inject = ['$http', '$templateCache', '$rootScope', '$compile', '$timeout', 'FoundationApi'];

  function NotificationFactory($http, $templateCache, $rootScope, $compile, $timeout, foundationApi) {
    return notificationFactory;

    function notificationFactory(config) {
      var self = this, //for prototype functions
          container = angular.element(config.container || document.body),
          id = config.id || foundationApi.generateUuid(),
          attached = false,
          destroyed = false,
          html,
          element,
          scope
      ;

      var props = [
        'position'
      ];

      assembleDirective();

      self.addNotification = addNotification;
      self.clearAll = clearAll;
      self.destroy = destroy;

      return {
        addNotification: addNotification,
        clearAll: clearAll,
        destroy: destroy
      };

      function checkStatus() {
        if(destroyed) {
          throw "Error: Notification Set was destroyed. Delete the object and create a new NotificationFactory instance."
        }
      }

      function addNotification(notification) {
        checkStatus();
        $timeout(function() {
          init(true, notification);
          foundationApi.publish(id, notification);
        }, 0, false);
      }

      function clearAll() {
        checkStatus();
        $timeout(function() {
          foundationApi.publish(id, 'clearall');
        }, 0, false);
      }

      function init(state, notification) {
        if(!attached && html.length > 0) {
          var modalEl = container.append(element);

          scope.notifications = [ notification ];
          $compile(element)(scope);
          attached = true;
        }
      }

      function assembleDirective() {
        html = '<zf-notification-set id="' + id + '"></zf-notification-set>';

        element = angular.element(html);

        scope = $rootScope.$new();

        for(var prop in props) {
          if(config[prop]) {
            element.attr(prop, config[prop]);
          }
        }
      }

      function destroy() {
        self.clearAll();
        setTimeout(function() {
          scope.$destroy();
          element.remove();
          destroyed = true;
        }, 3000);
      }

    }

  }
})();
<|endoftext|>


<commit_before>(function () {
	// Based on https://github.com/tc39/proposal-promise-finally/blob/master/polyfill.js
	var then = Function.prototype.bind.call(Function.prototype.call, Promise.prototype.then);

	// 7.3.20 SpeciesConstructor(O, defaultConstructor)
	var speciesConstructor = function (O, defaultConstructor) {
		// 7.3.20.1 Assert: Type(O) is Object.
		if (!O || (typeof O !== 'object' && typeof O !== 'function')) {
			throw new TypeError('Assertion failed: Type(O) is not Object');
		}
		// 7.3.20.2 Let C be ? Get(O, "constructor").
		var C = O.constructor;
		// 7.3.20.3 If C is undefined, return defaultConstructor.
		if (typeof C === 'undefined') {
			return defaultConstructor;
		}
		// 7.3.20.4 If Type(C) is not Object, throw a TypeError exception
		if (!C || (typeof C !== 'object' && typeof C !== 'function')) {
			throw new TypeError('O.constructor is not an Object');
		}
		// 7.3.20.5 Let S be ? Get(C, @@species).
		var S = typeof Symbol === 'function' && typeof Symbol.species === 'symbol' ? C[Symbol.species] : undefined;
		// 7.3.20.6 If S is either undefined or null, return defaultConstructor.
		if (S === undefined || S === null) {
			return defaultConstructor;
		}
		// 7.3.20.7 If IsConstructor(S) is true, return S.
		if (typeof S === 'function' && S.prototype) {
			return S;
		}
		// 7.3.20.8 Throw a TypeError exception.
		throw new TypeError('no constructor found');
	};

	var getPromise = function (C, handler) {
		return new C(function (resolve) {
			resolve(handler());
		});
	};

	var promiseFinally = function (onFinally) {
		// 1.1 Let promise be ths this value.
		var promise = this;

		if (typeof onFinally === 'function') {
			var handler = onFinally;
		} else {
			var handler = function () {};
		}

		// 1.2 If IsPromise(promise) is false, throw a TypeError exception.
		// N.B. IsPromise is called within Promise.prototype.then (25.4.5.3)
		var newPromise = then(
			this, // throws if IsPromise(this) is not true
			function (x) {
				return then(getPromise(C, handler), function () {
					return x;
				});
			},
			function (e) {
				return then(getPromise(C, handler), function () {
					throw e;
				});
			}
		);

		// 1.3 Let C be ? SpeciesConstructor(promise, %Promise%).
		var C = speciesConstructor(this, Promise); // throws if SpeciesConstructor throws

		// 1.4 Let resultCapablity be ? NewPromiseCapablity(C).
		// 1.5 Return PerformPromiseFinally(promise, onFinaaly, resultCapability).
		return newPromise;
	};

	Promise.prototype['finally'] = promiseFinally;
}());
<commit_msg>fix typo (#1323)<commit_after>(function () {
	// Based on https://github.com/tc39/proposal-promise-finally/blob/master/polyfill.js
	var then = Function.prototype.bind.call(Function.prototype.call, Promise.prototype.then);

	// 7.3.20 SpeciesConstructor(O, defaultConstructor)
	var speciesConstructor = function (O, defaultConstructor) {
		// 7.3.20.1 Assert: Type(O) is Object.
		if (!O || (typeof O !== 'object' && typeof O !== 'function')) {
			throw new TypeError('Assertion failed: Type(O) is not Object');
		}
		// 7.3.20.2 Let C be ? Get(O, "constructor").
		var C = O.constructor;
		// 7.3.20.3 If C is undefined, return defaultConstructor.
		if (typeof C === 'undefined') {
			return defaultConstructor;
		}
		// 7.3.20.4 If Type(C) is not Object, throw a TypeError exception
		if (!C || (typeof C !== 'object' && typeof C !== 'function')) {
			throw new TypeError('O.constructor is not an Object');
		}
		// 7.3.20.5 Let S be ? Get(C, @@species).
		var S = typeof Symbol === 'function' && typeof Symbol.species === 'symbol' ? C[Symbol.species] : undefined;
		// 7.3.20.6 If S is either undefined or null, return defaultConstructor.
		if (S === undefined || S === null) {
			return defaultConstructor;
		}
		// 7.3.20.7 If IsConstructor(S) is true, return S.
		if (typeof S === 'function' && S.prototype) {
			return S;
		}
		// 7.3.20.8 Throw a TypeError exception.
		throw new TypeError('no constructor found');
	};

	var getPromise = function (C, handler) {
		return new C(function (resolve) {
			resolve(handler());
		});
	};

	var promiseFinally = function (onFinally) {
		// 1.1 Let promise be the this value.
		var promise = this;

		if (typeof onFinally === 'function') {
			var handler = onFinally;
		} else {
			var handler = function () {};
		}

		// 1.2 If IsPromise(promise) is false, throw a TypeError exception.
		// N.B. IsPromise is called within Promise.prototype.then (25.4.5.3)
		var newPromise = then(
			this, // throws if IsPromise(this) is not true
			function (x) {
				return then(getPromise(C, handler), function () {
					return x;
				});
			},
			function (e) {
				return then(getPromise(C, handler), function () {
					throw e;
				});
			}
		);

		// 1.3 Let C be ? SpeciesConstructor(promise, %Promise%).
		var C = speciesConstructor(this, Promise); // throws if SpeciesConstructor throws

		// 1.4 Let resultCapablity be ? NewPromiseCapablity(C).
		// 1.5 Return PerformPromiseFinally(promise, onFinaaly, resultCapability).
		return newPromise;
	};

	Promise.prototype['finally'] = promiseFinally;
}());
<|endoftext|>