react-router and rebuild app structure in react (#1091)

* initial restructuring

* stuff

* lock version number

* just keep using current mapinfobox

* fix map upperRightUI layout

* make mapsWidth work and add mobile

* remove filterBoxOpen for now

* redo the mobile menu in react

* get account menu and invite lightbox working

* fixed maps scrolling

* make other routes work

* fix signed out home page

* fix accountbox toggling

* add metacode edit routes

* lots of fixes

* fix map chat layout and tab bug

* improve topic card readability and fix dragging bug

* fixup mapchat stuff

* fix up navigation to use react-router

* jquery no longer handling access requests

* handle case where user hasn't loaded yet

* this shouldn't have been removed

* add frame for topic view

* rewrite map instructions

* fix toast (and sign out bug)

* fix apps pages and missing routes

* made our request invite page look nice

* filter box in react

* forgot to add one proptype

* remove extra comments

* handle page title and mobile title updates

* reenable google analytics

* make filterbox use onclickoutside

* reenable topic view in react

* fix csrf auth token

* fix little homepage styling issue

* try putting preparevizdata in a timeout

* installing render log to count

* little fixes

* fixup filters

* make filter map function names more readable

* eslint helps

* renaming for clarity

* use onclickoutside for account/sign in box

* add some logging to see whether this is source of many renders

* turns out chatview was heavily hogging memory

* tiimeout not needed
This commit is contained in:
Connor Turland 2017-03-16 17:58:56 -04:00 committed by GitHub
parent 33276444c7
commit 47a74dd77b
80 changed files with 1934 additions and 1911 deletions

View file

@ -193,10 +193,6 @@ button.button.btn-no:hover {
display: block;
width: 830px;
}
.requestInvite {
display: block;
margin: -720px auto 0;
}
.new_session,
.new_user,
.edit_user,
@ -672,9 +668,21 @@ label {
position: relative;
/*overflow:hidden; */
}
.main.compressed {
width: calc(100% - 300px);
.compressed {
.upperRightUI {
right: 324px;
}
.upperRightMapButtons {
right: 434px;
}
.mapControls {
right: 324px;
}
.infoAndHelp {
right: 370px;
}
}
#infovis-canvas {
-webkit-touch-callout: none;
-webkit-user-select: none;
@ -775,9 +783,9 @@ label {
}
.sidebarAccountIcon img {
border-radius: 16px;
width: 32px;
}
.sidebarAccountBox {
display: none;
height: auto;
}
.authenticated .sidebarAccountBox {
@ -1039,7 +1047,6 @@ label[for="user_remember_me"] {
}
.sidebarFilterBox {
display:none;
width: 319px;
padding: 16px 0;
overflow-y: auto;
@ -3058,14 +3065,16 @@ and it won't be important on password protected instances */
/* request */
#wrapper .requestInvite {
.requestInvite {
width: 700px;
margin: 0 auto;
padding: 0 0 60px 0;
background: #FFFFFF;
color: white;
height: 100%;
overflow: hidden;
height: calc(100% - 52px);
z-index: 1;
position: relative;
left: 50%;
margin-left: -350px;
margin-top: 52px;
}
.home_bg {
@ -3148,4 +3157,4 @@ script.data-gratipay-username {
background: #FFF;
cursor: pointer;
font-family: din-regular;
}
}

View file

@ -54,7 +54,6 @@
width:100%;
height:100%;
position: absolute;
display: none;
}
.showcard .permission {
@ -233,7 +232,7 @@
background-repeat:no-repeat;
}
}
.contributor {
bottom: 7px;
margin-left: 40px;

View file

@ -30,6 +30,7 @@
height: 100%;
box-sizing: border-box;
padding-top: 92px;
overflow-y: auto;
}
/*.animations {
@ -46,26 +47,9 @@
transition-timing-function: ease-in-out;
}*/
.mapElement {
display: none;
}
.mapPage .mapElement,
.topicPage .mapElement {
display: block;
}
.mapPage .mapElementHidden,
.topicPage .mapElement.mapInfoBox,
.topicPage .mapElement.importDialog {
display:none;
}
.topicPage .starMap {
display: none;
}
/* loading */
#loading {
display: none;
width: 28px;
height: 28px;
position: fixed;
@ -184,10 +168,10 @@
}
.upperRightMapButtons {
top: -42px; /* puts it just offscreen */
right: 134px;
}
.mapPage .upperRightMapButtons, .topicPage .upperRightMapButtons {
top: 0;
.unauthenticated .upperRightMapButtons {
right: 115px;
}
.upperRightIcon {
@ -197,13 +181,7 @@
background-repeat: no-repeat;
cursor: pointer;
}
.mapPage .mapElement .importDialog {
display: none;
background-position: 0 0;
}
.mapPage.canEditMap .mapElement .importDialog {
display: block;
}
.sidebarFilterIcon {
background-position: -32px 0;
}
@ -236,6 +214,14 @@
/* end upperRightUI */
/* map wrapper */
.mapWrapper {
position:absolute;
width: 100%;
height: 100%;
}
/* end map wrapper */
/* yield */
@ -356,22 +342,15 @@
/* infoAndHelp */
.mapPage .infoAndHelp, .topicPage .infoAndHelp {
right: 70px;
}
.mapPage .openCheatsheet .tooltipsAbove, .topicPage .openCheatsheet .tooltipsAbove {
.openCheatsheet .tooltipsAbove {
right: 1px;
left: auto;
}
.unauthenticated .homePage .infoAndHelp {
display:none;
}
.infoAndHelp {
position: absolute;
bottom: 20px;
right: 20px;
right: 70px;
z-index: 3;
width: auto;
font-style: italic;
@ -392,16 +371,12 @@
}
.mapInfoIcon {
position: relative;
top: 56px; /* puts it just offscreen */
background-image: url(<%= asset_path('mapinfo_sprite.png') %>);
background-repeat:no-repeat;
background-image: url(<%= asset_path('mapinfo_sprite.png') %>);
background-repeat:no-repeat;
}
.mapInfoIcon:hover {
background-position: 0 -32px;
}
.mapPage .mapInfoIcon {
top: 0;
}
.starMap {
background-image: url(<%= asset_path('starmap_sprite.png') %>);
@ -419,9 +394,6 @@
background-position: 0 0;
}
.unauthenticated .mapPage .starMap {
display: none;
}
/* end infoAndHelp */
@ -430,24 +402,17 @@
.mapControls {
position: absolute;
bottom: 24px;
right:-32px; /* puts it just offscreen */
right:24px;
width:32px;
z-index: 3;
}
.mapPage .mapControls, .topicPage .mapControls {
right: 24px;
}
.topicPage .zoomExtents {
display: none;
}
.mapControl {
width:32px;
height:32px;
background-color: #424242;
background-repeat: no-repeat;
background-position: 0 0;
background-repeat: no-repeat;
background-position: 0 0;
cursor:pointer;
}
@ -587,10 +552,6 @@
left: -8px;
}
.openCheatsheet .tooltipsAbove {
left: -4px;
}
.sidebarAccountIcon .tooltipsUnder {
margin-left: -12px;
margin-top: 40px;
@ -671,8 +632,11 @@
/* explore maps */
#explore {
display: none;
#react-app {
position: absolute;
height: 100%;
width: 100%;
overflow-y: auto;
}
#exploreMaps {
@ -691,8 +655,13 @@
display: block;
}
.appsPage #exploreMapsHeader {
display: block;
.requestInviteHeader {
position: absolute;
width: 100%;
z-index:2;
background-color:#FAFAFA;
height: 52px;
box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16);
}
#exploreMapsHeader {
@ -829,7 +798,6 @@
height: 80px;
font-family: 'din-regular', helvetica, sans-serif;
font-size: 32px;
display: none;
text-align: center;
color: #999999;
z-index: 0;
@ -845,7 +813,6 @@
/* toast */
.toast {
display: none;
position: fixed;
bottom: 20px;
left: 20px;

View file

@ -1,7 +1,3 @@
#mobile_header {
display: none;
}
@media only screen and (max-width : 752px) and (min-width : 504px) {
.sidebarSearch .tt-hint, .sidebarSearch .sidebarSearchField {
width: 160px !important;
@ -51,10 +47,6 @@
display: none;
}
#mobile_header {
display: block;
}
.homeWrapper {
width: 96%;
padding: 0 2%;
@ -72,7 +64,7 @@
height: auto;
}
.homeVideo {
width: 100%;
width: 100% !important;
height: auto;
}
.fullWidthWrapper.withPartners {
@ -108,15 +100,23 @@
max-width: 360px;
}
#wrapper .requestInvite {
.requestInviteHeader {
display: none;
}
.requestInvite {
width: 100%;
padding: 0;
height: calc(100% - 50px);
z-index: 1;
position: relative;
left: 0;
margin-left: 0px;
margin-top: 50px;
}
#exploreMaps > div {
margin-top: 70px;
}
.mapper {
width: 100%;
margin: 0 0 30px 0;
@ -217,6 +217,7 @@
width: 100%;
box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16);
position: fixed;
z-index: 1;
}
#menu_icon {
@ -249,7 +250,6 @@
}
#mobile_menu {
display: none;
background: #EEE;
position: fixed;
top: 50px;
@ -257,6 +257,7 @@
padding: 10px;
width: 200px;
box-shadow: 3px 3px 3px rgba(0,0,0,0.23), 3px 3px 3px rgba(0,0,0,0.16);
z-index: 2;
li {
padding: 10px;
@ -274,16 +275,6 @@
}
}
/*
* the mobile menu, even if it's been opened by a user, should
* not show up if they resize their browser back to full size
*/
@media only screen and (max-width : 504px) {
#mobile_menu.visible {
display: block;
}
}
li.mobileMenuUser {
border-bottom: 1px solid #BBB;
}

View file

@ -1,7 +1,6 @@
.viewOnly {
float: left;
margin-left: 16px;
display: none;
height: 32px;
border: 1px solid #BDBDBD;
border-radius: 2px;
@ -23,7 +22,7 @@
}
.requestNotice {
display: none;
display: inline-block;
padding: 0 8px;
}
@ -42,16 +41,6 @@
.requestNotAccepted {
background-color: #c04f4f;
}
&.sendRequest .requestAccess {
display: inline-block;
}
&.sentRequest .requestPending {
display: inline-block;
}
&.requestDenied .requestNotAccepted {
display: inline-block;
}
}
.request_access {

View file

@ -7,7 +7,7 @@ class Message < ApplicationRecord
after_create :after_created
#after_create :after_created_async
def user_image
user.image.url
@ -21,7 +21,7 @@ class Message < ApplicationRecord
def after_created
ActionCable.server.broadcast 'map_' + resource.id.to_s, type: 'messageCreated', message: as_json
end
def after_created_async
FollowService.follow(resource, user, 'commented')
NotificationService.notify_followers(resource, 'map_message', self)

View file

@ -5,7 +5,7 @@
# %>
<script>
<% content_for :title, "Explore My Maps | Metamaps" %>
<% content_for :title, "My Maps | Metamaps" %>
<% content_for :mobile_title, "My Maps" %>
Metamaps.currentPage = "mine";

View file

@ -5,7 +5,7 @@
# %>
<script>
<% content_for :title, "Explore Shared Maps | Metamaps" %>
<% content_for :title, "Shared Maps | Metamaps" %>
<% content_for :mobile_title, "Shared With Me" %>
Metamaps.currentPage = "shared";

View file

@ -1,60 +0,0 @@
<%#
# @file
# The inner HTML of the account box that comes up in the bottom left
#%>
<% if current_user %>
<% account = current_user %>
<%= image_tag account.image.url(:sixtyfour), :size => "48x48", :class => "sidebarAccountImage" %>
<h3 class="accountHeader"><%= account.name.split[0...1][0] %></h3>
<ul>
<li class="accountListItem accountSettings">
<div class="accountIcon"></div>
<%= link_to "Settings", edit_user_url(account) %>
</li>
<% if account.admin %>
<li class="accountListItem accountAdmin">
<div class="accountIcon"></div>
<%= link_to "Admin", metacodes_path %>
</li>
<% end %>
<li class="accountListItem accountApps">
<div class="accountIcon"></div>
<%= link_to "Apps", oauth_authorized_applications_path %>
</li>
<li class="accountListItem accountInvite openLightbox" data-open="invite">
<div class="accountIcon"></div>
<span>Share Invite</span>
</li>
<li class="accountListItem accountLogout">
<div class="accountIcon"></div>
<%= link_to "Sign Out", "/logout", id: "Logout" %>
</li>
</ul>
<% else %>
<%= form_for(resource, :as => resource_name, :url => session_path(resource_name), :html => { class: "loginAnywhere" }) do |f| %>
<div class="accountImage"></div>
<div class="accountInput accountEmail">
<%= f.email_field :email, :placeholder => "Email" %>
</div>
<div class="accountInput accountPassword">
<%= f.password_field :password, :placeholder => "Password" %>
</div>
<div class="accountSubmit"><%= f.submit "SIGN IN" %></div>
<% if devise_mapping.rememberable? -%>
<div class="accountRememberMe">
<%= f.label :remember_me, "Stay signed in" %>
<%= f.check_box :remember_me %>
<div class="clearfloat"></div>
</div>
<% end -%>
<div class="clearfloat"></div>
<div class="accountForgotPass">
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
<%= link_to "Forgot password?", new_password_path(resource_name) %>
<% end -%>
</div>
<% end %>
<% end %>
<% # Rails.logger.info(stored_location_for(:user)) %>

View file

@ -1,15 +1,21 @@
<div id="loading"></div>
<script type="text/javascript">
Metamaps.ServerData.unreadNotificationsCount = <%= current_user ? user_unread_notification_count : 0 %>
Metamaps.ServerData.mapIsStarred = <%= current_user && @map && current_user.starred_map?(@map) ? true : false %>
Metamaps.ServerData.mobileTitle = "<%= yield(:mobile_title) %>"
Metamaps.ServerData.ActiveMapper = <%= current_user ? current_user.to_json({follows: true, email: true, follow_settings: true}).html_safe : nil %>
<% if devise_error_messages? %>
Metamaps.ServerData.toast = "<%= devise_error_messages! %>"
<% elsif notice %>
Metamaps.ServerData.toast = "<%= notice %>"
<% elsif alert %>
Metamaps.ServerData.toast = "<%= alert %>"
<% end %>
Metamaps.Loading.setup()
</script>
<%= render :partial => 'layouts/lightboxes' %>
<%= render :partial => 'layouts/templates' %>
<%= render :partial => 'shared/metacodeBgColors' %>
<script type="text/javascript" charset="utf-8">
<% if current_user %>
Metamaps.ServerData.ActiveMapper = <%= current_user.to_json({follows: true, email: true, follow_settings: true}).html_safe %>
<% else %>
Metamaps.ServerData.ActiveMapper = null
<% end %>
Metamaps.Loading.setup()
</script>
<%= render :partial => 'layouts/googleanalytics' if ENV["GA_TRACKING_CODE"].present? %>
</body>
</html>

View file

@ -10,6 +10,5 @@
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '<%= ENV["GA_TRACKING_CODE"] %>', 'auto');
ga('send', 'pageview');
</script>

View file

@ -1,17 +0,0 @@
<div class="mapControls mapElement">
<div class="zoomExtents mapControl"><div class="tooltips">Center View</div></div>
<div class="zoomIn mapControl"><div class="tooltips">Zoom In</div></div>
<div class="zoomOut mapControl"><div class="tooltips">Zoom Out</div></div>
</div>
<div class="infoAndHelp">
<%= render :partial => 'maps/mapinfobox' %>
<% starred = current_user && @map && current_user.starred_map?(@map)
starClass = starred ? 'starred' : ''
tooltip = starred ? 'Star' : 'Unstar' %>
<div class="starMap infoElement mapElement <%= starClass %>"><div class="tooltipsAbove"><%= tooltip %></div></div>
<div class="mapInfoIcon infoElement mapElement"><div class="tooltipsAbove">Map Info</div></div>
<div class="openCheatsheet openLightbox infoElement mapElement" data-open="cheatsheet"><div class="tooltipsAbove">Help</div></div>
<div class="clearfloat"></div>
</div>

View file

@ -1,67 +0,0 @@
<div id="mobile_header">
<div id="header_content">
<%= yield(:mobile_title) %>
</div>
<div id="menu_icon">
<% if user_unread_notification_count > 0 %>
<div class="unread-notifications-dot"></div>
<% end %>
</div>
</div>
<div id="mobile_menu">
<ul>
<% if not current_user %>
<li>
<%= link_to "Home", root_path %>
</li>
<% end %>
<% if current_user %>
<li class="mobileMenuUser">
<%= image_tag current_user.image.url(:sixtyfour), :size => "32x32" %>
<span><%= current_user.name %></span>
</li>
<li>
<%= link_to "New Map", new_map_path %>
</li>
<li>
<%= link_to "My Maps", explore_mine_path, :data => { :router => 'true'} %>
</li>
<li>
<%= link_to "Shared With Me", explore_shared_path, :data => { :router => 'true'} %>
</li>
<li>
<%= link_to "Starred By Me", explore_starred_path, :data => { :router => 'true'} %>
</li>
<% end %>
<li>
<%= link_to "All Maps", explore_active_path, :data => { :router => 'true'} %>
</li>
<% if not current_user %>
<li>
<%= link_to "Featured Maps", explore_featured_path, :data => { :router => 'true'} %>
</li>
<% end %>
<% if not current_user %>
<li>
<%= link_to "Request Invite", request_path %>
</li>
<li>
<%= link_to "Login", sign_in_path %>
</li>
<% end %>
<% if current_user %>
<li>
<%= link_to "Account", edit_user_url(current_user) %>
</li>
<li class="notifications">
<%= link_to "Notifications", notifications_path %>
<% if user_unread_notification_count > 0 %>
<div class="unread-notifications-dot"></div>
<% end %>
</li>
<li>
<%= link_to "Sign Out", "/logout", id: "Logout" %>
</li>
<% end %>
</ul>
</div>

View file

