metamaps--metamaps/frontend/src/Metamaps/Map/index.js

407 lines
12 KiB
JavaScript
Raw Normal View History

/* global $ */
2016-09-22 15:51:13 +00:00
import outdent from 'outdent'
2016-11-07 20:25:08 +00:00
import { find as _find } from 'lodash'
2016-09-23 00:05:26 +00:00
import Active from '../Active'
import AutoLayout from '../AutoLayout'
import Create from '../Create'
import DataModel from '../DataModel'
import DataModelMap from '../DataModel/Map'
2016-09-23 00:05:26 +00:00
import Filter from '../Filter'
import GlobalUI from '../GlobalUI'
import JIT from '../JIT'
import Loading from '../Loading'
2016-09-23 00:05:26 +00:00
import Realtime from '../Realtime'
import Router from '../Router'
import Selected from '../Selected'
import SynapseCard from '../SynapseCard'
Into master: two finger pan/zoom, map and topic follows (for internal testing) on the UI, map activity emails (#1084) * fix topic spec * fix synapse/mapping spec * brakeman csrf warning suppressed :| * follows for maps in the ui for internal testing only still (#1072) * follows for maps in the ui for testers * require user for these actions * match how map follow works * include ability to unfollow from email * fixup templates * add unfollow_from_email to the policies * Update _cheatsheet.html.erb Clean up text, clarify, and bring in line with current functionality * topicsRegex and synapsesRegex should allow commas (#1073) * even better import csv regexes * prevent double prompt on file drop import * topic card in react (#1031) * its coming along * links bar * scssify a bunch * metacode image working a bit better * metacode selector in react topic card * riek editing for name field on topic card * riek submit on enter * factor out Title and Links from Topic Card component, but not the listeners * create working Desc editor * styling is much better now * textarea min height for desc * disallow images in topic card markdown * shift enter is linebreak, enter is save * attachments split out, but it's pretty buggy * move listeners into Links.js * slightly wider metacodeTitle * fix positioning on metacode selector * fix metacode selection * move metacode and permissions into subcomponents * fixes * prevent editing on desc/title if not authorized to edit * fix topic card draggability * fix embedly * fix md test * remove the removed link card manually with jquery * fix test syntax * eslint * more eslin * reuse authorizedToEdit * convert metacode sets to a json object for react * add the html in react whoop * fix metacode styling * sort wasn't working * finishing metacode select * readd the above link input border * fix syntax * multiline title editable textarea * more portable metacode selector component * factor out #metacodeOptions into one react component with a callback :D:D:D * render metacodeOptions in right click menu with react * render metacodeOptions in right click menu with react * fix up right click menu's metacode editing * fix topic card title character counter * ignore metamaps secret bundle in ag * simplify Attachments props * factor out embedly card into its own component; it seems to help * link resetter * fix edit icon on title in topic card * move mapCount and synapseCount hover/click logic to react * fix up the showMore control * metacode selection tweaks * tweak links bar spacing in topic card * rubocop * remove TODOs * more badass permissions selector * close permission selector when you click outside * fix overeager metacode selector * more modular attachments component * fix bug in Desc.js * fix right click styling * permission changes are different than edit rights * bad module ref * ensure maxLength on topic titles * hellz yeah (#1074) * fix drop from two touches to one * don't commit activity service * ability to select/unselect all metacodes in custom set with keyboard shortcut (fix #390) (#1078) * ability to select/unselect all metacodes in custom set with keyboard shortcut * select all button * nicer all/none buttons * set up react testing (#1080) * install mocha-webpack. also switch hark to npm version instead of github version * well, mocha-webpack runs * add jsdom for tests * upgrade to webpack 2 * fix npm run test errors * ImportDialogBox component tests * Fixes bug where pressing delete key while editing text will suggest... (#1083) * Fixes bug where pressing delete key while editing text will suggest the deletion of selected map entities * Changed the DEL key to remove entities instead of delete them * temporarily disable code climate duplication engine * add topic following for internal testing * daily map activity emails (#1081) * data prepared, task setup * add the basics of the email template * cover granular permissions * unfollow this map * break out permissions tests better * rename so test runs
2017-03-07 03:49:46 +00:00
import TopicCard from '../Views/TopicCard'
2016-09-23 00:05:26 +00:00
import Visualize from '../Visualize'
2016-09-22 15:51:13 +00:00
import CheatSheet from './CheatSheet'
import InfoBox from './InfoBox'
const Map = {
events: {
editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper'
},
2016-11-07 20:25:08 +00:00
init: function(serverData) {
2016-09-22 15:51:13 +00:00
var self = Map
2016-11-07 20:25:08 +00:00
$('#wrapper').mousedown(function(e) {
if (e.button === 1) return false
})
$('.starMap').click(function() {
2016-09-22 15:51:13 +00:00
if ($(this).is('.starred')) self.unstar()
else self.star()
})
2016-11-07 20:25:08 +00:00
$('.sidebarFork').click(function() {
2016-09-22 15:51:13 +00:00
self.fork()
})
GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html()
self.updateStar()
InfoBox.init(serverData, function updateThumbnail() {
self.uploadMapScreenshot()
})
2016-10-03 00:32:37 +00:00
CheatSheet.init(serverData)
2016-09-22 15:51:13 +00:00
$('.viewOnly .requestAccess').click(self.requestAccess)
2016-09-22 15:51:13 +00:00
$(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper)
},
2016-11-07 20:25:08 +00:00
requestAccess: function() {
$('.viewOnly').removeClass('sendRequest').addClass('sentRequest')
const mapId = Active.Map.id
$.post({
url: `/maps/${mapId}/access_request`
})
2016-11-07 20:25:08 +00:00
GlobalUI.notifyUser('Map creator will be notified of your request')
},
2016-11-07 20:25:08 +00:00
setAccessRequest: function(requests, activeMapper) {
let className = 'isViewOnly '
if (activeMapper) {
2016-11-07 20:25:08 +00:00
const request = _find(requests, r => r.user_id === activeMapper.id)
if (!request) className += 'sendRequest'
else if (request && !request.answered) className += 'sentRequest'
else if (request && request.answered && !request.approved) className += 'requestDenied'
}
$('.viewOnly').removeClass('sendRequest sentRequest requestDenied').addClass(className)
},
2016-11-07 20:25:08 +00:00
launch: function(id) {
var start = function(data) {
Active.Map = new DataModelMap(data.map)
DataModel.Mappers = new DataModel.MapperCollection(data.mappers)
DataModel.Collaborators = new DataModel.MapperCollection(data.collaborators)
DataModel.Topics = new DataModel.TopicCollection(data.topics)
DataModel.Synapses = new DataModel.SynapseCollection(data.synapses)
DataModel.Mappings = new DataModel.MappingCollection(data.mappings)
DataModel.Messages = data.messages
DataModel.Stars = data.stars
DataModel.attachCollectionEvents()
2016-09-22 15:51:13 +00:00
var map = Active.Map
var mapper = Active.Mapper
2016-09-23 20:06:28 +00:00
document.title = map.get('name') + ' | Metamaps'
2016-09-23 19:47:37 +00:00
2016-09-22 15:51:13 +00:00
// add class to .wrapper for specifying whether you can edit the map
if (map.authorizeToEdit(mapper)) {
$('.wrapper').addClass('canEditMap')
2016-11-07 20:25:08 +00:00
} else {
Map.setAccessRequest(data.requests, mapper)
}
2016-09-22 15:51:13 +00:00
// add class to .wrapper for specifying if the map can
// be collaborated on
if (map.get('permission') === 'commons') {
$('.wrapper').addClass('commonsMap')
}
2016-11-07 20:25:08 +00:00
Map.updateStar()
2016-09-22 15:51:13 +00:00
// set filter mapper H3 text
$('#filter_by_mapper h3').html('MAPPERS')
// build and render the visualization
Visualize.type = 'ForceDirected'
JIT.prepareVizData()
// update filters
Filter.reset()
// reset selected arrays
Selected.reset()
// set the proper mapinfobox content
InfoBox.load()
2016-09-22 15:51:13 +00:00
// these three update the actual filter box with the right list items
Filter.checkMetacodes()
Filter.checkSynapses()
Filter.checkMappers()
Realtime.startActiveMap()
Loading.hide()
2016-11-07 20:25:08 +00:00
2016-09-22 15:51:13 +00:00
// for mobile
$('#header_content').html(map.get('name'))
}
$.ajax({
url: '/maps/' + id + '/contains.json',
success: start
})
},
2016-11-07 20:25:08 +00:00
end: function() {
2016-09-22 15:51:13 +00:00
if (Active.Map) {
$('.wrapper').removeClass('canEditMap commonsMap')
AutoLayout.resetSpiral()
$('.rightclickmenu').remove()
TopicCard.hideCard()
SynapseCard.hideCard()
Create.newTopic.hide(true) // true means force (and override pinned)
Create.newSynapse.hide()
Filter.close()
InfoBox.close()
2016-09-22 15:51:13 +00:00
Realtime.endActiveMap()
$('.viewOnly').removeClass('isViewOnly')
2016-09-22 15:51:13 +00:00
}
},
2016-11-07 20:25:08 +00:00
updateStar: function() {
if (!Active.Mapper || !DataModel.Stars) return
2016-09-22 15:51:13 +00:00
// update the star/unstar icon
2016-11-07 20:25:08 +00:00
if (DataModel.Stars.find(function(s) { return s.user_id === Active.Mapper.id })) {
2016-09-22 15:51:13 +00:00
$('.starMap').addClass('starred')
$('.starMap .tooltipsAbove').html('Unstar')
} else {
$('.starMap').removeClass('starred')
$('.starMap .tooltipsAbove').html('Star')
}
},
2016-11-07 20:25:08 +00:00
star: function() {
2016-09-22 15:51:13 +00:00
var self = Map
if (!Active.Map) return
$.post('/maps/' + Active.Map.id + '/star')
DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id })
DataModel.Maps.Starred.add(Active.Map)
2016-09-22 15:51:13 +00:00
GlobalUI.notifyUser('Map is now starred')
self.updateStar()
},
2016-11-07 20:25:08 +00:00
unstar: function() {
2016-09-22 15:51:13 +00:00
var self = Map
if (!Active.Map) return
$.post('/maps/' + Active.Map.id + '/unstar')
2016-11-07 20:25:08 +00:00
DataModel.Stars = DataModel.Stars.filter(function(s) { return s.user_id !== Active.Mapper.id })
DataModel.Maps.Starred.remove(Active.Map)
2016-11-07 20:25:08 +00:00
self.updateStar()
2016-09-22 15:51:13 +00:00
},
2016-11-07 20:25:08 +00:00
fork: function() {
2016-09-22 15:51:13 +00:00
GlobalUI.openLightbox('forkmap')
2016-11-07 20:25:08 +00:00
let nodesData = ''
let synapsesData = ''
let nodesArray = []
let synapsesArray = []
2016-09-22 15:51:13 +00:00
// collect the unfiltered topics
2016-11-07 20:25:08 +00:00
Visualize.mGraph.graph.eachNode(function(n) {
2016-09-22 15:51:13 +00:00
// if the opacity is less than 1 then it's filtered
if (n.getData('alpha') === 1) {
var id = n.getData('topic').id
2016-11-07 20:25:08 +00:00
nodesArray.push(id)
let x, y
2016-09-22 15:51:13 +00:00
if (n.pos.x && n.pos.y) {
x = n.pos.x
y = n.pos.y
} else {
2016-11-07 20:25:08 +00:00
x = Math.cos(n.pos.theta) * n.pos.rho
y = Math.sin(n.pos.theta) * n.pos.rho
2016-09-22 15:51:13 +00:00
}
2016-11-07 20:25:08 +00:00
nodesData += id + '/' + x + '/' + y + ','
2016-09-22 15:51:13 +00:00
}
})
// collect the unfiltered synapses
2016-11-07 20:25:08 +00:00
DataModel.Synapses.each(function(synapse) {
2016-09-22 15:51:13 +00:00
var desc = synapse.get('desc')
var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1
// make sure that both topics are being added, otherwise, it
// doesn't make sense to add the synapse
2016-11-07 20:25:08 +00:00
var topicsNotFiltered = nodesArray.indexOf(synapse.get('topic1_id')) > -1
topicsNotFiltered = topicsNotFiltered && nodesArray.indexOf(synapse.get('topic2_id')) > -1
2016-09-22 15:51:13 +00:00
if (descNotFiltered && topicsNotFiltered) {
2016-11-07 20:25:08 +00:00
synapsesArray.push(synapse.id)
2016-09-22 15:51:13 +00:00
}
})
2016-11-07 20:25:08 +00:00
synapsesData = synapsesArray.join()
nodesData = nodesData.slice(0, -1)
2016-09-22 15:51:13 +00:00
2016-11-07 20:25:08 +00:00
GlobalUI.CreateMap.topicsToMap = nodesData
GlobalUI.CreateMap.synapsesToMap = synapsesData
2016-09-22 15:51:13 +00:00
},
2016-11-07 20:25:08 +00:00
leavePrivateMap: function() {
2016-09-22 15:51:13 +00:00
var map = Active.Map
DataModel.Maps.Active.remove(map)
DataModel.Maps.Featured.remove(map)
2016-09-22 15:51:13 +00:00
Router.home()
GlobalUI.notifyUser('Sorry! That map has been changed to Private.')
},
2016-11-07 20:25:08 +00:00
cantEditNow: function() {
Realtime.turnOff(true) // true is for 'silence'
2016-09-22 15:51:13 +00:00
GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.')
Active.Map.trigger('changeByOther')
},
2016-11-07 20:25:08 +00:00
canEditNow: function() {
2016-09-22 15:51:13 +00:00
var confirmString = "You've been granted permission to edit this map. "
confirmString += 'Do you want to reload and enable realtime collaboration?'
var c = window.confirm(confirmString)
2016-09-22 15:51:13 +00:00
if (c) {
Router.maps(Active.Map.id)
}
},
2016-11-07 20:25:08 +00:00
editedByActiveMapper: function() {
2016-09-22 15:51:13 +00:00
if (Active.Mapper) {
DataModel.Mappers.add(Active.Mapper)
2016-09-22 15:51:13 +00:00
}
},
2016-11-07 20:25:08 +00:00
exportImage: function() {
Map.uploadMapScreenshot()
Map.offerScreenshotDownload()
GlobalUI.notifyUser('Note: this button is going away. Check the map card or the import box for setting the map thumbnail or downloading a screenshot.')
},
offerScreenshotDownload: () => {
const canvas = Map.getMapCanvasForScreenshots()
const filename = Map.getMapScreenshotFilename(Active.Map)
var downloadMessage = outdent`
Captured map screenshot!
<a id="map-screenshot-download-link"
href="${canvas.canvas.toDataURL()}"
download="${filename}"
>
DOWNLOAD
</a>`
GlobalUI.notifyUser(downloadMessage)
},
uploadMapScreenshot: () => {
const canvas = Map.getMapCanvasForScreenshots()
const filename = Map.getMapScreenshotFilename(Active.Map)
canvas.canvas.toBlob(imageBlob => {
const formData = new window.FormData()
formData.append('map[screenshot]', imageBlob, filename)
$.ajax({
type: 'PATCH',
dataType: 'json',
url: `/maps/${Active.Map.id}`,
data: formData,
processData: false,
contentType: false,
success: function(data) {
GlobalUI.notifyUser('Successfully updated map screenshot.')
},
error: function() {
GlobalUI.notifyUser('Failed to update map screenshot.')
}
})
})
},
getMapCanvasForScreenshots: () => {
2016-09-22 15:51:13 +00:00
var canvas = {}
canvas.canvas = document.createElement('canvas')
canvas.canvas.width = 1880 // 960
canvas.canvas.height = 1260 // 630
canvas.scaleOffsetX = 1
canvas.scaleOffsetY = 1
canvas.translateOffsetY = 0
canvas.translateOffsetX = 0
canvas.denySelected = true
2016-11-07 20:25:08 +00:00
canvas.getSize = function() {
2016-09-22 15:51:13 +00:00
if (this.size) return this.size
var canvas = this.canvas
this.size = {
2016-09-22 15:51:13 +00:00
width: canvas.width,
height: canvas.height
}
return this.size
2016-09-22 15:51:13 +00:00
}
2016-11-07 20:25:08 +00:00
canvas.scale = function(x, y) {
const px = this.scaleOffsetX * x
const py = this.scaleOffsetY * y
const dx = this.translateOffsetX * (x - 1) / px
const dy = this.translateOffsetY * (y - 1) / py
2016-09-22 15:51:13 +00:00
this.scaleOffsetX = px
this.scaleOffsetY = py
this.getCtx().scale(x, y)
this.translate(dx, dy)
}
2016-11-07 20:25:08 +00:00
canvas.translate = function(x, y) {
const sx = this.scaleOffsetX
const sy = this.scaleOffsetY
2016-09-22 15:51:13 +00:00
this.translateOffsetX += x * sx
this.translateOffsetY += y * sy
this.getCtx().translate(x, y)
}
2016-11-07 20:25:08 +00:00
canvas.getCtx = function() {
2016-09-22 15:51:13 +00:00
return this.canvas.getContext('2d')
}
// center it
canvas.getCtx().translate(1880 / 2, 1260 / 2)
var mGraph = Visualize.mGraph
var id = mGraph.root
var root = mGraph.graph.getNode(id)
var T = !!root.visited
// pass true to avoid basing it on a selection
JIT.zoomExtents(null, canvas, true)
2016-11-07 20:25:08 +00:00
const c = canvas.canvas
const ctx = canvas.getCtx()
const scale = canvas.scaleOffsetX
2016-09-22 15:51:13 +00:00
// draw a grey background
ctx.fillStyle = '#d8d9da'
2016-11-07 20:25:08 +00:00
const xPoint = (-(c.width / scale) / 2) - (canvas.translateOffsetX / scale)
const yPoint = (-(c.height / scale) / 2) - (canvas.translateOffsetY / scale)
2016-09-22 15:51:13 +00:00
ctx.fillRect(xPoint, yPoint, c.width / scale, c.height / scale)
// draw the graph
2016-11-07 20:25:08 +00:00
mGraph.graph.eachNode(function(node) {
2016-09-22 15:51:13 +00:00
var nodeAlpha = node.getData('alpha')
2016-11-07 20:25:08 +00:00
node.eachAdjacency(function(adj) {
2016-09-22 15:51:13 +00:00
var nodeTo = adj.nodeTo
if (!!nodeTo.visited === T && node.drawn && nodeTo.drawn) {
mGraph.fx.plotLine(adj, canvas)
}
})
if (node.drawn) {
mGraph.fx.plotNode(node, canvas)
}
if (!mGraph.labelsHidden) {
if (node.drawn && nodeAlpha >= 0.95) {
mGraph.labels.plotLabel(canvas, node)
} else {
mGraph.labels.hideLabel(node, false)
}
}
node.visited = !T
})
return canvas
},
getMapScreenshotFilename: map => {
2016-09-22 15:51:13 +00:00
var today = new Date()
var dd = today.getDate()
2016-11-07 20:25:08 +00:00
var mm = today.getMonth() + 1 // January is 0!
2016-09-22 15:51:13 +00:00
var yyyy = today.getFullYear()
if (dd < 10) {
dd = '0' + dd
}
if (mm < 10) {
mm = '0' + mm
}
today = mm + '/' + dd + '/' + yyyy
var mapName = map.get('name').split(' ').join(['-'])
const filename = `metamap-${map.id}-${mapName}-${today}.png`
return filename
2016-09-22 15:51:13 +00:00
}
}
2016-09-23 00:05:26 +00:00
export { CheatSheet, InfoBox }
2016-09-22 15:51:13 +00:00
export default Map