<commit_before>/**
 * Dashboards' model
 */

angular.module(PKG.name+'.feature.dashboard').factory('myDashboardsModel',
function (Widget, MyDataSource, $timeout) {

  var dSrc = new MyDataSource(),
      API_PATH = '/configuration/dashboards';

  function Dashboard (p) {
    p = p || {};
    angular.extend(
      this,
      {
        id: p.id,
        title: p.title || 'Dashboard',
        tabOrder: p.tabOrder || 0,
        columns: []
      }
    );

    if(angular.isArray(p.columns)) {
      angular.forEach(p.columns, function (c) {
        this.push(c.map(function (o) {
          return new Widget(o);
        }));
      }, this.columns);
    }
    else {
      // default is a single empty column
      this.columns.push([]);
    }
  }

  /**
   * rename a widget
   */
  Dashboard.prototype.renameWidget = function (widget, newName) {
    if(newName) {
      widget.title = newName;
      this.persist();
    }
  };

  /**
   * remove a widget from the active dashboard tab
   * @param  {object} w the widget object
   */
  Dashboard.prototype.removeWidget = function (widget) {
    var that = this;
    angular.forEach(that.columns, function (c, i) {
      that.columns[i] = c.filter(function (p) {
        return widget !== p;
      });
    });
    this.persist();
  };

  /**
   * add a widget to the active dashboard tab
   */
  Dashboard.prototype.addWidget = function (w) {
    var c = this.columns,
        index = 0,
        smallest;

    // find the column with the least widgets
    for (var i = 0; i < c.length; i++) {
      var len = c[i].length;
      if(smallest===undefined || (len < smallest)) {
        smallest = len;
        index = i;
      }
    }

    w = w || new Widget({
      title: 'just added'
    });

    this.columns[index].unshift(w);
    this.persist();
  };

  /**
   * rename dashboard tab
   */
  Dashboard.prototype.rename = function (newName) {
    if(newName) {
      this.title = newName;
      this.persist();
    }
  };

  /**
   * benefit from angular's toJsonReplacer
   */
  Dashboard.prototype.properties = function () {
    return angular.fromJson(angular.toJson(this));
  };

  /**
   * save to backend
   */
  Dashboard.prototype.persist = function () {
    var body = this.properties();

    if(this.id) { // updating
      dSrc.request(
        {
          method: 'PUT',
          _cdapNsPath: API_PATH + '/' + this.id,
          body: body
        }
      );
    }
    else { // saving
      dSrc.request(
        {
          method: 'POST',
          _cdapNsPath: API_PATH,
          body: body
        },
        (function (result) {
          this.id = result.id;
        }).bind(this)
      );
    }
  };

/* ------------------------------------------------------------------------- */

  function Model () {
    var data = [];
    data.activeIndex = 0;

    this.data = data;

    dSrc.request(
      {
        method: 'GET',
        _cdapNsPath: API_PATH
      },
      (function (result) {

        if(result.length) {
          // recreate saved dashboards
          angular.forEach(result, function (v, k) {
            var p = v.config;
            p.id = v.id;
            p.tabOrder = k;
            data.push(new Dashboard(p));
          });

        } else { // no dashboards yet
          this.add(); // create a new default one
        }
      }).bind(this)
    );

  }

  /**
   * direct access to the current tab's Dashboard instance
   */
  Model.prototype.current = function () {
    return this.data[this.data.activeIndex || 0];
  };

  /**
   * remove a dashboard tab
   */
  Model.prototype.remove = function (index) {
    var removed = this.data.splice(index, 1)[0];

    dSrc.request(
      {
        method: 'DELETE',
        _cdapNsPath: API_PATH + '/' + removed.id
      }
    );
  };

  /**
   * add a new dashboard tab
   */
  Model.prototype.add = function (properties, colCount) {
    properties = properties || {};

    // we will want this dashboard to get the lowest tabOrder value
    if(this.data.length) {
      properties.tabOrder = this.data.reduce(function(prev,curr){
        return Math.min(prev, curr.tabOrder-1);
      }, 0);
    }

    var d = new Dashboard(properties);

    // add columns as needed
    for (var i = 1; i < (colCount||3); i++) {
      d.columns.push([]);
    }

    // default widget in first column
    d.columns[0].push(new Widget());

    // save to backend
    d.persist();

    // newly created tab becomes active
    this.data.push(d);
    this.data.activeIndex = 0;
  };

  return new Model();
});

<commit_msg>todo: unit activeIndex/tabOrder dichotomy<commit_after>/**
 * Dashboards' model
 */

angular.module(PKG.name+'.feature.dashboard').factory('myDashboardsModel',
function (Widget, MyDataSource, $timeout) {

  var dSrc = new MyDataSource(),
      API_PATH = '/configuration/dashboards';

  function Dashboard (p) {
    p = p || {};
    angular.extend(
      this,
      {
        id: p.id,
        title: p.title || 'Dashboard',
        tabOrder: p.tabOrder || 0,
        columns: []
      }
    );

    if(angular.isArray(p.columns)) {
      angular.forEach(p.columns, function (c) {
        this.push(c.map(function (o) {
          return new Widget(o);
        }));
      }, this.columns);
    }
    else {
      // default is a single empty column
      this.columns.push([]);
    }
  }

  /**
   * rename a widget
   */
  Dashboard.prototype.renameWidget = function (widget, newName) {
    if(newName) {
      widget.title = newName;
      this.persist();
    }
  };

  /**
   * remove a widget from the active dashboard tab
   * @param  {object} w the widget object
   */
  Dashboard.prototype.removeWidget = function (widget) {
    var that = this;
    angular.forEach(that.columns, function (c, i) {
      that.columns[i] = c.filter(function (p) {
        return widget !== p;
      });
    });
    this.persist();
  };

  /**
   * add a widget to the active dashboard tab
   */
  Dashboard.prototype.addWidget = function (w) {
    var c = this.columns,
        index = 0,
        smallest;

    // find the column with the least widgets
    for (var i = 0; i < c.length; i++) {
      var len = c[i].length;
      if(smallest===undefined || (len < smallest)) {
        smallest = len;
        index = i;
      }
    }

    w = w || new Widget({
      title: 'just added'
    });

    this.columns[index].unshift(w);
    this.persist();
  };

  /**
   * rename dashboard tab
   */
  Dashboard.prototype.rename = function (newName) {
    if(newName) {
      this.title = newName;
      this.persist();
    }
  };

  /**
   * benefit from angular's toJsonReplacer
   */
  Dashboard.prototype.properties = function () {
    return angular.fromJson(angular.toJson(this));
  };

  /**
   * save to backend
   */
  Dashboard.prototype.persist = function () {
    var body = this.properties();

    if(this.id) { // updating
      dSrc.request(
        {
          method: 'PUT',
          _cdapNsPath: API_PATH + '/' + this.id,
          body: body
        }
      );
    }
    else { // saving
      dSrc.request(
        {
          method: 'POST',
          _cdapNsPath: API_PATH,
          body: body
        },
        (function (result) {
          this.id = result.id;
        }).bind(this)
      );
    }
  };

/* ------------------------------------------------------------------------- */

  function Model () {
    var data = [];
    data.activeIndex = 0;

    this.data = data;

    dSrc.request(
      {
        method: 'GET',
        _cdapNsPath: API_PATH
      },
      (function (result) {

        if(result.length) {
          // recreate saved dashboards
          angular.forEach(result, function (v, k) {
            var p = v.config;
            p.id = v.id;
            p.tabOrder = k;
            data.push(new Dashboard(p));
          });

        } else { // no dashboards yet
          this.add(); // create a new default one
        }
      }).bind(this)
    );

  }

  /**
   * direct access to the current tab's Dashboard instance
   */
  Model.prototype.current = function () {
    return this.data[this.data.activeIndex || 0];
  };

  /**
   * remove a dashboard tab
   */
  Model.prototype.remove = function (index) {
    var removed = this.data.splice(index, 1)[0];

    dSrc.request(
      {
        method: 'DELETE',
        _cdapNsPath: API_PATH + '/' + removed.id
      }
    );
  };

  /**
   * add a new dashboard tab
   */
  Model.prototype.add = function (properties, colCount) {
    properties = properties || {};

    // we will want this dashboard to get the lowest tabOrder value
    if(this.data.length) {
      properties.tabOrder = this.data.reduce(function(prev,curr){
        return Math.min(prev, curr.tabOrder-1);
      }, 0);
    }

    var d = new Dashboard(properties);

    // add columns as needed
    for (var i = 1; i < (colCount||3); i++) {
      d.columns.push([]);
    }

    // default widget in first column
    d.columns[0].push(new Widget());

    // save to backend
    d.persist();

    // newly created tab becomes active
    this.data.unshift(d);
    this.data.activeIndex = 0;
  };

  return new Model();
});

<|endoftext|>


<commit_before>import React, { Component } from 'react'
import classNames from 'classnames'
import Dropzone from 'react-dropzone'
import { SortableContainer, SortableElement, sortableHandle } from 'react-sortable-hoc'

import ImageItem from './ImageItem'
import Image from './Image'

import styles from './Images.module.css'

class Images extends React.Component {
  state = {
    files: [],
    images: []
  }

  static getDerivedStateFromProps(props, state) {
    if(props.images !== state.images && state.images.length == 0) {
      return {
        images: props.images,
      };
    }

    return null;
  }

  render () {
    const { images, files} = this.state

    return (
      <List images={images} files={files} onImagesChange={this.handleImagesChange} onFileUpload={this.handleFileUpload} onDrop={this.onDrop} onSortEnd={this._onSortEnd} axis={'xy'} useDragHandle />
    )
  }

  _onSortEnd = ({ oldIndex, newIndex }) => {
    let sort = this.state.images.slice();
    sort.splice(oldIndex, 1);
    sort.splice(newIndex, 0, this.state.images[oldIndex]);

    this.setState({
      images: sort
    }, () => {
      this.props.onImagesChange(sort)
    });
  }

  handleImagesChange = (id) => {
    let images = this.state.images.filter(i => i.id !== id)
    this.setState(state => ({
      images: images
    }))
  }

  onDrop = (files) => {
    this.setState(state => ({
      files: [...state.files, ...files]
    }));
  }

  handleFileUpload = image => {
    this.setState({
      images: [...this.state.images, image]
    }, () => {
      this.props.onImagesChange(this.state.images)
    });
  }
}

const DragHandle = sortableHandle(() =>
)

const Item = SortableElement(({ image, onImagesChange }) => {
  return (

      <DragHandle />
      <ImageItem image={image} onImagesChange={onImagesChange}></ImageItem>

  )
})