@ -1,107 +0,0 @@
<!-- from left to right on the screen -->
<div class="upperLeftUI">
<!-- home button -->
<div class="homeButton">
<a href="<%= root_url %>" <% if current_user && !noHardHomeLink %><%= 'data-router=true' %><% end %>>METAMAPS</a>
</div> <!-- end homeButton -->
<!-- search box -->
<div class="sidebarSearch">
<input type="text" class="sidebarSearchField" placeholder="Search for topics, maps, and mappers..." />
<div id="searchLoading"></div>
<div class="sidebarSearchIcon"></div>
<div class="clearfloat"></div>
</div> <!-- end sidebarSearch -->
<% request = current_user && @map && @allrequests.find{|a| a.user == current_user}
className = (@map and not policy(@map).update?) ? 'isViewOnly ' : ''
if @map
className += 'sendRequest' if not request
className += 'sentRequest' if request and not request.answered
className += 'requestDenied' if request and request.answered and not request.approved
end %>
<div class="viewOnly <%= className %>">
<div class="eyeball">View Only</div>
<% if current_user %>
<div class="requestAccess requestNotice">Request Access</div>
<div class="requestPending requestNotice">Request Pending</div>
<div class="requestNotAccepted requestNotice">Request Not Accepted</div>
<% end %>
</div>
<div class="clearfloat"></div>
</div><!-- end upperLeftUI -->
<div class="upperRightUI">
<div class="mapElement upperRightEl upperRightMapButtons">
<% if current_user %>
<div class="importDialog upperRightEl upperRightIcon mapElement openLightbox" data-open="import-dialog-lightbox">
<div class="tooltipsUnder">
Import Data
</div>
</div>
<% end %>
<!-- filtering -->
<div class="sidebarFilter upperRightEl">
<div class="sidebarFilterIcon upperRightIcon"><div class="tooltipsUnder">Filter</div></div>
<div class="sidebarFilterBox upperRightBox">
<%= render :partial => 'shared/filterBox' %>
</div>
</div> <!-- end sidebarFilter -->
<% if current_user %>
<!-- fork map -->
<div class="sidebarFork upperRightEl">
<div class="sidebarForkIcon upperRightIcon"><div class="tooltipsUnder">Save To New Map</div></div>
</div> <!-- end sidebarFork -->
<% end %>
<div class="clearfloat"></div>
</div> <!-- end mapElement -->
<% if current_user %>
<!-- create new map -->
<a href="<%= new_map_path %>" target="_blank" class="addMap upperRightEl upperRightIcon">
<div class="tooltipsUnder">
Create New Map
</div>
</a><!-- end addMap -->
<% end %>
<script type="text/javascript">
Metamaps.ServerData.unreadNotificationsCount = <%= user_unread_notification_count %>
</script>
<% if current_user.present? %>
<span id="notification_icon">
<%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_unread_notification_count > 0 ? 'unread' : 'read'}" do %>
<div class="tooltipsUnder">
Notifications
</div>
<% if user_unread_notification_count > 0 %>
<div class="unread-notifications-dot"></div>
<% end %>
<% end %>
</span>
<% end %>
<!-- Account / Sign in -->
<% if !(controller_name == "sessions" && action_name == "new") %>
<div class="sidebarAccount upperRightEl">
<div class="sidebarAccountIcon"><div class="tooltipsUnder">Account</div>
<% if current_user && current_user.image %>
<%= image_tag current_user.image.url(:thirtytwo), :size => "32x32" %>
<% elsif !current_user %>
SIGN IN
<div class="accountInnerArrow"></div>
<% end %>
</div>
<div class="sidebarAccountBox upperRightBox">
<%= render :partial => 'layouts/account' %>
</div>
</div><!-- end sidebarAccount -->
<% end %>
<div class="clearfloat"></div>
</div><!-- end upperRightUI -->

View file

@ -6,83 +6,18 @@
#%>
<%= render :partial => 'layouts/head' %>
<body class="<%= authenticated? ? "authenticated" : "unauthenticated" %> controller-<%= controller_name %> action-<%= action_name %>">
<div id="chat-box-wrapper"></div>
<a class='feedback-icon' target='_blank' href='https://hylo.com/c/metamaps'></a>
<%= content_tag :div, class: "main" do %>
<% classes = action_name == "home" ? "homePage" : ""
classes += action_name == "home" && authenticated? ? " explorePage" : ""
classes += controller_name == "maps" && action_name == "index" ? " explorePage" : ""
if controller_name == "maps" && action_name == "show"
classes += " mapPage"
if policy(@map).update?
classes += " canEditMap"
end
if @map.permission == "commons"
classes += " commonsMap"
end
end
classes += controller_name == "topics" && action_name == "show" ? " topicPage" : ""
%>
<div class="wrapper <%= classes %>" id="wrapper">
<%= render :partial => 'layouts/upperelements', :locals => { :noHardHomeLink => controller_name == "notifications" ? true : false } %>
<%= yield %>
<div class="showcard mapElement mapElementHidden" id="showcard"></div> <!-- the topic card -->
<% if authenticated? %>
<% # for creating and pulling in topics and synapses %>
<% if controller_name == 'maps' && action_name == "conversation" %>
<%= render :partial => 'maps/newtopicsecret' %>
<% else %>
<%= render :partial => 'maps/newtopic' %>
<% end %>
<%= render :partial => 'maps/newsynapse' %>
<% # for populating the change metacode list on the topic card %>
<%= render :partial => 'shared/metacodeoptions' %>
<% end %>
<%= render :partial => 'layouts/lowermapelements' %>
<div id="explore"></div>
<% if !(controller_name == 'maps' && action_name == "conversation") %>
<div id="instructions">
<div class="addTopic">
Double-click to<br>add a topic
</div>
<div class="tabKey">
Use Tab & Shift+Tab to select a metacode
</div>
<div class="enterKey">
Press Enter to add the topic
</div>
</div>
<% end %>
<div id="infovis"></div>
<%= render :partial => 'layouts/mobilemenu' %>
<p id="toast" class="toast">
<% if devise_error_messages? %>
<%= devise_error_messages! %>
<% end %>
<% if notice %>
<%= notice %>
<% end %>
<% if alert %>
<%= alert %>
<% end %>
</p>
<div id="loading"></div>
</div>
<div class="main" id="react-app"></div>
<%= yield %>
<% if authenticated? %>
<% # for creating and pulling in topics and synapses %>
<% if controller_name == 'maps' && action_name == "conversation" %>
<%= render :partial => 'maps/newtopicsecret' %>
<% else %>
<%= render :partial => 'maps/newtopic' %>
<% end %>
<%= render :partial => 'maps/newsynapse' %>
<% # for populating the change metacode list on the topic card %>
<%= render :partial => 'shared/metacodeoptions' %>
<% end %>
<%= render :partial => 'layouts/foot' %>

View file

@ -6,55 +6,26 @@
#%>
<%= render :partial => 'layouts/head' %>
<body class="<%= current_user ? 'authenticated' : 'unauthenticated' %>">
<a class='feedback-icon' target='_blank' href='https://hylo.com/c/metamaps'></a>
<%= content_tag :div, class: "main" do %>
<% if params[:controller] == 'doorkeeper/applications' || params[:controller] == 'doorkeeper/authorized_applications'
classes = 'appsPage'
else
classes = ''
end
%>
<div class="wrapper <%= classes %>" id="wrapper">
<%= render :partial => 'layouts/upperelements', :locals => {:noHardHomeLink => true } %>
<%= yield %>
<div id="exploreMapsHeader">
<div class="exploreMapsBar exploreElement">
<div class="exploreMapsMenu">
<div class="exploreMapsCenter">
<% if current_user && current_user.admin %>
<a href="<%= oauth_applications_path %>" class="activeMaps exploreMapsButton <%= params[:controller] == 'doorkeeper/applications' ? 'active' : nil %>">
<div class="exploreMapsIcon"></div>Registered Apps
</a>
<% end %>
<a href="<%= oauth_authorized_applications_path %>" class="authedApps exploreMapsButton <%= params[:controller] == 'doorkeeper/authorized_applications' ? 'active' : nil %>">
<div class="exploreMapsIcon"></div>Authorized Apps
</a>
<a href="/" class="myMaps exploreMapsButton">
<div class="exploreMapsIcon"></div>Maps
</a>
</div>
</div>
</div>
</div>
<p id="toast" class="toast">
<% if devise_error_messages? %>
<%= devise_error_messages! %>
<% elsif notice %>
<%= notice %>
<% end %>
</p>
<div id="loading"></div>
</div>
<% end %>
<div class="main" id="react-app"></div>
<%= yield %>
<div id="exploreMapsHeader">
<div class="exploreMapsBar exploreElement">
<div class="exploreMapsMenu">
<div class="exploreMapsCenter">
<% if current_user && current_user.admin %>
<a href="<%= oauth_applications_path %>" class="activeMaps exploreMapsButton <%= params[:controller] == 'doorkeeper/applications' ? 'active' : nil %>">
<div class="exploreMapsIcon"></div>Registered Apps
</a>
<% end %>
<a href="<%= oauth_authorized_applications_path %>" class="authedApps exploreMapsButton <%= params[:controller] == 'doorkeeper/authorized_applications' ? 'active' : nil %>">
<div class="exploreMapsIcon"></div>Authorized Apps
</a>
<a href="/" class="myMaps exploreMapsButton">
<div class="exploreMapsIcon"></div>Maps
</a>
</div>
</div>
</div>
</div>
<%= render :partial => 'layouts/foot' %>

View file

@ -6,7 +6,5 @@
<% content_for :title, "Request Invite | Metamaps" %>
<% content_for :mobile_title, "Request Invite" %>
<div id="yield">
<div class="requestInviteHeader"></div>
<iframe class="requestInvite" src="https://docs.google.com/forms/d/1lWoKPFHErsDfV5l7-SvcHxwX3vDi9nNNVW0rFMgJwgg/viewform?embedded=true" width="700" frameborder="0" marginheight="0" marginwidth="0">Loading...</iframe>
</div>

View file

@ -18,5 +18,6 @@
Metamaps.ServerData.Mappings = <%= @allmappings.to_json.html_safe %>;
Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>;
Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>;
Metamaps.ServerData.requests = <%= @allrequests.to_json.html_safe %>;
Metamaps.ServerData.VisualizeType = "ForceDirected";
</script>

View file

@ -18,5 +18,6 @@
Metamaps.ServerData.Mappings = <%= @allmappings.to_json.html_safe %>;
Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>;
Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>;
Metamaps.ServerData.requests = <%= @allrequests.to_json.html_safe %>;
Metamaps.ServerData.VisualizeType = "ForceDirected";
</script>

View file

@ -1,125 +0,0 @@
<%#
# @file
# this code generates the list of icons in the filter box in the upper right menu area
#%>
<%
@mappers = []
@synapses = []
@metacodes = []
@metacodelist = ''
@mapperlist = ''
@synapselist = ''
# There are essentially three functions happening here one to fill data to
#@mappers with all people who have mapped on the selected map, which
#actually gets checked twice once for topics or within @metacodes and once
#for synapses on the map. @synapses get filled with all synapses on the map
#and metacodes is filled with all the metacodes that are being used on the map.
if @map
@alltopics.each_with_index do |topic, index|
if @metacodes.index(topic.metacode) == nil
@metacodes.push(topic.metacode)
end
end
@allsynapses.each_with_index do |synapse, index|
if @synapses.index{|s| s.desc == synapse.desc} == nil
@synapses.push(synapse)
end
end
@allmappings.each_with_index do |mapping, index|
if @mappers.index(mapping.user) == nil
@mappers.push(mapping.user)
end
end
elsif @topic
@alltopics.each_with_index do |topic, index|
if @metacodes.index(topic.metacode) == nil
@metacodes.push(topic.metacode)
end
if @mappers.index(topic.user) == nil
@mappers.push(topic.user)
end
end
@allsynapses.each_with_index do |synapse, index|
if @synapses.index{|s| s.desc == synapse.desc} == nil
@synapses.push(synapse)
end
if @mappers.index(synapse.user) == nil
@mappers.push(synapse.user)
end
end
end
if @map || @topic
@metacodes.sort! {|x,y|
n1 = x.name || ""
n2 = y.name || ""
n1 <=> n2
}
@synapses.sort! {|x,y|
d1 = x.desc || ""
d2 = y.desc || ""
d1 <=> d2
}
@mappers.sort! {|x,y|
n1 = x.name || ""
n2 = y.name || ""
n1 <=> n2
}
@metacodes.each_with_index do |metacode, index|
@metacodelist += '<li data-id="' + metacode.id.to_s + '">'
@metacodelist += '<img src="' + asset_path(metacode.icon) + '" data-id="' + metacode.id.to_s + '" alt="' + metacode.name + '" />'
@metacodelist += '<p>' + metacode.name.downcase + '</p></li>'
end
@synapses.each_with_index do |synapse, index|
d = synapse.desc || ""
@synapselist += '<li data-id="' + d + '">'
@synapselist += '<img src="' + asset_path('synapse16.png') + '" alt="synapse icon" /><p>' + d
@synapselist += '</p></li>'
end
@mappers.each_with_index do |mapper, index|
@mapperlist += '<li data-id="' + mapper.id.to_s + '">'
@mapperlist += '<img src="' + mapper.image.url(:sixtyfour) + '" data-id="' + mapper.id.to_s + '" alt="' + mapper.name + '" />'
@mapperlist += '<p>' + mapper.name + '</p></li>'
end
end
%>
<div class="filterBox">
<h2>FILTER BY</h2>
<div id="filter_by_mapper" class="filterBySection">
<h3><%= @map ? "MAPPERS" : @topic ? "CREATORS" : "" %></h3>
<span class="hideAll hideAllMappers">NONE</span>
<span class="active showAll showAllMappers">ALL</span>
<div class="clearfloat"></div>
<ul>
<%= @mapperlist.html_safe %>
</ul>
<div class="clearfloat"></div>
</div>
<div id="filter_by_metacode" class="filterBySection">
<h3>METACODES</h3>
<span class="hideAll hideAllMetacodes">NONE</span>
<span class="active showAll showAllMetacodes">ALL</span>
<div class="clearfloat"></div>
<ul>
<%= @metacodelist.html_safe %>
</ul>
<div class="clearfloat"></div>
</div>
<div id="filter_by_synapse" class="filterBySection">
<h3>SYNAPSES</h3>
<span class="hideAll hideAllSynapses">NONE</span>
<span class="active showAll showAllSynapses">ALL</span>
<div class="clearfloat"></div>
<ul>
<%= @synapselist.html_safe %>
</ul>
<div class="clearfloat"></div>
</div>
</div> <!-- end .filterBox -->

View file

