metamaps/metamaps

View on GitHub
frontend/src/Metamaps/Map/InfoBox.js

Summary

Maintainability
C
1 day
Test Coverage
/* global $, Hogan, Bloodhound, Countable */

import outdent from 'outdent'
import { browserHistory } from 'react-router'

import Active from '../Active'
import DataModel from '../DataModel'
import GlobalUI, { ReactApp } from '../GlobalUI'
import Util from '../Util'

const InfoBox = {
  isOpen: false,
  selectingPermission: false,
  changePermissionText: "<div class='tooltips'>As the creator, you can change the permission of this map, and the permission of all the topics and synapses you have authority to change will change as well.</div>",
  nameHTML: outdent`
    <span class="best_in_place best_in_place_name"
      id="best_in_place_map_{{id}}_name"
      data-bip-url="/maps/{{id}}"
      data-bip-object="map"
      data-bip-attribute="name"
      data-bip-type="textarea"
      data-bip-activator="#mapInfoName"
      data-bip-value="{{name}}"
    >{{name}}</span>`,
  descHTML: outdent`
    <span class="best_in_place best_in_place_desc"
      id="best_in_place_map_{{id}}_desc"
      data-bip-url="/maps/{{id}}"
      data-bip-object="map"
      data-bip-attribute="desc"
      data-bip-nil="Click to add description..."
      data-bip-type="textarea"
      data-bip-activator="#mapInfoDesc"
      data-bip-value="{{desc}}"
    >{{desc}}</span>`,
  userImageUrl: '',
  html: '',
  init: function(serverData, updateThumbnail) {
    var self = InfoBox

    self.updateThumbnail = updateThumbnail

    $('.mapInfoBox').click(function(event) {
      event.stopPropagation()
    })
    $('body').click(self.close)

    self.attachEventListeners()

    self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html())

    self.userImageUrl = serverData['user.png']

    var querystring = window.location.search.replace(/^\?/, '')
    if (querystring === 'new') {
      self.open()
      $('.mapInfoBox').addClass('mapRequestTitle')
      $('#mapInfoName').trigger('click')
      $('#mapInfoName textarea').focus()
      $('#mapInfoName textarea').select()
    }
  },
  toggleBox: function(event) {
    var self = InfoBox

    if (self.isOpen) self.close()
    else self.open()

    event.stopPropagation()
  },
  open: function() {
    var self = InfoBox
    $('.mapInfoIcon div').addClass('hide')
    $('.mapInfoBox').fadeIn(200, function() {
      self.isOpen = true
    })
  },
  close: function() {
    var self = InfoBox
    $('.mapInfoIcon div').removeClass('hide')
    $('.mapInfoBox').fadeOut(200, function() {
      self.isOpen = false
      self.hidePermissionSelect()
      $('.mapContributors .tip').hide()
    })
  },
  load: function() {
    var self = InfoBox

    var map = Active.Map

    var obj = map.pick('permission', 'topic_count', 'synapse_count')

    var isCreator = map.authorizePermissionChange(Active.Mapper)
    var canEdit = map.authorizeToEdit(Active.Mapper)
    var relevantPeople = map.get('permission') === 'commons' ? DataModel.Mappers : DataModel.Collaborators
    var shareable = map.get('permission') !== 'private'

    obj['name'] = canEdit ? Hogan.compile(self.nameHTML).render({id: map.id, name: map.get('name')}) : map.get('name')
    obj['desc'] = canEdit ? Hogan.compile(self.descHTML).render({id: map.id, desc: map.get('desc')}) : map.get('desc')
    obj['map_creator_tip'] = isCreator ? self.changePermissionText : ''

    obj['contributor_count'] = relevantPeople.length
    obj['contributors_class'] = relevantPeople.length > 1 ? 'multiple' : ''
    obj['contributors_class'] += relevantPeople.length === 2 ? ' mTwo' : ''
    obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : self.userImageUrl
    obj['contributor_list'] = self.createContributorList()

    obj['user_name'] = isCreator ? 'You' : map.get('user_name')
    obj['created_at'] = map.get('created_at_clean')
    obj['updated_at'] = map.get('updated_at_clean')

    self.html = self.generateBoxHTML.render(obj)
    ReactApp.render()
    self.attachEventListeners()
  },
  attachEventListeners: function() {
    var self = InfoBox
    $('.mapInfoBox').click(function(event) {
      event.stopPropagation()
    })
    $('.mapInfoBox.canEdit .best_in_place').best_in_place()

    // because anyone who can edit the map can change the map title
    var bipName = $('.mapInfoBox .best_in_place_name')
    bipName.unbind('best_in_place:activate').bind('best_in_place:activate', function() {
      var $el = bipName.find('textarea')
      var el = $el[0]

      $el.attr('maxlength', '140')

      $('.mapInfoName').append('<div class="nameCounter forMap"></div>')

      var callback = function(data) {
        $('.nameCounter.forMap').html(data.all + '/140')
      }
      Countable.live(el, callback)
    })
    bipName.unbind('best_in_place:deactivate').bind('best_in_place:deactivate', function() {
      $('.nameCounter.forMap').remove()
    })

    $('.mapInfoName .best_in_place_name').unbind('ajax:success').bind('ajax:success', function() {
      var name = $(this).html()
      Active.Map.set('name', name)
      Active.Map.trigger('saved')
      // mobile menu
      $('#header_content').html(name)
      $('.mapInfoBox').removeClass('mapRequestTitle')
      document.title = `${name} | Metamaps`
      window.history.replaceState('', `${name} | Metamaps`, window.location.pathname)
    })

    $('.mapInfoDesc .best_in_place_desc').unbind('ajax:success').bind('ajax:success', function() {
      var desc = $(this).html()
      Active.Map.set('desc', desc)
      Active.Map.trigger('saved')
    })

    $('.mapInfoDesc .best_in_place_desc, .mapInfoName .best_in_place_name').unbind('keypress').keypress(function(e) {
      const ENTER = 13
      if (e.which === ENTER) {
        $(this).data('bestInPlaceEditor').update()
      }
    })

    $('.yourMap .mapPermission').unbind().click(self.onPermissionClick)
    // .yourMap in the unbind/bind is just a namespace for the events
    // not a reference to the class .yourMap on the .mapInfoBox
    $('.mapInfoBox.yourMap').unbind('.yourMap').bind('click.yourMap', self.hidePermissionSelect)

    $('.yourMap .mapInfoDelete').unbind().click(self.deleteActiveMap)
    $('.mapInfoThumbnail').unbind().click(self.updateThumbnail)

    $('.mapContributors span, #mapContribs').unbind().click(function(event) {
      $('.mapContributors .tip').toggle()
      event.stopPropagation()
    })
    $('.mapContributors .tip').unbind().click(function(event) {
      event.stopPropagation()
    })

    $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function() {
      $('.mapContributors .tip').hide()
    })

    self.addTypeahead()
  },
  addTypeahead: function() {
    var self = InfoBox

    if (!Active.Map) return

    // for autocomplete
    var collaborators = {
      name: 'collaborators',
      limit: 9999,
      display: function(s) { return s.label },
      templates: {
        notFound: function(s) {
          return Hogan.compile($('#collaboratorSearchTemplate').html()).render({
            value: 'No results',
            label: 'No results',
            rtype: 'noresult',
            profile: self.userImageUrl
          })
        },
        suggestion: function(s) {
          return Hogan.compile($('#collaboratorSearchTemplate').html()).render(s)
        }
      },
      source: new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
          url: '/search/mappers?term=%QUERY',
          wildcard: '%QUERY'
        }
      })
    }

    // for adding map collaborators, who will have edit rights
    if (Active.Mapper && Active.Mapper.id === Active.Map.get('user_id')) {
      $('.collaboratorSearchField').typeahead(
        {
          highlight: false
        },
        [collaborators]
      )
      $('.collaboratorSearchField').bind('typeahead:select', self.handleResultClick)
      $('.mapContributors .removeCollaborator').click(function() {
        self.removeCollaborator(parseInt($(this).data('id')))
      })
    }
  },
  removeCollaborator: function(collaboratorId) {
    var self = InfoBox
    DataModel.Collaborators.remove(DataModel.Collaborators.get(collaboratorId))
    var mapperIds = DataModel.Collaborators.models.map(function(mapper) { return mapper.id })
    $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds })
    self.updateNumbers()
  },
  addCollaborator: function(newCollaboratorId) {
    var self = InfoBox

    if (DataModel.Collaborators.get(newCollaboratorId)) {
      GlobalUI.notifyUser('That user already has access')
      return
    }

    function callback(mapper) {
      DataModel.Collaborators.add(mapper)
      var mapperIds = DataModel.Collaborators.models.map(function(mapper) { return mapper.id })
      $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds })
      var name = DataModel.Collaborators.get(newCollaboratorId).get('name')
      GlobalUI.notifyUser(name + ' will be notified')
      self.updateNumbers()
    }

    $.getJSON('/users/' + newCollaboratorId + '.json', callback)
  },
  handleResultClick: function(event, item) {
    var self = InfoBox

    self.addCollaborator(item.id)
    $('.collaboratorSearchField').typeahead('val', '')
  },
  updateNameDescPerm: function(name, desc, perm) {
    $('.mapInfoBox').removeClass('mapRequestTitle')
    $('.mapInfoName .best_in_place_name').html(name)
    $('.mapInfoDesc .best_in_place_desc').html(desc)
    $('.mapInfoBox .mapPermission').removeClass('commons public private').addClass(perm)
  },
  createContributorList: function() {
    var relevantPeople = Active.Map.get('permission') === 'commons' ? DataModel.Mappers : DataModel.Collaborators
    var activeMapperIsCreator = Active.Mapper && Active.Mapper.id === Active.Map.get('user_id')
    var string = ''
    string += '<ul>'

    relevantPeople.each(function(m) {
      var isCreator = Active.Map.get('user_id') === m.get('id')
      string += '<li><a href="/explore/mapper/' + m.get('id') + '">' + '<img class="rtUserImage" width="25" height="25" src="' + m.get('image') + '" />' + m.get('name')
      if (isCreator) string += ' (creator)'
      string += '</a>'
      if (activeMapperIsCreator && !isCreator) string += '<span class="removeCollaborator" data-id="' + m.get('id') + '"></span>'
      string += '</li>'
    })

    string += '</ul>'

    if (activeMapperIsCreator) {
      string += '<div class="collabSearchField"><span class="addCollab"></span><input class="collaboratorSearchField" placeholder="Add a collaborator"></input></div>'
    }
    return string
  },
  updateNumbers: function() {
    if (!Active.Map) return

    const self = InfoBox

    var relevantPeople = Active.Map.get('permission') === 'commons' ? DataModel.Mappers : DataModel.Collaborators

    let contributorsClass = ''
    if (relevantPeople.length === 2) {
      contributorsClass = 'multiple mTwo'
    } else if (relevantPeople.length > 2) {
      contributorsClass = 'multiple'
    }

    let contributorsImage = self.userImageUrl
    if (relevantPeople.length > 0) {
      // get the first contributor and use their image
      contributorsImage = relevantPeople.models[0].get('image')
    }
    $('.mapContributors img').attr('src', contributorsImage).removeClass('multiple mTwo').addClass(contributorsClass)
    $('.mapContributors span').text(relevantPeople.length)
    $('.mapContributors .tip').html(self.createContributorList())
    self.addTypeahead()
    $('.mapContributors .tip').unbind().click(function(event) {
      event.stopPropagation()
    })
    $('.mapTopics').text(DataModel.Topics.length)
    $('.mapSynapses').text(DataModel.Synapses.length)

    $('.mapEditedAt').html('<span>Last edited: </span>' + Util.nowDateFormatted())
  },
  onPermissionClick: function(event) {
    var self = InfoBox

    if (!self.selectingPermission) {
      self.selectingPermission = true
      $(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow
      if ($(this).hasClass('commons')) {
        $(this).append('<ul class="permissionSelect"><li class="public"></li><li class="private"></li></ul>')
      } else if ($(this).hasClass('public')) {
        $(this).append('<ul class="permissionSelect"><li class="commons"></li><li class="private"></li></ul>')
      } else if ($(this).hasClass('private')) {
        $(this).append('<ul class="permissionSelect"><li class="commons"></li><li class="public"></li></ul>')
      }
      $('.mapPermission .permissionSelect li').click(self.selectPermission)
      event.stopPropagation()
    }
  },
  hidePermissionSelect: function() {
    var self = InfoBox

    self.selectingPermission = false
    $('.mapPermission').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow
    $('.mapPermission .permissionSelect').remove()
  },
  selectPermission: function(event) {
    var self = InfoBox

    self.selectingPermission = false
    var permission = $(this).attr('class')
    Active.Map.save({
      permission: permission
    })
    Active.Map.updateMapWrapper()
    const shareable = permission === 'private' ? '' : 'shareable'
    $('.mapPermission').removeClass('commons public private minimize').addClass(permission)
    $('.mapPermission .permissionSelect').remove()
    $('.mapInfoBox').removeClass('shareable').addClass(shareable)
    event.stopPropagation()
  },
  deleteActiveMap: function() {
    var confirmString = 'Are you sure you want to delete this map? '
    confirmString += 'This action is irreversible. It will not delete the topics and synapses on the map.'

    var doIt = window.confirm(confirmString)
    var map = Active.Map
    var mapper = Active.Mapper
    var authorized = map.authorizePermissionChange(mapper)

    if (doIt && authorized) {
      InfoBox.close()
      DataModel.Maps.Active.remove(map)
      DataModel.Maps.Featured.remove(map)
      DataModel.Maps.Mine.remove(map)
      DataModel.Maps.Shared.remove(map)
      map.destroy()
      browserHistory.push('/')
      GlobalUI.notifyUser('Map eliminated')
    } else if (!authorized) {
      window.alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?")
    }
  }
}

export default InfoBox