const List = SortableContainer(({ images, files, onImagesChange, onFileUpload, onDrop }) => {
  return (

      <Dropzone onDrop={onDrop}>
        {({getRootProps, getInputProps}) => (
          <>

              <input {...getInputProps()} />
              Добавить фото

          </>
        )}
      </Dropzone>

      {images &&
        images.map((image, index) => (
          <Item key={image.id} index={index} image={image} onImagesChange={onImagesChange}/>
        ))
      }

      {files.map((file, _) =>
        <Image file={file} key={_} onFileUpload={onFileUpload}/>
      )}

  )
})

export default Images
<commit_msg>Поправлен косяк с удалением Images<commit_after>import React, { Component } from 'react'
import classNames from 'classnames'
import Dropzone from 'react-dropzone'
import { SortableContainer, SortableElement, sortableHandle } from 'react-sortable-hoc'

import ImageItem from './ImageItem'
import Image from './Image'

import styles from './Images.module.css'

class Images extends React.Component {
  state = {
    files: [],
    images: []
  }

  static getDerivedStateFromProps(props, state) {
    if(props.images !== state.images && state.images.length == 0) {
      return {
        images: props.images,
      };
    }

    return null;
  }

  render () {
    const { images, files} = this.state

    return (
      <List images={images} files={files} onImagesChange={this.handleImagesChange} onFileUpload={this.handleFileUpload} onDrop={this.onDrop} onSortEnd={this._onSortEnd} axis={'xy'} useDragHandle />
    )
  }

  _onSortEnd = ({ oldIndex, newIndex }) => {
    let sort = this.state.images.slice();
    sort.splice(oldIndex, 1);
    sort.splice(newIndex, 0, this.state.images[oldIndex]);

    this.setState({
      images: sort
    }, () => {
      this.props.onImagesChange(sort)
    });
  }

  handleImagesChange = (id) => {
    let images = this.state.images.filter(i => i.id !== id)
    this.setState({
      images: images
    }, () => {
      this.props.onImagesChange(images)
    })
  }

  onDrop = (files) => {
    this.setState(state => ({
      files: [...state.files, ...files]
    }));
  }

  handleFileUpload = image => {
    this.setState({
      images: [...this.state.images, image]
    }, () => {
      this.props.onImagesChange(this.state.images)
    });
  }
}

const DragHandle = sortableHandle(() =>
)

const Item = SortableElement(({ image, onImagesChange }) => {
  return (

      <DragHandle />
      <ImageItem image={image} onImagesChange={onImagesChange}></ImageItem>

  )
})

const List = SortableContainer(({ images, files, onImagesChange, onFileUpload, onDrop }) => {
  return (

      <Dropzone onDrop={onDrop}>
        {({getRootProps, getInputProps}) => (
          <>

              <input {...getInputProps()} />
              Добавить фото

          </>
        )}
      </Dropzone>

      {images &&
        images.map((image, index) => (
          <Item key={image.id} index={index} image={image} onImagesChange={onImagesChange}/>
        ))
      }

      {files.map((file, _) =>
        <Image file={file} key={_} onFileUpload={onFileUpload}/>
      )}

  )
})

export default Images
<|endoftext|>


<commit_before>import log from "../utils/log";
import _ from "lodash";

export default class BinxMapping {
  // The page instance of this class is modified to store the current list of points for rendering
  constructor(kind) {
    this.kind = kind;
    this.searchBox = null;
    this.searchMarkers = [];
    this.markerPointsToRender = [];
    this.markersRendered = [];
    this.mapRendered = false;
  }

  loadMap(callback) {
    if (window.googleMapInjected || this.googleMapsLoaded()) {
      return true;
    }
    // Add google maps script
    var js_file = document.createElement("script");
    js_file.type = "text/javascript";
    js_file.src = `https://maps.googleapis.com/maps/api/js?callback=${callback}&key=${window.pageInfo.google_maps_key}&libraries=places`;
    document.getElementsByTagName("head")[0].appendChild(js_file);
    window.googleMapInjected = true;
  }

  render(lat, lng, zoom = null) {
    if (zoom == null) {
      zoom = 13;
    }
    this.zoom = zoom;
    let myOptions = {
      zoom: zoom,
      center: new google.maps.LatLng(lat, lng),
      gestureHandling: "cooperative",
      mapTypeId: google.maps.MapTypeId.ROADMAP,
    };
    if (!window.infoWindow) {
      window.infoWindow = new google.maps.InfoWindow();
    }
    window.binxMap = new google.maps.Map(
      document.getElementById("map"),
      myOptions
    );
  }

  googleMapsLoaded() {
    return typeof google === "object" && typeof google.maps === "object";
  }

  fitMap() {
    let bounds = new google.maps.LatLngBounds();
    // Fit to markers
    for (let marker of Array.from(this.markersRendered)) {
      if (marker) {
        bounds.extend(marker.getPosition());
      }
    }
    binxMap.fitBounds(bounds);

    // Finish rendering map, then check - if too zoomed in (e.g. on one point), zoom out
    window.setTimeout(function () {
      if (binxMap.zoom > 16) {
        binxMap.setZoom(16);
      }
    }, 500);
  }

  boundingBoxParams() {
    if (!binxMapping.mapRendered) {
      return [];
    }
    let bounds = binxMap.getBounds();
    return [
      ["search_southwest_coords", bounds.getSouthWest().toUrlValue()],
      ["search_northeast_coords", bounds.getNorthEast().toUrlValue()],
    ];
  }

  renderAddressSearch() {
    if (this.searchBox != null) {
      return true;
    }
    $("#map").before(
      '<input id="placeSearch" class="controls form-control" type="text" placeholder="Search map">'
    );
    let input = document.getElementById("placeSearch");
    this.searchBox = new google.maps.places.SearchBox(input);
    window.binxMap.controls[google.maps.ControlPosition.TOP_RIGHT].push(input);
    $(input).addClass("searchOnMap"); // search box is initially hidden - display it when rendered on map
    // Bias SearchBox results towards current map's viewport.
    binxMap.addListener("bounds_changed", () =>
      binxMapping.searchBox.setBounds(binxMap.getBounds())
    );

    this.searchBox.addListener("places_changed", function () {
      let places = binxMapping.searchBox.getPlaces();
      if (places.length === 0) {
        log.debug("Unable to find that address");
      }
      // For each place, get the icon, name and location.
      let bounds = new google.maps.LatLngBounds();
      places.forEach(function (place) {
        if (!place.geometry) {
          log.debug("Returned place contains no geometry");
        }
        // Create a marker for each place
        binxMapping.searchMarkers.push(
          new google.maps.Marker({
            map: binxMap,
            icon:
              "http://maps.google.com/mapfiles/ms/micons/purple-pushpin.png",
            title: place.name,
            position: place.geometry.location,
          })
        );
        bounds.extend(place.geometry.location);
      });
      binxMap.fitBounds(bounds);
    });
  }

  removeMarkersWithoutMatchingIds(markers) {
    if (!binxMapping.markersRendered) {
      binxMapping.markersRendered = [];
    }

    const newIds = markers.map((m) => m.id);

    if (window.infoWindow != null) {
      window.infoWindow.close();
    }

    let markersToKeep = [];

    while (binxMapping.markersRendered.length > 0) {
      const marker = binxMapping.markersRendered.pop();
      // If marker isn't around, ignore it
      if (marker != null) {
        // If the id is a match, move it to the new array, otherwise remove it from the map
        if (
          typeof marker.binxId != "undefined" &&
          newIds.includes(marker.binxId)
        ) {
          markersToKeep.push(marker);
        } else {
          marker.setMap(null);
        }
      }
    }
    binxMapping.markersRendered = markersToKeep;

    // Ids make sure we aren't double rendering
    return (window.renderedMarkerIds = newIds);
  }

  clearMarkers() {
    if (!binxMapping.markersRendered) {
      binxMapping.markersRendered = [];
    }

    if (window.infoWindow != null) {
      window.infoWindow.close();
    }
    // Useful when the page reloads without fully reloading
    if (binxMapping.markersRendered.length) {
      for (let i of Array.from(binxMapping.markersRendered)) {
        i != null && i.setMap(null);
      }
    }
    // Empty the array
    binxMapping.markersRendered = [];
    // Ids make sure we aren't double rendering
    return (window.renderedMarkerIds = []);
  }

  markersInViewport() {
    let bounds = binxMap.getBounds();
    return _.filter(binxMapping.markersRendered, function (m) {
      return bounds.contains(m.getPosition());
    });
  }

  renderMarker(point) {
    let popupContent = "";
    if (binxMapping.kind == "parking_notifications") {
      popupContent = binxAppOrgParkingNotificationMapping.mapPopup(point);
    } else {
      log.debug(binxMapping.kind);
      popupContent = "Missing template!";
    }
    window.infoWindow.setContent(popupContent);
    // Ensure things are rendered before setting times, pause for a hot sec
    window.setTimeout(() => window.timeParser.localize(), 50);
  }

  openInfoWindow(marker, markerId, point) {
    window.infoWindow.setContent("");
    window.infoWindow.open(window.binxMap, marker);
    binxMapping.enableEscapeForInfoWindows();
    // For an unclear reason, this needs to return the rendered marker
    return binxMapping.renderMarker(point);
  }

  enableEscapeForInfoWindows() {
    // Enable using escape key to close info windows
    // Make sure we aren't adding duplicate handlers, sometimes we don't catch the close event
    $(window).off("keyup");
    // Add the trigger for the escape closing the window
    $(window).on("keyup", function (e) {
      if (e.keyCode === 27) {
        window.infoWindow.close();
        // Escape was pressed, infoWindow is closed, so remove the keyup handler
        $(window).off("keyup");
      }
      return true; // allow bubbling up
    });
    // on infowindow close, remove keyup handler - aka clean up after yourself
    google.maps.event.addListener(window.infoWindow, "closeclick", function () {
      $(window).off("keyup");
    });
  }

