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

View file

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

View file

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

View file

@ -1,7 +1,3 @@
#mobile_header {
display: none;
}
@media only screen and (max-width : 752px) and (min-width : 504px) { @media only screen and (max-width : 752px) and (min-width : 504px) {
.sidebarSearch .tt-hint, .sidebarSearch .sidebarSearchField { .sidebarSearch .tt-hint, .sidebarSearch .sidebarSearchField {
width: 160px !important; width: 160px !important;
@ -51,10 +47,6 @@
display: none; display: none;
} }
#mobile_header {
display: block;
}
.homeWrapper { .homeWrapper {
width: 96%; width: 96%;
padding: 0 2%; padding: 0 2%;
@ -72,7 +64,7 @@
height: auto; height: auto;
} }
.homeVideo { .homeVideo {
width: 100%; width: 100% !important;
height: auto; height: auto;
} }
.fullWidthWrapper.withPartners { .fullWidthWrapper.withPartners {
@ -108,15 +100,23 @@
max-width: 360px; max-width: 360px;
} }
#wrapper .requestInvite { .requestInviteHeader {
display: none;
}
.requestInvite {
width: 100%; width: 100%;
padding: 0; height: calc(100% - 50px);
z-index: 1;
position: relative;
left: 0;
margin-left: 0px;
margin-top: 50px;
} }
#exploreMaps > div { #exploreMaps > div {
margin-top: 70px; margin-top: 70px;
} }
.mapper { .mapper {
width: 100%; width: 100%;
margin: 0 0 30px 0; margin: 0 0 30px 0;
@ -217,6 +217,7 @@
width: 100%; width: 100%;
box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16);
position: fixed; position: fixed;
z-index: 1;
} }
#menu_icon { #menu_icon {
@ -249,7 +250,6 @@
} }
#mobile_menu { #mobile_menu {
display: none;
background: #EEE; background: #EEE;
position: fixed; position: fixed;
top: 50px; top: 50px;
@ -257,6 +257,7 @@
padding: 10px; padding: 10px;
width: 200px; width: 200px;
box-shadow: 3px 3px 3px rgba(0,0,0,0.23), 3px 3px 3px rgba(0,0,0,0.16); box-shadow: 3px 3px 3px rgba(0,0,0,0.23), 3px 3px 3px rgba(0,0,0,0.16);
z-index: 2;
li { li {
padding: 10px; 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 { li.mobileMenuUser {
border-bottom: 1px solid #BBB; border-bottom: 1px solid #BBB;
} }

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
# %> # %>
<script> <script>
<% content_for :title, "Explore Shared Maps | Metamaps" %> <% content_for :title, "Shared Maps | Metamaps" %>
<% content_for :mobile_title, "Shared With Me" %> <% content_for :mobile_title, "Shared With Me" %>
Metamaps.currentPage = "shared"; 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/lightboxes' %>
<%= render :partial => 'layouts/templates' %> <%= render :partial => 'layouts/templates' %>
<%= render :partial => 'shared/metacodeBgColors' %> <%= 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? %> <%= render :partial => 'layouts/googleanalytics' if ENV["GA_TRACKING_CODE"].present? %>
</body> </body>
</html> </html>

View file

@ -10,6 +10,5 @@
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '<%= ENV["GA_TRACKING_CODE"] %>', 'auto'); ga('create', '<%= ENV["GA_TRACKING_CODE"] %>', 'auto');
ga('send', 'pageview');
</script> </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' %> <%= render :partial => 'layouts/head' %>
<body class="<%= authenticated? ? "authenticated" : "unauthenticated" %> controller-<%= controller_name %> action-<%= action_name %>"> <body class="<%= authenticated? ? "authenticated" : "unauthenticated" %> controller-<%= controller_name %> action-<%= action_name %>">
<div class="main" id="react-app"></div>
<div id="chat-box-wrapper"></div> <%= yield %>
<% if authenticated? %>
<a class='feedback-icon' target='_blank' href='https://hylo.com/c/metamaps'></a> <% # for creating and pulling in topics and synapses %>
<% if controller_name == 'maps' && action_name == "conversation" %>
<%= content_tag :div, class: "main" do %> <%= render :partial => 'maps/newtopicsecret' %>
<% else %>
<% classes = action_name == "home" ? "homePage" : "" <%= render :partial => 'maps/newtopic' %>
classes += action_name == "home" && authenticated? ? " explorePage" : "" <% end %>
classes += controller_name == "maps" && action_name == "index" ? " explorePage" : "" <%= render :partial => 'maps/newsynapse' %>
if controller_name == "maps" && action_name == "show" <% # for populating the change metacode list on the topic card %>
classes += " mapPage" <%= render :partial => 'shared/metacodeoptions' %>
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>
<% end %> <% end %>
<%= render :partial => 'layouts/foot' %> <%= render :partial => 'layouts/foot' %>

View file

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

View file

@ -6,7 +6,5 @@
<% content_for :title, "Request Invite | Metamaps" %> <% content_for :title, "Request Invite | Metamaps" %>
<% content_for :mobile_title, "Request Invite" %> <% content_for :mobile_title, "Request Invite" %>
<div class="requestInviteHeader"></div>
<div id="yield">
<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> <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.Mappings = <%= @allmappings.to_json.html_safe %>;
Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>; Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>;
Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>; Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>;
Metamaps.ServerData.requests = <%= @allrequests.to_json.html_safe %>;
Metamaps.ServerData.VisualizeType = "ForceDirected"; Metamaps.ServerData.VisualizeType = "ForceDirected";
</script> </script>

View file

@ -18,5 +18,6 @@
Metamaps.ServerData.Mappings = <%= @allmappings.to_json.html_safe %>; Metamaps.ServerData.Mappings = <%= @allmappings.to_json.html_safe %>;
Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>; Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>;
Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>; Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>;
Metamaps.ServerData.requests = <%= @allrequests.to_json.html_safe %>;
Metamaps.ServerData.VisualizeType = "ForceDirected"; Metamaps.ServerData.VisualizeType = "ForceDirected";
</script> </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) { if (DataModel.Topics.length === 0) {
GlobalUI.showDiv('#instructions') Map.setHasLearnedTopicCreation(false)
} }
}, },
deleteSelectedNodes: function() { // refers to deleting topics permanently deleteSelectedNodes: function() { // refers to deleting topics permanently

View file

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

View file

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

View file

@ -28,12 +28,11 @@ const Synapse = Backbone.Model.extend({
this.on('change', this.updateEdgeView) this.on('change', this.updateEdgeView)
this.on('change:desc', Filter.checkSynapses, this) this.on('change:desc', Filter.checkSynapses, this)
}, },
prepareLiForFilter: function() { prepareDataForFilter: function() {
return outdent` return {
<li data-id="${this.get('desc')}"> desc: this.get('desc'),
<img src="${DataModel.synapseIconUrl}" alt="synapse icon" /> icon: DataModel.synapseIconUrl
<p>${this.get('desc')}</p> }
</li>`
}, },
authorizeToEdit: function(mapper) { 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 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() { attachCollectionEvents: function() {
DataModel.Topics.on('add remove', function(topic) { DataModel.Topics.on('add remove', function(topic) {
console.log('updating infobox and filters due to topic add or remove')
InfoBox.updateNumbers() InfoBox.updateNumbers()
Filter.checkMetacodes() Filter.checkMetacodes()
Filter.checkMappers() Filter.checkMappers()
}) })
DataModel.Synapses.on('add remove', function(synapse) { DataModel.Synapses.on('add remove', function(synapse) {
console.log('updating infobox and filters due to synapse add or remove')
InfoBox.updateNumbers() InfoBox.updateNumbers()
Filter.checkSynapses() Filter.checkSynapses()
Filter.checkMappers() Filter.checkMappers()
}) })
DataModel.Mappings.on('add remove', function(mapping) { DataModel.Mappings.on('add remove', function(mapping) {
console.log('updating infobox and filters due to mapping add or remove')
InfoBox.updateNumbers() InfoBox.updateNumbers()
Filter.checkSynapses() Filter.checkSynapses()
Filter.checkMetacodes() Filter.checkMetacodes()

View file

@ -5,13 +5,17 @@ import _ from 'lodash'
import Active from './Active' import Active from './Active'
import Control from './Control' import Control from './Control'
import DataModel from './DataModel' import DataModel from './DataModel'
import GlobalUI from './GlobalUI' import GlobalUI, { ReactApp } from './GlobalUI'
import Settings from './Settings' import Settings from './Settings'
import Visualize from './Visualize' import Visualize from './Visualize'
const Filter = { const Filter = {
dataForPresentation: {
metacodes: {},
mappers: {},
synapses: {}
},
filters: { filters: {
name: '',
metacodes: [], metacodes: [],
mappers: [], mappers: [],
synapses: [] synapses: []
@ -23,119 +27,26 @@ const Filter = {
}, },
isOpen: false, isOpen: false,
changing: 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() { reset: function() {
var self = Filter var self = Filter
self.filters.metacodes = [] self.filters.metacodes = []
self.filters.mappers = [] self.filters.mappers = []
self.filters.synapses = [] self.filters.synapses = []
self.visible.metacodes = [] self.visible.metacodes = []
self.visible.mappers = [] self.visible.mappers = []
self.visible.synapses = [] self.visible.synapses = []
self.dataForPresentation.metacodes = {}
$('#filter_by_metacode ul').empty() self.dataForPresentation.mappers = {}
$('#filter_by_mapper ul').empty() self.dataForPresentation.synapses = {}
$('#filter_by_synapse ul').empty() ReactApp.render()
$('.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)
}, },
// an abstraction function for checkMetacodes, checkMappers, checkSynapses to reduce // an abstraction function for checkMetacodes, checkMappers, checkSynapses to reduce
// code redundancy // code redundancy
/*
@param
*/
updateFilters: function(collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) { updateFilters: function(collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) {
var self = Filter var self = Filter
var newList = [] var newList = []
var removed = [] var removed = []
var added = [] var added = []
// the first option enables us to accept // the first option enables us to accept
// ['Topics', 'Synapses'] as 'collection' // ['Topics', 'Synapses'] as 'collection'
if (typeof collection === 'object') { if (typeof collection === 'object') {
@ -168,41 +79,24 @@ const Filter = {
} }
}) })
} }
removed = _.difference(self.filters[filtersToUse], newList) removed = _.difference(self.filters[filtersToUse], newList)
added = _.difference(newList, self.filters[filtersToUse]) added = _.difference(newList, self.filters[filtersToUse])
// remove the list items for things no longer present on the map
_.each(removed, function(identifier) { _.each(removed, function(identifier) {
$('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast', function() {
$(this).remove()
})
const index = self.visible[filtersToUse].indexOf(identifier) const index = self.visible[filtersToUse].indexOf(identifier)
self.visible[filtersToUse].splice(index, 1) 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) { _.each(added, function(identifier) {
model = DataModel[correlatedModel].get(identifier) || const model = DataModel[correlatedModel].get(identifier) ||
DataModel[correlatedModel].find(function(model) { DataModel[correlatedModel].find(function(m) {
return model.get(propertyToCheck) === identifier return m.get(propertyToCheck) === identifier
}) })
li = model.prepareLiForFilter() self.dataForPresentation[filtersToUse][identifier] = model.prepareDataForFilter()
jQueryLi = $(li).hide()
$('li', '#filter_by_' + listToModify + ' ul').add(jQueryLi.fadeIn('fast'))
.sort(sortAlpha).appendTo('#filter_by_' + listToModify + ' ul')
self.visible[filtersToUse].push(identifier) self.visible[filtersToUse].push(identifier)
}) })
// update the list of filters with the new list we just generated // update the list of filters with the new list we just generated
self.filters[filtersToUse] = newList self.filters[filtersToUse] = newList
ReactApp.render()
// make sure clicks on list items still trigger the right events
self.bindLiClicks()
}, },
checkMetacodes: function() { checkMetacodes: function() {
var self = Filter var self = Filter
@ -221,114 +115,49 @@ const Filter = {
var self = Filter var self = Filter
self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse') self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse')
}, },
filterAllMetacodes: function(e) { filterAllMetacodes: function(toVisible) {
var self = Filter var self = Filter
$('#filter_by_metacode ul li').addClass('toggledOff') self.visible.metacodes = toVisible ? self.filters.metacodes.slice() : []
$('.showAllMetacodes').removeClass('active') ReactApp.render()
$('.hideAllMetacodes').addClass('active')
self.visible.metacodes = []
self.passFilters() self.passFilters()
}, },
filterNoMetacodes: function(e) { filterAllMappers: function(toVisible) {
var self = Filter var self = Filter
$('#filter_by_metacode ul li').removeClass('toggledOff') self.visible.mappers = toVisible ? self.filters.mappers.slice() : []
$('.showAllMetacodes').addClass('active') ReactApp.render()
$('.hideAllMetacodes').removeClass('active')
self.visible.metacodes = self.filters.metacodes.slice()
self.passFilters() self.passFilters()
}, },
filterAllMappers: function(e) { filterAllSynapses: function(toVisible) {
var self = Filter var self = Filter
$('#filter_by_mapper ul li').addClass('toggledOff') self.visible.synapses = toVisible ? self.filters.synapses.slice() : []
$('.showAllMappers').removeClass('active') ReactApp.render()
$('.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.passFilters() self.passFilters()
}, },
// an abstraction function for toggleMetacode, toggleMapper, toggleSynapse // an abstraction function for toggleMetacode, toggleMapper, toggleSynapse
// to reduce code redundancy // to reduce code redundancy
// gets called in the context of a list item in a filter box // gets called in the context of a list item in a filter box
toggleLi: function(whichToFilter) { toggleLi: function(whichToFilter, id) {
var self = Filter var self = Filter
var id = $(this).attr('data-id')
if (self.visible[whichToFilter].indexOf(id) === -1) { if (self.visible[whichToFilter].indexOf(id) === -1) {
self.visible[whichToFilter].push(id) self.visible[whichToFilter].push(id)
$(this).removeClass('toggledOff')
} else { } else {
const index = self.visible[whichToFilter].indexOf(id) const index = self.visible[whichToFilter].indexOf(id)
self.visible[whichToFilter].splice(index, 1) self.visible[whichToFilter].splice(index, 1)
$(this).addClass('toggledOff')
} }
ReactApp.render()
self.passFilters() self.passFilters()
}, },
toggleMetacode: function() { toggleMetacode: function(id) {
var self = Filter var self = Filter
self.toggleLi.call(this, 'metacodes') self.toggleLi('metacodes', id)
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')
}
}, },
toggleMapper: function() { toggleMapper: function(id) {
var self = Filter var self = Filter
self.toggleLi.call(this, 'mappers') self.toggleLi('mappers', id)
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')
}
}, },
toggleSynapse: function() { toggleSynapse: function(id) {
var self = Filter var self = Filter
self.toggleLi.call(this, 'synapses') self.toggleLi('synapses', id)
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')
}
}, },
passFilters: function() { passFilters: function() {
var self = Filter 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') { if (GlobalUI.lightbox === 'forkmap') {
self.newMap.set('topicsToMap', self.topicsToMap) self.newMap.set('topicsToMap', self.topicsToMap)
self.newMap.set('synapsesToMap', self.synapsesToMap) 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' 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 ReactDOM from 'react-dom'
import outdent from 'outdent' import outdent from 'outdent'
import ImportDialogBox from '../../components/ImportDialogBox' import ImportDialogBox from '../../components/MapView/ImportDialogBox'
import PasteInput from '../PasteInput' import PasteInput from '../PasteInput'
import Map from '../Map' import Map from '../Map'
@ -19,7 +19,7 @@ const ImportDialog = {
self.closeLightbox = closeLightbox self.closeLightbox = closeLightbox
$('#lightbox_content').append($(outdent` $('#lightbox_content').append($(outdent`
<div class="lightboxContent" id="import-dialog-lightbox"> <div class="lightboxContent" id="import-dialog">
<div class="importDialogWrapper" /> <div class="importDialogWrapper" />
</div> </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 */ /* global $, Hogan, Bloodhound, CanvasLoader */
import { browserHistory } from 'react-router'
import Active from '../Active' import Active from '../Active'
import Router from '../Router'
const Search = { const Search = {
locked: false, locked: false,
@ -17,6 +18,7 @@ const Search = {
self.userIconUrl = serverData['user.png'] self.userIconUrl = serverData['user.png']
// this is similar to Metamaps.Loading, but it's for the search element // this is similar to Metamaps.Loading, but it's for the search element
if (!document.getElementById('searchLoading')) return
var loader = new CanvasLoader('searchLoading') var loader = new CanvasLoader('searchLoading')
loader.setColor('#4fb5c0') // default is '#000000' loader.setColor('#4fb5c0') // default is '#000000'
loader.setDiameter(24) // default is 40 loader.setDiameter(24) // default is 40
@ -189,11 +191,11 @@ const Search = {
if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) {
if (datum.rtype === 'topic') { if (datum.rtype === 'topic') {
Router.topics(datum.id) browserHistory.push(`/topics/${datum.id}`)
} else if (datum.rtype === 'map') { } else if (datum.rtype === 'map') {
Router.maps(datum.id) browserHistory.push(`/maps/${datum.id}`)
} else if (datum.rtype === 'mapper') { } 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 Create from '../Create'
import ReactApp from './ReactApp'
import Search from './Search' import Search from './Search'
import CreateMap from './CreateMap' import CreateMap from './CreateMap'
import Account from './Account'
import ImportDialog from './ImportDialog' import ImportDialog from './ImportDialog'
import NotificationIcon from './NotificationIcon'
const GlobalUI = { const GlobalUI = {
notifyTimeout: null, notifyTimeout: null,
@ -18,13 +17,12 @@ const GlobalUI = {
init: function(serverData) { init: function(serverData) {
const self = GlobalUI const self = GlobalUI
self.Search.init(serverData) self.ReactApp.init(serverData, self.openLightbox)
self.CreateMap.init(serverData) self.CreateMap.init(serverData)
self.Account.init(serverData)
self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox) 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 // bind lightbox clicks
$('.openLightbox').click(function(event) { $('.openLightbox').click(function(event) {
@ -112,10 +110,9 @@ const GlobalUI = {
_notifyUser: function(message, opts = {}) { _notifyUser: function(message, opts = {}) {
const self = GlobalUI const self = GlobalUI
const { leaveOpen = false, timeOut = 8000 } = opts const { leaveOpen = false, timeOut = 5000 } = opts
ReactApp.toast = message
$('#toast').html(message) ReactApp.render()
self.showDiv('#toast')
clearTimeout(self.notifyTimeOut) clearTimeout(self.notifyTimeOut)
if (!leaveOpen) { if (!leaveOpen) {
@ -134,7 +131,8 @@ const GlobalUI = {
const { message, opts } = self.notifyQueue.shift() const { message, opts } = self.notifyQueue.shift()
self._notifyUser(message, opts) self._notifyUser(message, opts)
} else { } else {
self.hideDiv('#toast') ReactApp.toast = null
ReactApp.render()
self.notifying = false self.notifying = false
} }
}, },
@ -153,5 +151,5 @@ const GlobalUI = {
} }
} }
export { Search, CreateMap, Account, ImportDialog, NotificationIcon } export { ReactApp, Search, CreateMap, ImportDialog }
export default GlobalUI export default GlobalUI

View file

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

View file

@ -50,15 +50,6 @@ const JIT = {
*/ */
init: function(serverData) { init: function(serverData) {
const self = JIT 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 = new Image()
self.topicDescImage.src = serverData['topic_description_signifier.png'] self.topicDescImage.src = serverData['topic_description_signifier.png']
@ -123,36 +114,22 @@ const JIT = {
prepareVizData: function() { prepareVizData: function() {
const self = JIT const self = JIT
let mapping let mapping
// reset/empty vizData
self.vizData = [] self.vizData = []
Visualize.loadLater = false Visualize.loadLater = false
const results = self.convertModelsToJIT(DataModel.Topics, DataModel.Synapses) const results = self.convertModelsToJIT(DataModel.Topics, DataModel.Synapses)
self.vizData = results[0] self.vizData = results[0]
// clean up the synapses array in case of any faulty data // clean up the synapses array in case of any faulty data
_.each(results[1], function(synapse) { _.each(results[1], function(synapse) {
mapping = synapse.getMapping() mapping = synapse.getMapping()
DataModel.Synapses.remove(synapse) DataModel.Synapses.remove(synapse)
if (DataModel.Mappings) DataModel.Mappings.remove(mapping) 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) { if (self.vizData.length === 0) {
GlobalUI.showDiv('#instructions') Map.setHasLearnedTopicCreation(false)
Visualize.loadLater = true Visualize.loadLater = true
} else { } else {
GlobalUI.hideDiv('#instructions') Map.setHasLearnedTopicCreation(true)
} }
Visualize.render() Visualize.render()
}, // prepareVizData }, // prepareVizData
edgeRender: function(adj, canvas) { edgeRender: function(adj, canvas) {
@ -1026,7 +1003,6 @@ const JIT = {
Create.newTopic.open() Create.newTopic.open()
} else if (!Mouse.didPan) { } else if (!Mouse.didPan) {
// SINGLE CLICK, no pan // SINGLE CLICK, no pan
Filter.close()
TopicCard.hideCard() TopicCard.hideCard()
SynapseCard.hideCard() SynapseCard.hideCard()
Create.newTopic.hide() Create.newTopic.hide()

View file

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

View file

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

View file

@ -1,16 +1,15 @@
/* global $, Hogan, Bloodhound, Countable */ /* global $, Hogan, Bloodhound, Countable */
import outdent from 'outdent' import outdent from 'outdent'
import { browserHistory } from 'react-router'
import Active from '../Active' import Active from '../Active'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI' import GlobalUI, { ReactApp } from '../GlobalUI'
import Router from '../Router'
import Util from '../Util' import Util from '../Util'
const InfoBox = { const InfoBox = {
isOpen: false, isOpen: false,
changing: false,
selectingPermission: 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>", 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` nameHTML: outdent`
@ -35,12 +34,12 @@ const InfoBox = {
data-bip-value="{{desc}}" data-bip-value="{{desc}}"
>{{desc}}</span>`, >{{desc}}</span>`,
userImageUrl: '', userImageUrl: '',
html: '',
init: function(serverData, updateThumbnail) { init: function(serverData, updateThumbnail) {
var self = InfoBox var self = InfoBox
self.updateThumbnail = updateThumbnail self.updateThumbnail = updateThumbnail
$('.mapInfoIcon').click(self.toggleBox)
$('.mapInfoBox').click(function(event) { $('.mapInfoBox').click(function(event) {
event.stopPropagation() event.stopPropagation()
}) })
@ -72,27 +71,18 @@ const InfoBox = {
open: function() { open: function() {
var self = InfoBox var self = InfoBox
$('.mapInfoIcon div').addClass('hide') $('.mapInfoIcon div').addClass('hide')
if (!self.isOpen && !self.changing) { $('.mapInfoBox').fadeIn(200, function() {
self.changing = true self.isOpen = true
$('.mapInfoBox').fadeIn(200, function() { })
self.changing = false
self.isOpen = true
})
}
}, },
close: function() { close: function() {
var self = InfoBox var self = InfoBox
$('.mapInfoIcon div').removeClass('hide') $('.mapInfoIcon div').removeClass('hide')
if (!self.changing) { $('.mapInfoBox').fadeOut(200, function() {
self.changing = true self.isOpen = false
$('.mapInfoBox').fadeOut(200, function() { self.hidePermissionSelect()
self.changing = false $('.mapContributors .tip').hide()
self.isOpen = false })
self.hidePermissionSelect()
$('.mapContributors .tip').hide()
})
}
}, },
load: function() { load: function() {
var self = InfoBox var self = InfoBox
@ -120,13 +110,8 @@ const InfoBox = {
obj['created_at'] = map.get('created_at_clean') obj['created_at'] = map.get('created_at_clean')
obj['updated_at'] = map.get('updated_at_clean') obj['updated_at'] = map.get('updated_at_clean')
var classes = isCreator ? 'yourMap' : '' self.html = self.generateBoxHTML.render(obj)
classes += canEdit ? ' canEdit' : '' ReactApp.render()
classes += shareable ? ' shareable' : ''
$('.mapInfoBox').removeClass('shareable yourMap canEdit')
.addClass(classes)
.html(self.generateBoxHTML.render(obj))
self.attachEventListeners() self.attachEventListeners()
}, },
attachEventListeners: function() { attachEventListeners: function() {
@ -192,7 +177,6 @@ const InfoBox = {
$('.mapContributors .tip').unbind().click(function(event) { $('.mapContributors .tip').unbind().click(function(event) {
event.stopPropagation() event.stopPropagation()
}) })
$('.mapContributors .tip li a').click(Router.intercept)
$('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function() { $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function() {
$('.mapContributors .tip').hide() $('.mapContributors .tip').hide()
@ -393,7 +377,7 @@ const InfoBox = {
DataModel.Maps.Mine.remove(map) DataModel.Maps.Mine.remove(map)
DataModel.Maps.Shared.remove(map) DataModel.Maps.Shared.remove(map)
map.destroy() map.destroy()
Router.home() browserHistory.push('/')
GlobalUI.notifyUser('Map eliminated') GlobalUI.notifyUser('Map eliminated')
} else if (!authorized) { } 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?") 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 outdent from 'outdent'
import { find as _find } from 'lodash' import { find as _find } from 'lodash'
import { browserHistory } from 'react-router'
import Active from '../Active' import Active from '../Active'
import AutoLayout from '../AutoLayout' import AutoLayout from '../AutoLayout'
@ -9,11 +10,10 @@ import Create from '../Create'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import DataModelMap from '../DataModel/Map' import DataModelMap from '../DataModel/Map'
import Filter from '../Filter' import Filter from '../Filter'
import GlobalUI from '../GlobalUI' import GlobalUI, { ReactApp } from '../GlobalUI'
import JIT from '../JIT' import JIT from '../JIT'
import Loading from '../Loading' import Loading from '../Loading'
import Realtime from '../Realtime' import Realtime from '../Realtime'
import Router from '../Router'
import Selected from '../Selected' import Selected from '../Selected'
import SynapseCard from '../SynapseCard' import SynapseCard from '../SynapseCard'
import TopicCard from '../Views/TopicCard' import TopicCard from '../Views/TopicCard'
@ -26,143 +26,126 @@ const Map = {
events: { events: {
editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper'
}, },
mapIsStarred: false,
requests: [],
userRequested: false,
requestAnswered: false,
requestApproved: false,
hasLearnedTopicCreation: true,
init: function(serverData) { init: function(serverData) {
var self = Map var self = Map
self.mapIsStarred = serverData.mapIsStarred
self.requests = serverData.requests
self.setAccessRequest()
$('#wrapper').mousedown(function(e) { $('#wrapper').mousedown(function(e) {
if (e.button === 1) return false 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() GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html()
self.updateStar()
InfoBox.init(serverData, function updateThumbnail() { InfoBox.init(serverData, function updateThumbnail() {
self.uploadMapScreenshot() self.uploadMapScreenshot()
}) })
CheatSheet.init(serverData) CheatSheet.init(serverData)
$('.viewOnly .requestAccess').click(self.requestAccess)
$(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper)
}, },
setHasLearnedTopicCreation: function(value) {
const self = Map
self.hasLearnedTopicCreation = value
ReactApp.render()
},
requestAccess: function() { 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 const mapId = Active.Map.id
$.post({ $.post({
url: `/maps/${mapId}/access_request` url: `/maps/${mapId}/access_request`
}) })
GlobalUI.notifyUser('Map creator will be notified of your request') GlobalUI.notifyUser('Map creator will be notified of your request')
}, },
setAccessRequest: function(requests, activeMapper) { setAccessRequest: function() {
let className = 'isViewOnly ' const self = Map
if (activeMapper) { if (Active.Mapper) {
const request = _find(requests, r => r.user_id === activeMapper.id) const request = _find(self.requests, r => r.user_id === Active.Mapper.id)
if (!request) className += 'sendRequest' if (!request) {
else if (request && !request.answered) className += 'sentRequest' self.userRequested = false
else if (request && request.answered && !request.approved) className += 'requestDenied' 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) { launch: function(id) {
var start = function(data) { const self = Map
Active.Map = new DataModelMap(data.map) var dataIsReadySetupMap = function() {
DataModel.Mappers = new DataModel.MapperCollection(data.mappers) Map.setAccessRequest()
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
Visualize.type = 'ForceDirected' Visualize.type = 'ForceDirected'
JIT.prepareVizData() JIT.prepareVizData()
// update filters
Filter.reset()
// reset selected arrays
Selected.reset() Selected.reset()
// set the proper mapinfobox content
InfoBox.load() InfoBox.load()
Filter.reset()
// these three update the actual filter box with the right list items
Filter.checkMetacodes() Filter.checkMetacodes()
Filter.checkSynapses() Filter.checkSynapses()
Filter.checkMappers() Filter.checkMappers()
Realtime.startActiveMap() Realtime.startActiveMap()
Loading.hide() Loading.hide()
document.title = Active.Map.get('name') + ' | Metamaps'
// for mobile ReactApp.mobileTitle = Active.Map.get('name')
$('#header_content').html(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() { end: function() {
if (Active.Map) { if (Active.Map) {
$('.wrapper').removeClass('canEditMap commonsMap') $('.main').removeClass('compressed')
AutoLayout.resetSpiral() AutoLayout.resetSpiral()
$('.rightclickmenu').remove() $('.rightclickmenu').remove()
TopicCard.hideCard() TopicCard.hideCard()
SynapseCard.hideCard() SynapseCard.hideCard()
Create.newTopic.hide(true) // true means force (and override pinned) Create.newTopic.hide(true) // true means force (and override pinned)
Create.newSynapse.hide() Create.newSynapse.hide()
Filter.close()
InfoBox.close() InfoBox.close()
Realtime.endActiveMap() Realtime.endActiveMap()
$('.viewOnly').removeClass('isViewOnly') self.requests = []
} self.hasLearnedTopicCreation = true
},
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')
} }
}, },
star: function() { star: function() {
@ -173,7 +156,8 @@ const Map = {
DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id })
DataModel.Maps.Starred.add(Active.Map) DataModel.Maps.Starred.add(Active.Map)
GlobalUI.notifyUser('Map is now starred') GlobalUI.notifyUser('Map is now starred')
self.updateStar() self.mapIsStarred = true
ReactApp.render()
}, },
unstar: function() { unstar: function() {
var self = Map var self = Map
@ -182,7 +166,8 @@ const Map = {
$.post('/maps/' + Active.Map.id + '/unstar') $.post('/maps/' + Active.Map.id + '/unstar')
DataModel.Stars = DataModel.Stars.filter(function(s) { return s.user_id !== Active.Mapper.id }) DataModel.Stars = DataModel.Stars.filter(function(s) { return s.user_id !== Active.Mapper.id })
DataModel.Maps.Starred.remove(Active.Map) DataModel.Maps.Starred.remove(Active.Map)
self.updateStar() self.mapIsStarred = false
ReactApp.render()
}, },
fork: function() { fork: function() {
GlobalUI.openLightbox('forkmap') GlobalUI.openLightbox('forkmap')
@ -232,7 +217,7 @@ const Map = {
var map = Active.Map var map = Active.Map
DataModel.Maps.Active.remove(map) DataModel.Maps.Active.remove(map)
DataModel.Maps.Featured.remove(map) DataModel.Maps.Featured.remove(map)
Router.home() browserHistory.push('/')
GlobalUI.notifyUser('Sorry! That map has been changed to Private.') GlobalUI.notifyUser('Sorry! That map has been changed to Private.')
}, },
cantEditNow: function() { cantEditNow: function() {
@ -245,7 +230,7 @@ const Map = {
confirmString += 'Do you want to reload and enable realtime collaboration?' confirmString += 'Do you want to reload and enable realtime collaboration?'
var c = window.confirm(confirmString) var c = window.confirm(confirmString)
if (c) { if (c) {
Router.maps(Active.Map.id) window.location.reload()
} }
}, },
editedByActiveMapper: function() { editedByActiveMapper: function() {

View file

@ -151,8 +151,6 @@ let Realtime = {
config: { DOUBLE_CLICK_TOLERANCE: 200 } config: { DOUBLE_CLICK_TOLERANCE: 200 }
}) })
self.room.videoAdded(self.handleVideoAdded) self.room.videoAdded(self.handleVideoAdded)
self.startActiveMap()
} // if Active.Mapper } // if Active.Mapper
}, },
addJuntoListeners: function() { addJuntoListeners: function() {
@ -201,9 +199,6 @@ let Realtime = {
self.leaveMap() self.leaveMap()
$('.collabCompass').remove() $('.collabCompass').remove()
if (self.room) self.room.leave() if (self.room) self.room.leave()
ChatView.hide()
ChatView.close()
ChatView.reset()
Cable.unsubscribeFromMap() Cable.unsubscribeFromMap()
}, },
turnOn: function(notify) { turnOn: function(notify) {
@ -228,7 +223,6 @@ let Realtime = {
ChatView.setNewMap() ChatView.setNewMap()
ChatView.addParticipant(self.activeMapper) ChatView.addParticipant(self.activeMapper)
ChatView.addMessages(new DataModel.MessageCollection(DataModel.Messages), true) ChatView.addMessages(new DataModel.MessageCollection(DataModel.Messages), true)
ChatView.show()
}, },
setupLocalEvents: function() { setupLocalEvents: function() {
var self = Realtime 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 Create from './Create'
import DataModel from './DataModel' import DataModel from './DataModel'
import Filter from './Filter' import Filter from './Filter'
import GlobalUI from './GlobalUI' import GlobalUI, { ReactApp } from './GlobalUI'
import JIT from './JIT' import JIT from './JIT'
import Loading from './Loading'
import Map from './Map' import Map from './Map'
import Router from './Router'
import Selected from './Selected' import Selected from './Selected'
import Settings from './Settings' import Settings from './Settings'
import SynapseCard from './SynapseCard' import SynapseCard from './SynapseCard'
@ -36,48 +36,41 @@ const Topic = {
} else callback(DataModel.Topics.get(id)) } else callback(DataModel.Topics.get(id))
}, },
launch: function(id) { launch: function(id) {
var start = function(data) { var dataIsReadySetupTopic = function() {
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
Visualize.type = 'RGraph' Visualize.type = 'RGraph'
JIT.prepareVizData() JIT.prepareVizData()
// update filters
Filter.reset()
// reset selected arrays
Selected.reset() Selected.reset()
Filter.reset()
// these three update the actual filter box with the right list items
Filter.checkMetacodes() Filter.checkMetacodes()
Filter.checkSynapses() Filter.checkSynapses()
Filter.checkMappers() Filter.checkMappers()
document.title = Active.Topic.get('name') + ' | Metamaps'
// for mobile ReactApp.mobileTitle = Active.Topic.get('name')
$('#header_content').html(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() { end: function() {
if (Active.Topic) { if (Active.Topic) {
$('.rightclickmenu').remove() $('.rightclickmenu').remove()
TopicCard.hideCard() TopicCard.hideCard()
SynapseCard.hideCard() SynapseCard.hideCard()
Filter.close()
} }
}, },
centerOn: function(nodeid, callback) { centerOn: function(nodeid, callback) {
@ -90,7 +83,6 @@ const Topic = {
if (callback) callback() if (callback) callback()
} }
}) })
Router.navigate('/topics/' + nodeid)
Active.Topic = DataModel.Topics.get(nodeid) Active.Topic = DataModel.Topics.get(nodeid)
} }
}, },
@ -293,8 +285,7 @@ const Topic = {
return return
} }
// hide the 'double-click to add a topic' message Map.setHasLearnedTopicCreation(true)
GlobalUI.hideDiv('#instructions')
$(document).trigger(Map.events.editedByActiveMapper) $(document).trigger(Map.events.editedByActiveMapper)
@ -327,8 +318,7 @@ const Topic = {
getTopicFromAutocomplete: function(id) { getTopicFromAutocomplete: function(id) {
var self = Topic var self = Topic
// hide the 'double-click to add a topic' message Map.setHasLearnedTopicCreation(true)
GlobalUI.hideDiv('#instructions')
$(document).trigger(Map.events.editedByActiveMapper) $(document).trigger(Map.events.editedByActiveMapper)

View file

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

View file

@ -1,21 +1,68 @@
/* global $ */ /* global $ */
import React from 'react'
import ReactDOM from 'react-dom' // TODO ensure this isn't a double import
import Active from '../Active' import Active from '../Active'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI' import GlobalUI, { ReactApp } from '../GlobalUI'
import Realtime from '../Realtime'
import Loading from '../Loading' import Loading from '../Loading'
import Maps from '../../components/Maps'
const ExploreMaps = { const ExploreMaps = {
pending: false, pending: false,
mapper: null, 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) { setCollection: function(collection) {
var self = ExploreMaps var self = ExploreMaps
if (self.collection) { if (self.collection) {
self.collection.off('add', self.render) self.collection.off('add', self.render)
self.collection.off('successOnFetch', self.handleSuccess) self.collection.off('successOnFetch', self.handleSuccess)
@ -26,55 +73,9 @@ const ExploreMaps = {
self.collection.on('successOnFetch', self.handleSuccess) self.collection.on('successOnFetch', self.handleSuccess)
self.collection.on('errorOnFetch', self.handleError) self.collection.on('errorOnFetch', self.handleError)
}, },
render: function(cb) { render: function() {
var self = ExploreMaps ReactApp.resize()
ReactApp.render()
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()
Loading.hide() Loading.hide()
}, },
loadMore: function() { loadMore: function() {
@ -85,14 +86,13 @@ const ExploreMaps = {
} }
self.render() self.render()
}, },
handleSuccess: function(cb) { handleSuccess: function() {
var self = ExploreMaps var self = ExploreMaps
self.pending = false self.pending = false
if (self.collection && self.collection.id === 'mapper') { if (self.collection && self.collection.id === 'mapper') {
self.fetchUserThenRender(cb) self.fetchUserThenRender()
} else { } else {
self.render(cb) self.render()
Loading.hide()
} }
}, },
handleError: function() { handleError: function() {
@ -103,8 +103,8 @@ const ExploreMaps = {
var self = ExploreMaps var self = ExploreMaps
if (self.mapper && self.mapper.id === self.collection.mapperId) { if (self.mapper && self.mapper.id === self.collection.mapperId) {
self.render(cb) self.render()
return Loading.hide() return
} }
// first load the mapper object and then call the render function // first load the mapper object and then call the render function
@ -112,14 +112,42 @@ const ExploreMaps = {
url: '/users/' + self.collection.mapperId + '/details.json', url: '/users/' + self.collection.mapperId + '/details.json',
success: function(response) { success: function(response) {
self.mapper = response self.mapper = response
self.render(cb) document.title = self.mapper.name + ' | Metamaps'
Loading.hide() ReactApp.mobileTitle = self.mapper.name
self.render()
}, },
error: function() { error: function() {
self.render(cb) self.render()
Loading.hide()
} }
}) })
},
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 Active from '../Active'
import Visualize from '../Visualize' import Visualize from '../Visualize'
import GlobalUI from '../GlobalUI' import GlobalUI, { ReactApp } from '../GlobalUI'
import ReactTopicCard from '../../components/TopicCard'
const TopicCard = { const TopicCard = {
openTopicCard: null, // stores the topic that's currently open openTopic: null, // stores the topic that's currently open
metacodeSets: [], metacodeSets: [],
redrawCanvas: () => {
Visualize.mGraph.plot()
},
init: function(serverData) { init: function(serverData) {
const self = TopicCard const self = TopicCard
self.metacodeSets = serverData.metacodeSets self.metacodeSets = serverData.metacodeSets
}, },
populateShowCard: function(topic) { onTopicFollow: topic => {
const self = TopicCard const self = TopicCard
ReactDOM.render( const isFollowing = topic.isFollowedBy(Active.Mapper)
React.createElement(ReactTopicCard, { $.post({
topic: topic, url: `/topics/${topic.id}/${isFollowing ? 'un' : ''}follow`
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')
}
}) })
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 var self = TopicCard
if (!opts) opts = {}
var topic = node.getData('topic') var topic = node.getData('topic')
self.openTopicCard = topic self.openTopic = topic
// populate the card that's about to show with the right topics data self.render()
self.populateShowCard(topic) $('.showcard').fadeIn('fast', () => {
return $('.showcard').fadeIn('fast', () => opts.complete && opts.complete()) $('.showcard').draggable({
handle: '.metacodeImage',
stop: function() {
$(this).height('auto')
}
})
opts.complete && opts.complete()
})
}, },
hideCard: function() { hideCard: function() {
var self = TopicCard var self = TopicCard
$('.showcard').fadeOut('fast') $('.showcard').fadeOut('fast')
self.openTopicCard = null self.openTopic = null
} }
} }

View file

@ -8,7 +8,6 @@ import Active from './Active'
import DataModel from './DataModel' import DataModel from './DataModel'
import JIT from './JIT' import JIT from './JIT'
import Loading from './Loading' import Loading from './Loading'
import Router from './Router'
import TopicCard from './Views/TopicCard' import TopicCard from './Views/TopicCard'
const Visualize = { const Visualize = {
@ -198,19 +197,6 @@ const Visualize = {
} }
} }
hold() 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() { clearVisualization: function() {
Visualize.mGraph.graph.empty() Visualize.mGraph.graph.empty()

View file

@ -9,8 +9,7 @@ import DataModel from './DataModel'
import Debug from './Debug' import Debug from './Debug'
import Filter from './Filter' import Filter from './Filter'
import GlobalUI, { import GlobalUI, {
Search, CreateMap, ImportDialog, Account as GlobalUIAccount, ReactApp, Search, CreateMap, ImportDialog
NotificationIcon
} from './GlobalUI' } from './GlobalUI'
import Import from './Import' import Import from './Import'
import JIT from './JIT' import JIT from './JIT'
@ -18,12 +17,10 @@ import Listeners from './Listeners'
import Loading from './Loading' import Loading from './Loading'
import Map, { CheatSheet, InfoBox } from './Map' import Map, { CheatSheet, InfoBox } from './Map'
import Mapper from './Mapper' import Mapper from './Mapper'
import Mobile from './Mobile'
import Mouse from './Mouse' import Mouse from './Mouse'
import Organize from './Organize' import Organize from './Organize'
import PasteInput from './PasteInput' import PasteInput from './PasteInput'
import Realtime from './Realtime' import Realtime from './Realtime'
import Router from './Router'
import Selected from './Selected' import Selected from './Selected'
import Settings from './Settings' import Settings from './Settings'
import Synapse from './Synapse' import Synapse from './Synapse'
@ -45,11 +42,10 @@ Metamaps.DataModel = DataModel
Metamaps.Debug = Debug Metamaps.Debug = Debug
Metamaps.Filter = Filter Metamaps.Filter = Filter
Metamaps.GlobalUI = GlobalUI Metamaps.GlobalUI = GlobalUI
Metamaps.GlobalUI.ReactApp = ReactApp
Metamaps.GlobalUI.Search = Search Metamaps.GlobalUI.Search = Search
Metamaps.GlobalUI.CreateMap = CreateMap Metamaps.GlobalUI.CreateMap = CreateMap
Metamaps.GlobalUI.Account = GlobalUIAccount
Metamaps.GlobalUI.ImportDialog = ImportDialog Metamaps.GlobalUI.ImportDialog = ImportDialog
Metamaps.GlobalUI.NotificationIcon = NotificationIcon
Metamaps.Import = Import Metamaps.Import = Import
Metamaps.JIT = JIT Metamaps.JIT = JIT
Metamaps.Listeners = Listeners Metamaps.Listeners = Listeners
@ -59,12 +55,10 @@ Metamaps.Map.CheatSheet = CheatSheet
Metamaps.Map.InfoBox = InfoBox Metamaps.Map.InfoBox = InfoBox
Metamaps.Maps = {} Metamaps.Maps = {}
Metamaps.Mapper = Mapper Metamaps.Mapper = Mapper
Metamaps.Mobile = Mobile
Metamaps.Mouse = Mouse Metamaps.Mouse = Mouse
Metamaps.Organize = Organize Metamaps.Organize = Organize
Metamaps.PasteInput = PasteInput Metamaps.PasteInput = PasteInput
Metamaps.Realtime = Realtime Metamaps.Realtime = Realtime
Metamaps.Router = Router
Metamaps.Selected = Selected Metamaps.Selected = Selected
Metamaps.Settings = Settings Metamaps.Settings = Settings
Metamaps.Synapse = Synapse Metamaps.Synapse = Synapse
@ -86,26 +80,6 @@ document.addEventListener('DOMContentLoaded', function() {
Metamaps[prop].init(Metamaps.ServerData) 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 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' import React, { PropTypes, Component } from 'react'
class NotificationIcon extends Component { class NotificationIcon extends Component {
static propTypes = {
unreadNotificationsCount: PropTypes.number
}
constructor(props) { constructor(props) {
super(props) super(props)
@ -31,8 +36,4 @@ class NotificationIcon extends Component {
} }
} }
NotificationIcon.propTypes = {
unreadNotificationsCount: PropTypes.number
}
export default NotificationIcon 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 React from 'react'
import Autolinker from 'autolinker' 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 }) 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 React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import _ from 'lodash' import _ from 'lodash'
const MapLink = props => { const MapLink = props => {
@ -9,16 +10,16 @@ const MapLink = props => {
} }
return ( return (
<a { ...otherProps } href={href} className={linkClass}> <Link { ...otherProps } to={href} className={linkClass}>
<div className="exploreMapsIcon"></div> <div className="exploreMapsIcon"></div>
{text} {text}
</a> </Link>
) )
} }
class Header extends Component { class Header extends Component {
render = () => { render = () => {
const { signedIn, section } = this.props const { signedIn, section, user } = this.props
const activeClass = (title) => { const activeClass = (title) => {
let forClass = 'exploreMapsButton' let forClass = 'exploreMapsButton'
@ -39,38 +40,33 @@ class Header extends Component {
<MapLink show={explore} <MapLink show={explore}
href={signedIn ? '/' : '/explore/active'} href={signedIn ? '/' : '/explore/active'}
linkClass={activeClass('active')} linkClass={activeClass('active')}
data-router="true"
text="All Maps" text="All Maps"
/> />
<MapLink show={signedIn && explore} <MapLink show={signedIn && explore}
href="/explore/mine" href="/explore/mine"
linkClass={activeClass('my')} linkClass={activeClass('my')}
data-router="true"
text="My Maps" text="My Maps"
/> />
<MapLink show={signedIn && explore} <MapLink show={signedIn && explore}
href="/explore/shared" href="/explore/shared"
linkClass={activeClass('shared')} linkClass={activeClass('shared')}
data-router="true"
text="Shared With Me" text="Shared With Me"
/> />
<MapLink show={signedIn && explore} <MapLink show={signedIn && explore}
href="/explore/starred" href="/explore/starred"
linkClass={activeClass('starred')} linkClass={activeClass('starred')}
data-router="true"
text="Starred By Me" text="Starred By Me"
/> />
<MapLink show={!signedIn && explore} <MapLink show={!signedIn && explore}
href="/explore/featured" href="/explore/featured"
linkClass={activeClass('featured')} linkClass={activeClass('featured')}
data-router="true"
text="Featured Maps" text="Featured Maps"
/> />
{mapper ? ( {mapper ? (
<div className='exploreMapsButton active mapperButton'> <div className='exploreMapsButton active mapperButton'>
<img className='exploreMapperImage' width='24' height='24' src={this.props.user.image} /> {user && <img className='exploreMapperImage' width='24' height='24' src={user.image} />}
<div className='exploreMapperName'>{this.props.user.name}&rsquo;s Maps</div> {user && <div className='exploreMapperName'>{user.name}&rsquo;s Maps</div>}
<div className='clearfloat'></div> <div className='clearfloat'></div>
</div> </div>
) : null } ) : null }

View file

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

View file

@ -4,59 +4,56 @@ import Header from './Header'
import MapperCard from './MapperCard' import MapperCard from './MapperCard'
import MapCard from './MapCard' 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 { class Maps extends Component {
constructor(props) { static propTypes = {
super(props) section: PropTypes.string,
this.state = { mapsWidth: 0 } 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() { static contextTypes = {
window && window.addEventListener('resize', this.resize) location: PropTypes.object
this.refs.maps.addEventListener('scroll', throttle(this.scroll, 500, { leading: true, trailing: false }))
this.resize()
} }
componentWillUnmount() { mapsDidMount = (node) => {
window && window.removeEventListener('resize', this.resize) if (node) {
} this.mapsDiv = node
node.addEventListener('scroll', throttle(this.scroll, 500, { leading: true, trailing: false }))
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 })
} }
scroll = () => { scroll = () => {
const { loadMore, moreToLoad, pending } = this.props const { loadMore, moreToLoad, pending } = this.props
const { maps } = this.refs const { mapsDiv } = this
if (moreToLoad && !pending && maps.scrollTop + maps.offsetHeight > maps.scrollHeight - 300) { if (moreToLoad && !pending && mapsDiv.scrollTop + mapsDiv.offsetHeight > mapsDiv.scrollHeight - 300) {
loadMore() loadMore()
} }
} }
render = () => { render = () => {
const { maps, currentUser, juntoState, pending, section, user, onStar, onRequest, onFollow } = this.props const { mobile, maps, mapsWidth, currentUser, juntoState, pending, section, user, onStar, onRequest, onMapFollow } = this.props
const style = { width: this.state.mapsWidth + 'px' } const style = { width: mapsWidth + 'px' }
const mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT
if (!maps) return null // do loading here instead
return ( return (
<div> <div>
<div id='exploreMaps' ref='maps'> <div id='exploreMaps' ref={this.mapsDidMount}>
<div style={ style }> <div style={ style }>
{ user ? <MapperCard user={ user } /> : null } { 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 } { 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 className='clearfloat'></div>
</div> </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 export default Maps

View file

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

View file

@ -1,6 +1,7 @@
/* global $ */ /* global $ */
import React, { PropTypes, Component } from 'react' import React, { PropTypes, Component } from 'react'
import { Link } from 'react-router'
import MetacodeSelect from '../MetacodeSelect' import MetacodeSelect from '../MetacodeSelect'
import Permission from './Permission' import Permission from './Permission'
@ -52,21 +53,21 @@ class Links extends Component {
} }
let output = [] let output = []
firstFiveLinks.forEach(obj => { 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 (extraLinks.length > 0) {
if (this.state.showMoreMaps) { if (this.state.showMoreMaps) {
extraLinks.forEach(obj => { 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...` 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>) output.push(<li key="showMore"><span className="showMore" onClick={this.toggleShowMoreMaps}>{text}</span></li>)
} }
return output return output
} }

View file

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