@ -76,7 +76,7 @@ const Control = {
}
if (DataModel.Topics.length === 0) {
GlobalUI.showDiv('#instructions')
Map.setHasLearnedTopicCreation(false)
}
},
deleteSelectedNodes: function() { // refers to deleting topics permanently

View file

@ -1,6 +1,7 @@
/* global $, Hogan, Bloodhound */
import DataModel from './DataModel'
import Map from './Map'
import Mouse from './Mouse'
import Selected from './Selected'
import Synapse from './Synapse'
@ -270,7 +271,7 @@ const Create = {
})
Create.newTopic.beingCreated = true
Create.newTopic.name = ''
GlobalUI.hideDiv('#instructions')
Map.setHasLearnedTopicCreation(true)
},
hide: function(force) {
if (force || !Create.newTopic.pinned) {
@ -281,7 +282,7 @@ const Create = {
Create.newTopic.pinned = false
}
if (DataModel.Topics.length === 0) {
GlobalUI.showDiv('#instructions')
Map.setHasLearnedTopicCreation(false)
}
Create.newTopic.beingCreated = false
},

View file

@ -9,12 +9,12 @@ const Mapper = Backbone.Model.extend({
toJSON: function(options) {
return _.omit(this.attributes, this.blacklist)
},
prepareLiForFilter: function() {
return outdent`
<li data-id="${this.id}">
<img src="${this.get('image')}" data-id="${this.id}" alt="${this.get('name')}" />
<p>${this.get('name')}</p>
</li>`
prepareDataForFilter: function() {
return {
id: this.id,
image: this.get('image'),
name: this.get('name')
}
},
followMap: function(id) {
const idIndex = this.get('follows').maps.indexOf(id)

View file

@ -9,12 +9,12 @@ const Metacode = Backbone.Model.extend({
image.src = this.get('icon')
this.set('image', image)
},
prepareLiForFilter: function() {
return outdent`
<li data-id="${this.id}">
<img src="${this.get('icon')}" data-id="${this.id}" alt="${this.get('name')}" />
<p>${this.get('name').toLowerCase()}</p>
</li>`
prepareDataForFilter: function() {
return {
id: this.id,
name: this.get('name'),
icon: this.get('icon')
}
}
})

View file

@ -28,12 +28,11 @@ const Synapse = Backbone.Model.extend({
this.on('change', this.updateEdgeView)
this.on('change:desc', Filter.checkSynapses, this)
},
prepareLiForFilter: function() {
return outdent`
<li data-id="${this.get('desc')}">
<img src="${DataModel.synapseIconUrl}" alt="synapse icon" />
<p>${this.get('desc')}</p>
</li>`
prepareDataForFilter: function() {
return {
desc: this.get('desc'),
icon: DataModel.synapseIconUrl
}
},
authorizeToEdit: function(mapper) {
if (mapper && (this.get('permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true

View file

@ -101,16 +101,19 @@ const DataModel = {
},
attachCollectionEvents: function() {
DataModel.Topics.on('add remove', function(topic) {
console.log('updating infobox and filters due to topic add or remove')
InfoBox.updateNumbers()
Filter.checkMetacodes()
Filter.checkMappers()
})
DataModel.Synapses.on('add remove', function(synapse) {
console.log('updating infobox and filters due to synapse add or remove')
InfoBox.updateNumbers()
Filter.checkSynapses()
Filter.checkMappers()
})
DataModel.Mappings.on('add remove', function(mapping) {
console.log('updating infobox and filters due to mapping add or remove')
InfoBox.updateNumbers()
Filter.checkSynapses()
Filter.checkMetacodes()

View file

@ -5,13 +5,17 @@ import _ from 'lodash'
import Active from './Active'
import Control from './Control'
import DataModel from './DataModel'
import GlobalUI from './GlobalUI'
import GlobalUI, { ReactApp } from './GlobalUI'
import Settings from './Settings'
import Visualize from './Visualize'
const Filter = {
dataForPresentation: {
metacodes: {},
mappers: {},
synapses: {}
},
filters: {
name: '',
metacodes: [],
mappers: [],
synapses: []
@ -23,119 +27,26 @@ const Filter = {
},
isOpen: false,
changing: false,
init: function() {
var self = Filter
$('.sidebarFilterIcon').click(self.toggleBox)
$('.sidebarFilterBox .showAllMetacodes').click(self.filterNoMetacodes)
$('.sidebarFilterBox .showAllSynapses').click(self.filterNoSynapses)
$('.sidebarFilterBox .showAllMappers').click(self.filterNoMappers)
$('.sidebarFilterBox .hideAllMetacodes').click(self.filterAllMetacodes)
$('.sidebarFilterBox .hideAllSynapses').click(self.filterAllSynapses)
$('.sidebarFilterBox .hideAllMappers').click(self.filterAllMappers)
self.bindLiClicks()
self.getFilterData()
},
toggleBox: function(event) {
var self = Filter
if (self.isOpen) self.close()
else self.open()
event.stopPropagation()
},
open: function() {
var self = Filter
GlobalUI.Account.close()
$('.sidebarFilterIcon div').addClass('hide')
if (!self.isOpen && !self.changing) {
self.changing = true
var height = $(document).height() - 108
$('.sidebarFilterBox').css('max-height', height + 'px').fadeIn(200, function() {
self.changing = false
self.isOpen = true
})
}
},
close: function() {
var self = Filter
$('.sidebarFilterIcon div').removeClass('hide')
if (!self.changing) {
self.changing = true
$('.sidebarFilterBox').fadeOut(200, function() {
self.changing = false
self.isOpen = false
})
}
},
reset: function() {
var self = Filter
self.filters.metacodes = []
self.filters.mappers = []
self.filters.synapses = []
self.visible.metacodes = []
self.visible.mappers = []
self.visible.synapses = []
$('#filter_by_metacode ul').empty()
$('#filter_by_mapper ul').empty()
$('#filter_by_synapse ul').empty()
$('.filterBox .showAll').addClass('active')
},
/*
Most of this data essentially depends on the ruby function which are happening for filter inside view filterBox
But what these function do is load this data into three accessible array within java : metacodes, mappers and synapses
*/
getFilterData: function() {
var self = Filter
var metacode, mapper, synapse
$('#filter_by_metacode li').each(function() {
metacode = $(this).attr('data-id')
self.filters.metacodes.push(metacode)
self.visible.metacodes.push(metacode)
})
$('#filter_by_mapper li').each(function() {
mapper = ($(this).attr('data-id'))
self.filters.mappers.push(mapper)
self.visible.mappers.push(mapper)
})
$('#filter_by_synapse li').each(function() {
synapse = ($(this).attr('data-id'))
self.filters.synapses.push(synapse)
self.visible.synapses.push(synapse)
})
},
bindLiClicks: function() {
var self = Filter
$('#filter_by_metacode ul li').unbind().click(self.toggleMetacode)
$('#filter_by_mapper ul li').unbind().click(self.toggleMapper)
$('#filter_by_synapse ul li').unbind().click(self.toggleSynapse)
self.dataForPresentation.metacodes = {}
self.dataForPresentation.mappers = {}
self.dataForPresentation.synapses = {}
ReactApp.render()
},
// an abstraction function for checkMetacodes, checkMappers, checkSynapses to reduce
// code redundancy
/*
@param
*/
updateFilters: function(collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) {
var self = Filter
var newList = []
var removed = []
var added = []
// the first option enables us to accept
// ['Topics', 'Synapses'] as 'collection'
if (typeof collection === 'object') {
@ -168,41 +79,24 @@ const Filter = {
}
})
}
removed = _.difference(self.filters[filtersToUse], newList)
added = _.difference(newList, self.filters[filtersToUse])
// remove the list items for things no longer present on the map
_.each(removed, function(identifier) {
$('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast', function() {
$(this).remove()
})
const index = self.visible[filtersToUse].indexOf(identifier)
self.visible[filtersToUse].splice(index, 1)
delete self.dataForPresentation[filtersToUse][identifier]
})
var model, li, jQueryLi
function sortAlpha(a, b) {
return a.childNodes[1].innerHTML.toLowerCase() > b.childNodes[1].innerHTML.toLowerCase() ? 1 : -1
}
// for each new filter to be added, create a list item for it and fade it in
_.each(added, function(identifier) {
model = DataModel[correlatedModel].get(identifier) ||
DataModel[correlatedModel].find(function(model) {
return model.get(propertyToCheck) === identifier
const model = DataModel[correlatedModel].get(identifier) ||
DataModel[correlatedModel].find(function(m) {
return m.get(propertyToCheck) === identifier
})
li = model.prepareLiForFilter()
jQueryLi = $(li).hide()
$('li', '#filter_by_' + listToModify + ' ul').add(jQueryLi.fadeIn('fast'))
.sort(sortAlpha).appendTo('#filter_by_' + listToModify + ' ul')
self.dataForPresentation[filtersToUse][identifier] = model.prepareDataForFilter()
self.visible[filtersToUse].push(identifier)
})
// update the list of filters with the new list we just generated
self.filters[filtersToUse] = newList
// make sure clicks on list items still trigger the right events
self.bindLiClicks()
ReactApp.render()
},
checkMetacodes: function() {
var self = Filter
@ -221,114 +115,49 @@ const Filter = {
var self = Filter
self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse')
},
filterAllMetacodes: function(e) {
filterAllMetacodes: function(toVisible) {
var self = Filter
$('#filter_by_metacode ul li').addClass('toggledOff')
$('.showAllMetacodes').removeClass('active')
$('.hideAllMetacodes').addClass('active')
self.visible.metacodes = []
self.visible.metacodes = toVisible ? self.filters.metacodes.slice() : []
ReactApp.render()
self.passFilters()
},
filterNoMetacodes: function(e) {
filterAllMappers: function(toVisible) {
var self = Filter
$('#filter_by_metacode ul li').removeClass('toggledOff')
$('.showAllMetacodes').addClass('active')
$('.hideAllMetacodes').removeClass('active')
self.visible.metacodes = self.filters.metacodes.slice()
self.visible.mappers = toVisible ? self.filters.mappers.slice() : []
ReactApp.render()
self.passFilters()
},
filterAllMappers: function(e) {
filterAllSynapses: function(toVisible) {
var self = Filter
$('#filter_by_mapper ul li').addClass('toggledOff')
$('.showAllMappers').removeClass('active')
$('.hideAllMappers').addClass('active')
self.visible.mappers = []
self.passFilters()
},
filterNoMappers: function(e) {
var self = Filter
$('#filter_by_mapper ul li').removeClass('toggledOff')
$('.showAllMappers').addClass('active')
$('.hideAllMappers').removeClass('active')
self.visible.mappers = self.filters.mappers.slice()
self.passFilters()
},
filterAllSynapses: function(e) {
var self = Filter
$('#filter_by_synapse ul li').addClass('toggledOff')
$('.showAllSynapses').removeClass('active')
$('.hideAllSynapses').addClass('active')
self.visible.synapses = []
self.passFilters()
},
filterNoSynapses: function(e) {
var self = Filter
$('#filter_by_synapse ul li').removeClass('toggledOff')
$('.showAllSynapses').addClass('active')
$('.hideAllSynapses').removeClass('active')
self.visible.synapses = self.filters.synapses.slice()
self.visible.synapses = toVisible ? self.filters.synapses.slice() : []
ReactApp.render()
self.passFilters()
},
// an abstraction function for toggleMetacode, toggleMapper, toggleSynapse
// to reduce code redundancy
// gets called in the context of a list item in a filter box
toggleLi: function(whichToFilter) {
toggleLi: function(whichToFilter, id) {
var self = Filter
var id = $(this).attr('data-id')
if (self.visible[whichToFilter].indexOf(id) === -1) {
self.visible[whichToFilter].push(id)
$(this).removeClass('toggledOff')
} else {
const index = self.visible[whichToFilter].indexOf(id)
self.visible[whichToFilter].splice(index, 1)
$(this).addClass('toggledOff')
}
ReactApp.render()
self.passFilters()
},
toggleMetacode: function() {
toggleMetacode: function(id) {
var self = Filter
self.toggleLi.call(this, 'metacodes')
if (self.visible.metacodes.length === self.filters.metacodes.length) {
$('.showAllMetacodes').addClass('active')
$('.hideAllMetacodes').removeClass('active')
} else if (self.visible.metacodes.length === 0) {
$('.showAllMetacodes').removeClass('active')
$('.hideAllMetacodes').addClass('active')
} else {
$('.showAllMetacodes').removeClass('active')
$('.hideAllMetacodes').removeClass('active')
}
self.toggleLi('metacodes', id)
},
toggleMapper: function() {
toggleMapper: function(id) {
var self = Filter
self.toggleLi.call(this, 'mappers')
if (self.visible.mappers.length === self.filters.mappers.length) {
$('.showAllMappers').addClass('active')
$('.hideAllMappers').removeClass('active')
} else if (self.visible.mappers.length === 0) {
$('.showAllMappers').removeClass('active')
$('.hideAllMappers').addClass('active')
} else {
$('.showAllMappers').removeClass('active')
$('.hideAllMappers').removeClass('active')
}
self.toggleLi('mappers', id)
},
toggleSynapse: function() {
toggleSynapse: function(id) {
var self = Filter
self.toggleLi.call(this, 'synapses')
if (self.visible.synapses.length === self.filters.synapses.length) {
$('.showAllSynapses').addClass('active')
$('.hideAllSynapses').removeClass('active')
} else if (self.visible.synapses.length === 0) {
$('.showAllSynapses').removeClass('active')
$('.hideAllSynapses').addClass('active')
} else {
$('.showAllSynapses').removeClass('active')
$('.hideAllSynapses').removeClass('active')
}
self.toggleLi('synapses', id)
},
passFilters: function() {
var self = Filter

View file

@ -1,55 +0,0 @@
/* global $ */
import Filter from '../Filter'
const Account = {
isOpen: false,
changing: false,
init: function() {
var self = Account
$('.sidebarAccountIcon').click(self.toggleBox)
$('.sidebarAccountBox').click(function(event) {
event.stopPropagation()
})
$('body').click(self.close)
},
toggleBox: function(event) {
var self = Account
if (self.isOpen) self.close()
else self.open()
event.stopPropagation()
},
open: function() {
var self = Account
Filter.close()
$('.sidebarAccountIcon .tooltipsUnder').addClass('hide')
if (!self.isOpen && !self.changing) {
self.changing = true
$('.sidebarAccountBox').fadeIn(200, function() {
self.changing = false
self.isOpen = true
$('.sidebarAccountBox #user_email').focus()
})
}
},
close: function() {
var self = Account
$('.sidebarAccountIcon .tooltipsUnder').removeClass('hide')
if (!self.changing) {
self.changing = true
$('.sidebarAccountBox #user_email').blur()
$('.sidebarAccountBox').fadeOut(200, function() {
self.changing = false
self.isOpen = false
})
}
}
}
export default Account

View file

@ -61,7 +61,7 @@ const CreateMap = {
if (GlobalUI.lightbox === 'forkmap') {
self.newMap.set('topicsToMap', self.topicsToMap)
self.newMap.set('synapsesToMap', self.synapsesToMap)
self.newMap.set('source_id', Active.Map.id)
if (Active.Map) self.newMap.set('source_id', Active.Map.id)
}
var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'

View file

@ -4,7 +4,7 @@ import React from 'react'
import ReactDOM from 'react-dom'
import outdent from 'outdent'
import ImportDialogBox from '../../components/ImportDialogBox'
import ImportDialogBox from '../../components/MapView/ImportDialogBox'
import PasteInput from '../PasteInput'
import Map from '../Map'
@ -19,7 +19,7 @@ const ImportDialog = {
self.closeLightbox = closeLightbox
$('#lightbox_content').append($(outdent`
<div class="lightboxContent" id="import-dialog-lightbox">
<div class="lightboxContent" id="import-dialog">
<div class="importDialogWrapper" />
</div>
`))

View file

@ -1,30 +0,0 @@
/* global $ */
import React from 'react'
import ReactDOM from 'react-dom'
import Active from '../Active'
import NotificationIconComponent from '../../components/NotificationIcon'
const NotificationIcon = {
unreadNotificationsCount: null,
init: function(serverData) {
const self = NotificationIcon
self.unreadNotificationsCount = serverData.unreadNotificationsCount
self.render()
},
render: function(newUnreadCount = null) {
if (newUnreadCount !== null) {
NotificationIcon.unreadNotificationsCount = newUnreadCount
}
if (Active.Mapper !== null) {
ReactDOM.render(React.createElement(NotificationIconComponent, {
unreadNotificationsCount: NotificationIcon.unreadNotificationsCount
}), $('#notification_icon').get(0))
}
}
}
export default NotificationIcon

View file

@ -0,0 +1,234 @@
/* global $ */
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, browserHistory } from 'react-router'
import { merge } from 'lodash'
import { notifyUser } from './index.js'
import ImportDialog from './ImportDialog'
import Active from '../Active'
import DataModel from '../DataModel'
import { ExploreMaps, ChatView, TopicCard } from '../Views'
import Filter from '../Filter'
import JIT from '../JIT'
import Realtime from '../Realtime'
import Map, { InfoBox } from '../Map'
import Topic from '../Topic'
import Visualize from '../Visualize'
import makeRoutes from '../../components/makeRoutes'
let routes
// 220 wide + 16 padding on both sides
const MAP_WIDTH = 252
const MOBILE_VIEW_BREAKPOINT = 504
const MOBILE_VIEW_PADDING = 40
const MAX_COLUMNS = 4
const ReactApp = {
mapId: null,
topicId: null,
unreadNotificationsCount: 0,
mapsWidth: 0,
toast: '',
mobile: false,
mobileTitle: '',
mobileTitleWidth: 0,
init: function(serverData, openLightbox) {
const self = ReactApp
self.unreadNotificationsCount = serverData.unreadNotificationsCount
self.mobileTitle = serverData.mobileTitle
self.openLightbox = openLightbox
routes = makeRoutes(serverData.ActiveMapper)
self.resize()
window && window.addEventListener('resize', self.resize)
},
handleUpdate: function(location) {
const self = ReactApp
const pathname = this.state.location.pathname
switch (pathname.split('/')[1]) {
case '':
if (Active.Mapper && Active.Mapper.id) {
$('#yield').hide()
ExploreMaps.updateFromPath(pathname)
self.mapId = null
Active.Map = null
Active.Topic = null
}
break
case 'explore':
$('#yield').hide()
ExploreMaps.updateFromPath(pathname)
self.mapId = null
self.topicId = null
Active.Map = null
Active.Topic = null
break
case 'topics':
$('#yield').hide()
Active.Map = null
self.mapId = null
self.topicId = pathname.split('/')[2]
break
case 'maps':
if (!pathname.includes('request_access')) {
$('#yield').hide()
Active.Topic = null
self.topicId = null
self.mapId = pathname.split('/')[2]
}
break
default:
$('#yield').show()
break
}
self.render()
window.ga && window.ga('send', 'pageview', pathname)
},
render: function() {
const self = ReactApp
const createElement = (Component, props) => <Component {...props} {...self.getProps()}/>
const app = <Router createElement={createElement} routes={routes} history={browserHistory} onUpdate={self.handleUpdate} />
console.log('rendering')
ReactDOM.render(app, document.getElementById('react-app'))
},
getProps: function() {
const self = ReactApp
return merge({
unreadNotificationsCount: self.unreadNotificationsCount,
currentUser: Active.Mapper,
toast: self.toast,
mobile: self.mobile,
mobileTitle: self.mobileTitle,
mobileTitleWidth: self.mobileTitleWidth,
mobileTitleClick: (e) => Active.Map && InfoBox.toggleBox(e),
openInviteLightbox: () => self.openLightbox('invite')
},
self.getMapProps(),
self.getTopicProps(),
self.getFilterProps(),
self.getCommonProps(),
self.getMapsProps(),
self.getTopicCardProps(),
self.getChatProps())
},
getMapProps: function() {
const self = ReactApp
return {
mapId: self.mapId,
map: Active.Map,
hasLearnedTopicCreation: Map.hasLearnedTopicCreation,
userRequested: Map.userRequested,
requestAnswered: Map.requestAnswered,
requestApproved: Map.requestApproved,
onRequestAccess: Map.requestAccess,
mapIsStarred: Map.mapIsStarred,
endActiveMap: Map.end,
launchNewMap: Map.launch,
toggleMapInfoBox: InfoBox.toggleBox,
infoBoxHtml: InfoBox.html,
openImportLightbox: () => ImportDialog.show(),
forkMap: Map.fork,
onMapStar: Map.star,
onMapUnstar: Map.unstar
}
},
getCommonProps: function() {
const self = ReactApp
return {
openHelpLightbox: () => self.openLightbox('cheatsheet'),
onZoomExtents: event => JIT.zoomExtents(event, Visualize.mGraph.canvas),
onZoomIn: JIT.zoomIn,
onZoomOut: JIT.zoomOut
}
},
getTopicCardProps: function() {
const self = ReactApp
return {
openTopic: TopicCard.openTopic,
metacodeSets: TopicCard.metacodeSets,
updateTopic: TopicCard.updateTopic,
onTopicFollow: TopicCard.onTopicFollow,
redrawCanvas: TopicCard.redrawCanvas
}
},
getTopicProps: function() {
const self = ReactApp
return {
topicId: self.topicId,
topic: Active.Topic,
endActiveTopic: Topic.end,
launchNewTopic: Topic.launch
}
},
getMapsProps: function() {
const self = ReactApp
return {
section: ExploreMaps.collection && ExploreMaps.collection.id,
maps: ExploreMaps.collection,
juntoState: Realtime.juntoState,
moreToLoad: ExploreMaps.collection && ExploreMaps.collection.page !== 'loadedAll',
user: ExploreMaps.collection && ExploreMaps.collection.id === 'mapper' ? ExploreMaps.mapper : null,
loadMore: ExploreMaps.loadMore,
pending: ExploreMaps.pending,
onStar: ExploreMaps.onStar,
onRequest: ExploreMaps.onRequest,
onMapFollow: ExploreMaps.onMapFollow,
mapsWidth: ReactApp.mapsWidth
}
},
getChatProps: function() {
const self = ReactApp
return {
unreadMessages: ChatView.unreadMessages,
conversationLive: ChatView.conversationLive,
isParticipating: ChatView.isParticipating,
onOpen: ChatView.onOpen,
onClose: ChatView.onClose,
leaveCall: Realtime.leaveCall,
joinCall: Realtime.joinCall,
inviteACall: Realtime.inviteACall,
inviteToJoin: Realtime.inviteToJoin,
participants: ChatView.participants ? ChatView.participants.models.map(p => p.attributes) : [],
messages: ChatView.messages ? ChatView.messages.models.map(m => m.attributes) : [],
videoToggleClick: ChatView.videoToggleClick,
cursorToggleClick: ChatView.cursorToggleClick,
soundToggleClick: ChatView.soundToggleClick,
inputBlur: ChatView.inputBlur,
inputFocus: ChatView.inputFocus,
handleInputMessage: ChatView.handleInputMessage
}
},
getFilterProps: function() {
const self = ReactApp
return {
filterData: Filter.dataForPresentation,
allForFiltering: Filter.filters,
visibleForFiltering: Filter.visible,
toggleMetacode: Filter.toggleMetacode,
toggleMapper: Filter.toggleMapper,
toggleSynapse: Filter.toggleSynapse,
filterAllMetacodes: Filter.filterAllMetacodes,
filterAllMappers: Filter.filterAllMappers,
filterAllSynapses: Filter.filterAllSynapses
}
},
resize: function() {
const self = ReactApp
const maps = ExploreMaps.collection
const currentUser = Active.Mapper
const user = maps && maps.id === 'mapper' ? ExploreMaps.mapper : null
const numCards = (maps ? maps.length : 0) + (user || currentUser ? 1 : 0)
const mapSpaces = Math.floor(document.body.clientWidth / MAP_WIDTH)
const mapsWidth = document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT
? document.body.clientWidth - MOBILE_VIEW_PADDING
: Math.min(MAX_COLUMNS, Math.min(numCards, mapSpaces)) * MAP_WIDTH
self.mapsWidth = mapsWidth
self.mobileTitleWidth = document ? document.body.clientWidth - 70 : 0
self.mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT
self.render()
}
}
export default ReactApp

View file

@ -1,7 +1,8 @@
/* global $, Hogan, Bloodhound, CanvasLoader */
import { browserHistory } from 'react-router'
import Active from '../Active'
import Router from '../Router'
const Search = {
locked: false,
@ -17,6 +18,7 @@ const Search = {
self.userIconUrl = serverData['user.png']
// this is similar to Metamaps.Loading, but it's for the search element
if (!document.getElementById('searchLoading')) return
var loader = new CanvasLoader('searchLoading')
loader.setColor('#4fb5c0') // default is '#000000'
loader.setDiameter(24) // default is 40
@ -189,11 +191,11 @@ const Search = {
if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) {
if (datum.rtype === 'topic') {
Router.topics(datum.id)
browserHistory.push(`/topics/${datum.id}`)
} else if (datum.rtype === 'map') {
Router.maps(datum.id)
browserHistory.push(`/maps/${datum.id}`)
} else if (datum.rtype === 'mapper') {
Router.explore('mapper', datum.id)
browserHistory.push(`/explore/mapper/${datum.id}`)
}
}
},

View file

@ -4,11 +4,10 @@ import clipboard from 'clipboard-js'
import Create from '../Create'
import ReactApp from './ReactApp'
import Search from './Search'
import CreateMap from './CreateMap'
import Account from './Account'
import ImportDialog from './ImportDialog'
import NotificationIcon from './NotificationIcon'
const GlobalUI = {
notifyTimeout: null,
@ -18,13 +17,12 @@ const GlobalUI = {
init: function(serverData) {
const self = GlobalUI
self.Search.init(serverData)
self.ReactApp.init(serverData, self.openLightbox)
self.CreateMap.init(serverData)
self.Account.init(serverData)
self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox)
self.NotificationIcon.init(serverData)
self.Search.init(serverData)
if ($('#toast').html().trim()) self.notifyUser($('#toast').html())
if (serverData.toast) self.notifyUser(serverData.toast)
// bind lightbox clicks
$('.openLightbox').click(function(event) {
@ -112,10 +110,9 @@ const GlobalUI = {
_notifyUser: function(message, opts = {}) {
const self = GlobalUI
const { leaveOpen = false, timeOut = 8000 } = opts
$('#toast').html(message)
self.showDiv('#toast')
const { leaveOpen = false, timeOut = 5000 } = opts
ReactApp.toast = message
ReactApp.render()
clearTimeout(self.notifyTimeOut)
if (!leaveOpen) {
@ -134,7 +131,8 @@ const GlobalUI = {
const { message, opts } = self.notifyQueue.shift()
self._notifyUser(message, opts)
} else {
self.hideDiv('#toast')
ReactApp.toast = null
ReactApp.render()
self.notifying = false
}
},
@ -153,5 +151,5 @@ const GlobalUI = {
}
}
export { Search, CreateMap, Account, ImportDialog, NotificationIcon }
export { ReactApp, Search, CreateMap, ImportDialog }
export default GlobalUI

View file

@ -316,7 +316,7 @@ const Import = {
success: opts.success
})
GlobalUI.hideDiv('#instructions')
Map.setHasLearnedTopicCreation(true)
},
createSynapseWithParameters: function(desc, category, permission,

View file

@ -50,15 +50,6 @@ const JIT = {
*/
init: function(serverData) {
const self = JIT
$('.zoomIn').click(self.zoomIn)
$('.zoomOut').click(self.zoomOut)
const zoomExtents = function(event) {
self.zoomExtents(event, Visualize.mGraph.canvas)
}
$('.zoomExtents').click(zoomExtents)
self.topicDescImage = new Image()
self.topicDescImage.src = serverData['topic_description_signifier.png']
@ -123,36 +114,22 @@ const JIT = {
prepareVizData: function() {
const self = JIT
let mapping
// reset/empty vizData
self.vizData = []
Visualize.loadLater = false
const results = self.convertModelsToJIT(DataModel.Topics, DataModel.Synapses)
self.vizData = results[0]
// clean up the synapses array in case of any faulty data
_.each(results[1], function(synapse) {
mapping = synapse.getMapping()
DataModel.Synapses.remove(synapse)
if (DataModel.Mappings) DataModel.Mappings.remove(mapping)
})
// set up addTopic instructions in case they delete all the topics
// i.e. if there are 0 topics at any time, it should have instructions again
$('#instructions div').hide()
if (Active.Map && Active.Map.authorizeToEdit(Active.Mapper)) {
$('#instructions div.addTopic').show()
}
if (self.vizData.length === 0) {
GlobalUI.showDiv('#instructions')
Map.setHasLearnedTopicCreation(false)
Visualize.loadLater = true
} else {
GlobalUI.hideDiv('#instructions')
Map.setHasLearnedTopicCreation(true)
}
Visualize.render()
}, // prepareVizData
edgeRender: function(adj, canvas) {
@ -1026,7 +1003,6 @@ const JIT = {
Create.newTopic.open()
} else if (!Mouse.didPan) {
// SINGLE CLICK, no pan
Filter.close()
TopicCard.hideCard()
SynapseCard.hideCard()
Create.newTopic.hide()

View file

@ -146,7 +146,6 @@ const Listeners = {
}
if (Active.Map && Realtime.inConversation) Realtime.positionVideos()
Mobile.resizeTitle()
})
},
centerAndReveal: function(nodes, opts) {

View file

@ -15,6 +15,7 @@ const Loading = {
Loading.loader.setDensity(41) // default is 40
Loading.loader.setRange(0.9) // default is 1.3
Loading.loader.show() // Hidden by default
$('#loading').hide()
}
}

View file

@ -1,16 +1,15 @@
/* global $, Hogan, Bloodhound, Countable */
import outdent from 'outdent'
import { browserHistory } from 'react-router'
import Active from '../Active'
import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI'
import Router from '../Router'
import GlobalUI, { ReactApp } from '../GlobalUI'
import Util from '../Util'
const InfoBox = {
isOpen: false,
changing: 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`
@ -35,12 +34,12 @@ const InfoBox = {
data-bip-value="{{desc}}"
>{{desc}}</span>`,
userImageUrl: '',
html: '',
init: function(serverData, updateThumbnail) {
var self = InfoBox
self.updateThumbnail = updateThumbnail
$('.mapInfoIcon').click(self.toggleBox)
$('.mapInfoBox').click(function(event) {
event.stopPropagation()
})
@ -72,27 +71,18 @@ const InfoBox = {
open: function() {
var self = InfoBox
$('.mapInfoIcon div').addClass('hide')
if (!self.isOpen && !self.changing) {
self.changing = true
$('.mapInfoBox').fadeIn(200, function() {
self.changing = false
self.isOpen = true
})
}
$('.mapInfoBox').fadeIn(200, function() {
self.isOpen = true
})
},
close: function() {
var self = InfoBox
$('.mapInfoIcon div').removeClass('hide')
if (!self.changing) {
self.changing = true
$('.mapInfoBox').fadeOut(200, function() {
self.changing = false
self.isOpen = false
self.hidePermissionSelect()
$('.mapContributors .tip').hide()
})
}
$('.mapInfoBox').fadeOut(200, function() {
self.isOpen = false
self.hidePermissionSelect()
$('.mapContributors .tip').hide()
})
},
load: function() {
var self = InfoBox
@ -120,13 +110,8 @@ const InfoBox = {
obj['created_at'] = map.get('created_at_clean')
obj['updated_at'] = map.get('updated_at_clean')
var classes = isCreator ? 'yourMap' : ''
classes += canEdit ? ' canEdit' : ''
classes += shareable ? ' shareable' : ''
$('.mapInfoBox').removeClass('shareable yourMap canEdit')
.addClass(classes)
.html(self.generateBoxHTML.render(obj))
self.html = self.generateBoxHTML.render(obj)
ReactApp.render()
self.attachEventListeners()
},
attachEventListeners: function() {
@ -192,7 +177,6 @@ const InfoBox = {
$('.mapContributors .tip').unbind().click(function(event) {
event.stopPropagation()
})
$('.mapContributors .tip li a').click(Router.intercept)
$('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function() {
$('.mapContributors .tip').hide()
@ -393,7 +377,7 @@ const InfoBox = {
DataModel.Maps.Mine.remove(map)
DataModel.Maps.Shared.remove(map)
map.destroy()
Router.home()
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?")

View file

@ -2,6 +2,7 @@
import outdent from 'outdent'
import { find as _find } from 'lodash'
import { browserHistory } from 'react-router'
import Active from '../Active'
import AutoLayout from '../AutoLayout'
@ -9,11 +10,10 @@ import Create from '../Create'
import DataModel from '../DataModel'
import DataModelMap from '../DataModel/Map'
import Filter from '../Filter'
import GlobalUI from '../GlobalUI'
import GlobalUI, { ReactApp } from '../GlobalUI'
import JIT from '../JIT'
import Loading from '../Loading'
import Realtime from '../Realtime'
import Router from '../Router'
import Selected from '../Selected'
import SynapseCard from '../SynapseCard'
import TopicCard from '../Views/TopicCard'
@ -26,143 +26,126 @@ const Map = {
events: {
editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper'
},
mapIsStarred: false,
requests: [],
userRequested: false,
requestAnswered: false,
requestApproved: false,
hasLearnedTopicCreation: true,
init: function(serverData) {
var self = Map
self.mapIsStarred = serverData.mapIsStarred
self.requests = serverData.requests
self.setAccessRequest()
$('#wrapper').mousedown(function(e) {
if (e.button === 1) return false
})
$('.starMap').click(function() {
if ($(this).is('.starred')) self.unstar()
else self.star()
})
$('.sidebarFork').click(function() {
self.fork()
})
GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html()
self.updateStar()
InfoBox.init(serverData, function updateThumbnail() {
self.uploadMapScreenshot()
})
CheatSheet.init(serverData)
$('.viewOnly .requestAccess').click(self.requestAccess)
$(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper)
},
setHasLearnedTopicCreation: function(value) {
const self = Map
self.hasLearnedTopicCreation = value
ReactApp.render()
},
requestAccess: function() {
$('.viewOnly').removeClass('sendRequest').addClass('sentRequest')
const self = Map
self.requests.push({
user_id: Active.Mapper.id,
answered: false,
approved: false
})
self.setAccessRequest()
const mapId = Active.Map.id
$.post({
url: `/maps/${mapId}/access_request`
})
GlobalUI.notifyUser('Map creator will be notified of your request')
},
setAccessRequest: function(requests, activeMapper) {
let className = 'isViewOnly '
if (activeMapper) {
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'
setAccessRequest: function() {
const self = Map
if (Active.Mapper) {
const request = _find(self.requests, r => r.user_id === Active.Mapper.id)
if (!request) {
self.userRequested = false
self.requestAnswered = false
self.requestApproved = false
}
else if (request && !request.answered) {
self.userRequested = true
self.requestAnswered = false
self.requestApproved = false
}
else if (request && request.answered && !request.approved) {
self.userRequested = true
self.requestAnswered = true
self.requestApproved = false
}
}
$('.viewOnly').removeClass('sendRequest sentRequest requestDenied').addClass(className)
ReactApp.render()
},
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()
var map = Active.Map
var mapper = Active.Mapper
document.title = map.get('name') + ' | Metamaps'
// add class to .wrapper for specifying whether you can edit the map
if (map.authorizeToEdit(mapper)) {
$('.wrapper').addClass('canEditMap')
} else {
Map.setAccessRequest(data.requests, mapper)
}
// add class to .wrapper for specifying if the map can
// be collaborated on
if (map.get('permission') === 'commons') {
$('.wrapper').addClass('commonsMap')
}
Map.updateStar()
// set filter mapper H3 text
$('#filter_by_mapper h3').html('MAPPERS')
// build and render the visualization
const self = Map
var dataIsReadySetupMap = function() {
Map.setAccessRequest()
Visualize.type = 'ForceDirected'
JIT.prepareVizData()
// update filters
Filter.reset()
// reset selected arrays
Selected.reset()
// set the proper mapinfobox content
InfoBox.load()
// these three update the actual filter box with the right list items
Filter.reset()
Filter.checkMetacodes()
Filter.checkSynapses()
Filter.checkMappers()
Realtime.startActiveMap()
Loading.hide()
// for mobile
$('#header_content').html(map.get('name'))
document.title = Active.Map.get('name') + ' | Metamaps'
ReactApp.mobileTitle = Active.Map.get('name')
ReactApp.render()
}
function isLoaded() {
if (InfoBox.generateBoxHTML) dataIsReadySetupMap()
else setTimeout(() => isLoaded(), 50)
}
if (Active.Map && Active.Map.id === id) {
isLoaded()
}
else {
Loading.show()
$.ajax({
url: '/maps/' + id + '/contains.json',
success: 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()
self.requests = data.requests
isLoaded()
}
})
}
$.ajax({
url: '/maps/' + id + '/contains.json',
success: start
})
},
end: function() {
if (Active.Map) {
$('.wrapper').removeClass('canEditMap commonsMap')
$('.main').removeClass('compressed')
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()
Realtime.endActiveMap()
$('.viewOnly').removeClass('isViewOnly')
}
},
updateStar: function() {
if (!Active.Mapper || !DataModel.Stars) return
// update the star/unstar icon
if (DataModel.Stars.find(function(s) { return s.user_id === Active.Mapper.id })) {
$('.starMap').addClass('starred')
$('.starMap .tooltipsAbove').html('Unstar')
} else {
$('.starMap').removeClass('starred')
$('.starMap .tooltipsAbove').html('Star')
self.requests = []
self.hasLearnedTopicCreation = true
}
},
star: function() {
@ -173,7 +156,8 @@ const Map = {
DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id })
DataModel.Maps.Starred.add(Active.Map)
GlobalUI.notifyUser('Map is now starred')
self.updateStar()
self.mapIsStarred = true
ReactApp.render()
},
unstar: function() {
var self = Map
@ -182,7 +166,8 @@ const Map = {
$.post('/maps/' + Active.Map.id + '/unstar')
DataModel.Stars = DataModel.Stars.filter(function(s) { return s.user_id !== Active.Mapper.id })
DataModel.Maps.Starred.remove(Active.Map)
self.updateStar()
self.mapIsStarred = false
ReactApp.render()
},
fork: function() {
GlobalUI.openLightbox('forkmap')
@ -232,7 +217,7 @@ const Map = {
var map = Active.Map
DataModel.Maps.Active.remove(map)
DataModel.Maps.Featured.remove(map)
Router.home()
browserHistory.push('/')
GlobalUI.notifyUser('Sorry! That map has been changed to Private.')
},
cantEditNow: function() {
@ -245,7 +230,7 @@ const Map = {
confirmString += 'Do you want to reload and enable realtime collaboration?'
var c = window.confirm(confirmString)
if (c) {
Router.maps(Active.Map.id)
window.location.reload()
}
},
editedByActiveMapper: function() {

View file

@ -151,8 +151,6 @@ let Realtime = {
config: { DOUBLE_CLICK_TOLERANCE: 200 }
})
self.room.videoAdded(self.handleVideoAdded)
self.startActiveMap()
} // if Active.Mapper
},
addJuntoListeners: function() {
@ -201,9 +199,6 @@ let Realtime = {
self.leaveMap()
$('.collabCompass').remove()
if (self.room) self.room.leave()
ChatView.hide()
ChatView.close()
ChatView.reset()
Cable.unsubscribeFromMap()
},
turnOn: function(notify) {
@ -228,7 +223,6 @@ let Realtime = {
ChatView.setNewMap()
ChatView.addParticipant(self.activeMapper)
ChatView.addMessages(new DataModel.MessageCollection(DataModel.Messages), true)
ChatView.show()
},
setupLocalEvents: function() {
var self = Realtime

View file

@ -1,244 +0,0 @@
/* global $ */
import Backbone from 'backbone'
try { Backbone.$ = window.$ } catch (err) {}
import Active from './Active'
import DataModel from './DataModel'
import GlobalUI from './GlobalUI'
import Loading from './Loading'
import Map from './Map'
import Topic from './Topic'
import Views from './Views'
import Visualize from './Visualize'
const _Router = Backbone.Router.extend({
currentPage: '',
currentSection: '',
timeoutId: undefined,
routes: {
'': 'home', // #home
'explore/:section': 'explore', // #explore/active
'explore/:section/:id': 'explore', // #explore/mapper/1234
'maps/:id': 'maps', // #maps/7
'topics/:id': 'topics' // #topics/7
},
home: function() {
let self = this
clearTimeout(this.timeoutId)
if (Active.Mapper) document.title = 'Explore Active Maps | Metamaps'
else document.title = 'Home | Metamaps'
this.currentSection = ''
this.currentPage = ''
$('.wrapper').removeClass('mapPage topicPage')
var classes = Active.Mapper ? 'homePage explorePage' : 'homePage'
$('.wrapper').addClass(classes)
var navigate = function() {
self.timeoutId = setTimeout(function() {
self.navigateAndTrack('')
}, 300)
}
// all this only for the logged in home page
if (Active.Mapper) {
$('.homeButton a').attr('href', '/')
GlobalUI.hideDiv('#yield')
GlobalUI.showDiv('#explore')
Views.ExploreMaps.setCollection(DataModel.Maps.Active)
if (DataModel.Maps.Active.length === 0) {
Views.ExploreMaps.pending = true
DataModel.Maps.Active.getMaps(navigate) // this will trigger an explore maps render
} else {
Views.ExploreMaps.render(navigate)
}
} else {
// logged out home page
GlobalUI.hideDiv('#explore')
GlobalUI.showDiv('#yield')
this.timeoutId = setTimeout(navigate, 500)
}
GlobalUI.hideDiv('#infovis')
GlobalUI.hideDiv('#instructions')
Map.end()
Topic.end()
Active.Map = null
Active.Topic = null
},
explore: function(section, id) {
var self = this
clearTimeout(this.timeoutId)
// just capitalize the variable section
// either 'featured', 'mapper', or 'active'
var capitalize = section.charAt(0).toUpperCase() + section.slice(1)
if (section === 'shared' || section === 'featured' || section === 'active' || section === 'starred') {
document.title = 'Explore ' + capitalize + ' Maps | Metamaps'
} else if (section === 'mapper') {
$.ajax({
url: '/users/' + id + '.json',
success: function(response) {
document.title = response.name + ' | Metamaps'
},
error: function() {}
})
} else if (section === 'mine') {
document.title = 'Explore My Maps | Metamaps'
}
if (Active.Mapper && section !== 'mapper') $('.homeButton a').attr('href', '/explore/' + section)
$('.wrapper').removeClass('homePage mapPage topicPage')
$('.wrapper').addClass('explorePage')
this.currentSection = 'explore'
this.currentPage = section
// this will mean it's a mapper page being loaded
if (id) {
if (DataModel.Maps.Mapper.mapperId !== id) {
// empty the collection if we are trying to load the maps
// collection of a different mapper than we had previously
DataModel.Maps.Mapper.reset()
DataModel.Maps.Mapper.page = 1
}
DataModel.Maps.Mapper.mapperId = id
}
Views.ExploreMaps.setCollection(DataModel.Maps[capitalize])
var navigate = function() {
var path = '/explore/' + self.currentPage
// alter url if for mapper profile page
if (self.currentPage === 'mapper') {
path += '/' + DataModel.Maps.Mapper.mapperId
}
self.navigateAndTrack(path)
}
var navigateTimeout = function() {
self.timeoutId = setTimeout(navigate, 300)
}
if (DataModel.Maps[capitalize].length === 0) {
Loading.show()
Views.ExploreMaps.pending = true
setTimeout(function() {
DataModel.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render
}, 300) // wait 300 milliseconds till the other animations are done to do the fetch
} else {
if (id) {
Views.ExploreMaps.fetchUserThenRender(navigateTimeout)
} else {
Views.ExploreMaps.render(navigateTimeout)
}
}
GlobalUI.showDiv('#explore')
GlobalUI.hideDiv('#yield')
GlobalUI.hideDiv('#infovis')
GlobalUI.hideDiv('#instructions')
Map.end()
Topic.end()
Active.Map = null
Active.Topic = null
},
maps: function(id) {
clearTimeout(this.timeoutId)
this.currentSection = 'map'
this.currentPage = id
$('.wrapper').removeClass('homePage explorePage topicPage')
$('.wrapper').addClass('mapPage')
// another class will be added to wrapper if you
// can edit this map '.canEditMap'
GlobalUI.hideDiv('#yield')
GlobalUI.hideDiv('#explore')
// clear the visualization, if there was one, before showing its div again
if (Visualize.mGraph) {
Visualize.clearVisualization()
}
GlobalUI.showDiv('#infovis')
Topic.end()
Active.Topic = null
Loading.show()
Map.end()
Map.launch(id)
},
topics: function(id) {
clearTimeout(this.timeoutId)
this.currentSection = 'topic'
this.currentPage = id
$('.wrapper').removeClass('homePage explorePage mapPage')
$('.wrapper').addClass('topicPage')
GlobalUI.hideDiv('#yield')
GlobalUI.hideDiv('#explore')
// clear the visualization, if there was one, before showing its div again
if (Visualize.mGraph) {
Visualize.clearVisualization()
}
GlobalUI.showDiv('#infovis')
Map.end()
Active.Map = null
Topic.end()
Topic.launch(id)
}
})
const Router = new _Router()
Router.navigateAndTrack = (fragment, options) => {
Router.navigate(fragment, options)
window.ga && window.ga('send', 'pageview', location.pathname, {title: document.title})
}
Router.intercept = function(evt) {
var segments
var href = {
prop: $(this).prop('href'),
attr: $(this).attr('href')
}
var root = window.location.protocol + '//' + window.location.host + Backbone.history.options.root
if (href.prop && href.prop === root) href.attr = ''
if (href.prop && href.prop.slice(0, root.length) === root) {
evt.preventDefault()
segments = href.attr.split('/')
segments.splice(0, 1) // pop off the element created by the first /
if (href.attr === '') {
Router.home()
} else {
Router[segments[0]](segments[1], segments[2])
}
}
}
Router.init = function() {
Backbone.history.start({
silent: true,
pushState: true,
root: '/'
})
$(document).on('click', 'a[data-router="true"]', Router.intercept)
}
export default Router

View file

@ -7,10 +7,10 @@ import AutoLayout from './AutoLayout'
import Create from './Create'
import DataModel from './DataModel'
import Filter from './Filter'
import GlobalUI from './GlobalUI'
import GlobalUI, { ReactApp } from './GlobalUI'
import JIT from './JIT'
import Loading from './Loading'
import Map from './Map'
import Router from './Router'
import Selected from './Selected'
import Settings from './Settings'
import SynapseCard from './SynapseCard'
@ -36,48 +36,41 @@ const Topic = {
} else callback(DataModel.Topics.get(id))
},
launch: function(id) {
var start = function(data) {
Active.Topic = new DataModel.Topic(data.topic)
DataModel.Creators = new DataModel.MapperCollection(data.creators)
DataModel.Topics = new DataModel.TopicCollection([data.topic].concat(data.relatives))
DataModel.Synapses = new DataModel.SynapseCollection(data.synapses)
DataModel.attachCollectionEvents()
document.title = Active.Topic.get('name') + ' | Metamaps'
// set filter mapper H3 text
$('#filter_by_mapper h3').html('CREATORS')
// build and render the visualization
var dataIsReadySetupTopic = function() {
Visualize.type = 'RGraph'
JIT.prepareVizData()
// update filters
Filter.reset()
// reset selected arrays
Selected.reset()
// these three update the actual filter box with the right list items
Filter.reset()
Filter.checkMetacodes()
Filter.checkSynapses()
Filter.checkMappers()
// for mobile
$('#header_content').html(Active.Topic.get('name'))
document.title = Active.Topic.get('name') + ' | Metamaps'
ReactApp.mobileTitle = Active.Topic.get('name')
ReactApp.render()
}
if (Active.Topic && Active.Topic.id === id) {
dataIsReadySetupTopic()
}
else {
Loading.show()
$.ajax({
url: '/topics/' + id + '/network.json',
success: function(data) {
Active.Topic = new DataModel.Topic(data.topic)
DataModel.Creators = new DataModel.MapperCollection(data.creators)
DataModel.Topics = new DataModel.TopicCollection([data.topic].concat(data.relatives))
DataModel.Synapses = new DataModel.SynapseCollection(data.synapses)
DataModel.attachCollectionEvents()
dataIsReadySetupTopic()
}
})
}
$.ajax({
url: '/topics/' + id + '/network.json',
success: start
})
},
end: function() {
if (Active.Topic) {
$('.rightclickmenu').remove()
TopicCard.hideCard()
SynapseCard.hideCard()
Filter.close()
}
},
centerOn: function(nodeid, callback) {
@ -90,7 +83,6 @@ const Topic = {
if (callback) callback()
}
})
Router.navigate('/topics/' + nodeid)
Active.Topic = DataModel.Topics.get(nodeid)
}
},
@ -293,8 +285,7 @@ const Topic = {
return
}
// hide the 'double-click to add a topic' message
GlobalUI.hideDiv('#instructions')
Map.setHasLearnedTopicCreation(true)
$(document).trigger(Map.events.editedByActiveMapper)
@ -327,8 +318,7 @@ const Topic = {
getTopicFromAutocomplete: function(id) {
var self = Topic
// hide the 'double-click to add a topic' message
GlobalUI.hideDiv('#instructions')
Map.setHasLearnedTopicCreation(true)
$(document).trigger(Map.events.editedByActiveMapper)

View file

@ -10,14 +10,14 @@ import ReactDOM from 'react-dom'
import Active from '../Active'
import DataModel from '../DataModel'
import Realtime from '../Realtime'
import MapChat from '../../components/MapChat'
import ReactApp from '../GlobalUI/ReactApp'
const ChatView = {
isOpen: false,
unreadMessages: 0,
messages: new Backbone.Collection(),
conversationLive: false,
isParticipating: false,
mapChat: null,
domId: 'chat-box-wrapper',
init: function(urls) {
const self = ChatView
@ -34,46 +34,32 @@ const ChatView = {
},
setNewMap: function() {
const self = ChatView
self.unreadMessages = 0
self.isOpen = false
self.conversationLive = false
self.isParticipating = false
self.alertSound = true // whether to play sounds on arrival of new messages or not
self.cursorsShowing = true
self.videosShowing = true
self.participants = new Backbone.Collection()
self.messages = new Backbone.Collection()
self.render()
},
show: () => {
$('#' + ChatView.domId).show()
},
hide: () => {
$('#' + ChatView.domId).hide()
},
render: () => {
if (!Active.Map) return
const self = ChatView
self.mapChat = ReactDOM.render(React.createElement(MapChat, {
conversationLive: self.conversationLive,
isParticipating: self.isParticipating,
onOpen: self.onOpen,
onClose: self.onClose,
leaveCall: Realtime.leaveCall,
joinCall: Realtime.joinCall,
inviteACall: Realtime.inviteACall,
inviteToJoin: Realtime.inviteToJoin,
participants: self.participants.models.map(p => p.attributes),
messages: self.messages.models.map(m => m.attributes),
videoToggleClick: self.videoToggleClick,
cursorToggleClick: self.cursorToggleClick,
soundToggleClick: self.soundToggleClick,
inputBlur: self.inputBlur,
inputFocus: self.inputFocus,
handleInputMessage: self.handleInputMessage
}), document.getElementById(ChatView.domId))
ReactApp.render()
},
onOpen: () => {
const self = ChatView
self.isOpen = true
self.unreadMessages = 0
self.render()
$(document).trigger(ChatView.events.openTray)
},
onClose: () => {
const self = ChatView
self.isOpen = false
$(document).trigger(ChatView.events.closeTray)
},
addParticipant: participant => {
@ -119,12 +105,6 @@ const ChatView = {
ChatView.participants.forEach(p => p.set({isParticipating: false, isPending: false}))
ChatView.render()
},
close: () => {
ChatView.mapChat && ChatView.mapChat.close()
},
open: () => {
ChatView.mapChat && ChatView.mapChat.open()
},
videoToggleClick: function() {
ChatView.videosShowing = !ChatView.videosShowing
$(document).trigger(ChatView.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff)
@ -144,11 +124,10 @@ const ChatView = {
},
addMessage: (message, isInitial, wasMe) => {
const self = ChatView
if (!isInitial) self.mapChat.newMessage()
if (!isInitial && !self.isOpen) self.unreadMessages += 1
if (!wasMe && !isInitial && self.alertSound) self.sound.play('receivechat')
self.messages.add(message)
self.render()
if (!isInitial) self.mapChat.scroll()
if (!isInitial && self.isOpen) self.render()
},
sendChatMessage: message => {
var self = ChatView
@ -174,23 +153,9 @@ const ChatView = {
// passed to this function
addMessages: (messages, isInitial, wasMe) => {
messages.models.forEach(m => ChatView.addMessage(m, isInitial, wasMe))
},
reset: () => {
ChatView.mapChat && ChatView.mapChat.reset()
ChatView.participants && ChatView.participants.reset()
ChatView.messages && ChatView.messages.reset()
ChatView.render()
}
}
// ChatView.prototype.scrollMessages = function(duration) {
// duration = duration || 0
// this.$messages.animate({
// scrollTop: this.$messages[0].scrollHeight
// }, duration)
// }
/**
* @class
* @static

View file

@ -1,21 +1,68 @@
/* global $ */
import React from 'react'
import ReactDOM from 'react-dom' // TODO ensure this isn't a double import
import Active from '../Active'
import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI'
import Realtime from '../Realtime'
import GlobalUI, { ReactApp } from '../GlobalUI'
import Loading from '../Loading'
import Maps from '../../components/Maps'
const ExploreMaps = {
pending: false,
mapper: null,
updateFromPath: function(path) {
const self = ExploreMaps
const [_unused, generalSection, specificSection, id] = path.split('/')
if (generalSection === 'explore') {
const capitalize = specificSection.charAt(0).toUpperCase() + specificSection.slice(1)
self.setCollection(DataModel.Maps[capitalize])
switch (capitalize) {
case 'Active':
document.title = 'Explore Active Maps | Metamaps'
ReactApp.mobileTitle = 'Recently Active'
break
case 'Featured':
document.title = 'Explore Featured Maps | Metamaps'
ReactApp.mobileTitle = 'Featured Maps'
break
case 'Starred':
document.title = 'Starred Maps | Metamaps'
ReactApp.mobileTitle = 'Starred Maps'
break
case 'Shared':
document.title = 'Shared Maps | Metamaps'
ReactApp.mobileTitle = 'Shared With Me'
break
case 'Mine':
document.title = 'My Maps | Metamaps'
ReactApp.mobileTitle = 'My Maps'
break
}
} else if (generalSection === '') {
self.setCollection(DataModel.Maps.Active)
document.title = 'Explore Active Maps | Metamaps'
ReactApp.mobileTitle = 'Recently Active'
}
if (id) {
if (self.collection.mapperId !== id) {
// empty the collection if we are trying to load the maps
// collection of a different mapper than we had previously
self.collection.reset()
self.collection.page = 1
self.render()
}
self.collection.mapperId = id
}
if (self.collection.length === 0) {
Loading.show()
self.pending = true
self.collection.getMaps()
} else {
id ? self.fetchUserThenRender() : self.render()
}
},
setCollection: function(collection) {
var self = ExploreMaps
if (self.collection) {
self.collection.off('add', self.render)
self.collection.off('successOnFetch', self.handleSuccess)
@ -26,55 +73,9 @@ const ExploreMaps = {
self.collection.on('successOnFetch', self.handleSuccess)
self.collection.on('errorOnFetch', self.handleError)
},
render: function(cb) {
var self = ExploreMaps
if (!self.collection) return
var exploreObj = {
currentUser: Active.Mapper,
section: self.collection.id,
maps: self.collection,
juntoState: Realtime.juntoState,
moreToLoad: self.collection.page !== 'loadedAll',
user: self.collection.id === 'mapper' ? self.mapper : null,
loadMore: self.loadMore,
pending: self.pending,
onStar: function(map) {
$.post('/maps/' + map.id + '/star')
map.set('star_count', map.get('star_count') + 1)
if (DataModel.Stars) DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: map.id })
DataModel.Maps.Starred.add(map)
GlobalUI.notifyUser('Map is now starred')
self.render()
},
onRequest: function(map) {
$.post({
url: `/maps/${map.id}/access_request`
})
GlobalUI.notifyUser('You will be notified by email if request accepted')
},
onFollow: function(map) {
const isFollowing = map.isFollowedBy(Active.Mapper)
$.post({
url: `/maps/${map.id}/${isFollowing ? 'un' : ''}follow`
})
if (isFollowing) {
GlobalUI.notifyUser('You are no longer following this map')
Active.Mapper.unfollowMap(map.id)
} else {
GlobalUI.notifyUser('You are now following this map')
Active.Mapper.followMap(map.id)
}
self.render()
}
}
ReactDOM.render(
React.createElement(Maps, exploreObj),
document.getElementById('explore')
).resize()
if (cb) cb()
render: function() {
ReactApp.resize()
ReactApp.render()
Loading.hide()
},
loadMore: function() {
@ -85,14 +86,13 @@ const ExploreMaps = {
}
self.render()
},
handleSuccess: function(cb) {
handleSuccess: function() {
var self = ExploreMaps
self.pending = false
if (self.collection && self.collection.id === 'mapper') {
self.fetchUserThenRender(cb)
self.fetchUserThenRender()
} else {
self.render(cb)
Loading.hide()
self.render()
}
},
handleError: function() {
@ -103,8 +103,8 @@ const ExploreMaps = {
var self = ExploreMaps
if (self.mapper && self.mapper.id === self.collection.mapperId) {
self.render(cb)
return Loading.hide()
self.render()
return
}
// first load the mapper object and then call the render function
@ -112,14 +112,42 @@ const ExploreMaps = {
url: '/users/' + self.collection.mapperId + '/details.json',
success: function(response) {
self.mapper = response
self.render(cb)
Loading.hide()
document.title = self.mapper.name + ' | Metamaps'
ReactApp.mobileTitle = self.mapper.name
self.render()
},
error: function() {
self.render(cb)
Loading.hide()
self.render()
}
})
},
onStar: function(map) {
$.post('/maps/' + map.id + '/star')
map.set('star_count', map.get('star_count') + 1)
if (DataModel.Stars) DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: map.id })
DataModel.Maps.Starred.add(map)
GlobalUI.notifyUser('Map is now starred')
ReactApp.render()
},
onRequest: function(map) {
$.post({
url: `/maps/${map.id}/access_request`
})
GlobalUI.notifyUser('You will be notified by email if request accepted')
},
onMapFollow: function(map) {
const isFollowing = map.isFollowedBy(Active.Mapper)
$.post({
url: `/maps/${map.id}/${isFollowing ? 'un' : ''}follow`
})
if (isFollowing) {
GlobalUI.notifyUser('You are no longer following this map')
Active.Mapper.unfollowMap(map.id)
} else {
GlobalUI.notifyUser('You are now following this map')
Active.Mapper.followMap(map.id)
}
ReactApp.render()
}
}

View file

@ -5,69 +5,59 @@ import ReactDOM from 'react-dom'
import Active from '../Active'
import Visualize from '../Visualize'
import GlobalUI from '../GlobalUI'
import ReactTopicCard from '../../components/TopicCard'
import GlobalUI, { ReactApp } from '../GlobalUI'
const TopicCard = {
openTopicCard: null, // stores the topic that's currently open
openTopic: null, // stores the topic that's currently open
metacodeSets: [],
redrawCanvas: () => {
Visualize.mGraph.plot()
},
init: function(serverData) {
const self = TopicCard
self.metacodeSets = serverData.metacodeSets
},
populateShowCard: function(topic) {
onTopicFollow: topic => {
const self = TopicCard
ReactDOM.render(
React.createElement(ReactTopicCard, {
topic: topic,
ActiveMapper: Active.Mapper,
updateTopic: obj => {
topic.save(obj, { success: topic => self.populateShowCard(topic) })
},
onFollow: () => {
const isFollowing = topic.isFollowedBy(Active.Mapper)
$.post({
url: `/topics/${topic.id}/${isFollowing ? 'un' : ''}follow`
})
if (isFollowing) {
GlobalUI.notifyUser('You are no longer following this topic')
Active.Mapper.unfollowTopic(topic.id)
} else {
GlobalUI.notifyUser('You are now following this topic')
Active.Mapper.followTopic(topic.id)
}
self.populateShowCard(topic)
},
metacodeSets: self.metacodeSets,
redrawCanvas: () => {
Visualize.mGraph.plot()
}
}),
document.getElementById('showcard')
)
// initialize draggability
$('.showcard').draggable({
handle: '.metacodeImage',
stop: function() {
$(this).height('auto')
}
const isFollowing = topic.isFollowedBy(Active.Mapper)
$.post({
url: `/topics/${topic.id}/${isFollowing ? 'un' : ''}follow`
})
if (isFollowing) {
GlobalUI.notifyUser('You are no longer following this topic')
Active.Mapper.unfollowTopic(topic.id)
} else {
GlobalUI.notifyUser('You are now following this topic')
Active.Mapper.followTopic(topic.id)
}
self.render()
},
showCard: function(node, opts) {
updateTopic: (topic, obj) => {
const self = TopicCard
topic.save(obj, { success: self.render })
},
render: function() {
ReactApp.render()
},
showCard: function(node, opts = {}) {
var self = TopicCard
if (!opts) opts = {}
var topic = node.getData('topic')
self.openTopicCard = topic
// populate the card that's about to show with the right topics data
self.populateShowCard(topic)
return $('.showcard').fadeIn('fast', () => opts.complete && opts.complete())
self.openTopic = topic
self.render()
$('.showcard').fadeIn('fast', () => {
$('.showcard').draggable({
handle: '.metacodeImage',
stop: function() {
$(this).height('auto')
}
})
opts.complete && opts.complete()
})
},
hideCard: function() {
var self = TopicCard
$('.showcard').fadeOut('fast')
self.openTopicCard = null
self.openTopic = null
}
}

View file

@ -8,7 +8,6 @@ import Active from './Active'
import DataModel from './DataModel'
import JIT from './JIT'
import Loading from './Loading'
import Router from './Router'
import TopicCard from './Views/TopicCard'
const Visualize = {
@ -198,19 +197,6 @@ const Visualize = {
}
}
hold()
// update the url now that the map is ready
clearTimeout(Router.timeoutId)
Router.timeoutId = setTimeout(function() {
var m = Active.Map
var t = Active.Topic
if (m && window.location.pathname !== '/maps/' + m.id) {
Router.navigateAndTrack('/maps/' + m.id)
} else if (t && window.location.pathname !== '/topics/' + t.id) {
Router.navigateAndTrack('/topics/' + t.id)
}
}, 800)
},
clearVisualization: function() {
Visualize.mGraph.graph.empty()

View file

@ -9,8 +9,7 @@ import DataModel from './DataModel'
import Debug from './Debug'
import Filter from './Filter'
import GlobalUI, {
Search, CreateMap, ImportDialog, Account as GlobalUIAccount,
NotificationIcon
ReactApp, Search, CreateMap, ImportDialog
} from './GlobalUI'
import Import from './Import'
import JIT from './JIT'
@ -18,12 +17,10 @@ import Listeners from './Listeners'
import Loading from './Loading'
import Map, { CheatSheet, InfoBox } from './Map'
import Mapper from './Mapper'
import Mobile from './Mobile'
import Mouse from './Mouse'
import Organize from './Organize'
import PasteInput from './PasteInput'
import Realtime from './Realtime'
import Router from './Router'
import Selected from './Selected'
import Settings from './Settings'
import Synapse from './Synapse'
@ -45,11 +42,10 @@ Metamaps.DataModel = DataModel
Metamaps.Debug = Debug
Metamaps.Filter = Filter
Metamaps.GlobalUI = GlobalUI
Metamaps.GlobalUI.ReactApp = ReactApp
Metamaps.GlobalUI.Search = Search
Metamaps.GlobalUI.CreateMap = CreateMap
Metamaps.GlobalUI.Account = GlobalUIAccount
Metamaps.GlobalUI.ImportDialog = ImportDialog
Metamaps.GlobalUI.NotificationIcon = NotificationIcon
Metamaps.Import = Import
Metamaps.JIT = JIT
Metamaps.Listeners = Listeners
@ -59,12 +55,10 @@ Metamaps.Map.CheatSheet = CheatSheet
Metamaps.Map.InfoBox = InfoBox
Metamaps.Maps = {}
Metamaps.Mapper = Mapper
Metamaps.Mobile = Mobile
Metamaps.Mouse = Mouse
Metamaps.Organize = Organize
Metamaps.PasteInput = PasteInput
Metamaps.Realtime = Realtime
Metamaps.Router = Router
Metamaps.Selected = Selected
Metamaps.Settings = Settings
Metamaps.Synapse = Synapse
@ -86,26 +80,6 @@ document.addEventListener('DOMContentLoaded', function() {
Metamaps[prop].init(Metamaps.ServerData)
}
}
// load whichever page you are on
if (Metamaps.currentSection === 'explore') {
const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1)
Views.ExploreMaps.setCollection(DataModel.Maps[capitalize])
if (Metamaps.currentPage === 'mapper') {
Views.ExploreMaps.fetchUserThenRender()
} else {
Views.ExploreMaps.render()
}
GlobalUI.showDiv('#explore')
} else if (Metamaps.currentSection === '' && Active.Mapper) {
Views.ExploreMaps.setCollection(DataModel.Maps.Active)
Views.ExploreMaps.render()
GlobalUI.showDiv('#explore')
} else if (Active.Map || Active.Topic) {
Loading.show()
JIT.prepareVizData()
GlobalUI.showDiv('#infovis')
}
})
export default Metamaps

View file

@ -0,0 +1,47 @@
import React, { Component, PropTypes } from 'react'
import onClickOutsideAddon from 'react-onclickoutside'
class AccountMenu extends Component {
static propTypes = {
currentUser: PropTypes.object,
onInviteClick: PropTypes.func,
closeBox: PropTypes.func
}
handleClickOutside = () => {
this.props.closeBox()
}
render () {
const { currentUser, onInviteClick } = this.props
return <div>
<img className="sidebarAccountImage" src={currentUser.get('image')} width="48" height="48" />
<h3 className="accountHeader">{currentUser.get('name')}</h3>
<ul>
<li className="accountListItem accountSettings">
<div className="accountIcon"></div>
<a href={`/users/${currentUser.id}/edit`}>Settings</a>
</li>
<li className="accountListItem accountAdmin">
<div className="accountIcon"></div>
<a href="/metacodes">Admin</a>
</li>
<li className="accountListItem accountApps">
<div className="accountIcon"></div>
<a href="/oauth/authorized_applications">Apps</a>
</li>
<li className="accountListItem accountInvite" onClick={onInviteClick}>
<div className="accountIcon"></div>
<span>Share Invite</span>
</li>
<li className="accountListItem accountLogout">
<div className="accountIcon"></div>
<a id="Logout" href="/logout">Sign Out</a>
</li>
</ul>
</div>
}
}
export default onClickOutsideAddon(AccountMenu)

View file

@ -0,0 +1,57 @@
import React, { Component, PropTypes } from 'react'
import onClickOutsideAddon from 'react-onclickoutside'
class LoginForm extends Component {
static propTypes = {
loginFormAuthToken: PropTypes.string,
closeBox: PropTypes.func
}
constructor(props) {
super(props)
this.state = { token: '' }
}
componentDidMount() {
const token = document.head.getElementsByTagName('meta')['csrf-token'].content
this.setState({token})
}
emailInputDidMount(node) {
node.focus()
}
handleClickOutside = () => {
this.props.closeBox()
}
render () {
return <form className="loginAnywhere" id="new_user" action="/login" acceptCharset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓" />
<input type="hidden" name="authenticity_token" value={this.state.token} />
<div className="accountImage"></div>
<div className="accountInput accountEmail">
<input placeholder="Email" type="email" name="user[email]" id="user_email" ref={this.emailInputDidMount}/>
</div>
<div className="accountInput accountPassword">
<input placeholder="Password" type="password" name="user[password]" id="user_password" />
</div>
<div className="accountSubmit">
<input type="submit" name="commit" value="SIGN IN" />
</div>
<div className="accountRememberMe">
<label htmlFor="user_remember_me">Stay signed in</label>
<input name="user[remember_me]" type="hidden" value="0" />
<input type="checkbox" value="1" name="user[remember_me]" id="user_remember_me" />
<div className="clearfloat"></div>
</div>
<div className="clearfloat"></div>
<div className="accountForgotPass">
<a href="/users/password/new">Forgot password?</a>
</div>
</form>
}
}
export default onClickOutsideAddon(LoginForm)

View file

@ -0,0 +1,66 @@
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
class MobileHeader extends Component {
static propTypes = {
unreadNotificationsCount: PropTypes.number,
currentUser: PropTypes.object,
mobileTitle: PropTypes.string,
mobileTitleWidth: PropTypes.number,
onTitleClick: PropTypes.func
}
constructor(props) {
super(props)
this.state = {open: false}
}
toggle = () => {
this.setState({open: !this.state.open})
}
render() {
const { unreadNotificationsCount, currentUser, mobileTitle, mobileTitleWidth, onTitleClick } = this.props
const { open } = this.state
return <div>
<div id="mobile_header">
<div id="header_content" style={{width: `${mobileTitleWidth}px`}} onClick={onTitleClick}>
{mobileTitle}
</div>
<div id="menu_icon" onClick={this.toggle}>
{unreadNotificationsCount > 0 && <div className="unread-notifications-dot"></div>}
</div>
</div>
{open && <div id="mobile_menu">
{currentUser && <ul onClick={this.toggle}>
<li className="mobileMenuUser">
<Link to={`/explore/mapper/${currentUser.id}`}>
<img src={currentUser.get('image')} width="32" height="32" />
<span>{currentUser.get('name')}</span>
</Link>
</li>
<li><a href="/maps/new">New Map</a></li>
<li><Link to="/explore/mine">My Maps</Link></li>
<li><Link to="/explore/shared">Shared With Me</Link></li>
<li><Link to="/explore/starred">Starred By Me</Link></li>
<li><Link to="/explore/active">All Maps</Link></li>
<li><a href={`/users/${currentUser.id}/edit`}>Account</a></li>
<li className="notifications">
<a href="/notifications">Notifications</a>
{unreadNotificationsCount > 0 && <div className="unread-notifications-dot"></div>}
</li>
<li><a id="Logout" href="/logout">Sign Out</a></li>
</ul>}
{!currentUser && <ul onClick={this.toggle}>
<li><a href="/">Home</a></li>
<li><Link to="/explore/active">All Maps</Link></li>
<li><Link to="/explore/featured">Featured Maps</Link></li>
<li><a href="/request">Request Invite</a></li>
<li><a href="/login">Login</a></li>
</ul>}
</div>}
</div>
}
}
export default MobileHeader

View file

@ -1,6 +1,11 @@
import React, { PropTypes, Component } from 'react'
class NotificationIcon extends Component {
static propTypes = {
unreadNotificationsCount: PropTypes.number
}
constructor(props) {
super(props)
@ -31,8 +36,4 @@ class NotificationIcon extends Component {
}
}
NotificationIcon.propTypes = {
unreadNotificationsCount: PropTypes.number
}
export default NotificationIcon

View file

@ -0,0 +1,15 @@
import React, { Component, PropTypes } from 'react'
class Toast extends Component {
static propTypes = {
message: PropTypes.string
}
render () {
const { message } = this.props
const html = {__html: message}
return message ? <p id="toast" className="toast" dangerouslySetInnerHTML={html} /> : null
}
}
export default Toast

View file

@ -0,0 +1,38 @@
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
class UpperLeftUI extends Component {
static propTypes = {
currentUser: PropTypes.object,
map: PropTypes.object,
userRequested: PropTypes.bool,
requestAnswered: PropTypes.bool,
requestApproved: PropTypes.bool,
onRequestClick: PropTypes.func
}
render () {
const { map, currentUser, userRequested, requestAnswered, requestApproved, onRequestClick } = this.props
return <div className="upperLeftUI">
<div className="homeButton">
{currentUser && <Link to="/">METAMAPS</Link>}
{!currentUser && <a href="/">METAMAPS</a>}
</div>
<div className="sidebarSearch">
<input type="text" className="sidebarSearchField" placeholder="Search for topics, maps, and mappers..." />
<div id="searchLoading"></div>
<div className="sidebarSearchIcon"></div>
<div className="clearfloat"></div>
</div>
{map && !map.authorizeToEdit(currentUser) && <div className="viewOnly">
<div className="eyeball">View Only</div>
{currentUser && !userRequested && <div className="requestAccess requestNotice" onClick={onRequestClick}>Request Access</div>}
{userRequested && !requestAnswered && <div className="requestPending requestNotice">Request Pending</div>}
{userRequested && requestAnswered && !requestApproved && <div className="requestNotAccepted requestNotice">Request Not Accepted</div>}
</div>}
<div className="clearfloat"></div>
</div>
}
}
export default UpperLeftUI

View file

@ -0,0 +1,58 @@
import React, { Component, PropTypes } from 'react'
import AccountMenu from './AccountMenu'
import LoginForm from './LoginForm'
import NotificationIcon from './NotificationIcon'
class UpperRightUI extends Component {
static propTypes = {
currentUser: PropTypes.object,
signInPage: PropTypes.bool,
unreadNotificationsCount: PropTypes.number,
openInviteLightbox: PropTypes.func
}
constructor(props) {
super(props)
this.state = {accountBoxOpen: false}
}
reset = () => {
this.setState({accountBoxOpen: false})
}
toggleAccountBox = () => {
this.setState({accountBoxOpen: !this.state.accountBoxOpen})
}
render () {
const { currentUser, signInPage, unreadNotificationsCount, openInviteLightbox } = this.props
const { accountBoxOpen } = this.state
return <div className="upperRightUI">
{currentUser && <a href="/maps/new" target="_blank" className="addMap upperRightEl upperRightIcon">
<div className="tooltipsUnder">
Create New Map
</div>
</a>}
{currentUser && <span id="notification_icon">
<NotificationIcon unreadNotificationsCount={unreadNotificationsCount} />
</span>}
{!signInPage && <div className="sidebarAccount upperRightEl">
<div className="sidebarAccountIcon ignore-react-onclickoutside" onClick={this.toggleAccountBox}>
<div className="tooltipsUnder">Account</div>
{currentUser && <img src={currentUser.get('image')} />}
{!currentUser && 'SIGN IN'}
{!currentUser && <div className="accountInnerArrow"></div>}
</div>
{accountBoxOpen && <div className="sidebarAccountBox upperRightBox">
{currentUser
? <AccountMenu onInviteClick={openInviteLightbox} currentUser={currentUser} closeBox={this.reset} />
: <LoginForm closeBox={this.reset} />}
</div>}
</div>}
<div className="clearfloat"></div>
</div>
}
}
export default UpperRightUI

View file

@ -0,0 +1,67 @@
import React, { Component, PropTypes } from 'react'
import MobileHeader from './MobileHeader'
import UpperLeftUI from './UpperLeftUI'
import UpperRightUI from './UpperRightUI'
import Toast from './Toast'
class App extends Component {
static propTypes = {
children: PropTypes.object,
toast: PropTypes.string,
unreadNotificationsCount: PropTypes.number,
location: PropTypes.object,
mobile: PropTypes.bool,
mobileTitle: PropTypes.string,
mobileTitleWidth: PropTypes.number,
mobileTitleClick: PropTypes.func,
openInviteLightbox: PropTypes.func,
map: PropTypes.object,
userRequested: PropTypes.bool,
requestAnswered: PropTypes.bool,
requestApproved: PropTypes.bool,
onRequestAccess: PropTypes.func
}
static childContextTypes = {
location: PropTypes.object
}
getChildContext () {
const { location } = this.props
return {location}
}
render () {
const { children, toast, unreadNotificationsCount, openInviteLightbox,
mobile, mobileTitle, mobileTitleWidth, mobileTitleClick, location,
map, userRequested, requestAnswered, requestApproved,
onRequestAccess } = this.props
const { pathname } = location || {}
// this fixes a bug that happens otherwise when you logout
const currentUser = this.props.currentUser && this.props.currentUser.id ? this.props.currentUser : null
const unauthedHome = pathname === '/' && !currentUser
return <div className="wrapper" id="wrapper">
{mobile && <MobileHeader currentUser={currentUser}
unreadNotificationsCount={unreadNotificationsCount}
mobileTitle={mobileTitle}
mobileTitleWidth={mobileTitleWidth}
onTitleClick={mobileTitleClick} />}
{!unauthedHome && <UpperLeftUI currentUser={currentUser}
map={map}
userRequested={userRequested}
requestAnswered={requestAnswered}
requestApproved={requestApproved}
onRequestClick={onRequestAccess} />}
{!mobile && <UpperRightUI currentUser={currentUser}
unreadNotificationsCount={unreadNotificationsCount}
openInviteLightbox={openInviteLightbox}
signInPage={pathname === '/login'} />}
<Toast message={toast} />
{!mobile && currentUser && <a className='feedback-icon' target='_blank' href='https://hylo.com/c/metamaps'></a>}
{children}
</div>
}
}
export default App

View file

@ -1,193 +0,0 @@
import React, { PropTypes, Component } from 'react'
import Unread from './Unread'
import Participant from './Participant'
import Message from './Message'
import NewMessage from './NewMessage'
import Util from '../../Metamaps/Util'
function makeList(messages) {
let currentHeader
return messages ? messages.map(m => {
let heading = false
if (!currentHeader) {
heading = true
currentHeader = m
} else {
// not same user or time diff of greater than 3 minutes
heading = m.user_id !== currentHeader.user_id || Math.floor(Math.abs(new Date(currentHeader.created_at) - new Date(m.created_at)) / 60000) > 3
currentHeader = heading ? m : currentHeader
}
return <Message {...m} key={m.id} heading={heading}/>
}) : null
}
class MapChat extends Component {
constructor(props) {
super(props)
this.state = {
unreadMessages: 0,
open: false,
messageText: '',
alertSound: true, // whether to play sounds on arrival of new messages or not
cursorsShowing: true,
videosShowing: true
}
}
reset = () => {
this.setState({
unreadMessages: 0,
open: false,
messageText: '',
alertSound: true, // whether to play sounds on arrival of new messages or not
cursorsShowing: true,
videosShowing: true
})
}
close = () => {
this.setState({open: false})
this.props.onClose()
this.messageInput.blur()
}
open = () => {
this.scroll()
this.setState({open: true, unreadMessages: 0})
this.props.onOpen()
this.messageInput.focus()
}
newMessage = () => {
if (!this.state.open) this.setState({unreadMessages: this.state.unreadMessages + 1})
}
scroll = () => {
this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight
}
toggleDrawer = () => {
if (this.state.open) this.close()
else if (!this.state.open) this.open()
}
toggleAlertSound = () => {
this.setState({alertSound: !this.state.alertSound})
this.props.soundToggleClick()
}
toggleCursorsShowing = () => {
this.setState({cursorsShowing: !this.state.cursorsShowing})
this.props.cursorToggleClick()
}
toggleVideosShowing = () => {
this.setState({videosShowing: !this.state.videosShowing})
this.props.videoToggleClick()
}
handleChange = key => e => {
this.setState({
[key]: e.target.value
})
}
handleTextareaKeyUp = e => {
if (e.which === 13) {
e.preventDefault()
const text = Util.removeEmoji(this.state.messageText)
this.props.handleInputMessage(text)
this.setState({ messageText: '' })
}
}
focusMessageInput = () => {
if (!this.messageInput) return
this.messageInput.focus()
}
render = () => {
const rightOffset = this.state.open ? '0' : '-300px'
const { conversationLive, isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props
const { videosShowing, cursorsShowing, alertSound, unreadMessages } = this.state
return (
<div className="chat-box"
style={{ right: rightOffset }}
>
<div className="junto-header">
PARTICIPANTS
<div onClick={this.toggleVideosShowing} className={`video-toggle ${videosShowing ? '' : 'active'}`} />
<div onClick={this.toggleCursorsShowing} className={`cursor-toggle ${cursorsShowing ? '' : 'active'}`} />
</div>
<div className="participants">
{conversationLive && <div className="conversation-live">
LIVE
{isParticipating && <span className="call-action leave" onClick={this.props.leaveCall}>
LEAVE
</span>}
{!isParticipating && <span className="call-action join" onClick={this.props.joinCall}>
JOIN
</span>}
</div>}
{participants.map(participant => <Participant
key={participant.id}
{...participant}
inviteACall={inviteACall}
inviteToJoin={inviteToJoin}
conversationLive={conversationLive}
mapperIsLive={isParticipating}/>
)}
</div>
<div className="chat-header">
CHAT
<div onClick={this.toggleAlertSound} className={`sound-toggle ${alertSound ? '' : 'active'}`}></div>
</div>
<div className={`chat-button ${conversationLive ? 'active' : ''}`} onClick={this.toggleDrawer}>
<div className="tooltips">Chat</div>
<Unread count={unreadMessages} />
</div>
<div className="chat-messages" ref={div => { this.messagesDiv = div }}>
{makeList(messages)}
</div>
<NewMessage messageText={this.state.messageText}
focusMessageInput={this.focusMessageInput}
handleChange={this.handleChange('messageText')}
textAreaProps={{
className: 'chat-input',
ref: textarea => { this.messageInput = textarea },
placeholder: 'Send a message...',
onKeyUp: this.handleTextareaKeyUp,
onFocus: this.props.inputFocus,
onBlur: this.props.inputBlur
}}
/>
</div>
)
}
}
MapChat.propTypes = {
conversationLive: PropTypes.bool,
isParticipating: PropTypes.bool,
onOpen: PropTypes.func,
onClose: PropTypes.func,
leaveCall: PropTypes.func,
joinCall: PropTypes.func,
inviteACall: PropTypes.func,
inviteToJoin: PropTypes.func,
videoToggleClick: PropTypes.func,
cursorToggleClick: PropTypes.func,
soundToggleClick: PropTypes.func,
participants: PropTypes.arrayOf(PropTypes.shape({
color: PropTypes.string, // css color
id: PropTypes.number,
image: PropTypes.string, // image url
self: PropTypes.bool,
username: PropTypes.string,
isParticipating: PropTypes.bool,
isPending: PropTypes.bool
}))
}
export default MapChat

View file

@ -0,0 +1,23 @@
import React, { Component, PropTypes } from 'react'
class Instructions extends Component {
static propTypes = {
mobile: PropTypes.bool,
hasLearnedTopicCreation: PropTypes.bool
}
render() {
const { hasLearnedTopicCreation, mobile } = this.props
return hasLearnedTopicCreation ? null : <div id="instructions">
{!mobile && <div className="addTopic">
Double-click to<br/>add a topic
</div>}
{mobile && <div className="addTopic">
Double-tap to<br/>add a topic
</div>}
</div>
}
}
export default Instructions

View file

@ -1,6 +1,6 @@
import React from 'react'
import Autolinker from 'autolinker'
import Util from '../../Metamaps/Util'
import Util from '../../../Metamaps/Util'
const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false })

View file

@ -0,0 +1,188 @@
import React, { PropTypes, Component } from 'react'
import Unread from './Unread'
import Participant from './Participant'
import Message from './Message'
import NewMessage from './NewMessage'
import Util from '../../../Metamaps/Util'
function makeList(messages) {
let currentHeader
return messages ? messages.map(m => {
let heading = false
if (!currentHeader) {
heading = true
currentHeader = m
} else {
// not same user or time diff of greater than 3 minutes
heading = m.user_id !== currentHeader.user_id || Math.floor(Math.abs(new Date(currentHeader.created_at) - new Date(m.created_at)) / 60000) > 3
currentHeader = heading ? m : currentHeader
}
return <Message {...m} key={m.id} heading={heading}/>
}) : null
}
class MapChat extends Component {
constructor(props) {
super(props)
this.state = {
messageText: '',
alertSound: true, // whether to play sounds on arrival of new messages or not
cursorsShowing: true,
videosShowing: true
}
}
componentDidUpdate(prevProps) {
const { messages } = this.props
const prevMessages = prevProps.messages
if (messages.length !== prevMessages.length) setTimeout(() => this.scroll(), 50)
}
reset = () => {
this.setState({
messageText: '',
alertSound: true, // whether to play sounds on arrival of new messages or not
cursorsShowing: true,
videosShowing: true
})
}
close = () => {
this.props.onClose()
}
open = () => {
this.props.onOpen()
setTimeout(() => this.scroll(), 50)
}
scroll = () => {
// hack: figure out how to do this right
this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight + 100
}
toggleDrawer = () => {
if (this.props.chatOpen) this.close()
else if (!this.props.chatOpen) this.open()
}
toggleAlertSound = () => {
this.setState({alertSound: !this.state.alertSound})
this.props.soundToggleClick()
}
toggleCursorsShowing = () => {
this.setState({cursorsShowing: !this.state.cursorsShowing})
this.props.cursorToggleClick()
}
toggleVideosShowing = () => {
this.setState({videosShowing: !this.state.videosShowing})
this.props.videoToggleClick()
}
handleChange = key => e => {
this.setState({
[key]: e.target.value
})
}
handleTextareaKeyUp = e => {
if (e.which === 13) {
e.preventDefault()
const text = Util.removeEmoji(this.state.messageText)
this.props.handleInputMessage(text)
this.setState({ messageText: '' })
}
}
render = () => {
const { unreadMessages, chatOpen, conversationLive,
isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props
const { videosShowing, cursorsShowing, alertSound } = this.state
const rightOffset = chatOpen ? '0' : '-300px'
return (
<div id="chat-box-wrapper">
<div className="chat-box"
style={{ right: rightOffset }}
>
<div className="junto-header">
PARTICIPANTS
<div onClick={this.toggleVideosShowing} className={`video-toggle ${videosShowing ? '' : 'active'}`} />
<div onClick={this.toggleCursorsShowing} className={`cursor-toggle ${cursorsShowing ? '' : 'active'}`} />
</div>
<div className="participants">
{conversationLive && <div className="conversation-live">
LIVE
{isParticipating && <span className="call-action leave" onClick={this.props.leaveCall}>
LEAVE
</span>}
{!isParticipating && <span className="call-action join" onClick={this.props.joinCall}>
JOIN
</span>}
</div>}
{participants.map(participant => <Participant
key={participant.id}
{...participant}
inviteACall={inviteACall}
inviteToJoin={inviteToJoin}
conversationLive={conversationLive}
mapperIsLive={isParticipating}/>
)}
</div>
<div className="chat-header">
CHAT
<div onClick={this.toggleAlertSound} className={`sound-toggle ${alertSound ? '' : 'active'}`}></div>
</div>
<div className={`chat-button ${conversationLive ? 'active' : ''}`} onClick={this.toggleDrawer}>
<div className="tooltips">Chat</div>
<Unread count={unreadMessages} />
</div>
<div className="chat-messages" ref={div => { this.messagesDiv = div }}>
{makeList(messages)}
</div>
{chatOpen && <NewMessage messageText={this.state.messageText}
focusMessageInput={this.focusMessageInput}
handleChange={this.handleChange('messageText')}
textAreaProps={{
className: 'chat-input',
ref: textarea => { textarea && textarea.focus() },
placeholder: 'Send a message...',
onKeyUp: this.handleTextareaKeyUp,
onFocus: this.props.inputFocus,
onBlur: this.props.inputBlur
}}
/>}
</div>
</div>
)
}
}
MapChat.propTypes = {
unreadMessages: PropTypes.number,
chatOpen: PropTypes.bool,
conversationLive: PropTypes.bool,
isParticipating: PropTypes.bool,
onOpen: PropTypes.func,
onClose: PropTypes.func,
leaveCall: PropTypes.func,
joinCall: PropTypes.func,
inviteACall: PropTypes.func,
inviteToJoin: PropTypes.func,
videoToggleClick: PropTypes.func,
cursorToggleClick: PropTypes.func,
soundToggleClick: PropTypes.func,
participants: PropTypes.arrayOf(PropTypes.shape({
color: PropTypes.string, // css color
id: PropTypes.number,
image: PropTypes.string, // image url
self: PropTypes.bool,
username: PropTypes.string,
isParticipating: PropTypes.bool,
isPending: PropTypes.bool
}))
}
export default MapChat

View file

@ -0,0 +1,23 @@
import React, { Component, PropTypes } from 'react'
class MapInfoBox extends Component {
static propTypes = {
currentUser: PropTypes.object,
map: PropTypes.object,
infoBoxHtml: PropTypes.string
}
render () {
const { currentUser, map, infoBoxHtml } = this.props
if (!map) return null
const html = {__html: infoBoxHtml}
const isCreator = map.authorizePermissionChange(currentUser)
const canEdit = map.authorizeToEdit(currentUser)
let classes = 'mapInfoBox mapElement mapElementHidden permission '
classes += isCreator ? 'yourMap' : ''
classes += canEdit ? ' canEdit' : ''
return <div className={classes} dangerouslySetInnerHTML={html}></div>
}
}
export default MapInfoBox

View file

@ -0,0 +1,127 @@
import React, { Component, PropTypes } from 'react'
import DataVis from '../common/DataVis'
import UpperOptions from '../common/UpperOptions'
import InfoAndHelp from '../common/InfoAndHelp'
import Instructions from './Instructions'
import VisualizationControls from '../common/VisualizationControls'
import MapChat from './MapChat'
import TopicCard from '../TopicCard'
export default class MapView extends Component {
static propTypes = {
mobile: PropTypes.bool,
mapId: PropTypes.string,
map: PropTypes.object,
mapIsStarred: PropTypes.bool,
onMapStar: PropTypes.func,
onMapUnstar: PropTypes.func,
filterData: PropTypes.object,
allForFiltering: PropTypes.object,
visibleForFiltering: PropTypes.object,
toggleMetacode: PropTypes.func,
toggleMapper: PropTypes.func,
toggleSynapse: PropTypes.func,
filterAllMetacodes: PropTypes.func,
filterAllMappers: PropTypes.func,
filterAllSynapses: PropTypes.func,
toggleMapInfoBox: PropTypes.func,
infoBoxHtml: PropTypes.string,
currentUser: PropTypes.object,
endActiveMap: PropTypes.func,
launchNewMap: PropTypes.func,
openImportLightbox: PropTypes.func,
forkMap: PropTypes.func,
openHelpLightbox: PropTypes.func,
onZoomExtents: PropTypes.func,
onZoomIn: PropTypes.func,
onZoomOut: PropTypes.func,
hasLearnedTopicCreation: PropTypes.bool
}
constructor(props) {
super(props)
this.state = {
chatOpen: false
}
}
componentWillUnmount() {
this.endMap()
}
endMap() {
this.setState({
chatOpen: false
})
this.mapChat.reset()
this.upperOptions.reset()
this.props.endActiveMap()
}
componentDidUpdate(prevProps) {
const oldMapId = prevProps.mapId
const { mapId, launchNewMap } = this.props
if (!oldMapId && mapId) launchNewMap(mapId)
else if (oldMapId && mapId && oldMapId !== mapId) {
this.endMap()
launchNewMap(mapId)
}
else if (oldMapId && !mapId) this.endMap()
}
render = () => {
const { mobile, map, currentUser, onOpen, onClose,
toggleMapInfoBox, infoBoxHtml, allForFiltering, visibleForFiltering,
toggleMetacode, toggleMapper, toggleSynapse, filterAllMetacodes,
filterAllMappers, filterAllSynapses, filterData,
openImportLightbox, forkMap, openHelpLightbox,
mapIsStarred, onMapStar, onMapUnstar,
onZoomExtents, onZoomIn, onZoomOut, hasLearnedTopicCreation } = this.props
const { chatOpen } = this.state
const onChatOpen = () => {
this.setState({chatOpen: true})
onOpen()
}
const onChatClose = () => {
this.setState({chatOpen: false})
onClose()
}
const canEditMap = map && map.authorizeToEdit(currentUser)
// TODO: stop using {...this.props} and make explicit
return <div className="mapWrapper">
<UpperOptions ref={x => this.upperOptions = x}
map={map}
currentUser={currentUser}
onImportClick={openImportLightbox}
onForkClick={forkMap}
canEditMap={canEditMap}
filterData={filterData}
allForFiltering={allForFiltering}
visibleForFiltering={visibleForFiltering}
toggleMetacode={toggleMetacode}
toggleMapper={toggleMapper}
toggleSynapse={toggleSynapse}
filterAllMetacodes={filterAllMetacodes}
filterAllMappers={filterAllMappers}
filterAllSynapses={filterAllSynapses} />
<DataVis />
<TopicCard {...this.props} />
{currentUser && <Instructions mobile={mobile} hasLearnedTopicCreation={hasLearnedTopicCreation} />}
{currentUser && <MapChat {...this.props} onOpen={onChatOpen} onClose={onChatClose} chatOpen={chatOpen} ref={x => this.mapChat = x} />}
<VisualizationControls map={map}
onClickZoomExtents={onZoomExtents}
onClickZoomIn={onZoomIn}
onClickZoomOut={onZoomOut} />
<InfoAndHelp mapIsStarred={mapIsStarred}
currentUser={currentUser}
map={map}
onInfoClick={toggleMapInfoBox}
onMapStar={onMapStar}
onMapUnstar={onMapUnstar}
onHelpClick={openHelpLightbox}
infoBoxHtml={infoBoxHtml} />
</div>
}
}

View file

@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import _ from 'lodash'
const MapLink = props => {
@ -9,16 +10,16 @@ const MapLink = props => {
}
return (
<a { ...otherProps } href={href} className={linkClass}>
<Link { ...otherProps } to={href} className={linkClass}>
<div className="exploreMapsIcon"></div>
{text}
</a>
</Link>
)
}
class Header extends Component {
render = () => {
const { signedIn, section } = this.props
const { signedIn, section, user } = this.props
const activeClass = (title) => {
let forClass = 'exploreMapsButton'
@ -39,38 +40,33 @@ class Header extends Component {
<MapLink show={explore}
href={signedIn ? '/' : '/explore/active'}
linkClass={activeClass('active')}
data-router="true"
text="All Maps"
/>
<MapLink show={signedIn && explore}
href="/explore/mine"
linkClass={activeClass('my')}
data-router="true"
text="My Maps"
/>
<MapLink show={signedIn && explore}
href="/explore/shared"
linkClass={activeClass('shared')}
data-router="true"
text="Shared With Me"
/>
<MapLink show={signedIn && explore}
href="/explore/starred"
linkClass={activeClass('starred')}
data-router="true"
text="Starred By Me"
/>
<MapLink show={!signedIn && explore}
href="/explore/featured"
linkClass={activeClass('featured')}
data-router="true"
text="Featured Maps"
/>
{mapper ? (
<div className='exploreMapsButton active mapperButton'>
<img className='exploreMapperImage' width='24' height='24' src={this.props.user.image} />
<div className='exploreMapperName'>{this.props.user.name}&rsquo;s Maps</div>
{user && <img className='exploreMapperImage' width='24' height='24' src={user.image} />}
{user && <div className='exploreMapperName'>{user.name}&rsquo;s Maps</div>}
<div className='clearfloat'></div>
</div>
) : null }

View file

@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import { find, values } from 'lodash'
import Util from '../../Metamaps/Util'
@ -24,7 +25,7 @@ class Menu extends Component {
}
render = () => {
const { currentUser, map, onStar, onRequest, onFollow } = this.props
const { currentUser, map, onStar, onRequest, onMapFollow } = this.props
const isFollowing = map.isFollowedBy(currentUser)
const style = { display: this.state.open ? 'block' : 'none' }
@ -37,7 +38,7 @@ class Menu extends Component {
<ul className='menuItems' style={ style }>
<li className='star' onClick={ () => { this.toggle() && onStar(map) }}>Star Map</li>
{ !map.authorizeToEdit(currentUser) && <li className='request' onClick={ () => { this.toggle() && onRequest(map) }}>Request Access</li> }
{ Util.isTester(currentUser) && <li className='follow' onClick={ () => { this.toggle() && onFollow(map) }}>{isFollowing ? 'Unfollow' : 'Follow'}</li> }
{ Util.isTester(currentUser) && <li className='follow' onClick={ () => { this.toggle() && onMapFollow(map) }}>{isFollowing ? 'Unfollow' : 'Follow'}</li> }
</ul>
</div>
}
@ -47,7 +48,7 @@ Menu.propTypes = {
map: PropTypes.object.isRequired,
onStar: PropTypes.func.isRequired,
onRequest: PropTypes.func.isRequired,
onFollow: PropTypes.func.isRequired
onMapFollow: PropTypes.func.isRequired
}
const Metadata = (props) => {
@ -78,13 +79,13 @@ const Metadata = (props) => {
}
const checkAndWrapInA = (shouldWrap, classString, mapId, element) => {
if (shouldWrap) return <a className={ classString } href={ `/maps/${mapId}` } data-router="true">{ element }</a>
if (shouldWrap) return <Link className={ classString } to={ `/maps/${mapId}` } >{ element }</Link>
else return element
}
class MapCard extends Component {
render = () => {
const { map, mobile, juntoState, currentUser, onRequest, onStar, onFollow } = this.props
const { map, mobile, juntoState, currentUser, onRequest, onStar, onMapFollow } = this.props
const hasMap = (juntoState.liveMaps[map.id] && values(juntoState.liveMaps[map.id]).length) || null
const realtimeMap = juntoState.liveMaps[map.id]
@ -135,7 +136,7 @@ class MapCard extends Component {
</div>) }
{ !mobile && hasMapper && <div className='mapHasMapper'><MapperList mappers={ mapperList } /></div> }
{ !mobile && hasConversation && <div className='mapHasConversation'><MapperList mappers={ mapperList } /></div> }
{ !mobile && currentUser && <Menu currentUser={ currentUser } map={ map } onStar= { onStar } onRequest={ onRequest } onFollow={ onFollow } /> }
{ !mobile && currentUser && <Menu currentUser={ currentUser } map={ map } onStar= { onStar } onRequest={ onRequest } onMapFollow={ onMapFollow } /> }
</div>
</div>) }
</div>
@ -150,7 +151,7 @@ MapCard.propTypes = {
currentUser: PropTypes.object,
onStar: PropTypes.func.isRequired,
onRequest: PropTypes.func.isRequired,
onFollow: PropTypes.func.isRequired
onMapFollow: PropTypes.func.isRequired
}
export default MapCard

View file

@ -4,59 +4,56 @@ import Header from './Header'
import MapperCard from './MapperCard'
import MapCard from './MapCard'
// 220 wide + 16 padding on both sides
const MAP_WIDTH = 252
const MOBILE_VIEW_BREAKPOINT = 504
const MOBILE_VIEW_PADDING = 40
const MAX_COLUMNS = 4
class Maps extends Component {
constructor(props) {
super(props)
this.state = { mapsWidth: 0 }
static propTypes = {
section: PropTypes.string,
maps: PropTypes.object,
juntoState: PropTypes.object,
moreToLoad: PropTypes.bool,
user: PropTypes.object,
currentUser: PropTypes.object,
loadMore: PropTypes.func,
pending: PropTypes.bool,
onStar: PropTypes.func,
onRequest: PropTypes.func,
onMapFollow: PropTypes.func,
mapsWidth: PropTypes.number,
mobile: PropTypes.bool
}
componentDidMount() {
window && window.addEventListener('resize', this.resize)
this.refs.maps.addEventListener('scroll', throttle(this.scroll, 500, { leading: true, trailing: false }))
this.resize()
static contextTypes = {
location: PropTypes.object
}
componentWillUnmount() {
window && window.removeEventListener('resize', this.resize)
}
resize = () => {
const { maps, user, currentUser } = this.props
const numCards = maps.length + (user || currentUser ? 1 : 0)
const mapSpaces = Math.floor(document.body.clientWidth / MAP_WIDTH)
const mapsWidth = document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT
? document.body.clientWidth - MOBILE_VIEW_PADDING
: Math.min(MAX_COLUMNS, Math.min(numCards, mapSpaces)) * MAP_WIDTH
this.setState({ mapsWidth })
mapsDidMount = (node) => {
if (node) {
this.mapsDiv = node
node.addEventListener('scroll', throttle(this.scroll, 500, { leading: true, trailing: false }))
}
}
scroll = () => {
const { loadMore, moreToLoad, pending } = this.props
const { maps } = this.refs
if (moreToLoad && !pending && maps.scrollTop + maps.offsetHeight > maps.scrollHeight - 300) {
const { mapsDiv } = this
if (moreToLoad && !pending && mapsDiv.scrollTop + mapsDiv.offsetHeight > mapsDiv.scrollHeight - 300) {
loadMore()
}
}
render = () => {
const { maps, currentUser, juntoState, pending, section, user, onStar, onRequest, onFollow } = this.props
const style = { width: this.state.mapsWidth + 'px' }
const mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT
const { mobile, maps, mapsWidth, currentUser, juntoState, pending, section, user, onStar, onRequest, onMapFollow } = this.props
const style = { width: mapsWidth + 'px' }
if (!maps) return null // do loading here instead
return (
<div>
<div id='exploreMaps' ref='maps'>
<div id='exploreMaps' ref={this.mapsDidMount}>
<div style={ style }>
{ user ? <MapperCard user={ user } /> : null }
{ currentUser && !user && !(pending && maps.length === 0) ? <div className="map newMap"><a href="/maps/new"><div className="newMapImage"></div><span>Create new map...</span></a></div> : null }
{ maps.models.map(map => <MapCard key={ map.id } map={ map } mobile={ mobile } juntoState={ juntoState } currentUser={ currentUser } onStar={ onStar } onRequest={ onRequest } onFollow={ onFollow } />) }
{ maps.models.map(map => <MapCard key={ map.id } map={ map } mobile={ mobile } juntoState={ juntoState } currentUser={ currentUser } onStar={ onStar } onRequest={ onRequest } onMapFollow={ onMapFollow } />) }
<div className='clearfloat'></div>
</div>
</div>
@ -69,18 +66,4 @@ class Maps extends Component {
}
}
Maps.propTypes = {
section: PropTypes.string.isRequired,
maps: PropTypes.object.isRequired,
juntoState: PropTypes.object.isRequired,
moreToLoad: PropTypes.bool.isRequired,
user: PropTypes.object,
currentUser: PropTypes.object,
loadMore: PropTypes.func,
pending: PropTypes.bool.isRequired,
onStar: PropTypes.func.isRequired,
onRequest: PropTypes.func.isRequired,
onFollow: PropTypes.func.isRequired
}
export default Maps

View file

@ -2,8 +2,8 @@ import React, { PropTypes, Component } from 'react'
class Follow extends Component {
render = () => {
const { isFollowing, onFollow } = this.props
return <div className='topicFollow' onClick={onFollow}>
const { isFollowing, onTopicFollow } = this.props
return <div className='topicFollow' onClick={onTopicFollow}>
{isFollowing ? 'Unfollow' : 'Follow'}
</div>
}
@ -11,7 +11,7 @@ class Follow extends Component {
Follow.propTypes = {
isFollowing: PropTypes.bool,
onFollow: PropTypes.func
onTopicFollow: PropTypes.func
}
export default Follow

View file

@ -1,6 +1,7 @@
/* global $ */
import React, { PropTypes, Component } from 'react'
import { Link } from 'react-router'
import MetacodeSelect from '../MetacodeSelect'
import Permission from './Permission'
@ -52,21 +53,21 @@ class Links extends Component {
}
let output = []
firstFiveLinks.forEach(obj => {
output.push(<li key={obj.mapId}><a href={`/maps/${obj.mapId}`}>{obj.mapName}</a></li>)
output.push(<li key={obj.mapId}><Link to={`/maps/${obj.mapId}`}>{obj.mapName}</Link></li>)
})
if (extraLinks.length > 0) {
if (this.state.showMoreMaps) {
extraLinks.forEach(obj => {
output.push(<li key={obj.mapId} className="hideExtra extraText"><a href={`/maps/${obj.mapId}`}>{obj.mapName}</a></li>)
output.push(<li key={obj.mapId} className="hideExtra extraText"><Link to={`/maps/${obj.mapId}`}>{obj.mapName}</Link></li>)
})
}
const text = this.state.showMoreMaps ? 'See less...' : `See ${extraLinks.length} more...`
output.push(<li key="showMore"><span className="showMore" onClick={this.toggleShowMoreMaps}>{text}</span></li>)
}
return output
}

View file

@ -7,12 +7,18 @@ import Attachments from './Attachments'
import Follow from './Follow'
import Util from '../../Metamaps/Util'
class ReactTopicCard extends Component {
render = () => {
const { topic, ActiveMapper, onFollow } = this.props
const authorizedToEdit = topic.authorizeToEdit(ActiveMapper)
const isFollowing = topic.isFollowedBy(ActiveMapper)
const { currentUser, onTopicFollow, updateTopic } = this.props
const topic = this.props.openTopic
if (!topic) return null
const wrappedUpdateTopic = obj => updateTopic(topic, obj)
const wrappedTopicFollow = () => onTopicFollow(topic)
const authorizedToEdit = topic.authorizeToEdit(currentUser)
const isFollowing = topic.isFollowedBy(currentUser)
const hasAttachment = topic.get('link') && topic.get('link') !== ''
let classname = 'permission'
@ -21,31 +27,33 @@ class ReactTopicCard extends Component {
} else {
classname += ' cannotEdit'
}
if (topic.authorizePermissionChange(ActiveMapper)) classname += ' yourTopic'
if (topic.authorizePermissionChange(currentUser)) classname += ' yourTopic'
return (
<div className={classname}>
<div className={`CardOnGraph ${hasAttachment ? 'hasAttachment' : ''}`} id={`topic_${topic.id}`}>
<Title name={topic.get('name')}
authorizedToEdit={authorizedToEdit}
onChange={this.props.updateTopic}
/>
<Links topic={topic}
ActiveMapper={this.props.ActiveMapper}
updateTopic={this.props.updateTopic}
metacodeSets={this.props.metacodeSets}
redrawCanvas={this.props.redrawCanvas}
/>
<Desc desc={topic.get('desc')}
authorizedToEdit={authorizedToEdit}
onChange={this.props.updateTopic}
/>
<Attachments topic={topic}
authorizedToEdit={authorizedToEdit}
updateTopic={this.props.updateTopic}
/>
{Util.isTester(ActiveMapper) && <Follow isFollowing={isFollowing} onFollow={onFollow} />}
<div className="clearfloat"></div>
<div className="showcard mapElement mapElementHidden" id="showcard">
<div className={classname}>
<div className={`CardOnGraph ${hasAttachment ? 'hasAttachment' : ''}`} id={`topic_${topic.id}`}>
<Title name={topic.get('name')}
authorizedToEdit={authorizedToEdit}
onChange={wrappedUpdateTopic}
/>
<Links topic={topic}
ActiveMapper={this.props.currentUser}
updateTopic={wrappedUpdateTopic}
metacodeSets={this.props.metacodeSets}
redrawCanvas={this.props.redrawCanvas}
/>
<Desc desc={topic.get('desc')}
authorizedToEdit={authorizedToEdit}
onChange={wrappedUpdateTopic}
/>
<Attachments topic={topic}
authorizedToEdit={authorizedToEdit}
updateTopic={wrappedUpdateTopic}
/>
{Util.isTester(currentUser) && <Follow isFollowing={isFollowing} onTopicFollow={wrappedTopicFollow} />}
<div className="clearfloat"></div>
</div>
</div>
</div>
)
@ -53,10 +61,10 @@ class ReactTopicCard extends Component {
}
ReactTopicCard.propTypes = {
topic: PropTypes.object,
ActiveMapper: PropTypes.object,
openTopic: PropTypes.object,
currentUser: PropTypes.object,
updateTopic: PropTypes.func,
onFollow: PropTypes.func,
onTopicFollow: PropTypes.func,
metacodeSets: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
metacodes: PropTypes.arrayOf(PropTypes.shape({

View file

@ -0,0 +1,81 @@
import React, { Component, PropTypes } from 'react'
import DataVis from '../common/DataVis'
import UpperOptions from '../common/UpperOptions'
import InfoAndHelp from '../common/InfoAndHelp'
import VisualizationControls from '../common/VisualizationControls'
import TopicCard from '../TopicCard'
export default class TopicView extends Component {
static propTypes = {
mobile: PropTypes.bool,
topicId: PropTypes.string,
topic: PropTypes.object,
filterData: PropTypes.object,
allForFiltering: PropTypes.object,
visibleForFiltering: PropTypes.object,
toggleMetacode: PropTypes.func,
toggleMapper: PropTypes.func,
toggleSynapse: PropTypes.func,
filterAllMetacodes: PropTypes.func,
filterAllMappers: PropTypes.func,
filterAllSynapses: PropTypes.func,
currentUser: PropTypes.object,
endActiveTopic: PropTypes.func,
launchNewTopic: PropTypes.func,
openHelpLightbox: PropTypes.func,
forkMap: PropTypes.func,
onZoomIn: PropTypes.func,
onZoomOut: PropTypes.func
}
componentWillUnmount() {
this.endTopic()
}
endTopic() {
this.upperOptions.reset()
this.props.endActiveTopic()
}
componentDidUpdate(prevProps) {
const oldTopicId = prevProps.topicId
const { topicId, launchNewTopic } = this.props
if (!oldTopicId && topicId) launchNewTopic(topicId)
else if (oldTopicId && topicId && oldTopicId !== topicId) {
this.endTopic()
launchNewTopic(topicId)
}
else if (oldTopicId && !topicId) this.endTopic()
}
render = () => {
const { mobile, topic, currentUser, allForFiltering, visibleForFiltering,
toggleMetacode, toggleMapper, toggleSynapse, filterAllMetacodes,
filterAllMappers, filterAllSynapses, filterData, forkMap,
openHelpLightbox, onZoomIn, onZoomOut } = this.props
// TODO: stop using {...this.props} and make explicit
return <div className="topicWrapper">
<UpperOptions ref={x => this.upperOptions = x}
currentUser={currentUser}
topic={topic}
onForkClick={forkMap}
filterData={filterData}
allForFiltering={allForFiltering}
visibleForFiltering={visibleForFiltering}
toggleMetacode={toggleMetacode}
toggleMapper={toggleMapper}
toggleSynapse={toggleSynapse}
filterAllMetacodes={filterAllMetacodes}
filterAllMappers={filterAllMappers}
filterAllSynapses={filterAllSynapses} />
<DataVis />
<TopicCard {...this.props} />
<VisualizationControls onClickZoomIn={onZoomIn}
onClickZoomOut={onZoomOut} />
<InfoAndHelp topic={topic}
onHelpClick={openHelpLightbox} />
</div>
}
}

View file

@ -0,0 +1,12 @@
import React, { Component, PropTypes } from 'react'
class DataVis extends Component {
static propTypes = {
}
render () {
return <div id="infovis" />
}
}
export default DataVis

View file

@ -0,0 +1,118 @@
import React, { Component, PropTypes } from 'react'
import onClickOutsideAddon from 'react-onclickoutside'
class FilterBox extends Component {
static propTypes = {
topic: PropTypes.object,
map: PropTypes.object,
filterData: PropTypes.object,
allForFiltering: PropTypes.object,
visibleForFiltering: PropTypes.object,
toggleMetacode: PropTypes.func,
toggleMapper: PropTypes.func,
toggleSynapse: PropTypes.func,
filterAllMetacodes: PropTypes.func,
filterAllMappers: PropTypes.func,
filterAllSynapses: PropTypes.func,
closeBox: PropTypes.func
}
handleClickOutside = () => {
this.props.closeBox()
}
render () {
const { topic, map, filterData, allForFiltering, visibleForFiltering, toggleMetacode,
toggleMapper, toggleSynapse, filterAllMetacodes,
filterAllMappers, filterAllSynapses } = this.props
const style = {
maxHeight: document.body.clientHeight - 108 + 'px'
}
const mapperAllClass = "showAll showAllMappers"
+ (allForFiltering.mappers.length === visibleForFiltering.mappers.length ? ' active' : '')
const mapperNoneClass = "hideAll hideAllMappers"
+ (visibleForFiltering.mappers.length === 0 ? ' active' : '')
const metacodeAllClass = "showAll showAllMetacodes"
+ (allForFiltering.metacodes.length === visibleForFiltering.metacodes.length ? ' active' : '')
const metacodeNoneClass = "hideAll hideAllMetacodes"
+ (visibleForFiltering.metacodes.length === 0 ? ' active' : '')
const synapseAllClass = "showAll showAllSynapses"
+ (allForFiltering.synapses.length === visibleForFiltering.synapses.length ? ' active' : '')
const synapseNoneClass = "hideAll hideAllSynapses"
+ (visibleForFiltering.synapses.length === 0 ? ' active' : '')
return map || topic ? <div className="sidebarFilterBox upperRightBox" style={style}>
<div className="filterBox">
<h2>FILTER BY</h2>
<div id="filter_by_mapper" className="filterBySection">
{map && <h3>MAPPERS</h3>}
{topic && <h3>CREATORS</h3>}
<span className={mapperNoneClass} onClick={() => filterAllMappers()}>NONE</span>
<span className={mapperAllClass} onClick={() => filterAllMappers(true)}>ALL</span>
<div className="clearfloat"></div>
<ul>
{allForFiltering.mappers.map(mapperId => {
const data = filterData.mappers[mapperId]
const isVisible = visibleForFiltering.mappers.indexOf(mapperId) > -1
return <Mapper visible={isVisible} id={mapperId} image={data.image} name={data.name} toggle={toggleMapper} />
})}
</ul>
<div className="clearfloat"></div>
</div>
<div id="filter_by_metacode" className="filterBySection">
<h3>METACODES</h3>
<span className={metacodeNoneClass} onClick={() => filterAllMetacodes()}>NONE</span>
<span className={metacodeAllClass} onClick={() => filterAllMetacodes(true)}>ALL</span>
<div className="clearfloat"></div>
<ul>
{allForFiltering.metacodes.map(metacodeId => {
const data = filterData.metacodes[metacodeId]
const isVisible = visibleForFiltering.metacodes.indexOf(metacodeId) > -1
return <Metacode visible={isVisible} id={metacodeId} icon={data.icon} name={data.name} toggle={toggleMetacode} />
})}
</ul>
<div className="clearfloat"></div>
</div>
<div id="filter_by_synapse" className="filterBySection">
<h3>SYNAPSES</h3>
<span className={synapseNoneClass} onClick={() => filterAllSynapses()}>NONE</span>
<span className={synapseAllClass} onClick={() => filterAllSynapses(true)}>ALL</span>
<div className="clearfloat"></div>
<ul>
{allForFiltering.synapses.map(synapseDesc => {
const data = filterData.synapses[synapseDesc]
const isVisible = visibleForFiltering.synapses.indexOf(synapseDesc) > -1
return <Synapse visible={isVisible} desc={synapseDesc} icon={data.icon} toggle={toggleSynapse} />
})}
</ul>
<div className="clearfloat"></div>
</div>
</div>
</div> : null
}
}
function Mapper({ visible, name, id, image, toggle }) {
return <li onClick={() => toggle(id)} key={id} className={visible ? '' : 'toggledOff'}>
<img src={image} alt={name} />
<p>{name}</p>
</li>
}
function Metacode({ visible, name, id, icon, toggle }) {
return <li onClick={() => toggle(id)} key={id} className={visible ? '' : 'toggledOff'}>
<img src={icon} alt={name} />
<p>{name.toLowerCase()}</p>
</li>
}
function Synapse({ visible, desc, icon, toggle }) {
return <li onClick={() => toggle(desc)} key={desc.replace(/ /g, '')} className={visible ? '' : 'toggledOff'}>
<img src={icon} alt="synapse icon" />
<p>{desc}</p>
</li>
}
export default onClickOutsideAddon(FilterBox)

View file

@ -0,0 +1,38 @@
import React, { Component, PropTypes } from 'react'
import MapInfoBox from '../MapView/MapInfoBox'
class InfoAndHelp extends Component {
static propTypes = {
mapIsStarred: PropTypes.bool,
currentUser: PropTypes.object,
map: PropTypes.object,
onHelpClick: PropTypes.func,
onMapStar: PropTypes.func,
onMapUnstar: PropTypes.func,
onInfoClick: PropTypes.func,
infoBoxhtml: PropTypes.string
}
render () {
const { mapIsStarred, map, currentUser, onInfoClick, infoBoxHtml, onMapStar, onMapUnstar, onHelpClick } = this.props
const starclassName = mapIsStarred ? 'starred' : ''
const tooltip = mapIsStarred ? 'Unstar' : 'Star'
const onStarClick = mapIsStarred ? onMapUnstar : onMapStar
return <div className="infoAndHelp">
{map && <MapInfoBox map={map} currentUser={currentUser} infoBoxHtml={infoBoxHtml} />}
{map && currentUser && <div className={`starMap infoElement mapElement ${starclassName}`} onClick={onStarClick}>
<div className="tooltipsAbove">{tooltip}</div>
</div>}
{map && <div className="mapInfoIcon infoElement mapElement" onClick={onInfoClick}>
<div className="tooltipsAbove">Map Info</div>
</div>}
<div className="openCheatsheet infoElement mapElement" onClick={onHelpClick}>
<div className="tooltipsAbove">Help</div>
</div>
<div className="clearfloat"></div>
</div>
}
}
export default InfoAndHelp

View file

@ -0,0 +1,73 @@
import React, { Component, PropTypes } from 'react'
import FilterBox from '../common/FilterBox'
export default class UpperOptions extends Component {
static propTypes = {
currentUser: PropTypes.object,
map: PropTypes.object,
topic: PropTypes.object,
canEditMap: PropTypes.bool,
onImportClick: PropTypes.func,
onForkClick: PropTypes.func,
filterData: PropTypes.object,
allForFiltering: PropTypes.object,
visibleForFiltering: PropTypes.object,
toggleMetacode: PropTypes.func,
toggleMapper: PropTypes.func,
toggleSynapse: PropTypes.func,
filterAllMetacodes: PropTypes.func,
filterAllMappers: PropTypes.func,
filterAllSynapses: PropTypes.func,
}
constructor(props) {
super(props)
this.state = {filterBoxOpen: false}
}
reset = () => {
this.setState({filterBoxOpen: false})
}
toggleFilterBox = event => {
this.setState({filterBoxOpen: !this.state.filterBoxOpen})
}
render () {
const { currentUser, map, topic, canEditMap, filterBoxHtml, onFilterClick, onImportClick, onForkClick,
filterData, allForFiltering, visibleForFiltering, toggleMetacode, toggleMapper, toggleSynapse,
filterAllMetacodes, filterAllMappers, filterAllSynapses } = this.props
const { filterBoxOpen } = this.state
return <div className="mapElement upperRightEl upperRightMapButtons upperRightUI">
{map && canEditMap && <div className="importDialog upperRightEl upperRightIcon mapElement" onClick={onImportClick}>
<div className="tooltipsUnder">
Import Data
</div>
</div>}
<div className="sidebarFilter upperRightEl">
<div className="sidebarFilterIcon upperRightIcon ignore-react-onclickoutside" onClick={this.toggleFilterBox}>
<div className="tooltipsUnder">Filter</div>
</div>
{filterBoxOpen && <FilterBox filterData={filterData}
map={map}
topic={topic}
allForFiltering={allForFiltering}
visibleForFiltering={visibleForFiltering}
toggleMetacode={toggleMetacode}
toggleMapper={toggleMapper}
toggleSynapse={toggleSynapse}
filterAllMetacodes={filterAllMetacodes}
filterAllMappers={filterAllMappers}
filterAllSynapses={filterAllSynapses}
closeBox={() => this.reset()} />}
</div>
{currentUser && <div className="sidebarFork upperRightEl">
<div className="sidebarForkIcon upperRightIcon" onClick={onForkClick}>
<div className="tooltipsUnder">Save To New Map</div>
</div>
</div>}
<div className="clearfloat"></div>
</div>
}
}

View file

@ -0,0 +1,25 @@
import React, { Component, PropTypes } from 'react'
export default class VisualizationControls extends Component {
static propTypes = {
map: PropTypes.object,
onClickZoomExtents: PropTypes.func,
onClickZoomIn: PropTypes.func,
onClickZoomOut: PropTypes.func
}
render () {
const { map, onClickZoomExtents, onClickZoomIn, onClickZoomOut } = this.props
return <div className="mapControls mapElement">
{map && <div className="zoomExtents mapControl" onClick={onClickZoomExtents}>
<div className="tooltips">Center View</div>
</div>}
<div className="zoomIn mapControl" onClick={onClickZoomIn}>
<div className="tooltips">Zoom In</div>
</div>
<div className="zoomOut mapControl" onClick={onClickZoomOut}>
<div className="tooltips">Zoom Out</div>
</div>
</div>
}
}

View file

@ -0,0 +1,70 @@
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import App from './App'
import Maps from './Maps'
import MapView from './MapView'
import TopicView from './TopicView'
function nullComponent(props) {
return null
}
export default function makeRoutes (currentUser) {
const homeComponent = currentUser && currentUser.id ? Maps : nullComponent
return <Route path="/" component={App} >
<IndexRoute component={homeComponent} />
<Route path="explore">
<Route path="active" component={Maps} />
<Route path="featured" component={Maps} />
<Route path="mine" component={Maps} />
<Route path="shared" component={Maps} />
<Route path="starred" component={Maps} />
<Route path="mapper/:id" component={Maps} />
</Route>
<Route path="maps/:id">
<IndexRoute component={MapView} />
<Route path="conversation" component={MapView} />
<Route path="request_access" component={nullComponent} />
</Route>
<Route path="topics/:id" component={TopicView} />
<Route path="login" component={nullComponent} />
<Route path="join" component={nullComponent} />
<Route path="request" component={nullComponent} />
<Route path="notifications">
<IndexRoute component={nullComponent} />
<Route path=":id" component={nullComponent} />
</Route>
<Route path="users">
<Route path=":id/edit" component={nullComponent} />
<Route path="password/new" component={nullComponent} />
<Route path="password/edit" component={nullComponent} />
</Route>
<Route path="metacodes">
<IndexRoute component={nullComponent} />
<Route path="new" component={nullComponent} />
<Route path=":id/edit" component={nullComponent} />
</Route>
<Route path="metacode_sets">
<IndexRoute component={nullComponent} />
<Route path="new" component={nullComponent} />
<Route path=":id/edit" component={nullComponent} />
</Route>
<Route path="oauth">
<Route path="token/info" component={nullComponent} />
<Route path="authorize">
<IndexRoute component={nullComponent} />
<Route path=":code" component={nullComponent} />
</Route>
<Route path="authorized_applications">
<IndexRoute component={nullComponent} />
<Route path=":id" component={nullComponent} />
</Route>
<Route path="applications">
<IndexRoute component={nullComponent} />
<Route path="new" component={nullComponent} />
<Route path=":id" component={nullComponent} />
<Route path=":id/edit" component={nullComponent} />
</Route>
</Route>
</Route>
}

View file

@ -46,6 +46,7 @@
"react-dom": "15.4.2",
"react-dropzone": "3.9.1",
"react-onclickoutside": "5.9.0",
"react-router": "3.0.2",
"redux": "3.6.0",
"riek": "1.0.7",
"simplewebrtc": "2.2.2",