  addMarkers({ fitMap = false, renderAddressSearch = true }) {
    log.debug(`adding markers - fitMap: ${fitMap}`);
    while (binxMapping.markerPointsToRender.length > 0) {
      let point = binxMapping.markerPointsToRender.shift();
      let markerId = point.id;
      // Only render if the point isn't already rendered
      if (!_.find(binxMapping.markersRendered, ["binxId", point.id])) {
        let marker = new google.maps.Marker({
          position: new google.maps.LatLng(point.lat, point.lng),
          map: window.binxMap,
          binxId: markerId,
        });

        google.maps.event.addListener(
          marker,
          "click",
          ((marker, markerId) =>
            function () {
              return binxMapping.openInfoWindow(marker, markerId, point);
            })(marker, markerId)
        );
        binxMapping.markersRendered.push(marker);
      }
    }
    // If we're suppose to fit the map to the markers - and if there are markers that have been rendered - fit it
    if (fitMap == true && binxMapping.markersRendered.length > 0) {
      binxMapping.fitMap();
    }
    // If we're suppose to include the address search, do it
    if (renderAddressSearch == true) {
      binxMapping.renderAddressSearch();
    }
    if (binxMapping.mapRendered) {
      return true;
    }
    // If map isn't rendered, give the page a second to catch up
    window.setTimeout(function () {
      binxMapping.mapRendered = true;
    }, 1000);
  }
}
<commit_msg>make markers slightly opaque (#1709)<commit_after>import log from "../utils/log";
import _ from "lodash";

export default class BinxMapping {
  // The page instance of this class is modified to store the current list of points for rendering
  constructor(kind) {
    this.kind = kind;
    this.searchBox = null;
    this.searchMarkers = [];
    this.markerPointsToRender = [];
    this.markersRendered = [];
    this.mapRendered = false;
  }

  loadMap(callback) {
    if (window.googleMapInjected || this.googleMapsLoaded()) {
      return true;
    }
    // Add google maps script
    var js_file = document.createElement("script");
    js_file.type = "text/javascript";
    js_file.src = `https://maps.googleapis.com/maps/api/js?callback=${callback}&key=${window.pageInfo.google_maps_key}&libraries=places`;
    document.getElementsByTagName("head")[0].appendChild(js_file);
    window.googleMapInjected = true;
  }

  render(lat, lng, zoom = null) {
    if (zoom == null) {
      zoom = 13;
    }
    this.zoom = zoom;
    let myOptions = {
      zoom: zoom,
      center: new google.maps.LatLng(lat, lng),
      gestureHandling: "cooperative",
      mapTypeId: google.maps.MapTypeId.ROADMAP,
    };
    if (!window.infoWindow) {
      window.infoWindow = new google.maps.InfoWindow();
    }
    window.binxMap = new google.maps.Map(
      document.getElementById("map"),
      myOptions
    );
  }

  googleMapsLoaded() {
    return typeof google === "object" && typeof google.maps === "object";
  }

  fitMap() {
    let bounds = new google.maps.LatLngBounds();
    // Fit to markers
    for (let marker of Array.from(this.markersRendered)) {
      if (marker) {
        bounds.extend(marker.getPosition());
      }
    }
    binxMap.fitBounds(bounds);

    // Finish rendering map, then check - if too zoomed in (e.g. on one point), zoom out
    window.setTimeout(function () {
      if (binxMap.zoom > 16) {
        binxMap.setZoom(16);
      }
    }, 500);
  }

  boundingBoxParams() {
    if (!binxMapping.mapRendered) {
      return [];
    }
    let bounds = binxMap.getBounds();
    return [
      ["search_southwest_coords", bounds.getSouthWest().toUrlValue()],
      ["search_northeast_coords", bounds.getNorthEast().toUrlValue()],
    ];
  }

  renderAddressSearch() {
    if (this.searchBox != null) {
      return true;
    }
    $("#map").before(
      '<input id="placeSearch" class="controls form-control" type="text" placeholder="Search map">'
    );
    let input = document.getElementById("placeSearch");
    this.searchBox = new google.maps.places.SearchBox(input);
    window.binxMap.controls[google.maps.ControlPosition.TOP_RIGHT].push(input);
    $(input).addClass("searchOnMap"); // search box is initially hidden - display it when rendered on map
    // Bias SearchBox results towards current map's viewport.
    binxMap.addListener("bounds_changed", () =>
      binxMapping.searchBox.setBounds(binxMap.getBounds())
    );

    this.searchBox.addListener("places_changed", function () {
      let places = binxMapping.searchBox.getPlaces();
      if (places.length === 0) {
        log.debug("Unable to find that address");
      }
      // For each place, get the icon, name and location.
      let bounds = new google.maps.LatLngBounds();
      places.forEach(function (place) {
        if (!place.geometry) {
          log.debug("Returned place contains no geometry");
        }
        // Create a marker for each place
        binxMapping.searchMarkers.push(
          new google.maps.Marker({
            map: binxMap,
            icon:
              "http://maps.google.com/mapfiles/ms/micons/purple-pushpin.png",
            title: place.name,
            position: place.geometry.location,
          })
        );
        bounds.extend(place.geometry.location);
      });
      binxMap.fitBounds(bounds);
    });
  }

  removeMarkersWithoutMatchingIds(markers) {
    if (!binxMapping.markersRendered) {
      binxMapping.markersRendered = [];
    }

    const newIds = markers.map((m) => m.id);

    if (window.infoWindow != null) {
      window.infoWindow.close();
    }

    let markersToKeep = [];

    while (binxMapping.markersRendered.length > 0) {
      const marker = binxMapping.markersRendered.pop();
      // If marker isn't around, ignore it
      if (marker != null) {
        // If the id is a match, move it to the new array, otherwise remove it from the map
        if (
          typeof marker.binxId != "undefined" &&
          newIds.includes(marker.binxId)
        ) {
          markersToKeep.push(marker);
        } else {
          marker.setMap(null);
        }
      }
    }
    binxMapping.markersRendered = markersToKeep;

    // Ids make sure we aren't double rendering
    return (window.renderedMarkerIds = newIds);
  }

  clearMarkers() {
    if (!binxMapping.markersRendered) {
      binxMapping.markersRendered = [];
    }

    if (window.infoWindow != null) {
      window.infoWindow.close();
    }
    // Useful when the page reloads without fully reloading
    if (binxMapping.markersRendered.length) {
      for (let i of Array.from(binxMapping.markersRendered)) {
        i != null && i.setMap(null);
      }
    }
    // Empty the array
    binxMapping.markersRendered = [];
    // Ids make sure we aren't double rendering
    return (window.renderedMarkerIds = []);
  }

  markersInViewport() {
    let bounds = binxMap.getBounds();
    return _.filter(binxMapping.markersRendered, function (m) {
      return bounds.contains(m.getPosition());
    });
  }

  renderMarker(point) {
    let popupContent = "";
    if (binxMapping.kind == "parking_notifications") {
      popupContent = binxAppOrgParkingNotificationMapping.mapPopup(point);
    } else {
      log.debug(binxMapping.kind);
      popupContent = "Missing template!";
    }
    window.infoWindow.setContent(popupContent);
    // Ensure things are rendered before setting times, pause for a hot sec
    window.setTimeout(() => window.timeParser.localize(), 50);
  }

  openInfoWindow(marker, markerId, point) {
    window.infoWindow.setContent("");
    window.infoWindow.open(window.binxMap, marker);
    binxMapping.enableEscapeForInfoWindows();
    // For an unclear reason, this needs to return the rendered marker
    return binxMapping.renderMarker(point);
  }

  enableEscapeForInfoWindows() {
    // Enable using escape key to close info windows
    // Make sure we aren't adding duplicate handlers, sometimes we don't catch the close event
    $(window).off("keyup");
    // Add the trigger for the escape closing the window
    $(window).on("keyup", function (e) {
      if (e.keyCode === 27) {
        window.infoWindow.close();
        // Escape was pressed, infoWindow is closed, so remove the keyup handler
        $(window).off("keyup");
      }
      return true; // allow bubbling up
    });
    // on infowindow close, remove keyup handler - aka clean up after yourself
    google.maps.event.addListener(window.infoWindow, "closeclick", function () {
      $(window).off("keyup");
    });
  }

  addMarkers({ fitMap = false, renderAddressSearch = true }) {
    log.debug(`adding markers - fitMap: ${fitMap}`);
    while (binxMapping.markerPointsToRender.length > 0) {
      let point = binxMapping.markerPointsToRender.shift();
      let markerId = point.id;
      let markerOpacity = binxMapping.kind == "parking_notifications" ? 0.6 : 1;
      // Only render if the point isn't already rendered
      if (!_.find(binxMapping.markersRendered, ["binxId", point.id])) {
        let marker = new google.maps.Marker({
          position: new google.maps.LatLng(point.lat, point.lng),
          map: window.binxMap,
          binxId: markerId,
          opacity: markerOpacity,
        });

        google.maps.event.addListener(
          marker,
          "click",
          ((marker, markerId) =>
            function () {
              return binxMapping.openInfoWindow(marker, markerId, point);
            })(marker, markerId)
        );
        binxMapping.markersRendered.push(marker);
      }
    }
    // If we're suppose to fit the map to the markers - and if there are markers that have been rendered - fit it
    if (fitMap == true && binxMapping.markersRendered.length > 0) {
      binxMapping.fitMap();
    }
    // If we're suppose to include the address search, do it
    if (renderAddressSearch == true) {
      binxMapping.renderAddressSearch();
    }
    if (binxMapping.mapRendered) {
      return true;
    }
    // If map isn't rendered, give the page a second to catch up
    window.setTimeout(function () {
      binxMapping.mapRendered = true;
    }, 1000);
  }
}
<|endoftext|>


<commit_before>//======================================================================
// ExCL is an open source mobile platform for museums that feature basic
// museum information and extends visitor engagement with museum exhibits.
// Copyright (C) 2014  Children's Museum of Houston and the Regents of the
// University of California.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//=====================================================================

function languageService(){
};

languageService.prototype.getLanguageOptionsFourLetterCodeFromJSON = function(){
	var languageOptionsFourLetter = new Array();
	languageOptionsFourLetter = this.cloneLanguageOptions();
	Ti.API.info("Language options: " + languageOptionsFourLetter.toString());
	return languageOptionsFourLetter;
};

languageService.prototype.cloneLanguageOptions = function(){
	clonedLanguageOptions = new Array();
	languageOptions = Alloy.Globals.museumJSON.data.museum.lang_options;
	if (languageOptions instanceof Array){
		for (index in Alloy.Globals.museumJSON.data.museum.lang_options){
			clonedLanguageOptions[index] = Alloy.Globals.museumJSON.data.museum.lang_options[index];
		}
	}
	else{
		Ti.API.info("Not an array- single language");
		clonedLanguageOptions.push(languageOptions);
	}

	return clonedLanguageOptions;
};

languageService.prototype.getLanguageOptionsFullWord = function(languageOptionsFourLetter){
	var languageOptionsFullWord = [];
	for (index in languageOptionsFourLetter){
		languageOptionsFullWord[index] = this.switchFormOfLanguageCode(languageOptionsFourLetter[index], 'fullWord');
	}
	return languageOptionsFullWord;
};

languageService.prototype.switchFormOfLanguageCode = function(fourLetter, desiredForm){
	index = desiredForm == 'twoLetter'? 0 : 2;
	var toReturn = "";
	var length = this.languageLibrary.length;
	for (i = 0; i<length; i++){
		if (this.languageLibrary[i][1] == fourLetter){
			return this.languageLibrary[i][index];
		}
	}
	return ""; //TODO: Accommodate language not found
};

languageService.prototype.getLanguageOptionsTwoLetterCode = function(languageOptionsFourLetter){
	var languageOptionsTwoLetter = [];
	for (index in languageOptionsFourLetter){
		languageOptionsTwoLetter[index] = this.switchFormOfLanguageCode(languageOptionsFourLetter[index], 'twoLetter');
	}
	return languageOptionsTwoLetter;
};

languageService.prototype.configureCancel = function(languageOptionsFourLetter, languageOptionsFullWord){
	languageOptionsFourLetter.push('CANCEL');
	if (OS_IOS){
		languageOptionsFullWord.push('Cancel');
	}
	cancelIndex = OS_IOS? (languageOptionsFullWord.length - 1) : languageOptionsFullWord.length;
	return cancelIndex;
};

languageService.prototype.displayDialog = function(){
	languageOptionsFourLetterCode = this.getLanguageOptionsFourLetterCodeFromJSON();
	languageOptionsFullWord = this.getLanguageOptionsFullWord(languageOptionsFourLetterCode);
	languageOptionsTwoLetterCode = this.getLanguageOptionsTwoLetterCode(languageOptionsFourLetterCode);

	cancelIndex = this.configureCancel(languageOptionsFourLetterCode, languageOptionsFullWord);
	selectedIndex = languageOptionsFourLetterCode.indexOf(Alloy.Models.app.get('currentLanguage'));
	if (selectedIndex == -1){
		selectedIndex = languageOptionsTwoLetterCode.indexOf(Alloy.Models.app.get('currentLanguage'));
		//If currentLanguage was pulled from device, it will be in two-letter form. Must check for this to set proper default choice
	}

	var languageDialog = Titanium.UI.createOptionDialog({
		options : languageOptionsFullWord,
		cancel : cancelIndex,
		selectedIndex : selectedIndex
	});
	languageDialog.addEventListener("click", function(e){
		languageService.prototype.languageDialogClickListener(e, languageOptionsFourLetterCode);
	});

	languageDialog.show();
};

languageService.prototype.languageDialogClickListener = function(e, languageOptionsFourLetterCode){
	var newLanguage = languageOptionsFourLetterCode[e.index];
	if (newLanguage != 'CANCEL' && newLanguage.substr(0,1) !=  Alloy.Models.app.get('currentLanguage').substr(0,1)){
		Alloy.Models.app.set('currentLanguage', newLanguage);
	}
	else{
		Alloy.Globals.navController.closeMenuWithoutAnimation();
	}
};

languageService.prototype.languageLibrary = [
	['af', 'af', 'Afrikaans'],
	['ar', 'ar', 'العربية', 'rtl'],
	['be', 'be_BY', 'Беларуская мова'],
	['bg', 'bg_BG', 'български'],
	['bs', 'bs_BA', 'Bosanski'],
	['ca', 'ca', 'Català'],
	['cs', 'cs_CZ', 'Čeština'],
	['cy', 'cy', 'Cymraeg'],
	['da', 'da_DK', 'Dansk'],
	['de', 'de_DE', 'Deutsch'],
	['el', 'el', 'Ελληνικά'],
	['en', 'en_CA', 'English'],
	['en', 'en_US', 'English'],
	['eo', 'eo', 'Esperanto'],
	['es', 'es_CL', 'Español'],
	['es', 'es_ES', 'Español'],
	['es', 'es_PE', 'Español'],
	['es', 'es_VE', 'Español'],
	['et', 'et', 'Eesti'],
	['fa', 'fa_AF', 'فارسی', 'rtl'],
	['fa', 'fa_IR', 'فارسی', 'rtl'],
	['fi', 'fi', 'Suomi'],
	['fo', 'fo', 'Føroyskt'],
	['fr', 'fr_FR', 'Français'],
	['fy', 'fy', 'Frysk'],
	['gd', 'gd', 'Gàidhlig'],
	['gl', 'gl_ES', 'Galego'],
	['he', 'he_IL', 'עברית', 'rtl'],
	['hi', 'hi_IN', 'हिन्दी'],
	['hr', 'hr', 'Hrvatski'],
	['hu', 'hu_HU', 'Magyar'],
	['id', 'id_ID', 'Bahasa Indonesia'],
	['is', 'is_IS', 'Íslenska'],
	['it', 'it_IT', 'Italiano'],
	['ja', 'ja', '日本語'],
	['jv', 'jv_ID', 'Basa Jawa'],
	['ka', 'ka_GE', 'ქართული'],
	['kk', 'kk', 'Қазақ тілі'],
	['ko', 'ko_KR', '한국어'],
	['ku', 'ckb', 'کوردی', 'rtl'],
	['lo', 'lo', 'ພາສາລາວ'],
	['lt', 'lt_LT', 'Lietuviškai'],
	['lv', 'lv', 'Latviešu valoda'],
	['mk', 'mk_MK', 'македонски јазик'],
	['mn', 'mn', 'Монгол хэл'],
	['ms', 'ms_MY', 'Bahasa Melayu'],
	['my', 'my_MM', 'ဗမာစာ'],
	['nb', 'nb_NO', 'Norsk Bokmål'],
	['ne', 'ne_NP', 'नेपाली'],
	['nl', 'nl_NL', 'Nederlands'],
	['nn', 'nn_NO', 'Norsk Nynorsk'],
	['pl', 'pl_PL', 'Polski'],
	['pt', 'pt_BR', 'Português'],
	['pt', 'pt_PT', 'Português'],
	['ro', 'ro_RO', 'Română'],
	['ru', 'ru_RU', 'Русский'],
	['si', 'si_LK', 'සිංහල'],
	['sk', 'sk_SK', 'Slovenčina'],
	['sl', 'sl_SI', 'Slovenščina'],
	['so', 'so_SO', 'Af-Soomaali'],
	['sq', 'sq', 'Shqip'],
	['sr', 'sr_RS', 'Српски језик'],
	['su', 'su_ID', 'Basa Sunda'],
	['sv', 'sv_SE', 'Svenska'],
	['ta', 'ta_LK', 'தமிழ்'],
	['th', 'th', 'ไทย'],
	['tr', 'tr_TR', 'Türkçe'],
	['ug', 'ug_CN', 'Uyƣurqə'],
	['uk', 'uk', 'Українська'],
	['ur', 'ur', 'اردو', 'rtl'],
	['uz', 'uz_UZ', 'Oʻzbek'],
	['vec', 'vec', 'Vèneto'],
	['vi', 'vi', 'Tiếng Việt'],
	['zh', 'zh_CN', '中文 (中国)'],
	['zh', 'zh_HK', '中文 (香港)'],
	['zh', 'zh_TW', '中文 (台灣)'],
];

module.exports = languageService;<commit_msg>jen's language fix<commit_after>//======================================================================
// ExCL is an open source mobile platform for museums that feature basic
// museum information and extends visitor engagement with museum exhibits.
// Copyright (C) 2014  Children's Museum of Houston and the Regents of the
// University of California.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//=====================================================================

function languageService(){
};

languageService.prototype.getLanguageOptionsFourLetterCodeFromJSON = function(){
	var languageOptionsFourLetter = new Array();
	languageOptionsFourLetter = this.cloneLanguageOptions();
	Ti.API.info("Language options: " + languageOptionsFourLetter.toString());
	return languageOptionsFourLetter;
};

languageService.prototype.cloneLanguageOptions = function(){
	clonedLanguageOptions = new Array();
	languageOptions = Alloy.Globals.museumJSON.data.museum.lang_options;
	if (languageOptions instanceof Array){
		for (index in Alloy.Globals.museumJSON.data.museum.lang_options){
			clonedLanguageOptions[index] = Alloy.Globals.museumJSON.data.museum.lang_options[index];
		}
	}
	else{
		Ti.API.info("Not an array- single language");
		clonedLanguageOptions.push(languageOptions);
	}

	return clonedLanguageOptions;
};

languageService.prototype.getLanguageOptionsFullWord = function(languageOptionsFourLetter){
	var languageOptionsFullWord = [];
	for (index in languageOptionsFourLetter){
		languageOptionsFullWord[index] = this.switchFormOfLanguageCode(languageOptionsFourLetter[index], 'fullWord');
	}
	return languageOptionsFullWord;
};

languageService.prototype.switchFormOfLanguageCode = function(fourLetter, desiredForm){
	index = desiredForm == 'twoLetter'? 0 : 2;
	var toReturn = "";
	var length = this.languageLibrary.length;
	for (i = 0; i<length; i++){
		if (this.languageLibrary[i][1] == fourLetter){
			return this.languageLibrary[i][index];
		}
	}
	return ""; //TODO: Accommodate language not found
};

languageService.prototype.getLanguageOptionsTwoLetterCode = function(languageOptionsFourLetter){
	var languageOptionsTwoLetter = [];
	for (index in languageOptionsFourLetter){
		languageOptionsTwoLetter[index] = this.switchFormOfLanguageCode(languageOptionsFourLetter[index], 'twoLetter');
	}
	return languageOptionsTwoLetter;
};

languageService.prototype.configureCancel = function(languageOptionsFourLetter, languageOptionsFullWord){
	languageOptionsFourLetter.push('CANCEL');
	if (OS_IOS){
		languageOptionsFullWord.push('Cancel');
	}
	cancelIndex = OS_IOS? (languageOptionsFullWord.length - 1) : languageOptionsFullWord.length;
	return cancelIndex;
};

languageService.prototype.displayDialog = function(){
	languageOptionsFourLetterCode = this.getLanguageOptionsFourLetterCodeFromJSON();
	languageOptionsFullWord = this.getLanguageOptionsFullWord(languageOptionsFourLetterCode);
	languageOptionsTwoLetterCode = this.getLanguageOptionsTwoLetterCode(languageOptionsFourLetterCode);

	cancelIndex = this.configureCancel(languageOptionsFourLetterCode, languageOptionsFullWord);
	selectedIndex = languageOptionsFourLetterCode.indexOf(Alloy.Models.app.get('currentLanguage'));
	if (selectedIndex == -1){
		selectedIndex = languageOptionsTwoLetterCode.indexOf(Alloy.Models.app.get('currentLanguage'));
		//If currentLanguage was pulled from device, it will be in two-letter form. Must check for this to set proper default choice
	}

	var languageDialog = Titanium.UI.createOptionDialog({
		options : languageOptionsFullWord,
		cancel : cancelIndex,
		selectedIndex : selectedIndex
	});
	languageDialog.addEventListener("click", function(e){
		languageService.prototype.languageDialogClickListener(e, languageOptionsFourLetterCode);
	});

	languageDialog.show();
};

languageService.prototype.languageDialogClickListener = function(e, languageOptionsFourLetterCode){
	var newLanguage = languageOptionsFourLetterCode[e.index];
	if (newLanguage != 'CANCEL' /*&& newLanguage.substr(0,1) !=  Alloy.Models.app.get('currentLanguage').substr(0,1)*/){
		Ti.API.info("________________Setting language to " + newLanguage);
		Alloy.Models.app.set('currentLanguage', newLanguage);
	}
	else{
		Alloy.Globals.navController.closeMenuWithoutAnimation();
	}
};

languageService.prototype.languageLibrary = [
	['af', 'af', 'Afrikaans'],
	['ar', 'ar', 'العربية', 'rtl'],
	['be', 'be_BY', 'Беларуская мова'],
	['bg', 'bg_BG', 'български'],
	['bs', 'bs_BA', 'Bosanski'],
	['ca', 'ca', 'Català'],
	['cs', 'cs_CZ', 'Čeština'],
	['cy', 'cy', 'Cymraeg'],
	['da', 'da_DK', 'Dansk'],
	['de', 'de_DE', 'Deutsch'],
	['el', 'el', 'Ελληνικά'],
	['en', 'en_CA', 'English'],
	['en', 'en_US', 'English'],
	['eo', 'eo', 'Esperanto'],
	['es', 'es_CL', 'Español'],
	['es', 'es_ES', 'Español'],
	['es', 'es_PE', 'Español'],
	['es', 'es_VE', 'Español'],
	['et', 'et', 'Eesti'],
	['fa', 'fa_AF', 'فارسی', 'rtl'],
	['fa', 'fa_IR', 'فارسی', 'rtl'],
	['fi', 'fi', 'Suomi'],
	['fo', 'fo', 'Føroyskt'],
	['fr', 'fr_FR', 'Français'],
	['fy', 'fy', 'Frysk'],
	['gd', 'gd', 'Gàidhlig'],
	['gl', 'gl_ES', 'Galego'],
	['he', 'he_IL', 'עברית', 'rtl'],
	['hi', 'hi_IN', 'हिन्दी'],
	['hr', 'hr', 'Hrvatski'],
	['hu', 'hu_HU', 'Magyar'],
	['id', 'id_ID', 'Bahasa Indonesia'],
	['is', 'is_IS', 'Íslenska'],
	['it', 'it_IT', 'Italiano'],
	['ja', 'ja', '日本語'],
	['jv', 'jv_ID', 'Basa Jawa'],
	['ka', 'ka_GE', 'ქართული'],
	['kk', 'kk', 'Қазақ тілі'],
	['ko', 'ko_KR', '한국어'],
	['ku', 'ckb', 'کوردی', 'rtl'],
	['lo', 'lo', 'ພາສາລາວ'],
	['lt', 'lt_LT', 'Lietuviškai'],
	['lv', 'lv', 'Latviešu valoda'],
	['mk', 'mk_MK', 'македонски јазик'],
	['mn', 'mn', 'Монгол хэл'],
	['ms', 'ms_MY', 'Bahasa Melayu'],
	['my', 'my_MM', 'ဗမာစာ'],
	['nb', 'nb_NO', 'Norsk Bokmål'],
	['ne', 'ne_NP', 'नेपाली'],
	['nl', 'nl_NL', 'Nederlands'],
	['nn', 'nn_NO', 'Norsk Nynorsk'],
	['pl', 'pl_PL', 'Polski'],
	['pt', 'pt_BR', 'Português'],
	['pt', 'pt_PT', 'Português'],
	['ro', 'ro_RO', 'Română'],
	['ru', 'ru_RU', 'Русский'],
	['si', 'si_LK', 'සිංහල'],
	['sk', 'sk_SK', 'Slovenčina'],
	['sl', 'sl_SI', 'Slovenščina'],
	['so', 'so_SO', 'Af-Soomaali'],
	['sq', 'sq', 'Shqip'],
	['sr', 'sr_RS', 'Српски језик'],
	['su', 'su_ID', 'Basa Sunda'],
	['sv', 'sv_SE', 'Svenska'],
	['ta', 'ta_LK', 'தமிழ்'],
	['th', 'th', 'ไทย'],
	['tr', 'tr_TR', 'Türkçe'],
	['ug', 'ug_CN', 'Uyƣurqə'],
	['uk', 'uk', 'Українська'],
	['ur', 'ur', 'اردو', 'rtl'],
	['uz', 'uz_UZ', 'Oʻzbek'],
	['vec', 'vec', 'Vèneto'],
	['vi', 'vi', 'Tiếng Việt'],
	['zh', 'zh_CN', '中文 (中国)'],
	['zh', 'zh_HK', '中文 (香港)'],
	['zh', 'zh_TW', '中文 (台灣)'],
];

module.exports = languageService;<|endoftext|>


<commit_before>//Variables for URLs
var URL_CATEGORY_AUTO = "auto/category/";
var URL_BOOKMARK_AUTO = "auto/bookmark/";
var URL_PAGE_OPEN = "open/";
var URL_BOOKMARK_LIST = "bookmark/category/";
var URL_BOOKMARK_BY_NAME = "bookmark/name/";
var URL_CATEGORY = "category/";

//Variables for selector strings
var CLASS_CATEGORY = ".category";
var CLASS_DEL_CATEGORY = ".delete-cat";
var CLASS_BOOKMARK = ".bookmark";
var CLASS_UI_MENU_ITEM = ".ui-menu-item";

//Variables for getting length of classes
var LEN_DEL_CATEGORY = "delete-cat ".length;
var LEN_BOOK_CATEGORY = "col-md-12 bookmark ".length;

//TODO Auxiliary functions - START
function valueInSelector(valueOf, selector) {
    var result = false;
    $(selector).each(function () {
        if (valueOf.val() == $.trim($(this).text())) {
            result = true;
        }
    });
    return result;
}

function getClassString(curObj, start) {
    return $(curObj).attr('class').substr(start);
}

//TODO Auxiliary functions - END
//TODO Controller functions - START
//Variables for Events
var EV_CLICK = "click";

//Variables for selectors
var CATEGORY_INPUT = $("#category_inp");
var CATEGORY_LIST_SEARCH = $("#category_list_search");
var TOP_WRAPPER = $("#top-wrapper");
var BOOKMARK_NAME = $("#bookmark-name");
var CATEGORY_BOX = $("#category-box");
var CATEGORY_LIST_ADD = $("#category_list_add");
var ADD_BOOKMARKS = $("#add-bookmark");
var BOOKMARK_LIST = $("#bookmarks-list");

//Variables for selector Strings
var strCategoryInput = 'CATEGORY_INPUT';
var strCategoryListSearch = 'CATEGORY_LIST_SEARCH';
var strTopWrapper = 'TOP_WRAPPER';
var strBookmarkName = 'BOOKMARK_NAME';
var strCategoryBox = 'CATEGORY_BOX';
var strCategoryListAdd = 'CATEGORY_LIST_ADD';
var strAddBookmarks = 'ADD_BOOKMARKS';
var strBookmarkList = 'BOOKMARK_LIST';

//Id Map
var idMap = {};
idMap[strCategoryInput] = CATEGORY_INPUT;
idMap[strCategoryListSearch] = CATEGORY_LIST_SEARCH;
idMap[strTopWrapper] = TOP_WRAPPER;
idMap[strBookmarkName] = BOOKMARK_NAME;
idMap[strCategoryBox] = CATEGORY_BOX;
idMap[strCategoryListAdd] = CATEGORY_LIST_ADD;
idMap[strAddBookmarks] = ADD_BOOKMARKS;
idMap[strBookmarkList] = BOOKMARK_LIST;

function callClear(e, uniqueId) {
    var curObj = idMap[uniqueId];
    switch (uniqueId) {
        case strCategoryInput:
        case strCategoryBox:
            curObj.val("");
            break;
        case strCategoryListSearch:
            callClear(e, strBookmarkList);
        case strCategoryListAdd:
        case strBookmarkList:
            curObj.html("");
            break;
        default:
            alert("callClear NONE");
    }
}

function callUpdateBookList(e, uniqueId) {
    var curObj = idMap[uniqueId];
    switch (uniqueId) {
        case strBookmarkList:
            $.ajax({
                url: URL_BOOKMARK_LIST,
                data: {'ids': getAllCategories()},
                success: function (output) {
                    curObj.html(output);
                }
            });
            break;
        default:
            alert("callUpdateBookList NONE");
    }
}

function callEnterEvent(e, uniqueId) {
    var curObj = idMap[uniqueId];
    switch (uniqueId) {
        case strCategoryInput:
        case strCategoryBox:
            if (testCategory(curObj)) {
                callAddCategory(e,  uniqueId);
            }
            break;
        default:
            alert("callEnterEvent NONE");
    }
}

function callAddCategory(e, uniqueId) {
    var curObj = idMap[uniqueId];
    switch (uniqueId) {
        case strCategoryInput:
            addCategory(e, curObj, {
                cat_list: CATEGORY_LIST_SEARCH,
                event_id: strBookmarkList
            });
            break;
        case strCategoryBox:
            addCategory(e, curObj, {
                cat_list: CATEGORY_LIST_ADD
            });
            break;
        default:
            alert("callAddCategory NONE");
    }
}

function bindEvents(e, uniqueId) {
    if (e.keyCode == 13) {
        callEnterEvent(e, uniqueId);
    } else if (e.altKey && e.keyCode == "N".charCodeAt(0)) {
    }
}

//TODO Controller functions - END
//TODO Body Load - START
$(function () {
    TOP_WRAPPER.toggle();

    CATEGORY_INPUT.autocomplete({
        source: function (req, res) {
            $.getJSON(URL_CATEGORY_AUTO, { excludes: getAllCategories(), term: CATEGORY_INPUT.val() },
                res);
        }
    });

    CATEGORY_BOX.autocomplete({
        source: function (req, res) {
            $.getJSON(URL_CATEGORY_AUTO, { excludes: getAllCategories(), term: CATEGORY_BOX.val() },
                res);
        }
    });

    BOOKMARK_NAME.autocomplete({
        source: URL_BOOKMARK_AUTO
    });
});
//TODO Body Load - END
//TODO Auxiliary functions for categories - START
function getAllCategories() {
    var classIds = [];
    $(CLASS_DEL_CATEGORY).each(function () {
        classIds.push(getClassString(this, 11));
    });
    return classIds.join(",");
}

function testCategory(categoryInp) {
    if (valueInSelector(categoryInp, CLASS_UI_MENU_ITEM) == false) {
        alert("Not a category");
    } else if (valueInSelector(categoryInp, CLASS_CATEGORY) == true) {
        alert("Category already added");
        categoryInp.val("");
    } else {
        return true;
    }
    return false;
}

function addCategory(e, curObj, data) {
    $.ajax({
        url: URL_CATEGORY,
        data: {'value': $(curObj).val()},
        success: function (output) {
            data.cat_list.append(output);
            $(curObj).val("");
            if (data.event_id !== undefined) {
                callUpdateBookList(e,  data.event_id);
            }
        }
    });
}

function deleteCategory(curObj) {
    var categoryId = getClassString(curObj, LEN_DEL_CATEGORY);
    $(CLASS_CATEGORY + "." + categoryId).remove();
}
//TODO Auxiliary functions for categories - END

CATEGORY_INPUT.on({
    keyup: function (e) {
        bindEvents(e, strCategoryInput);
    }
});

CATEGORY_BOX.on({
    keyup: function (e) {
        bindEvents(e, strCategoryBox);
    }
});

//Actions for delete of Category - Search
CATEGORY_LIST_SEARCH.on(EV_CLICK, CLASS_DEL_CATEGORY, function (e) {
    deleteCategory(this);
    callUpdateBookList(e,  strBookmarkList)
});

//Actions for delete of Category - Add
CATEGORY_LIST_ADD.on(EV_CLICK, CLASS_DEL_CATEGORY, function () {
    deleteCategory(this);
});

//Actions for Bookmark
BOOKMARK_LIST.on(EV_CLICK, CLASS_BOOKMARK, function () {
    $.ajax({
        url: URL_PAGE_OPEN,
        data: {'id': getClassString(this, LEN_BOOK_CATEGORY)}
    });
});

//Actions for Show/Hide Bookmark addition screen
ADD_BOOKMARKS.on({
    click: function (e) {
        TOP_WRAPPER.toggle();
        callClear(e,  strCategoryInput);
        callClear(e, strCategoryListSearch);
    }
});

//TODO Action for BookMark Name
BOOKMARK_NAME.on({
    keyup: function (e) {
        bindEvents(e, strBookmarkName);
    },

    "enterKey": function () {
        $.ajax({
            url: URL_BOOKMARK_BY_NAME,
            data: {'name': BOOKMARK_NAME.val()},
            success: function (output) {
                //TODO
            }
        })
    }
});<commit_msg>Added uniqueId to selector instead of previous mapping from strings to selector. Made binding of keys easier<commit_after>//Variables for URLs
var URL_CATEGORY_AUTO = "auto/category/";
var URL_BOOKMARK_AUTO = "auto/bookmark/";
var URL_PAGE_OPEN = "open/";
var URL_BOOKMARK_LIST = "bookmark/category/";
var URL_BOOKMARK_BY_NAME = "bookmark/name/";
var URL_CATEGORY = "category/";

//Variables for selector strings
var CLASS_CATEGORY = ".category";
var CLASS_DEL_CATEGORY = ".delete-cat";
var CLASS_BOOKMARK = ".bookmark";
var CLASS_UI_MENU_ITEM = ".ui-menu-item";

//Variables for getting length of classes
var LEN_DEL_CATEGORY = "delete-cat ".length;
var LEN_BOOK_CATEGORY = "col-md-12 bookmark ".length;

//TODO Auxiliary functions - START
function valueInSelector(valueOf, selector) {
    var result = false;
    $(selector).each(function () {
        if (valueOf.val() == $.trim($(this).text())) {
            result = true;
        }
    });
    return result;
}

function getClassString(curObj, start) {
    return $(curObj).attr('class').substr(start);
}

function initSelector(uniqueId, customId) {
    var selector = $(uniqueId);
    selector.attr('customId', customId);
    return selector;
}
//TODO Auxiliary functions - END
//TODO Controller functions - START
//Variables for Events
var EV_CLICK = "click";

//Variables for selector Strings
var strCategoryInput = 'CATEGORY_INPUT';
var strCategoryListSearch = 'CATEGORY_LIST_SEARCH';
var strTopWrapper = 'TOP_WRAPPER';
var strBookmarkName = 'BOOKMARK_NAME';
var strCategoryBox = 'CATEGORY_BOX';
var strCategoryListAdd = 'CATEGORY_LIST_ADD';
var strAddBookmarks = 'ADD_BOOKMARKS';
var strBookmarkList = 'BOOKMARK_LIST';

//Variables for selectors
var CATEGORY_INPUT = initSelector("#category_inp", strCategoryInput);
var CATEGORY_LIST_SEARCH = initSelector("#category_list_search", strCategoryListSearch);
var TOP_WRAPPER = initSelector("#top-wrapper", strTopWrapper);
var BOOKMARK_NAME = initSelector("#bookmark-name", strBookmarkName);
var CATEGORY_BOX = initSelector("#category-box", strCategoryBox);
var CATEGORY_LIST_ADD = initSelector("#category_list_add", strCategoryBox);
var ADD_BOOKMARKS = initSelector("#add-bookmark", strAddBookmarks);
var BOOKMARK_LIST = initSelector("#bookmarks-list", strBookmarkList);

function callClear(e, curObj) {
    switch (curObj.attr("customId")) {
        case strCategoryInput:
        case strCategoryBox:
            curObj.val("");
            break;
        case strCategoryListSearch:
            callClear(e, BOOKMARK_LIST);
        case strCategoryListAdd:
        case strBookmarkList:
            curObj.html("");
            break;
        default:
            alert("callClear NONE");
    }
}

function callUpdateBookList(e, curObj) {
    switch (curObj.attr("customId")) {
        case strBookmarkList:
            $.ajax({
                url: URL_BOOKMARK_LIST,
                data: {'ids': getAllCategories()},
                success: function (output) {
                    curObj.html(output);
                }
            });
            break;
        default:
            alert("callUpdateBookList NONE");
    }
}

function callEnterEvent(e, curObj) {
    switch (curObj.attr("customId")) {
        case strCategoryInput:
        case strCategoryBox:
            if (testCategory(curObj)) {
                callAddCategory(e, curObj);
            }
            break;
        default:
            alert("callEnterEvent NONE");
    }
}

function callAddCategory(e, curObj) {
    switch (curObj.attr("customId")) {
        case strCategoryInput:
            addCategory(e, curObj, {
                cat_list: CATEGORY_LIST_SEARCH,
                event_sel: BOOKMARK_LIST
            });
            break;
        case strCategoryBox:
            addCategory(e, curObj, {
                cat_list: CATEGORY_LIST_ADD
            });
            break;
        default:
            alert("callAddCategory NONE");
    }
}

function bindEvents(e, curObj) {
    if (e.keyCode == 13) {
        callEnterEvent(e, curObj);
    } else if (e.altKey && e.keyCode == "N".charCodeAt(0)) {
    }
}

//TODO Controller functions - END
//TODO Body Load - START
$(function () {
    TOP_WRAPPER.hide();

    CATEGORY_INPUT.autocomplete({
        source: function (req, res) {
            $.getJSON(URL_CATEGORY_AUTO, { excludes: getAllCategories(), term: CATEGORY_INPUT.val() },
                res);
        }
    });

    CATEGORY_BOX.autocomplete({
        source: function (req, res) {
            $.getJSON(URL_CATEGORY_AUTO, { excludes: getAllCategories(), term: CATEGORY_BOX.val() },
                res);
        }
    });

    BOOKMARK_NAME.autocomplete({
        source: URL_BOOKMARK_AUTO
    });
});
//TODO Body Load - END
//TODO Auxiliary functions for categories - START
function getAllCategories() {
    var classIds = [];
    $(CLASS_DEL_CATEGORY).each(function () {
        classIds.push(getClassString(this, 11));
    });
    return classIds.join(",");
}

function testCategory(categoryInp) {
    if (valueInSelector(categoryInp, CLASS_UI_MENU_ITEM) == false) {
        alert("Not a category");
    } else if (valueInSelector(categoryInp, CLASS_CATEGORY) == true) {
        alert("Category already added");
        categoryInp.val("");
    } else {
        return true;
    }
    return false;
}

function addCategory(e, curObj, data) {
    $.ajax({
        url: URL_CATEGORY,
        data: {'value': $(curObj).val()},
        success: function (output) {
            data.cat_list.append(output);
            $(curObj).val("");
            if (data.event_sel !== undefined) {
                callUpdateBookList(e, data.event_sel);
            }
        }
    });
}

function deleteCategory(curObj) {
    var categoryId = getClassString(curObj, LEN_DEL_CATEGORY);
    $(CLASS_CATEGORY + "." + categoryId).remove();
}
//TODO Auxiliary functions for categories - END

$("#category_inp,#category-box,#bookmark-name").on({
    keyup: function (e) {
        bindEvents(e, $(this));
    }}
);

//Actions for delete of Category - Search
CATEGORY_LIST_SEARCH.on(EV_CLICK, CLASS_DEL_CATEGORY, function (e) {
    deleteCategory(this);
    callUpdateBookList(e, BOOKMARK_LIST)
});

//Actions for delete of Category - Add
CATEGORY_LIST_ADD.on(EV_CLICK, CLASS_DEL_CATEGORY, function () {
    deleteCategory(this);
});

//Actions for Bookmark
BOOKMARK_LIST.on(EV_CLICK, CLASS_BOOKMARK, function () {
    $.ajax({
        url: URL_PAGE_OPEN,
        data: {'id': getClassString(this, LEN_BOOK_CATEGORY)}
    });
});

//Actions for Show/Hide Bookmark addition screen
ADD_BOOKMARKS.on(
    'click', function (e) {
        bindEvents(e, ADD_BOOKMARKS);
        TOP_WRAPPER.toggle();
        callClear(e, CATEGORY_INPUT);
        callClear(e, CATEGORY_LIST_SEARCH);
    }
);<|endoftext|>


<commit_before>var NetworkLayer = AbstractLayer.extend(function(base) {
    return {
        init: function(dimensions, layer, iteration, modelURL) {
            base.init.call(this, dimensions);
            this.layer = layer;
            this.iteration = iteration;
            this.modelURL = modelURL;
        },

        getList: function(type, callback) {
            var self = this,
                layer = this.layer,
                iteration = this.iteration;

            self.getJSON(layer + "/" + iteration + "/" + type, function(error, list) {
                callback(null, list);
            });
        },

        /* Public */

        getActiveColumns: function(callback) {
            this.getList("active_columns", callback);
        },

        getActiveCells: function(callback) {
            this.getList("active_cells", callback);
        },
    };
});

Fiber.mixin(NetworkLayer, NetworkMixin);
<commit_msg>Added predicted cells<commit_after>var NetworkLayer = AbstractLayer.extend(function(base) {
    return {
        init: function(dimensions, layer, iteration, modelURL) {
            base.init.call(this, dimensions);
            this.layer = layer;
            this.iteration = iteration;
            this.modelURL = modelURL;
        },

        getList: function(type, callback) {
            var self = this,
                layer = this.layer,
                iteration = this.iteration;

            self.getJSON(layer + "/" + iteration + "/" + type, function(error, list) {
                callback(null, list);
            });
        },

        /* Public */

        getActiveColumns: function(callback) {
            this.getList("active_columns", callback);
        },

        getActiveCells: function(callback) {
            this.getList("active_cells", callback);
        },

        getPredictedCells: function(callback) {
            this.getList("predicted_cells", callback);
        }
    };
});

Fiber.mixin(NetworkLayer, NetworkMixin);
<|endoftext|>


<commit_before>import $ from "jquery";

import render_unsubscribe_private_stream_modal from "../templates/confirm_dialog/confirm_unsubscribe_private_stream.hbs";
import render_stream_member_list_entry from "../templates/stream_settings/stream_member_list_entry.hbs";
import render_stream_subscription_request_result from "../templates/stream_settings/stream_subscription_request_result.hbs";

import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog";
import {$t, $t_html} from "./i18n";
import * as input_pill from "./input_pill";
import * as ListWidget from "./list_widget";
import {page_params} from "./page_params";
import * as peer_data from "./peer_data";
import * as people from "./people";
import * as pill_typeahead from "./pill_typeahead";
import * as settings_data from "./settings_data";
import * as stream_pill from "./stream_pill";
import * as sub_store from "./sub_store";
import * as ui from "./ui";
import * as user_group_pill from "./user_group_pill";
import * as user_pill from "./user_pill";

export let pill_widget;

function create_item_from_text(text, current_items) {
    const funcs = [
        stream_pill.create_item_from_stream_name,
        user_group_pill.create_item_from_group_name,
        user_pill.create_item_from_email,
    ];
    for (const func of funcs) {
        const item = func(text, current_items);
        if (item) {
            return item;
        }
    }
    return undefined;
}

function get_text_from_item(item) {
    const funcs = [
        stream_pill.get_stream_name_from_item,
        user_group_pill.get_group_name_from_item,
        user_pill.get_email_from_item,
    ];
    for (const func of funcs) {
        const text = func(item);
        if (text) {
            return text;
        }
    }
    return undefined;
}

function format_member_list_elem(person) {
    return render_stream_member_list_entry({
        name: person.full_name,
        user_id: person.user_id,
        email: settings_data.email_for_user_settings(person),
        displaying_for_admin: page_params.is_admin,
        show_email: settings_data.show_email(),
    });
}

function get_stream_id(target) {
    // TODO: Use more precise selector.
    const row = $(target).closest(
        ".stream-row, .stream_settings_header, .subscription_settings, .save-button",
    );
    return Number.parseInt(row.attr("data-stream-id"), 10);
}

function get_sub_for_target(target) {
    const stream_id = get_stream_id(target);
    if (!stream_id) {
        blueslip.error("Cannot find stream id for target");
        return undefined;
    }

    const sub = sub_store.get(stream_id);
    if (!sub) {
        blueslip.error("get_sub_for_target() failed id lookup: " + stream_id);
        return undefined;
    }
    return sub;
}

function show_stream_subscription_request_result({
    message,
    add_class,
    remove_class,
    subscribed_users,
    already_subscribed_users,
    ignored_deactivated_users,
}) {
    const stream_subscription_req_result_elem = $(
        ".stream_subscription_request_result",
    ).expectOne();
    const html = render_stream_subscription_request_result({
        message,
        subscribed_users,
        already_subscribed_users,
        ignored_deactivated_users,
    });
    ui.get_content_element(stream_subscription_req_result_elem).html(html);
    if (add_class) {
        stream_subscription_req_result_elem.addClass(add_class);
    }
    if (remove_class) {
        stream_subscription_req_result_elem.removeClass(remove_class);
    }
}

export function enable_subscriber_management({sub, parent_container}) {
    const stream_id = sub.stream_id;

    const pill_container = parent_container.find(".pill-container");

    pill_widget = input_pill.create({
        container: pill_container,
        create_item_from_text,
        get_text_from_item,
    });

    const user_ids = peer_data.get_subscribers(stream_id);
    const users = people.get_users_from_ids(user_ids);
    people.sort_but_pin_current_user_on_top(users);

    function get_users_for_subscriber_typeahead() {
        const potential_subscribers = peer_data.potential_subscribers(stream_id);
        return user_pill.filter_taken_users(potential_subscribers, pill_widget);
    }

    const list_container = parent_container.find(".subscriber_table");
    list_container.empty();

    const simplebar_container = parent_container.find(".subscriber_list_container");

    ListWidget.create(list_container, users, {
        name: "stream_subscribers/" + stream_id,
        modifier(item) {
            return format_member_list_elem(item);
        },
        filter: {
            element: $(`[data-stream-id='${CSS.escape(stream_id)}'] .search`),
            predicate(person, value) {
                const matcher = people.build_person_matcher(value);
                const match = matcher(person);

                return match;
            },
        },
        simplebar_container,
    });

    const opts = {
        user_source: get_users_for_subscriber_typeahead,
        stream: true,
        user_group: true,
        user: true,
    };
    pill_typeahead.set_up(pill_container.find(".input"), pill_widget, opts);
}

export function invite_user_to_stream(user_ids, sub, success, failure) {
    // TODO: use stream_id when backend supports it
    const stream_name = sub.name;
    return channel.post({
        url: "/json/users/me/subscriptions",
        data: {
            subscriptions: JSON.stringify([{name: stream_name}]),
            principals: JSON.stringify(user_ids),
        },
        success,
        error: failure,
    });
}

export function remove_user_from_stream(user_id, sub, success, failure) {
    // TODO: use stream_id when backend supports it
    const stream_name = sub.name;
    return channel.del({
        url: "/json/users/me/subscriptions",
        data: {subscriptions: JSON.stringify([stream_name]), principals: JSON.stringify([user_id])},
        success,
        error: failure,
    });
}

function submit_add_subscriber_form(e) {
    const sub = get_sub_for_target(e.target);
    if (!sub) {
        blueslip.error(".subscriber_list_add form submit fails");
        return;
    }

    let user_ids = user_pill.get_user_ids(pill_widget);
    user_ids = user_ids.concat(stream_pill.get_user_ids(pill_widget));
    user_ids = user_ids.concat(user_group_pill.get_user_ids(pill_widget));
    const deactivated_users = new Set();
    user_ids = user_ids.filter((user_id) => {
        if (!people.is_person_active(user_id)) {
            deactivated_users.add(user_id);
            return false;
        }
        return true;
    });

    user_ids = new Set(user_ids);

    if (user_ids.has(page_params.user_id) && sub.subscribed) {
        // We don't want to send a request to subscribe ourselves
        // if we are already subscribed to this stream. This
        // case occurs when creating user pills from a stream.
        user_ids.delete(page_params.user_id);
    }
    let ignored_deactivated_users;
    if (deactivated_users.size > 0) {
        ignored_deactivated_users = Array.from(deactivated_users);
        ignored_deactivated_users = ignored_deactivated_users.map((user_id) =>
            people.get_by_user_id(user_id),
        );
    }
    if (user_ids.size === 0) {
        show_stream_subscription_request_result({
            message: $t({defaultMessage: "No user to subscribe."}),
            add_class: "text-error",
            remove_class: "text-success",
            ignored_deactivated_users,
        });
        return;
    }
    user_ids = Array.from(user_ids);

    function invite_success(data) {
        pill_widget.clear();
        const subscribed_users = Object.keys(data.subscribed).map((email) =>
            people.get_by_email(email),
        );
        const already_subscribed_users = Object.keys(data.already_subscribed).map((email) =>
            people.get_by_email(email),
        );

        show_stream_subscription_request_result({
            add_class: "text-success",
            remove_class: "text-error",
            subscribed_users,
            already_subscribed_users,
            ignored_deactivated_users,
        });
    }

    function invite_failure(xhr) {
        const error = JSON.parse(xhr.responseText);
        show_stream_subscription_request_result({
            message: error.msg,
            add_class: "text-error",
            remove_class: "text-success",
        });
    }

    invite_user_to_stream(user_ids, sub, invite_success, invite_failure);
}

export function initialize() {
    $("#subscriptions_table").on("keyup", ".subscriber_list_add form", (e) => {
        if (e.key === "Enter") {
            e.preventDefault();
            submit_add_subscriber_form(e);
        }
    });

    $("#subscriptions_table").on("submit", ".subscriber_list_add form", (e) => {
        e.preventDefault();
        submit_add_subscriber_form(e);
    });

    $("#subscriptions_table").on("submit", ".subscriber_list_remove form", (e) => {
        e.preventDefault();

        const list_entry = $(e.target).closest("tr");
        const target_user_id = Number.parseInt(list_entry.attr("data-subscriber-id"), 10);

        const sub = get_sub_for_target(e.target);
        if (!sub) {
            blueslip.error(".subscriber_list_remove form submit fails");
            return;
        }
        let message;

        function removal_success(data) {
            if (data.removed.length > 0) {
                // Remove the user from the subscriber list.
                list_entry.remove();
                message = $t({defaultMessage: "Unsubscribed successfully!"});
                // The rest of the work is done via the subscription -> remove event we will get
            } else {
                message = $t({defaultMessage: "User is already not subscribed."});
            }
            show_stream_subscription_request_result({
                message,
                add_class: "text-success",
                remove_class: "text-remove",
            });
        }

        function removal_failure() {
            show_stream_subscription_request_result({
                message: $t({defaultMessage: "Error removing user from this stream."}),
                add_class: "text-error",
                remove_class: "text-success",
            });
        }

        function remove_user_from_private_stream() {
            remove_user_from_stream(target_user_id, sub, removal_success, removal_failure);
        }

        if (sub.invite_only && people.is_my_user_id(target_user_id)) {
            const html_body = render_unsubscribe_private_stream_modal();

            confirm_dialog.launch({
                html_heading: $t_html(
                    {defaultMessage: "Unsubscribe from {stream_name}"},
                    {stream_name: sub.name},
                ),
                html_body,
                on_click: remove_user_from_private_stream,
            });
            return;
        }

        remove_user_from_stream(target_user_id, sub, removal_success, removal_failure);
    });
}
<commit_msg>refactor: Limit scope of e.target.<commit_after>import $ from "jquery";

import render_unsubscribe_private_stream_modal from "../templates/confirm_dialog/confirm_unsubscribe_private_stream.hbs";
import render_stream_member_list_entry from "../templates/stream_settings/stream_member_list_entry.hbs";
import render_stream_subscription_request_result from "../templates/stream_settings/stream_subscription_request_result.hbs";

import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog";
import {$t, $t_html} from "./i18n";
import * as input_pill from "./input_pill";
import * as ListWidget from "./list_widget";
import {page_params} from "./page_params";
import * as peer_data from "./peer_data";
import * as people from "./people";
import * as pill_typeahead from "./pill_typeahead";
import * as settings_data from "./settings_data";
import * as stream_pill from "./stream_pill";
import * as sub_store from "./sub_store";
import * as ui from "./ui";
import * as user_group_pill from "./user_group_pill";
import * as user_pill from "./user_pill";

export let pill_widget;

function create_item_from_text(text, current_items) {
    const funcs = [
        stream_pill.create_item_from_stream_name,
        user_group_pill.create_item_from_group_name,
        user_pill.create_item_from_email,
    ];
    for (const func of funcs) {
        const item = func(text, current_items);
        if (item) {
            return item;
        }
    }
    return undefined;
}

function get_text_from_item(item) {
    const funcs = [
        stream_pill.get_stream_name_from_item,
        user_group_pill.get_group_name_from_item,
        user_pill.get_email_from_item,
    ];
    for (const func of funcs) {
        const text = func(item);
        if (text) {
            return text;
        }
    }
    return undefined;
}

function format_member_list_elem(person) {
    return render_stream_member_list_entry({
        name: person.full_name,
        user_id: person.user_id,
        email: settings_data.email_for_user_settings(person),
        displaying_for_admin: page_params.is_admin,
        show_email: settings_data.show_email(),
    });
}

function get_stream_id(target) {
    // TODO: Use more precise selector.
    const row = $(target).closest(
        ".stream-row, .stream_settings_header, .subscription_settings, .save-button",
    );
    const stream_id = Number.parseInt(row.attr("data-stream-id"), 10);

    if (!stream_id) {
        blueslip.error("Cannot find stream id for target");
        return undefined;
    }

    return stream_id;
}

function get_sub(stream_id) {
    const sub = sub_store.get(stream_id);
    if (!sub) {
        blueslip.error("get_sub() failed id lookup: " + stream_id);
        return undefined;
    }
    return sub;
}

function show_stream_subscription_request_result({
    message,
    add_class,
    remove_class,
    subscribed_users,
    already_subscribed_users,
    ignored_deactivated_users,
}) {
    const stream_subscription_req_result_elem = $(
        ".stream_subscription_request_result",
    ).expectOne();
    const html = render_stream_subscription_request_result({
        message,
        subscribed_users,
        already_subscribed_users,
        ignored_deactivated_users,
    });
    ui.get_content_element(stream_subscription_req_result_elem).html(html);
    if (add_class) {
        stream_subscription_req_result_elem.addClass(add_class);
    }
    if (remove_class) {
        stream_subscription_req_result_elem.removeClass(remove_class);
    }
}

export function enable_subscriber_management({sub, parent_container}) {
    const stream_id = sub.stream_id;

    const pill_container = parent_container.find(".pill-container");

    pill_widget = input_pill.create({
        container: pill_container,
        create_item_from_text,
        get_text_from_item,
    });

    const user_ids = peer_data.get_subscribers(stream_id);
    const users = people.get_users_from_ids(user_ids);
    people.sort_but_pin_current_user_on_top(users);

    function get_users_for_subscriber_typeahead() {
        const potential_subscribers = peer_data.potential_subscribers(stream_id);
        return user_pill.filter_taken_users(potential_subscribers, pill_widget);
    }

    const list_container = parent_container.find(".subscriber_table");
    list_container.empty();

    const simplebar_container = parent_container.find(".subscriber_list_container");

    ListWidget.create(list_container, users, {
        name: "stream_subscribers/" + stream_id,
        modifier(item) {
            return format_member_list_elem(item);
        },
        filter: {
            element: $(`[data-stream-id='${CSS.escape(stream_id)}'] .search`),
            predicate(person, value) {
                const matcher = people.build_person_matcher(value);
                const match = matcher(person);

                return match;
            },
        },
        simplebar_container,
    });

    const opts = {
        user_source: get_users_for_subscriber_typeahead,
        stream: true,
        user_group: true,
        user: true,
    };
    pill_typeahead.set_up(pill_container.find(".input"), pill_widget, opts);
}

export function invite_user_to_stream(user_ids, sub, success, failure) {
    // TODO: use stream_id when backend supports it
    const stream_name = sub.name;
    return channel.post({
        url: "/json/users/me/subscriptions",
        data: {
            subscriptions: JSON.stringify([{name: stream_name}]),
            principals: JSON.stringify(user_ids),
        },
        success,
        error: failure,
    });
}

export function remove_user_from_stream(user_id, sub, success, failure) {
    // TODO: use stream_id when backend supports it
    const stream_name = sub.name;
    return channel.del({
        url: "/json/users/me/subscriptions",
        data: {subscriptions: JSON.stringify([stream_name]), principals: JSON.stringify([user_id])},
        success,
        error: failure,
    });
}

function submit_add_subscriber_form(stream_id) {
    const sub = get_sub(stream_id);
    if (!sub) {
        return;
    }

    let user_ids = user_pill.get_user_ids(pill_widget);
    user_ids = user_ids.concat(stream_pill.get_user_ids(pill_widget));
    user_ids = user_ids.concat(user_group_pill.get_user_ids(pill_widget));
    const deactivated_users = new Set();
    user_ids = user_ids.filter((user_id) => {
        if (!people.is_person_active(user_id)) {
            deactivated_users.add(user_id);
            return false;
        }
        return true;
    });

    user_ids = new Set(user_ids);

    if (user_ids.has(page_params.user_id) && sub.subscribed) {
        // We don't want to send a request to subscribe ourselves
        // if we are already subscribed to this stream. This
        // case occurs when creating user pills from a stream.
        user_ids.delete(page_params.user_id);
    }
    let ignored_deactivated_users;
    if (deactivated_users.size > 0) {
        ignored_deactivated_users = Array.from(deactivated_users);
        ignored_deactivated_users = ignored_deactivated_users.map((user_id) =>
            people.get_by_user_id(user_id),
        );
    }
    if (user_ids.size === 0) {
        show_stream_subscription_request_result({
            message: $t({defaultMessage: "No user to subscribe."}),
            add_class: "text-error",
            remove_class: "text-success",
            ignored_deactivated_users,
        });
        return;
    }
    user_ids = Array.from(user_ids);

    function invite_success(data) {
        pill_widget.clear();
        const subscribed_users = Object.keys(data.subscribed).map((email) =>
            people.get_by_email(email),
        );
        const already_subscribed_users = Object.keys(data.already_subscribed).map((email) =>
            people.get_by_email(email),
        );

        show_stream_subscription_request_result({
            add_class: "text-success",
            remove_class: "text-error",
            subscribed_users,
            already_subscribed_users,
            ignored_deactivated_users,
        });
    }

    function invite_failure(xhr) {
        const error = JSON.parse(xhr.responseText);
        show_stream_subscription_request_result({
            message: error.msg,
            add_class: "text-error",
            remove_class: "text-success",
        });
    }

    invite_user_to_stream(user_ids, sub, invite_success, invite_failure);
}

export function initialize() {
    $("#subscriptions_table").on("keyup", ".subscriber_list_add form", (e) => {
        if (e.key === "Enter") {
            e.preventDefault();
            const stream_id = get_stream_id(e.target);
            submit_add_subscriber_form(stream_id);
        }
    });

    $("#subscriptions_table").on("submit", ".subscriber_list_add form", (e) => {
        e.preventDefault();
        const stream_id = get_stream_id(e.target);
        submit_add_subscriber_form(stream_id);
    });

    $("#subscriptions_table").on("submit", ".subscriber_list_remove form", (e) => {
        e.preventDefault();

        const list_entry = $(e.target).closest("tr");
        const target_user_id = Number.parseInt(list_entry.attr("data-subscriber-id"), 10);

        const stream_id = get_stream_id(e.target);
        const sub = get_sub(stream_id);
        if (!sub) {
            return;
        }
        let message;

        function removal_success(data) {
            if (data.removed.length > 0) {
                // Remove the user from the subscriber list.
                list_entry.remove();
                message = $t({defaultMessage: "Unsubscribed successfully!"});
                // The rest of the work is done via the subscription -> remove event we will get
            } else {
                message = $t({defaultMessage: "User is already not subscribed."});
            }
            show_stream_subscription_request_result({
                message,
                add_class: "text-success",
                remove_class: "text-remove",
            });
        }

        function removal_failure() {
            show_stream_subscription_request_result({
                message: $t({defaultMessage: "Error removing user from this stream."}),
                add_class: "text-error",
                remove_class: "text-success",
            });
        }

        function remove_user_from_private_stream() {
            remove_user_from_stream(target_user_id, sub, removal_success, removal_failure);
        }

        if (sub.invite_only && people.is_my_user_id(target_user_id)) {
            const html_body = render_unsubscribe_private_stream_modal();

            confirm_dialog.launch({
                html_heading: $t_html(
                    {defaultMessage: "Unsubscribe from {stream_name}"},
                    {stream_name: sub.name},
                ),
                html_body,
                on_click: remove_user_from_private_stream,
            });
            return;
        }

        remove_user_from_stream(target_user_id, sub, removal_success, removal_failure);
    });
}
<|endoftext|>