Merge pull request #1139 from metamaps/feature/right.click.react
Right click/context menu redone in React
This commit is contained in:
commit
322da431eb
11 changed files with 413 additions and 342 deletions
|
@ -4,12 +4,13 @@ import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { Router, browserHistory } from 'react-router'
|
import { Router, browserHistory } from 'react-router'
|
||||||
import { merge } from 'lodash'
|
import { merge } from 'lodash'
|
||||||
|
import apply from 'async/apply'
|
||||||
|
|
||||||
import { notifyUser } from './index.js'
|
import { notifyUser } from './index.js'
|
||||||
import ImportDialog from './ImportDialog'
|
import ImportDialog from './ImportDialog'
|
||||||
import Active from '../Active'
|
import Active from '../Active'
|
||||||
import DataModel from '../DataModel'
|
import DataModel from '../DataModel'
|
||||||
import { ExploreMaps, ChatView, TopicCard } from '../Views'
|
import { ExploreMaps, ChatView, TopicCard, ContextMenu } from '../Views'
|
||||||
import Filter from '../Filter'
|
import Filter from '../Filter'
|
||||||
import JIT from '../JIT'
|
import JIT from '../JIT'
|
||||||
import Realtime from '../Realtime'
|
import Realtime from '../Realtime'
|
||||||
|
@ -113,6 +114,7 @@ const ReactApp = {
|
||||||
self.getFilterProps(),
|
self.getFilterProps(),
|
||||||
self.getCommonProps(),
|
self.getCommonProps(),
|
||||||
self.getMapsProps(),
|
self.getMapsProps(),
|
||||||
|
self.getContextMenuProps(),
|
||||||
self.getTopicCardProps(),
|
self.getTopicCardProps(),
|
||||||
self.getChatProps())
|
self.getChatProps())
|
||||||
},
|
},
|
||||||
|
@ -155,6 +157,28 @@ const ReactApp = {
|
||||||
onTopicFollow: Topic.onTopicFollow
|
onTopicFollow: Topic.onTopicFollow
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getContextMenuProps: function() {
|
||||||
|
const { render } = ReactApp
|
||||||
|
return {
|
||||||
|
// values
|
||||||
|
contextMenu: !!(ContextMenu.clickedNode || ContextMenu.clickedEdge),
|
||||||
|
contextNode: ContextMenu.clickedNode,
|
||||||
|
contextEdge: ContextMenu.clickedEdge,
|
||||||
|
contextPos: ContextMenu.pos,
|
||||||
|
contextFetchingSiblingsData: ContextMenu.fetchingSiblingsData,
|
||||||
|
contextSiblingsData: ContextMenu.siblingsData,
|
||||||
|
// functions
|
||||||
|
contextDelete: apply(ContextMenu.delete, render),
|
||||||
|
contextRemove: apply(ContextMenu.remove, render),
|
||||||
|
contextHide: apply(ContextMenu.hide, render),
|
||||||
|
contextCenterOn: apply(ContextMenu.centerOn, render),
|
||||||
|
contextPopoutTopic: apply(ContextMenu.popoutTopic, render),
|
||||||
|
contextUpdatePermissions: apply(ContextMenu.updatePermissions, render),
|
||||||
|
contextOnMetacodeSelect: apply(ContextMenu.onMetacodeSelect, render),
|
||||||
|
contextFetchSiblings: apply(ContextMenu.fetchSiblings, render),
|
||||||
|
contextPopulateSiblings: apply(ContextMenu.populateSiblings, render)
|
||||||
|
}
|
||||||
|
},
|
||||||
getTopicProps: function() {
|
getTopicProps: function() {
|
||||||
const self = ReactApp
|
const self = ReactApp
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
/* global $, Image, CanvasLoader */
|
/* global $, Image */
|
||||||
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import outdent from 'outdent'
|
|
||||||
import clipboard from 'clipboard-js'
|
import clipboard from 'clipboard-js'
|
||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
|
|
||||||
import $jit from '../patched/JIT'
|
import $jit from '../patched/JIT'
|
||||||
|
|
||||||
import MetacodeSelect from '../components/MetacodeSelect'
|
|
||||||
|
|
||||||
import Active from './Active'
|
import Active from './Active'
|
||||||
|
import ContextMenu from './Views/ContextMenu'
|
||||||
import Control from './Control'
|
import Control from './Control'
|
||||||
import Create from './Create'
|
import Create from './Create'
|
||||||
import DataModel from './DataModel'
|
import DataModel from './DataModel'
|
||||||
|
@ -349,7 +345,7 @@ const JIT = {
|
||||||
// Add also a click handler to nodes
|
// Add also a click handler to nodes
|
||||||
onClick: function(node, eventInfo, e) {
|
onClick: function(node, eventInfo, e) {
|
||||||
// remove the rightclickmenu
|
// remove the rightclickmenu
|
||||||
$('.rightclickmenu').remove()
|
ContextMenu.reset(ReactApp.render)
|
||||||
|
|
||||||
if (Mouse.boxStartCoordinates) {
|
if (Mouse.boxStartCoordinates) {
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
|
@ -390,7 +386,7 @@ const JIT = {
|
||||||
// Add also a click handler to nodes
|
// Add also a click handler to nodes
|
||||||
onRightClick: function(node, eventInfo, e) {
|
onRightClick: function(node, eventInfo, e) {
|
||||||
// remove the rightclickmenu
|
// remove the rightclickmenu
|
||||||
$('.rightclickmenu').remove()
|
ContextMenu.reset(ReactApp.render)
|
||||||
|
|
||||||
if (Mouse.boxStartCoordinates) {
|
if (Mouse.boxStartCoordinates) {
|
||||||
Create.newSynapse.hide()
|
Create.newSynapse.hide()
|
||||||
|
@ -1006,7 +1002,7 @@ const JIT = {
|
||||||
TopicCard.hideCard()
|
TopicCard.hideCard()
|
||||||
SynapseCard.hideCard()
|
SynapseCard.hideCard()
|
||||||
Create.newTopic.hide()
|
Create.newTopic.hide()
|
||||||
$('.rightclickmenu').remove()
|
ContextMenu.reset(ReactApp.render)
|
||||||
// reset the draw synapse positions to false
|
// reset the draw synapse positions to false
|
||||||
Mouse.synapseStartCoordinates = []
|
Mouse.synapseStartCoordinates = []
|
||||||
Mouse.synapseEndCoordinates = null
|
Mouse.synapseEndCoordinates = null
|
||||||
|
@ -1346,230 +1342,12 @@ const JIT = {
|
||||||
selectNodeOnRightClickHandler: function(node, e) {
|
selectNodeOnRightClickHandler: function(node, e) {
|
||||||
// the 'node' variable is a JIT node, the one that was clicked on
|
// the 'node' variable is a JIT node, the one that was clicked on
|
||||||
// the 'e' variable is the click event
|
// the 'e' variable is the click event
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
if (Visualize.mGraph.busy) return
|
if (Visualize.mGraph.busy) return
|
||||||
|
|
||||||
// select the node
|
|
||||||
Control.selectNode(node, e)
|
Control.selectNode(node, e)
|
||||||
|
ContextMenu.selectNode(ReactApp.render, node, {x: e.clientX, y: e.clientY})
|
||||||
// delete old right click menu
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
// create new menu for clicked on node
|
|
||||||
const rightclickmenu = document.createElement('div')
|
|
||||||
rightclickmenu.className = 'rightclickmenu'
|
|
||||||
// prevent the custom context menu from immediately opening the default context menu as well
|
|
||||||
rightclickmenu.setAttribute('oncontextmenu', 'return false')
|
|
||||||
|
|
||||||
// add the proper options to the menu
|
|
||||||
let menustring = '<ul>'
|
|
||||||
|
|
||||||
const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper)
|
|
||||||
|
|
||||||
const disabled = authorized ? '' : 'disabled'
|
|
||||||
|
|
||||||
if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>'
|
|
||||||
if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>'
|
|
||||||
if (Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>'
|
|
||||||
if (Active.Map && Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>'
|
|
||||||
|
|
||||||
if (Active.Topic) {
|
|
||||||
menustring += '<li class="rc-center"><div class="rc-icon"></div>Center this topic<div class="rc-keyboard">Alt+E</div></li>'
|
|
||||||
}
|
|
||||||
|
|
||||||
menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>'
|
|
||||||
|
|
||||||
if (Active.Mapper) {
|
|
||||||
const options = outdent`
|
|
||||||
<ul>
|
|
||||||
<li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li>
|
|
||||||
<li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li>
|
|
||||||
<li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li>
|
|
||||||
</ul>`
|
|
||||||
|
|
||||||
menustring += '<li class="rc-spacer"></li>'
|
|
||||||
|
|
||||||
menustring += outdent`
|
|
||||||
<li class="rc-permission">
|
|
||||||
<div class="rc-icon"></div>
|
|
||||||
Change permissions
|
|
||||||
${options}
|
|
||||||
<div class="expandLi"></div>
|
|
||||||
</li>`
|
|
||||||
|
|
||||||
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode<div id="metacodeOptionsWrapper"></div><div class="expandLi"></div></li>'
|
|
||||||
}
|
|
||||||
if (Active.Topic) {
|
|
||||||
if (!Active.Mapper) {
|
|
||||||
menustring += '<li class="rc-spacer"></li>'
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up the get sibling menu as a "lazy load"
|
|
||||||
// only fill in the submenu when they hover over the get siblings list item
|
|
||||||
const siblingMenu = outdent`
|
|
||||||
<ul id="fetchSiblingList">
|
|
||||||
<li class="fetchAll">All<div class="rc-keyboard">Alt+R</div></li>
|
|
||||||
<li id="loadingSiblings"></li>
|
|
||||||
</ul>`
|
|
||||||
menustring += '<li class="rc-siblings"><div class="rc-icon"></div>Reveal siblings' + siblingMenu + '<div class="expandLi"></div></li>'
|
|
||||||
}
|
|
||||||
|
|
||||||
menustring += '</ul>'
|
|
||||||
rightclickmenu.innerHTML = menustring
|
|
||||||
|
|
||||||
// position the menu where the click happened
|
|
||||||
const position = {}
|
|
||||||
const RIGHTCLICK_WIDTH = 300
|
|
||||||
const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static
|
|
||||||
const SUBMENUS_WIDTH = 256
|
|
||||||
const MAX_SUBMENU_HEIGHT = 270
|
|
||||||
const windowWidth = $(window).width()
|
|
||||||
const windowHeight = $(window).height()
|
|
||||||
|
|
||||||
if (windowWidth - e.clientX < SUBMENUS_WIDTH) {
|
|
||||||
position.right = windowWidth - e.clientX
|
|
||||||
$(rightclickmenu).addClass('moveMenusToLeft')
|
|
||||||
} else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) {
|
|
||||||
position.right = windowWidth - e.clientX
|
|
||||||
} else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) {
|
|
||||||
position.left = e.clientX
|
|
||||||
$(rightclickmenu).addClass('moveMenusToLeft')
|
|
||||||
} else {
|
|
||||||
position.left = e.clientX
|
|
||||||
}
|
|
||||||
|
|
||||||
if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) {
|
|
||||||
position.bottom = windowHeight - e.clientY
|
|
||||||
$(rightclickmenu).addClass('moveMenusUp')
|
|
||||||
} else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) {
|
|
||||||
position.top = e.clientY
|
|
||||||
$(rightclickmenu).addClass('moveMenusUp')
|
|
||||||
} else {
|
|
||||||
position.top = e.clientY
|
|
||||||
}
|
|
||||||
|
|
||||||
$(rightclickmenu).css(position)
|
|
||||||
// add the menu to the page
|
|
||||||
$('#wrapper').append(rightclickmenu)
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
React.createElement(MetacodeSelect, {
|
|
||||||
onMetacodeSelect: metacodeId => {
|
|
||||||
if (Selected.Nodes.length > 1) {
|
|
||||||
// batch update multiple topics
|
|
||||||
Control.updateSelectedMetacodes(metacodeId)
|
|
||||||
} else {
|
|
||||||
const topic = DataModel.Topics.get(node.id)
|
|
||||||
topic.save({
|
|
||||||
metacode_id: metacodeId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
$(rightclickmenu).remove()
|
|
||||||
},
|
|
||||||
metacodeSets: ReactApp.metacodeSets
|
|
||||||
}),
|
|
||||||
document.getElementById('metacodeOptionsWrapper')
|
|
||||||
)
|
|
||||||
|
|
||||||
// attach events to clicks on the list items
|
|
||||||
|
|
||||||
// delete the selected things from the database
|
|
||||||
if (authorized) {
|
|
||||||
$('.rc-delete').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
Control.deleteSelected()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the selected things from the map
|
|
||||||
if (Active.Topic || authorized) {
|
|
||||||
$('.rc-remove').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
Control.removeSelectedEdges()
|
|
||||||
Control.removeSelectedNodes()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide selected nodes and synapses until refresh
|
|
||||||
$('.rc-hide').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
Control.hideSelectedEdges()
|
|
||||||
Control.hideSelectedNodes()
|
|
||||||
})
|
|
||||||
|
|
||||||
// when in radial, center on the topic you picked
|
|
||||||
$('.rc-center').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
Topic.centerOn(node.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
// open the entity in a new tab
|
|
||||||
$('.rc-popout').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
const win = window.open('/topics/' + node.id, '_blank')
|
|
||||||
win.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
// change the permission of all the selected nodes and synapses that you were the originator of
|
|
||||||
$('.rc-permission li').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
// $(this).text() will be 'commons' 'public' or 'private'
|
|
||||||
Control.updateSelectedPermissions($(this).text())
|
|
||||||
})
|
|
||||||
|
|
||||||
// fetch relatives
|
|
||||||
let fetchSent = false
|
|
||||||
$('.rc-siblings').hover(function() {
|
|
||||||
if (!fetchSent) {
|
|
||||||
JIT.populateRightClickSiblings(node)
|
|
||||||
fetchSent = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
$('.rc-siblings .fetchAll').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
// data-id is a metacode id
|
|
||||||
Topic.fetchRelatives(node)
|
|
||||||
})
|
|
||||||
}, // selectNodeOnRightClickHandler,
|
}, // selectNodeOnRightClickHandler,
|
||||||
populateRightClickSiblings: function(node) {
|
|
||||||
// depending on how many topics are selected, do different things
|
|
||||||
const topic = node.getData('topic')
|
|
||||||
|
|
||||||
// add a loading icon for now
|
|
||||||
const loader = new CanvasLoader('loadingSiblings')
|
|
||||||
loader.setColor('#4FC059') // default is '#000000'
|
|
||||||
loader.setDiameter(15) // default is 40
|
|
||||||
loader.setDensity(41) // default is 40
|
|
||||||
loader.setRange(0.9) // default is 1.3
|
|
||||||
loader.show() // Hidden by default
|
|
||||||
|
|
||||||
const topics = DataModel.Topics.map(function(t) { return t.id })
|
|
||||||
const topicsString = topics.join()
|
|
||||||
|
|
||||||
const successCallback = function(data) {
|
|
||||||
$('#loadingSiblings').remove()
|
|
||||||
|
|
||||||
for (var key in data) {
|
|
||||||
const string = `${DataModel.Metacodes.get(key).get('name')} (${data[key]})`
|
|
||||||
$('#fetchSiblingList').append(`<li class="getSiblings" data-id="${key}">${string}</li>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.rc-siblings .getSiblings').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
// data-id is a metacode id
|
|
||||||
Topic.fetchRelatives(node, $(this).attr('data-id'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: 'GET',
|
|
||||||
url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topicsString,
|
|
||||||
success: successCallback,
|
|
||||||
error: function() {}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectEdgeOnClickHandler: function(adj, e) {
|
selectEdgeOnClickHandler: function(adj, e) {
|
||||||
if (Visualize.mGraph.busy) return
|
if (Visualize.mGraph.busy) return
|
||||||
|
|
||||||
|
@ -1611,113 +1389,14 @@ const JIT = {
|
||||||
}
|
}
|
||||||
}, // selectEdgeOnClickHandler
|
}, // selectEdgeOnClickHandler
|
||||||
selectEdgeOnRightClickHandler: function(adj, e) {
|
selectEdgeOnRightClickHandler: function(adj, e) {
|
||||||
// the 'node' variable is a JIT node, the one that was clicked on
|
// the 'adj' variable is a JIT adjacency, the one that was clicked on
|
||||||
// the 'e' variable is the click event
|
// the 'e' variable is the click event
|
||||||
|
|
||||||
if (adj.getData('alpha') === 0) return // don't do anything if the edge is filtered
|
if (adj.getData('alpha') === 0) return // don't do anything if the edge is filtered
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
if (Visualize.mGraph.busy) return
|
if (Visualize.mGraph.busy) return
|
||||||
|
|
||||||
Control.selectEdge(adj)
|
Control.selectEdge(adj)
|
||||||
|
ContextMenu.selectEdge(ReactApp.render, adj, {x: e.clientX, y: e.clientY})
|
||||||
// delete old right click menu
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
// create new menu for clicked on node
|
|
||||||
const rightclickmenu = document.createElement('div')
|
|
||||||
rightclickmenu.className = 'rightclickmenu'
|
|
||||||
// prevent the custom context menu from immediately opening the default context menu as well
|
|
||||||
rightclickmenu.setAttribute('oncontextmenu', 'return false')
|
|
||||||
|
|
||||||
// add the proper options to the menu
|
|
||||||
let menustring = '<ul>'
|
|
||||||
|
|
||||||
const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper)
|
|
||||||
|
|
||||||
const disabled = authorized ? '' : 'disabled'
|
|
||||||
|
|
||||||
if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>'
|
|
||||||
if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>'
|
|
||||||
if (Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>'
|
|
||||||
if (Active.Map && Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>'
|
|
||||||
|
|
||||||
if (Active.Map && Active.Mapper) menustring += '<li class="rc-spacer"></li>'
|
|
||||||
|
|
||||||
if (Active.Mapper) {
|
|
||||||
const permOptions = outdent`
|
|
||||||
<ul>
|
|
||||||
<li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li>
|
|
||||||
<li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> </ul>`
|
|
||||||
|
|
||||||
menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + permOptions + '<div class="expandLi"></div></li>'
|
|
||||||
}
|
|
||||||
|
|
||||||
menustring += '</ul>'
|
|
||||||
rightclickmenu.innerHTML = menustring
|
|
||||||
|
|
||||||
// position the menu where the click happened
|
|
||||||
const position = {}
|
|
||||||
const RIGHTCLICK_WIDTH = 300
|
|
||||||
const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static
|
|
||||||
const SUBMENUS_WIDTH = 256
|
|
||||||
const MAX_SUBMENU_HEIGHT = 270
|
|
||||||
const windowWidth = $(window).width()
|
|
||||||
const windowHeight = $(window).height()
|
|
||||||
|
|
||||||
if (windowWidth - e.clientX < SUBMENUS_WIDTH) {
|
|
||||||
position.right = windowWidth - e.clientX
|
|
||||||
$(rightclickmenu).addClass('moveMenusToLeft')
|
|
||||||
} else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) {
|
|
||||||
position.right = windowWidth - e.clientX
|
|
||||||
} else position.left = e.clientX
|
|
||||||
|
|
||||||
if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) {
|
|
||||||
position.bottom = windowHeight - e.clientY
|
|
||||||
$(rightclickmenu).addClass('moveMenusUp')
|
|
||||||
} else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) {
|
|
||||||
position.top = e.clientY
|
|
||||||
$(rightclickmenu).addClass('moveMenusUp')
|
|
||||||
} else position.top = e.clientY
|
|
||||||
|
|
||||||
$(rightclickmenu).css(position)
|
|
||||||
|
|
||||||
// add the menu to the page
|
|
||||||
$('#wrapper').append(rightclickmenu)
|
|
||||||
|
|
||||||
// attach events to clicks on the list items
|
|
||||||
|
|
||||||
// delete the selected things from the database
|
|
||||||
if (authorized) {
|
|
||||||
$('.rc-delete').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
Control.deleteSelected()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the selected things from the map
|
|
||||||
if (authorized) {
|
|
||||||
$('.rc-remove').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
Control.removeSelectedEdges()
|
|
||||||
Control.removeSelectedNodes()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide selected nodes and synapses until refresh
|
|
||||||
$('.rc-hide').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
Control.hideSelectedEdges()
|
|
||||||
Control.hideSelectedNodes()
|
|
||||||
})
|
|
||||||
|
|
||||||
// change the permission of all the selected nodes and synapses that you were the originator of
|
|
||||||
$('.rc-permission li').click(function() {
|
|
||||||
$('.rightclickmenu').remove()
|
|
||||||
// $(this).text() will be 'commons' 'public' or 'private'
|
|
||||||
Control.updateSelectedPermissions($(this).text())
|
|
||||||
})
|
|
||||||
}, // selectEdgeOnRightClickHandler
|
}, // selectEdgeOnRightClickHandler
|
||||||
SmoothPanning: function() {
|
SmoothPanning: function() {
|
||||||
const sx = Visualize.mGraph.canvas.scaleOffsetX
|
const sx = Visualize.mGraph.canvas.scaleOffsetX
|
||||||
|
|
|
@ -152,12 +152,12 @@ const Listeners = {
|
||||||
var node = nodes[nodes.length - 1]
|
var node = nodes[nodes.length - 1]
|
||||||
if (opts.center && opts.reveal) {
|
if (opts.center && opts.reveal) {
|
||||||
Topic.centerOn(node.id, function() {
|
Topic.centerOn(node.id, function() {
|
||||||
Topic.fetchRelatives(nodes)
|
Topic.fetchSiblings(nodes)
|
||||||
})
|
})
|
||||||
} else if (opts.center) {
|
} else if (opts.center) {
|
||||||
Topic.centerOn(node.id)
|
Topic.centerOn(node.id)
|
||||||
} else if (opts.reveal) {
|
} else if (opts.reveal) {
|
||||||
Topic.fetchRelatives(nodes)
|
Topic.fetchSiblings(nodes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import Loading from '../Loading'
|
||||||
import Realtime from '../Realtime'
|
import Realtime from '../Realtime'
|
||||||
import Selected from '../Selected'
|
import Selected from '../Selected'
|
||||||
import SynapseCard from '../SynapseCard'
|
import SynapseCard from '../SynapseCard'
|
||||||
|
import ContextMenu from '../Views/ContextMenu'
|
||||||
import TopicCard from '../Views/TopicCard'
|
import TopicCard from '../Views/TopicCard'
|
||||||
import Visualize from '../Visualize'
|
import Visualize from '../Visualize'
|
||||||
|
|
||||||
|
@ -137,7 +138,7 @@ const Map = {
|
||||||
if (Active.Map) {
|
if (Active.Map) {
|
||||||
$('.main').removeClass('compressed')
|
$('.main').removeClass('compressed')
|
||||||
AutoLayout.resetSpiral()
|
AutoLayout.resetSpiral()
|
||||||
$('.rightclickmenu').remove()
|
ContextMenu.reset(ReactApp.render)
|
||||||
TopicCard.hideCard()
|
TopicCard.hideCard()
|
||||||
SynapseCard.hideCard()
|
SynapseCard.hideCard()
|
||||||
Create.newTopic.hide(true) // true means force (and override pinned)
|
Create.newTopic.hide(true) // true means force (and override pinned)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Selected from './Selected'
|
||||||
import Settings from './Settings'
|
import Settings from './Settings'
|
||||||
import SynapseCard from './SynapseCard'
|
import SynapseCard from './SynapseCard'
|
||||||
import TopicCard from './Views/TopicCard'
|
import TopicCard from './Views/TopicCard'
|
||||||
|
import ContextMenu from './Views/ContextMenu'
|
||||||
import Util from './Util'
|
import Util from './Util'
|
||||||
import Visualize from './Visualize'
|
import Visualize from './Visualize'
|
||||||
|
|
||||||
|
@ -68,13 +69,13 @@ const Topic = {
|
||||||
},
|
},
|
||||||
end: function() {
|
end: function() {
|
||||||
if (Active.Topic) {
|
if (Active.Topic) {
|
||||||
$('.rightclickmenu').remove()
|
ContextMenu.reset(ReactApp.render)
|
||||||
TopicCard.hideCard()
|
TopicCard.hideCard()
|
||||||
SynapseCard.hideCard()
|
SynapseCard.hideCard()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
centerOn: function(nodeid, callback) {
|
centerOn: function(nodeid, callback) {
|
||||||
// don't clash with fetchRelatives
|
// don't clash with fetchSiblings
|
||||||
if (!Visualize.mGraph.busy) {
|
if (!Visualize.mGraph.busy) {
|
||||||
Visualize.mGraph.onClick(nodeid, {
|
Visualize.mGraph.onClick(nodeid, {
|
||||||
hideLabels: false,
|
hideLabels: false,
|
||||||
|
@ -100,10 +101,10 @@ const Topic = {
|
||||||
}
|
}
|
||||||
ReactApp.render()
|
ReactApp.render()
|
||||||
},
|
},
|
||||||
fetchRelatives: function(nodes, metacodeId) {
|
fetchSiblings: function(nodes, metacodeId) {
|
||||||
var self = this
|
var self = this
|
||||||
|
|
||||||
var node = $.isArray(nodes) ? nodes[0] : nodes
|
var node = Array.isArray(nodes) ? nodes[0] : nodes
|
||||||
|
|
||||||
var topics = DataModel.Topics.map(function(t) { return t.id })
|
var topics = DataModel.Topics.map(function(t) { return t.id })
|
||||||
var topicsString = topics.join()
|
var topicsString = topics.join()
|
||||||
|
@ -155,8 +156,8 @@ const Topic = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if ($.isArray(nodes) && nodes.length > 1) {
|
if (Array.isArray(nodes) && nodes.length > 1) {
|
||||||
self.fetchRelatives(nodes.slice(1), metacodeId)
|
self.fetchSiblings(nodes.slice(1), metacodeId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
108
frontend/src/Metamaps/Views/ContextMenu.js
Normal file
108
frontend/src/Metamaps/Views/ContextMenu.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/* global $ */
|
||||||
|
import Control from '../Control'
|
||||||
|
import DataModel from '../DataModel'
|
||||||
|
import Selected from '../Selected'
|
||||||
|
import Topic from '../Topic'
|
||||||
|
|
||||||
|
const ContextMenu = {
|
||||||
|
clickedNode: null,
|
||||||
|
clickedEdge: null,
|
||||||
|
pos: {x: 0, y: 0},
|
||||||
|
fetchingSiblingsData: false,
|
||||||
|
siblingsData: null,
|
||||||
|
selectNode: (render, node, pos) => {
|
||||||
|
ContextMenu.pos = pos
|
||||||
|
ContextMenu.clickedNode = node
|
||||||
|
ContextMenu.clickedEdge = null
|
||||||
|
ContextMenu.fetchingSiblingsData = false
|
||||||
|
ContextMenu.siblingsData = null
|
||||||
|
render()
|
||||||
|
},
|
||||||
|
selectEdge: (render, edge, pos) => {
|
||||||
|
ContextMenu.pos = pos
|
||||||
|
ContextMenu.clickedNode = null
|
||||||
|
ContextMenu.clickedEdge = edge
|
||||||
|
ContextMenu.fetchingSiblingsData = false
|
||||||
|
ContextMenu.siblingsData = null
|
||||||
|
render()
|
||||||
|
},
|
||||||
|
reset: (render) => {
|
||||||
|
ContextMenu.fetchingSiblingsData = false
|
||||||
|
ContextMenu.siblingsData = null
|
||||||
|
ContextMenu.clickedNode = null
|
||||||
|
ContextMenu.clickedEdge = null
|
||||||
|
render()
|
||||||
|
},
|
||||||
|
delete: (render) => {
|
||||||
|
Control.deleteSelected()
|
||||||
|
ContextMenu.reset(render)
|
||||||
|
},
|
||||||
|
remove: (render) => {
|
||||||
|
Control.removeSelectedEdges()
|
||||||
|
Control.removeSelectedNodes()
|
||||||
|
ContextMenu.reset(render)
|
||||||
|
},
|
||||||
|
hide: (render) => {
|
||||||
|
Control.hideSelectedEdges()
|
||||||
|
Control.hideSelectedNodes()
|
||||||
|
ContextMenu.reset(render)
|
||||||
|
},
|
||||||
|
centerOn: (render, id) => {
|
||||||
|
Topic.centerOn(id)
|
||||||
|
ContextMenu.reset(render)
|
||||||
|
},
|
||||||
|
popoutTopic: (render, id) => {
|
||||||
|
ContextMenu.reset(render)
|
||||||
|
const win = window.open(`/topics/${id}`, '_blank')
|
||||||
|
win.focus()
|
||||||
|
},
|
||||||
|
updatePermissions: (render, permission) => {
|
||||||
|
// will be 'commons' 'public' or 'private'
|
||||||
|
Control.updateSelectedPermissions(permission)
|
||||||
|
ContextMenu.reset(render)
|
||||||
|
},
|
||||||
|
onMetacodeSelect: (render, id, metacodeId) => {
|
||||||
|
if (Selected.Nodes.length > 1) {
|
||||||
|
// batch update multiple topics
|
||||||
|
Control.updateSelectedMetacodes(metacodeId)
|
||||||
|
} else {
|
||||||
|
const topic = DataModel.Topics.get(id)
|
||||||
|
topic.save({
|
||||||
|
metacode_id: metacodeId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ContextMenu.reset(render)
|
||||||
|
},
|
||||||
|
fetchSiblings: (render, node, metacodeId) => {
|
||||||
|
Topic.fetchSiblings(node, metacodeId)
|
||||||
|
ContextMenu.reset(render)
|
||||||
|
},
|
||||||
|
populateSiblings: (render, id) => {
|
||||||
|
// depending on how many topics are selected, do different things
|
||||||
|
ContextMenu.fetchingSiblingsData = true
|
||||||
|
render()
|
||||||
|
|
||||||
|
const topics = DataModel.Topics.map(function(t) { return t.id })
|
||||||
|
const topicsString = topics.join()
|
||||||
|
|
||||||
|
const successCallback = function(data) {
|
||||||
|
ContextMenu.fetchingSiblingsData = false
|
||||||
|
|
||||||
|
// adjust the data for consumption by react
|
||||||
|
for (var key in data) {
|
||||||
|
data[key] = `${DataModel.Metacodes.get(key).get('name')} (${data[key]})`
|
||||||
|
}
|
||||||
|
ContextMenu.siblingsData = data
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
url: `/topics/${id}/relative_numbers.json?network=${topicsString}`,
|
||||||
|
success: successCallback,
|
||||||
|
error: function() {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextMenu
|
|
@ -1,5 +1,6 @@
|
||||||
/* global $ */
|
/* global $ */
|
||||||
|
|
||||||
|
import ContextMenu from './ContextMenu'
|
||||||
import ExploreMaps from './ExploreMaps'
|
import ExploreMaps from './ExploreMaps'
|
||||||
import ChatView from './ChatView'
|
import ChatView from './ChatView'
|
||||||
import VideoView from './VideoView'
|
import VideoView from './VideoView'
|
||||||
|
@ -12,6 +13,7 @@ const Views = {
|
||||||
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
|
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
|
||||||
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
|
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
|
||||||
},
|
},
|
||||||
|
ContextMenu,
|
||||||
ExploreMaps,
|
ExploreMaps,
|
||||||
ChatView,
|
ChatView,
|
||||||
VideoView,
|
VideoView,
|
||||||
|
@ -19,5 +21,5 @@ const Views = {
|
||||||
TopicCard
|
TopicCard
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ExploreMaps, ChatView, VideoView, Room, TopicCard }
|
export { ContextMenu, ExploreMaps, ChatView, VideoView, Room, TopicCard }
|
||||||
export default Views
|
export default Views
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import ContextMenu from '../common/ContextMenu'
|
||||||
import DataVis from '../common/DataVis'
|
import DataVis from '../common/DataVis'
|
||||||
import UpperOptions from '../common/UpperOptions'
|
import UpperOptions from '../common/UpperOptions'
|
||||||
import InfoAndHelp from '../common/InfoAndHelp'
|
import InfoAndHelp from '../common/InfoAndHelp'
|
||||||
|
@ -12,6 +13,7 @@ import TopicCard from '../TopicCard'
|
||||||
export default class MapView extends Component {
|
export default class MapView extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
contextMenu: PropTypes.bool,
|
||||||
mobile: PropTypes.bool,
|
mobile: PropTypes.bool,
|
||||||
mapId: PropTypes.string,
|
mapId: PropTypes.string,
|
||||||
map: PropTypes.object,
|
map: PropTypes.object,
|
||||||
|
@ -79,7 +81,8 @@ export default class MapView extends Component {
|
||||||
filterAllMappers, filterAllSynapses, filterData,
|
filterAllMappers, filterAllSynapses, filterData,
|
||||||
openImportLightbox, forkMap, openHelpLightbox,
|
openImportLightbox, forkMap, openHelpLightbox,
|
||||||
mapIsStarred, onMapStar, onMapUnstar, openTopic,
|
mapIsStarred, onMapStar, onMapUnstar, openTopic,
|
||||||
onZoomExtents, onZoomIn, onZoomOut, hasLearnedTopicCreation } = this.props
|
onZoomExtents, onZoomIn, onZoomOut, hasLearnedTopicCreation,
|
||||||
|
contextMenu } = this.props
|
||||||
const { chatOpen } = this.state
|
const { chatOpen } = this.state
|
||||||
const onChatOpen = () => {
|
const onChatOpen = () => {
|
||||||
this.setState({chatOpen: true})
|
this.setState({chatOpen: true})
|
||||||
|
@ -109,6 +112,7 @@ export default class MapView extends Component {
|
||||||
filterAllSynapses={filterAllSynapses} />
|
filterAllSynapses={filterAllSynapses} />
|
||||||
<DataVis />
|
<DataVis />
|
||||||
{openTopic && <TopicCard {...this.props} />}
|
{openTopic && <TopicCard {...this.props} />}
|
||||||
|
{contextMenu && <ContextMenu {...this.props} />}
|
||||||
{currentUser && <Instructions mobile={mobile} hasLearnedTopicCreation={hasLearnedTopicCreation} />}
|
{currentUser && <Instructions mobile={mobile} hasLearnedTopicCreation={hasLearnedTopicCreation} />}
|
||||||
{currentUser && <MapChat {...this.props} onOpen={onChatOpen} onClose={onChatClose} chatOpen={chatOpen} ref={x => this.mapChat = x} />}
|
{currentUser && <MapChat {...this.props} onOpen={onChatOpen} onClose={onChatClose} chatOpen={chatOpen} ref={x => this.mapChat = x} />}
|
||||||
<VisualizationControls map={map}
|
<VisualizationControls map={map}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import ContextMenu from '../common/ContextMenu'
|
||||||
import DataVis from '../common/DataVis'
|
import DataVis from '../common/DataVis'
|
||||||
import UpperOptions from '../common/UpperOptions'
|
import UpperOptions from '../common/UpperOptions'
|
||||||
import InfoAndHelp from '../common/InfoAndHelp'
|
import InfoAndHelp from '../common/InfoAndHelp'
|
||||||
|
@ -10,6 +11,7 @@ import TopicCard from '../TopicCard'
|
||||||
export default class TopicView extends Component {
|
export default class TopicView extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
contextMenu: PropTypes.bool,
|
||||||
mobile: PropTypes.bool,
|
mobile: PropTypes.bool,
|
||||||
topicId: PropTypes.string,
|
topicId: PropTypes.string,
|
||||||
topic: PropTypes.object,
|
topic: PropTypes.object,
|
||||||
|
@ -55,7 +57,7 @@ export default class TopicView extends Component {
|
||||||
const { mobile, topic, currentUser, allForFiltering, visibleForFiltering,
|
const { mobile, topic, currentUser, allForFiltering, visibleForFiltering,
|
||||||
toggleMetacode, toggleMapper, toggleSynapse, filterAllMetacodes,
|
toggleMetacode, toggleMapper, toggleSynapse, filterAllMetacodes,
|
||||||
filterAllMappers, filterAllSynapses, filterData, forkMap,
|
filterAllMappers, filterAllSynapses, filterData, forkMap,
|
||||||
openHelpLightbox, onZoomIn, onZoomOut } = this.props
|
openHelpLightbox, onZoomIn, onZoomOut, contextMenu } = this.props
|
||||||
// TODO: stop using {...this.props} and make explicit
|
// TODO: stop using {...this.props} and make explicit
|
||||||
return <div className="topicWrapper">
|
return <div className="topicWrapper">
|
||||||
<UpperOptions ref={x => this.upperOptions = x}
|
<UpperOptions ref={x => this.upperOptions = x}
|
||||||
|
@ -73,6 +75,7 @@ export default class TopicView extends Component {
|
||||||
filterAllSynapses={filterAllSynapses} />
|
filterAllSynapses={filterAllSynapses} />
|
||||||
<DataVis />
|
<DataVis />
|
||||||
<TopicCard {...this.props} />
|
<TopicCard {...this.props} />
|
||||||
|
{contextMenu && <ContextMenu {...this.props} />}
|
||||||
<VisualizationControls onClickZoomIn={onZoomIn}
|
<VisualizationControls onClickZoomIn={onZoomIn}
|
||||||
onClickZoomOut={onZoomOut} />
|
onClickZoomOut={onZoomOut} />
|
||||||
<InfoAndHelp topic={topic}
|
<InfoAndHelp topic={topic}
|
||||||
|
|
248
frontend/src/components/common/ContextMenu.js
Normal file
248
frontend/src/components/common/ContextMenu.js
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import MetacodeSelect from '../MetacodeSelect'
|
||||||
|
|
||||||
|
class ContextMenu extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
topicId: PropTypes.string,
|
||||||
|
mapId: PropTypes.string,
|
||||||
|
currentUser: PropTypes.object,
|
||||||
|
map: PropTypes.object,
|
||||||
|
contextNode: PropTypes.object,
|
||||||
|
contextEdge: PropTypes.object,
|
||||||
|
contextPos: PropTypes.object,
|
||||||
|
contextFetchingSiblingsData: PropTypes.bool,
|
||||||
|
contextSiblingsData: PropTypes.object,
|
||||||
|
metacodeSets: PropTypes.array,
|
||||||
|
contextDelete: PropTypes.func,
|
||||||
|
contextRemove: PropTypes.func,
|
||||||
|
contextHide: PropTypes.func,
|
||||||
|
contextCenterOn: PropTypes.func,
|
||||||
|
contextPopoutTopic: PropTypes.func,
|
||||||
|
contextUpdatePermissions: PropTypes.func,
|
||||||
|
contextOnMetacodeSelect: PropTypes.func,
|
||||||
|
contextFetchSiblings: PropTypes.func,
|
||||||
|
contextPopulateSiblings: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
populateSiblingsSent: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPositionData = () => {
|
||||||
|
const { contextPos } = this.props
|
||||||
|
let extraClasses = []
|
||||||
|
const position = {}
|
||||||
|
// TODO: make these dynamic values so that the ContextMenu can
|
||||||
|
// change height and still work properly
|
||||||
|
const RIGHTCLICK_WIDTH = 300
|
||||||
|
const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static
|
||||||
|
const SUBMENUS_WIDTH = 256
|
||||||
|
const MAX_SUBMENU_HEIGHT = 270
|
||||||
|
const windowWidth = document.documentElement.clientWidth
|
||||||
|
const windowHeight = document.documentElement.clientHeight
|
||||||
|
|
||||||
|
if (windowWidth - contextPos.x < SUBMENUS_WIDTH) {
|
||||||
|
position.right = windowWidth - contextPos.x
|
||||||
|
extraClasses.push('moveMenusToLeft')
|
||||||
|
} else if (windowWidth - contextPos.x < RIGHTCLICK_WIDTH) {
|
||||||
|
position.right = windowWidth - contextPos.x
|
||||||
|
} else if (windowWidth - contextPos.x < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) {
|
||||||
|
position.left = contextPos.x
|
||||||
|
extraClasses.push('moveMenusToLeft')
|
||||||
|
} else {
|
||||||
|
position.left = contextPos.x
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowHeight - contextPos.y < MAX_SUBMENU_HEIGHT) {
|
||||||
|
position.bottom = windowHeight - contextPos.y
|
||||||
|
extraClasses.push('moveMenusUp')
|
||||||
|
} else if (windowHeight - contextPos.y < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) {
|
||||||
|
position.top = contextPos.y
|
||||||
|
extraClasses.push('moveMenusUp')
|
||||||
|
} else {
|
||||||
|
position.top = contextPos.y
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pos: {
|
||||||
|
top: position.top && position.top + 'px',
|
||||||
|
bottom: position.bottom && position.bottom + 'px',
|
||||||
|
left: position.left && position.left + 'px',
|
||||||
|
right: position.right && position.right + 'px'
|
||||||
|
},
|
||||||
|
extraClasses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hide = () => {
|
||||||
|
const { contextHide } = this.props
|
||||||
|
return <li className='rc-hide' onClick={contextHide}>
|
||||||
|
<div className='rc-icon' />
|
||||||
|
Hide until refresh
|
||||||
|
<div className='rc-keyboard'>Ctrl+H</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
remove = () => {
|
||||||
|
const { contextRemove, map, currentUser } = this.props
|
||||||
|
const canEditMap = map && map.authorizeToEdit(currentUser)
|
||||||
|
if (!canEditMap) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <li className='rc-remove' onClick={contextRemove}>
|
||||||
|
<div className='rc-icon' />
|
||||||
|
Remove from map
|
||||||
|
<div className='rc-keyboard'>Ctrl+M</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = () => {
|
||||||
|
const { contextDelete, map, currentUser } = this.props
|
||||||
|
const canEditMap = map && map.authorizeToEdit(currentUser)
|
||||||
|
if (!canEditMap) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <li className='rc-delete' onClick={contextDelete}>
|
||||||
|
<div className='rc-icon' />
|
||||||
|
Delete
|
||||||
|
<div className='rc-keyboard'>Ctrl+D</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
center = () => {
|
||||||
|
const { contextCenterOn, contextNode, topicId } = this.props
|
||||||
|
if (!(contextNode && topicId)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <li className='rc-center'
|
||||||
|
onClick={() => contextCenterOn(contextNode.id)}>
|
||||||
|
<div className='rc-icon' />
|
||||||
|
Center this topic
|
||||||
|
<div className='rc-keyboard'>Alt+E</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
popout = () => {
|
||||||
|
const { contextPopoutTopic, contextNode } = this.props
|
||||||
|
if (!contextNode) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <li className='rc-popout'
|
||||||
|
onClick={() => contextPopoutTopic(contextNode.id)}>
|
||||||
|
<div className='rc-icon' />
|
||||||
|
Open in new tab
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
permission = () => {
|
||||||
|
const { currentUser, contextUpdatePermissions } = this.props
|
||||||
|
if (!currentUser) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <li className='rc-permission'>
|
||||||
|
<div className='rc-icon' />
|
||||||
|
Change permissions
|
||||||
|
<ul>
|
||||||
|
<li className='changeP toCommons'
|
||||||
|
onClick={() => contextUpdatePermissions('commons')}>
|
||||||
|
<div className='rc-perm-icon' />
|
||||||
|
commons
|
||||||
|
</li>
|
||||||
|
<li className='changeP toPublic'
|
||||||
|
onClick={() => contextUpdatePermissions('public')}>
|
||||||
|
<div className='rc-perm-icon' />
|
||||||
|
public
|
||||||
|
</li>
|
||||||
|
<li className='changeP toPrivate'
|
||||||
|
onClick={() => contextUpdatePermissions('private')}>
|
||||||
|
<div className='rc-perm-icon' />
|
||||||
|
private
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className='expandLi' />
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
metacode = () => {
|
||||||
|
const { metacodeSets, contextOnMetacodeSelect,
|
||||||
|
currentUser, contextNode } = this.props
|
||||||
|
if (!currentUser) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <li className='rc-metacode'>
|
||||||
|
<div className='rc-icon' />
|
||||||
|
Change metacode
|
||||||
|
<div id='metacodeOptionsWrapper'>
|
||||||
|
<MetacodeSelect
|
||||||
|
onMetacodeSelect={id => {
|
||||||
|
contextOnMetacodeSelect(contextNode && contextNode.id, id)
|
||||||
|
}}
|
||||||
|
metacodeSets={metacodeSets} />
|
||||||
|
</div>
|
||||||
|
<div className='expandLi' />
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
siblings = () => {
|
||||||
|
const { contextPopulateSiblings, contextFetchSiblings,
|
||||||
|
contextSiblingsData, contextFetchingSiblingsData,
|
||||||
|
topicId, contextNode } = this.props
|
||||||
|
const populateSiblings = () => {
|
||||||
|
if (!this.state.populateSiblingsSent) {
|
||||||
|
contextPopulateSiblings(contextNode.id)
|
||||||
|
this.setState({populateSiblingsSent: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(contextNode && topicId)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <li className='rc-siblings'
|
||||||
|
onMouseOver={populateSiblings}>
|
||||||
|
<div className='rc-icon' />
|
||||||
|
Reveal siblings
|
||||||
|
<ul id='fetchSiblingList'>
|
||||||
|
<li className='fetchAll'
|
||||||
|
onClick={() => contextFetchSiblings(contextNode)}>
|
||||||
|
All
|
||||||
|
<div className='rc-keyboard'>Alt+R</div>
|
||||||
|
</li>
|
||||||
|
{contextSiblingsData && Object.keys(contextSiblingsData).map(key => {
|
||||||
|
return <li key={key}
|
||||||
|
onClick={() => contextFetchSiblings(contextNode, key)}>
|
||||||
|
{contextSiblingsData[key]}
|
||||||
|
</li>
|
||||||
|
})}
|
||||||
|
{contextFetchingSiblingsData && <li id='loadingSiblings'>loading...</li>}
|
||||||
|
</ul>
|
||||||
|
<div className='expandLi' />
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { contextNode, currentUser, topicId } = this.props
|
||||||
|
const positionData = this.getPositionData()
|
||||||
|
const style = Object.assign({}, {position: 'absolute'}, positionData.pos)
|
||||||
|
const showSpacer = currentUser || (contextNode && topicId)
|
||||||
|
|
||||||
|
return <div style={style}
|
||||||
|
className={'rightclickmenu ' + positionData.extraClasses.join(' ')}>
|
||||||
|
<ul>
|
||||||
|
{this.hide()}
|
||||||
|
{this.remove()}
|
||||||
|
{this.delete()}
|
||||||
|
{this.center()}
|
||||||
|
{this.popout()}
|
||||||
|
{showSpacer && <li className='rc-spacer' />}
|
||||||
|
{this.permission()}
|
||||||
|
{this.metacode()}
|
||||||
|
{this.siblings()}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextMenu
|
|
@ -21,6 +21,7 @@
|
||||||
"homepage": "https://github.com/metamaps/metamaps#readme",
|
"homepage": "https://github.com/metamaps/metamaps#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajaxq": "0.0.7",
|
"ajaxq": "0.0.7",
|
||||||
|
"async": "2.5.0",
|
||||||
"attachmediastream": "2.0.0",
|
"attachmediastream": "2.0.0",
|
||||||
"autolinker": "1.4.3",
|
"autolinker": "1.4.3",
|
||||||
"babel-cli": "6.26.0",
|
"babel-cli": "6.26.0",
|
||||||
|
|
Loading…
Add table
Reference in a new issue