topic card in react (#1031)

* its coming along

* links bar

* scssify a bunch

* metacode image working a bit better

* metacode selector in react topic card

* riek editing for name field on topic card

* riek submit on enter

* factor out Title and Links from Topic Card component, but not the listeners

* create working Desc editor

* styling is much better now

* textarea min height for desc

* disallow images in topic card markdown

* shift enter is linebreak, enter is save

* attachments split out, but it's pretty buggy

* move listeners into Links.js

* slightly wider metacodeTitle

* fix positioning on metacode selector

* fix metacode selection

* move metacode and permissions into subcomponents

* fixes

* prevent editing on desc/title if not authorized to edit

* fix topic card draggability

* fix embedly

* fix md test

* remove the removed link card manually with jquery

* fix test syntax

* eslint

* more eslin

* reuse authorizedToEdit

* convert metacode sets to a json object for react

* add the html in react whoop

* fix metacode styling

* sort wasn't working

* finishing metacode select

* readd the above link input border

* fix syntax

* multiline title editable textarea

* more portable metacode selector component

* factor out #metacodeOptions into one react component with a callback :D:D:D

* render metacodeOptions in right click menu with react

* render metacodeOptions in right click menu with react

* fix up right click menu's metacode editing

* fix topic card title character counter

* ignore metamaps secret bundle in ag

* simplify Attachments props

* factor out embedly card into its own component; it seems to help

* link resetter

* fix edit icon on title in topic card

* move mapCount and synapseCount hover/click logic to react

* fix up the showMore control

* metacode selection tweaks

* tweak links bar spacing in topic card

* rubocop

* remove TODOs

* more badass permissions selector

* close permission selector when you click outside

* fix overeager metacode selector

* more modular attachments component

* fix bug in Desc.js

* fix right click styling

* permission changes are different than edit rights

* bad module ref

* ensure maxLength on topic titles
This commit is contained in:
Connor Turland 2017-02-26 11:42:47 -05:00 committed by GitHub
parent 47d0faadf2
commit 4deb3f5ab9
29 changed files with 1097 additions and 919 deletions

1
.agignore Normal file
View file

@ -0,0 +1 @@
app/assets/javascripts/metamaps.secret.bundle.js

View file

@ -1250,7 +1250,7 @@ h3.filterBox {
box-shadow: 0px 3px 3px rgba(0,0,0,0.12), 0 3px 3px rgba(0,0,0,0.24);
}
.rightclickmenu .rc-permission:hover > ul,
.rightclickmenu .rc-metacode:hover > ul,
.rightclickmenu .rc-metacode:hover #metacodeOptions > ul,
.rightclickmenu .rc-siblings:hover > ul {
display: block;
}
@ -1279,7 +1279,7 @@ h3.filterBox {
.rightclickmenu li.toPrivate .rc-perm-icon {
background-position: -24px 0;
}
.rightclickmenu .rc-metacode > ul > li,
.rightclickmenu .rc-metacode #metacodeOptions > ul > li,
.rightclickmenu .rc-siblings > ul > li {
padding: 6px 24px 6px 8px;
white-space: nowrap;

View file

@ -6,6 +6,11 @@
font-family: helvetica;
color: #727272;
line-height: 11px;
display: none;
}
.riek-editing + .nameCounter {
display: block;
}
.nameCounter.forMap {
@ -85,6 +90,11 @@
display: table-cell;
vertical-align: middle;
padding: 0 16px;
&.riek-editing {
position: absolute;
top: 32px;
}
}
.canEdit #titleActivator:hover {
background-image: url(<%= asset_data_uri('edit.png') %>);
@ -93,12 +103,12 @@
cursor: text;
}
.showcard .best_in_place_name textarea, .showcard .best_in_place_name input {
.showcard .title .riek-editing {
font-family: 'din-regular', sans-serif;
color: #424242;
font-size: 18px;
line-height: 22px;
height: 15px;
height: 3em;
padding: 5px 0;
width: 100%;
margin: 0;
@ -122,7 +132,7 @@
height: auto;
}
.CardOnGraph .best_in_place_desc textarea {
.CardOnGraph .desc .riek-editing {
font-size: 13px;
line-height:15px;
font-family: helvetica, sans-serif;
@ -167,13 +177,14 @@
* End Markdown styling
*/
.CardOnGraph .best_in_place_desc {
.CardOnGraph .riek_desc {
display:block;
margin-top:2px;
padding-right: 18px;
margin-right: 8px;
min-height: 7em;
}
.canEdit .CardOnGraph .best_in_place_desc:hover {
.canEdit .CardOnGraph .riek_desc:hover {
background-image: url(<%= asset_data_uri('edit.png') %>);
background-position: top right;
background-repeat: no-repeat;
@ -185,52 +196,64 @@
}
.CardOnGraph .links {
position:relative;
position: relative;
border-bottom: 1px solid #BDBDBD;
border-top: 1px solid #BDBDBD;
background-color: #e0e0e0;
}
.linkItem {
float:left;
height:46px;
.linkItem {
float: left;
z-index: 1;
position: relative;
color: #424242;
font-size: 14px;
line-height:14px;
height:12px;
padding:17px 0;
}
.linkItem a {
color: #424242;
}
line-height: 14px;
.CardOnGraph .icon {
position:absolute;
width:100%;
z-index:1;
a {
color: #424242;
}
}
.icon {
position: absolute;
z-index: 1;
padding: 0;
height: 48px;
}
.linkItem.contributor {
margin-left:40px;
z-index:1;
padding:17px 16px 17px 30px;
width: 100%;
.metacodeImage {
cursor: move;
position: relative;
}
.contributor .contributorIcon {
position: absolute;
left: -23px;
top: 1px;
width: 46px;
height: 46px;
background-size:46px 46px;
background-position:0 0;
background-repeat:no-repeat;
}
}
.contributor {
bottom: 7px;
margin-left: 40px;
.contributorIcon {
position: relative;
vertical-align: middle;
border-radius: 16px;
margin: 5px;
top: 8px;
left: 0;
border-radius: 16px;
}
}
.contributor:hover .contributorName {
display: block;
}
span {
font-family: 'din-regular', sans-serif;
font-size: 14px;
}
.contributorName {
.contributorName {
display: none;
position: absolute;
background: black;
@ -242,70 +265,9 @@
font-size: 12px;
padding: 3px 5px 2px;
white-space: nowrap;
margin-top: 36px;
margin-left: -32px;
}
margin-top: 8px;
.contributor div:before {
content: '';
position: absolute;
top: 128%;
left: 13px;
margin-top: -30px;
width: 0;
height: 0;
border-bottom: 4px solid #000000;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
.linkItem.mapCount {
margin-left: 12px;
width: 24px;
padding:17px 0 17px 36px;
}
.linkItem.mapCount .mapCountIcon {
position: absolute;
top: 8px;
left: 0;
width: 32px;
height: 32px;
background-image: url(<%= asset_data_uri('map32_sprite.png') %>);
background-repeat: no-repeat;
background-position: 0 0;
cursor: pointer;
}
.linkItem.mapCount:hover .mapCountIcon {
background-position: 0 -32px;
}
.linkItem.mapCount:hover .hoverTip {
display: block;
}
.CardOnGraph .mapCount .tip, .CardonGraph .mapCount .hoverTip {
top: 44px;
left: 0px;
font-size: 12px !important;
}
.hoverTip {
white-space: nowrap;
font-family: 'din-regular';
top: 44px;
left: 0px;
font-size: 12px !important;
display: none;
position: absolute;
background: black;
color: white;
border-radius: 4px;
line-height: 17px;
padding: 3px 5px 2px;
z-index: 100;
}
.CardOnGraph .mapCount .tip:before, .CardOnGraph .mapCount .hoverTip:before {
&:before {
content: '';
position: absolute;
top: 26px;
@ -316,9 +278,77 @@
border-bottom: 4px solid #000000;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
}
}
.CardOnGraph .mapCount .tip li {
&:hover .contributorName {
display: block;
}
}
.mapCount {
padding:17px 0 17px 36px;
margin-left: 12px;
.mapCountIcon {
position: absolute;
top: 8px;
left: 0;
width: 32px;
height: 32px;
background-image: url(<%= asset_data_uri('map32_sprite.png') %>);
background-repeat: no-repeat;
background-position: 0 0;
cursor: pointer;
}
&:hover .mapCountIcon {
background-position: 0 -32px;
}
.tip, .hoverTip {
top: 44px;
left: 0px;
font-size: 12px !important;
&:before {
content: '';
position: absolute;
top: 26px;
left: 10px;
margin-top: -30px;
width: 0;
height: 0;
border-bottom: 4px solid #000000;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
}
.hoverTip {
white-space: nowrap;
font-family: 'din-regular';
top: 44px;
left: 0px;
font-size: 12px !important;
position: absolute;
background: black;
color: white;
border-radius: 4px;
line-height: 17px;
padding: 3px 5px 2px;
z-index: 100;
}
.tip a {
color: white;
}
.tip a:hover {
color: #757575;
}
.tip li {
list-style-type: none;
white-space: nowrap;
overflow: hidden;
@ -330,32 +360,15 @@
font-size: 14px;
line-height: 14px;
position: relative;
}
}
}
.CardOnGraph .mapCount li.hideExtra {
display: none;
}
.showMore {
cursor: pointer;
color: #4FC059;
}
.mapCount .tip a {
color: white;
}
.mapCount .tip a:hover {
color: #757575;
}
.linkItem.synapseCount {
margin-left: 2px;
.synapseCount {
margin-left: 26px;
width: 24px;
padding:17px 0 17px 32px;
}
.linkItem.synapseCount .synapseCountIcon {
.synapseCountIcon {
position: absolute;
top: 8px;
left: 0;
@ -364,12 +377,11 @@
background-image: url(<%= asset_data_uri('synapse32_sprite.png') %>);
background-repeat: no-repeat;
background-position: 0 0;
}
.linkItem.synapseCount:hover .synapseCountIcon {
}
hover .synapseCountIcon {
background-position: 0 -32px;
}
.CardOnGraph .synapseCount .tip {
}
.tip {
position: absolute;
background: black;
width: auto;
@ -382,13 +394,9 @@
line-height: 12px;
padding: 4px 4px 4px;
z-index: 100;
}
}
.CardOnGraph .synapseCount:hover .tip {
display: block;
}
.CardOnGraph .synapseCount .tip:before {
.tip:before {
content: '';
position: absolute;
margin-top: -8px;
@ -398,8 +406,14 @@
border-bottom: 4px solid black;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
}
}
.showMore {
cursor: pointer;
color: #4FC059;
}
.mapPerm {
width: 32px;
@ -470,7 +484,7 @@ cursor: pointer;
text-transform: uppercase;
position: absolute;
line-height: 24px;
height:24px;
height: 26px;
font-size: 24px;
display: none;
width: 90%;
@ -496,29 +510,19 @@ cursor: pointer;
}
.CardOnGraph .metacodeImage {
cursor:move;
width:46px;
height:46px;
position:absolute;
left:-23px;
top:0;
background-size:46px 46px;
background-position:0 0;
background-repeat:no-repeat;
.CardOnGraph .metacodeName {
display: inline-block;
}
#metacodeOptions {
display:none;
}
.CardOnGraph .metacodeSelect {
display:none;
width:auto;
z-index: 2;
position: absolute;
background: #EAEAEA;
left: 300px;
white-space: nowrap;
position: absolute;
left: 300px;
top: -1px;
}
.CardOnGraph .metacodeSelect ul {
position: relative;
@ -610,7 +614,6 @@ background-color: #E0E0E0;
display:block;
}
.CardOnGraph .tip {
display:none;
position: absolute;
background: black;
top: 35px;
@ -623,21 +626,19 @@ background-color: #E0E0E0;
z-index:100;
}
#embedlyLink {
display: none;
}
#embedlyLinkLoader {
margin: 0 auto;
width: 28px;
}
.CardOnGraph .attachments {
border-top: 1px solid #BDBDBD;
.CardOnGraph .link-adder {
width:100%;
height:47px;
position: relative;
border-top: 1px solid #BDBDBD;
}
.attachments a {
.link-adder a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -752,7 +753,6 @@ font-family: 'din-regular', helvetica, sans-serif;
-moz-border-radius-bottomright: 8px;
-webkit-border-bottom-right-radius: 8px;
border-bottom-right-radius: 8px;
display: none;
margin: 8px;
}
@ -839,10 +839,10 @@ font-family: 'din-regular', helvetica, sans-serif;
line-height: 16px;
}
.canEdit #edit_synapse_desc:hover {
.canEdit span.titleWrapper:hover {
background-image: url(<%= asset_data_uri('edit.png') %>);
background-repeat: no-repeat;
background-position: 164px center;
background-position: 95% 95%;
cursor: text;
}

View file

@ -1,55 +1,5 @@
# frozen_string_literal: true
module ApplicationHelper
def metacodeset
metacodes = current_user.settings.metacodes
return false unless metacodes[0].include?('metacodeset')
return 'Most' if metacodes[0].sub('metacodeset-', '') == 'Most'
return 'Recent' if metacodes[0].sub('metacodeset-', '') == 'Recent'
MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i)
end
def user_metacodes
@m = current_user.settings.metacodes
set = metacodeset
@metacodes = if set && set == 'Most'
Metacode.where(id: current_user.most_used_metacodes).to_a
elsif set && set == 'Recent'
Metacode.where(id: current_user.recent_metacodes).to_a
elsif set
set.metacodes.to_a
else
Metacode.where(id: @m).to_a
end
focus_code = user_metacode()
if focus_code != nil && @metacodes.index{|m| m.id == focus_code.id} == nil
@metacodes.push(focus_code)
end
@metacodes
.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }
if focus_code != nil
@metacodes.rotate!(@metacodes.index{|m| m.id == focus_code.id})
else
@metacodes.rotate!(-1)
end
end
def user_metacode
current_user.settings.metacode_focus ? Metacode.find(current_user.settings.metacode_focus.to_i) : nil
end
def user_most_used_metacodes
@metacodes = current_user.most_used_metacodes.map { |id| Metacode.find(id) }
end
def user_recent_metacodes
@metacodes = current_user.recent_metacodes.map { |id| Metacode.find(id) }
end
def invite_link
"#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '')
end

View file

@ -1,3 +0,0 @@
# frozen_string_literal: true
module MetacodeSetsHelper
end

View file

@ -1,3 +1,78 @@
# frozen_string_literal: true
module MetacodesHelper
def metacodeset
metacodes = current_user.settings.metacodes
return false unless metacodes[0].include?('metacodeset')
return 'Most' if metacodes[0].sub('metacodeset-', '') == 'Most'
return 'Recent' if metacodes[0].sub('metacodeset-', '') == 'Recent'
MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i)
end
def user_metacodes
@m = current_user.settings.metacodes
set = metacodeset
@metacodes = if set && set == 'Most'
Metacode.where(id: current_user.most_used_metacodes).to_a
elsif set && set == 'Recent'
Metacode.where(id: current_user.recent_metacodes).to_a
elsif set
set.metacodes.to_a
else
Metacode.where(id: @m).to_a
end
focus_code = user_metacode
if !focus_code.nil? && @metacodes.index { |m| m.id == focus_code.id }.nil?
@metacodes.push(focus_code)
end
@metacodes.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }
if !focus_code.nil?
@metacodes.rotate!(@metacodes.index { |m| m.id == focus_code.id })
else
@metacodes.rotate!(-1)
end
end
def user_metacode
current_user.settings.metacode_focus ? Metacode.find(current_user.settings.metacode_focus.to_i) : nil
end
def user_most_used_metacodes
@metacodes = current_user.most_used_metacodes.map { |id| Metacode.find(id) }
end
def user_recent_metacodes
@metacodes = current_user.recent_metacodes.map { |id| Metacode.find(id) }
end
def metacode_sets_json
metacode_sets = []
metacode_sets << {
name: 'Recently Used',
metacodes: user_recent_metacodes
.map { |m| { id: m.id, icon_path: asset_path(m.icon), name: m.name } }
}
metacode_sets << {
name: 'Most Used',
metacodes: user_most_used_metacodes
.map { |m| { id: m.id, icon_path: asset_path(m.icon), name: m.name } }
}
metacode_sets += MetacodeSet.order('name').all.map do |set|
{
name: set.name,
metacodes: set.metacodes.order('name')
.map { |m| { id: m.id, icon_path: asset_path(m.icon), name: m.name } }
}
end
metacode_sets << {
name: 'All',
metacodes: Metacode.order('name').all
.map { |m| { id: m.id, icon_path: asset_path(m.icon), name: m.name } }
}
metacode_sets.to_json
end
end

View file

@ -181,72 +181,4 @@
<div class="clearfloat"></div>
</div>
</script>
<script type="text/template" id="topicCardTemplate">
<div class="CardOnGraph {{hasAttachment}}" id="topic_{{id}}">
<span class="title">
<div class="titleWrapper" id="titleActivator">
<span class="best_in_place best_in_place_name"
data-bip-url="/topics/{{id}}"
data-bip-object="topic"
data-bip-attribute="name"
data-bip-activator="#titleActivator"
data-bip-value="{{name}}"
data-bip-type="textarea"
>
{{name}}
</span>
</div>
</span>
<div class="links">
<div class="linkItem icon">
<div class="metacodeTitle {{metacode_class}}">
{{metacode}}
<div class="expandMetacodeSelect"></div>
</div>
<div class="metacodeImage" style="background-image:url({{imgsrc}});" title="click and drag to move card"></div>
<div class="metacodeSelect">{{{metacode_select}}}</div>
</div>
<div class="linkItem contributor">
<a href="/explore/mapper/{{userid}}" target="_blank"><img src="<%= asset_path('user.png') %>" class="contributorIcon" width="32" height="32" /></a>
<div class="contributorName">{{username}}</div>
</div>
<div class="linkItem mapCount">
<div class="mapCountIcon"></div>
{{map_count}}
<div class ="hoverTip">Click to see which maps topic appears on</div>
<div class="tip"><ul>{{{inmaps}}}</ul></div>
</div>
<a href="/topics/{{id}}" target="_blank" class="linkItem synapseCount">
<div class="synapseCountIcon"></div>
{{synapse_count}}
<div class="tip">Click to see this topics synapses</div>
</a>
<div class="linkItem mapPerm {{mk_permission}}" title="{{permission}}"></div>
<div class="clearfloat"></div>
</div>
<div class="scroll">
<div class="desc">
<span class="best_in_place best_in_place_desc"
data-bip-url="/topics/{{id}}"
data-bip-object="topic"
data-bip-nil="{{desc_nil}}"
data-bip-attribute="desc"
data-bip-type="textarea"
data-bip-value="{{desc_markdown}}"
>
{{{desc_html}}}
</span>
<div class="clearfloat"></div>
</div>
</div>
<div class="embeds">
{{{embeds}}}
</div>
<div class="attachments {{attachmentsHidden}}">
{{{attachments}}}
</div>
<div class="clearfloat"></div>
</div>
</script>
</div>

View file

@ -3,61 +3,7 @@
# this code generates the list of icons that will drop down in the metacode select list on the topic card
#%>
<div id="metacodeOptions">
<ul>
<li>
<span>Recently Used</span>
<div class="expandMetacodeSet"></div>
<ul>
<% user_recent_metacodes().each do |m| %>
<li data-id="<%= m.id.to_s %>">
<img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" />
<div class="mSelectName"><%= m.name %></div>
<div class="clearfloat"></div>
</li>
<% end %>
</ul>
</li>
<li>
<span>Most Used</span>
<div class="expandMetacodeSet"></div>
<ul>
<% user_most_used_metacodes().each do |m| %>
<li data-id="<%= m.id.to_s %>">
<img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" />
<div class="mSelectName"><%= m.name %></div>
<div class="clearfloat"></div>
</li>
<% end %>
</ul>
</li>
<% MetacodeSet.order("name").all.each do |set| %>
<li>
<span><%= set.name %></span>
<div class="expandMetacodeSet"></div>
<ul>
<% set.metacodes.sort { |a, b| a.name <=> b.name }.each do |m| %>
<li data-id="<%= m.id.to_s %>">
<img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" />
<div class="mSelectName"><%= m.name %></div>
<div class="clearfloat"></div>
</li>
<% end %>
</ul>
</li>
<% end %>
<li>
<span>All</span>
<div class="expandMetacodeSet"></div>
<ul>
<% Metacode.order("name").all.each do |m| %>
<li data-id="<%= m.id.to_s %>">
<img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" />
<div class="mSelectName"><%= m.name %></div>
<div class="clearfloat"></div>
</li>
<% end %>
</ul>
</li>
</ul>
</div>
<script>
Metamaps.ServerData = Metamaps.ServerData || {}
Metamaps.ServerData.metacodeSets = <%= raw metacode_sets_json %>
</script>

View file

@ -4,7 +4,7 @@ try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active'
import Filter from '../Filter'
import TopicCard from '../TopicCard'
import TopicCard from '../Views/TopicCard'
import Visualize from '../Visualize'
import DataModel from './index'

View file

@ -2,9 +2,14 @@
import _ from 'lodash'
import outdent from 'outdent'
import clipboard from 'clipboard-js'
import React from 'react'
import ReactDOM from 'react-dom'
import $jit from '../patched/JIT'
import MetacodeSelect from '../components/MetacodeSelect'
import Active from './Active'
import Control from './Control'
import Create from './Create'
@ -18,10 +23,9 @@ import Settings from './Settings'
import Synapse from './Synapse'
import SynapseCard from './SynapseCard'
import Topic from './Topic'
import TopicCard from './TopicCard'
import TopicCard from './Views/TopicCard'
import Util from './Util'
import Visualize from './Visualize'
import clipboard from 'clipboard-js'
let panningInt
@ -1418,9 +1422,7 @@ const JIT = {
<div class="expandLi"></div>
</li>`
const metacodeOptions = $('#metacodeOptions').html()
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode' + metacodeOptions + '<div class="expandLi"></div></li>'
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode<div id="metacodeOptionsWrapper"></div><div class="expandLi"></div></li>'
}
if (Active.Topic) {
if (!Active.Mapper) {
@ -1475,6 +1477,25 @@ const JIT = {
// add the menu to the page
$('#wrapper').append(rightclickmenu)
ReactDOM.render(
React.createElement(MetacodeSelect, {
onMetacodeSelect: metacodeId => {
if (Selected.Nodes.length > 1) {
// batch update multiple topics
Control.updateSelectedMetacodes(metacodeId)
} else {
const topic = DataModel.Topics.get(node.id)
topic.save({
metacode_id: metacodeId
})
}
$(rightclickmenu).remove()
},
metacodeSets: TopicCard.metacodeSets
}),
document.getElementById('metacodeOptionsWrapper')
)
// attach events to clicks on the list items
// delete the selected things from the database
@ -1521,13 +1542,6 @@ const JIT = {
Control.updateSelectedPermissions($(this).text())
})
// change the metacode of all the selected nodes that you have edit permission for
$('.rc-metacode li li').click(function() {
$('.rightclickmenu').remove()
//
Control.updateSelectedMetacodes($(this).attr('data-id'))
})
// fetch relatives
let fetchSent = false
$('.rc-siblings').hover(function() {

View file

@ -16,7 +16,7 @@ import Realtime from '../Realtime'
import Router from '../Router'
import Selected from '../Selected'
import SynapseCard from '../SynapseCard'
import TopicCard from '../TopicCard'
import TopicCard from '../Views/TopicCard'
import Visualize from '../Visualize'
import CheatSheet from './CheatSheet'

View file

@ -14,7 +14,7 @@ import Router from './Router'
import Selected from './Selected'
import Settings from './Settings'
import SynapseCard from './SynapseCard'
import TopicCard from './TopicCard'
import TopicCard from './Views/TopicCard'
import Util from './Util'
import Visualize from './Visualize'

View file

@ -1,474 +0,0 @@
/* global $, CanvasLoader, Countable, Hogan, embedly */
import Active from './Active'
import DataModel from './DataModel'
import GlobalUI from './GlobalUI'
import Mapper from './Mapper'
import Router from './Router'
import Util from './Util'
import Visualize from './Visualize'
const TopicCard = {
openTopicCard: null, // stores the topic that's currently open
authorizedToEdit: false, // stores boolean for edit permission for open topic card
RAILS_ENV: undefined,
init: function(serverData) {
var self = TopicCard
if (serverData.RAILS_ENV) {
self.RAILS_ENV = serverData.RAILS_ENV
} else {
console.error('RAILS_ENV is not defined! See TopicCard.js init function.')
}
// initialize best_in_place editing
$('.authenticated div.permission.canEdit .best_in_place').best_in_place()
TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html())
// initialize topic card draggability and resizability
$('.showcard').draggable({
handle: '.metacodeImage',
stop: function() {
$(this).height('auto')
}
})
embedly('on', 'card.rendered', self.embedlyCardRendered)
},
/**
* Will open the Topic Card for the node that it's passed
* @param {$jit.Graph.Node} node
*/
showCard: function(node, opts) {
var self = TopicCard
if (!opts) opts = {}
var topic = node.getData('topic')
self.openTopicCard = topic
self.authorizedToEdit = topic.authorizeToEdit(Active.Mapper)
// populate the card that's about to show with the right topics data
self.populateShowCard(topic)
return $('.showcard').fadeIn('fast', function() {
if (opts.complete) {
opts.complete()
}
})
},
hideCard: function() {
var self = TopicCard
$('.showcard').fadeOut('fast')
self.openTopicCard = null
self.authorizedToEdit = false
},
embedlyCardRendered: function(iframe) {
$('#embedlyLinkLoader').hide()
// means that the embedly call returned 404 not found
if ($('#embedlyLink')[0]) {
$('#embedlyLink').css('display', 'block').fadeIn('fast')
$('.embeds').addClass('nonEmbedlyLink')
}
$('.CardOnGraph').addClass('hasAttachment')
},
showLinkRemover: function() {
if (TopicCard.authorizedToEdit && $('#linkremove').length === 0) {
$('.embeds').append('<div id="linkremove"></div>')
$('#linkremove').click(TopicCard.removeLink)
}
},
removeLink: function() {
var self = TopicCard
self.openTopicCard.save({
link: null
})
$('.embeds').empty().removeClass('nonEmbedlyLink')
$('#addLinkInput input').val('')
$('.attachments').removeClass('hidden')
$('.CardOnGraph').removeClass('hasAttachment')
},
showLinkLoader: function() {
var loader = new CanvasLoader('embedlyLinkLoader')
loader.setColor('#4fb5c0') // default is '#000000'
loader.setDiameter(28) // default is 40
loader.setDensity(41) // default is 40
loader.setRange(0.9) // default is 1.3
loader.show() // Hidden by default
},
showLink: function(topic) {
var e = embedly('card', document.getElementById('embedlyLink'))
if (!e && TopicCard.RAILS_ENV !== 'development') {
TopicCard.handleInvalidLink()
} else if (!e) {
$('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show()
$('#embedlyLinkLoader').hide()
}
},
bindShowCardListeners: function(topic) {
var self = TopicCard
var showCard = document.getElementById('showcard')
var authorized = self.authorizedToEdit
// get mapper image
var setMapperImage = function(mapper) {
$('.contributorIcon').attr('src', mapper.get('image'))
}
Mapper.get(topic.get('user_id'), setMapperImage)
// starting embed.ly
var resetFunc = function() {
$('#addLinkInput input').val('')
$('#addLinkInput input').focus()
}
var inputEmbedFunc = function(event) {
var element = this
setTimeout(function() {
var text = $(element).val()
if (event.type === 'paste' || (event.type === 'keyup' && event.which === 13)) {
// TODO evaluate converting this to '//' no matter what (infer protocol)
if (text.slice(0, 7) !== 'http://' &&
text.slice(0, 8) !== 'https://' &&
text.slice(0, 2) !== '//') {
text = '//' + text
}
topic.save({
link: text
})
var embedlyEl = $('<a/>', {
id: 'embedlyLink',
'data-card-description': '0',
href: text
}).html(text)
$('.attachments').addClass('hidden')
$('.embeds').append(embedlyEl)
$('.embeds').append('<div id="embedlyLinkLoader"></div>')
self.showLinkLoader()
self.showLink(topic)
}
}, 100)
}
$('#addLinkReset').click(resetFunc)
$('#addLinkInput input').bind('paste keyup', inputEmbedFunc)
// initialize the link card, if there is a link
if (topic.get('link') && topic.get('link') !== '') {
self.showLinkLoader()
self.showLink(topic)
self.showLinkRemover()
}
var selectingMetacode = false
// attach the listener that shows the metacode title when you hover over the image
$('.showcard .metacodeImage').mouseenter(function() {
$('.showcard .icon').css('z-index', '4')
$('.showcard .metacodeTitle').show()
})
$('.showcard .linkItem.icon').mouseleave(function() {
if (!selectingMetacode) {
$('.showcard .metacodeTitle').hide()
$('.showcard .icon').css('z-index', '1')
}
})
var metacodeLiClick = function() {
selectingMetacode = false
var metacodeId = parseInt($(this).attr('data-id'))
var metacode = DataModel.Metacodes.get(metacodeId)
$('.CardOnGraph').find('.metacodeTitle').html(metacode.get('name'))
.append('<div class="expandMetacodeSelect"></div>')
.attr('class', 'metacodeTitle mbg' + metacode.id)
$('.CardOnGraph').find('.metacodeImage').css('background-image', 'url(' + metacode.get('icon') + ')')
topic.save({
metacode_id: metacode.id
})
Visualize.mGraph.plot()
$('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge')
$('.metacodeTitle').hide()
$('.showcard .icon').css('z-index', '1')
}
var openMetacodeSelect = function(event) {
var TOPICCARD_WIDTH = 300
var METACODESELECT_WIDTH = 404
var MAX_METACODELIST_HEIGHT = 270
if (!selectingMetacode) {
selectingMetacode = true
// this is to make sure the metacode
// select is accessible onscreen, when opened
// while topic card is close to the right
// edge of the screen
var windowWidth = $(window).width()
var showcardLeft = parseInt($('.showcard').css('left'))
var distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH)
if (distanceFromEdge < METACODESELECT_WIDTH) {
$('.metacodeSelect').addClass('onRightEdge')
}
// this is to make sure the metacode
// select is accessible onscreen, when opened
// while topic card is close to the bottom
// edge of the screen
var windowHeight = $(window).height()
var showcardTop = parseInt($('.showcard').css('top'))
var topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom'))
var distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight)
if (distanceFromBottom < MAX_METACODELIST_HEIGHT) {
$('.metacodeSelect').addClass('onBottomEdge')
}
$('.metacodeSelect').show()
event.stopPropagation()
}
}
var hideMetacodeSelect = function() {
selectingMetacode = false
$('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge')
$('.metacodeTitle').hide()
$('.showcard .icon').css('z-index', '1')
}
if (authorized) {
$('.showcard .metacodeTitle').click(openMetacodeSelect)
$('.showcard').click(hideMetacodeSelect)
$('.metacodeSelect > ul > li').click(function(event) {
event.stopPropagation()
})
$('.metacodeSelect li li').click(metacodeLiClick)
var bipName = $(showCard).find('.best_in_place_name')
bipName.bind('best_in_place:activate', function() {
var $el = bipName.find('textarea')
var el = $el[0]
$el.attr('maxlength', '140')
$('.showcard .title').append('<div class="nameCounter forTopic"></div>')
var callback = function(data) {
$('.nameCounter.forTopic').html(data.all + '/140')
}
Countable.live(el, callback)
})
bipName.bind('best_in_place:deactivate', function() {
$('.nameCounter.forTopic').remove()
})
bipName.keypress(function(e) {
const ENTER = 13
if (e.which === ENTER) { // enter
$(this).data('bestInPlaceEditor').update()
}
})
// bind best_in_place ajax callbacks
bipName.bind('ajax:success', function() {
var name = Util.decodeEntities($(this).html())
topic.set('name', name)
topic.trigger('saved')
})
// this is for all subsequent renders after in-place editing the desc field
const bipDesc = $(showCard).find('.best_in_place_desc')
bipDesc.bind('ajax:success', function() {
var desc = $(this).html() === $(this).data('bip-nil')
? ''
: $(this).text()
topic.set('desc', desc)
$(this).data('bip-value', desc)
this.innerHTML = Util.mdToHTML(desc)
topic.trigger('saved')
})
bipDesc.keypress(function(e) {
// allow typing Enter with Shift+Enter
const ENTER = 13
if (e.shiftKey === false && e.which === ENTER) {
$(this).data('bestInPlaceEditor').update()
}
})
}
var permissionLiClick = function(event) {
selectingPermission = false
var permission = $(this).attr('class')
topic.save({
permission: permission,
defer_to_map_id: null
})
$('.showcard .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2))
$('.showcard .permissionSelect').remove()
event.stopPropagation()
}
var openPermissionSelect = function(event) {
if (!selectingPermission) {
selectingPermission = true
$(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow
if ($(this).hasClass('co')) {
$(this).append('<ul class="permissionSelect"><li class="public"></li><li class="private"></li></ul>')
} else if ($(this).hasClass('pu')) {
$(this).append('<ul class="permissionSelect"><li class="commons"></li><li class="private"></li></ul>')
} else if ($(this).hasClass('pr')) {
$(this).append('<ul class="permissionSelect"><li class="commons"></li><li class="public"></li></ul>')
}
$('.showcard .permissionSelect li').click(permissionLiClick)
event.stopPropagation()
}
}
var hidePermissionSelect = function() {
selectingPermission = false
$('.showcard .yourTopic .mapPerm').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow
$('.showcard .permissionSelect').remove()
}
// ability to change permission
var selectingPermission = false
if (topic.authorizePermissionChange(Active.Mapper)) {
$('.showcard .yourTopic .mapPerm').click(openPermissionSelect)
$('.showcard').click(hidePermissionSelect)
}
$('.links .mapCount').unbind().click(function(event) {
$('.mapCount .tip').toggle()
$('.showcard .hoverTip').toggleClass('hide')
event.stopPropagation()
})
$('.mapCount .tip').unbind().click(function(event) {
event.stopPropagation()
})
$('.showcard').unbind('.hideTip').bind('click.hideTip', function() {
$('.mapCount .tip').hide()
$('.showcard .hoverTip').removeClass('hide')
})
$('.mapCount .tip li a').click(Router.intercept)
var originalText = $('.showMore').html()
$('.mapCount .tip .showMore').unbind().toggle(
function(event) {
$('.extraText').toggleClass('hideExtra')
$('.showMore').html('Show less...')
},
function(event) {
$('.extraText').toggleClass('hideExtra')
$('.showMore').html(originalText)
})
$('.mapCount .tip showMore').unbind().click(function(event) {
event.stopPropagation()
})
},
handleInvalidLink: function() {
var self = TopicCard
self.removeLink()
GlobalUI.notifyUser('Invalid link')
},
populateShowCard: function(topic) {
var self = TopicCard
var showCard = document.getElementById('showcard')
$(showCard).find('.permission').remove()
var topicForTemplate = self.buildObject(topic)
var html = self.generateShowcardHTML.render(topicForTemplate)
if (topic.authorizeToEdit(Active.Mapper)) {
let perm = document.createElement('div')
var string = 'permission canEdit'
if (topic.authorizePermissionChange(Active.Mapper)) string += ' yourTopic'
perm.className = string
perm.innerHTML = html
showCard.appendChild(perm)
} else {
let perm = document.createElement('div')
perm.className = 'permission cannotEdit'
perm.innerHTML = html
showCard.appendChild(perm)
}
TopicCard.bindShowCardListeners(topic)
},
generateShowcardHTML: null, // will be initialized into a Hogan template within init function
// generateShowcardHTML
buildObject: function(topic) {
var nodeValues = {}
var authorized = topic.authorizeToEdit(Active.Mapper)
if (!authorized) {
} else {
}
nodeValues.attachmentsHidden = ''
if (topic.get('link') && topic.get('link') !== '') {
nodeValues.embeds = '<a href="' + topic.get('link') + '" id="embedlyLink" target="_blank" data-card-description="0">'
nodeValues.embeds += topic.get('link')
nodeValues.embeds += '</a><div id="embedlyLinkLoader"></div>'
nodeValues.attachmentsHidden = 'hidden'
nodeValues.hasAttachment = 'hasAttachment'
} else {
nodeValues.embeds = ''
nodeValues.hasAttachment = ''
}
if (authorized) {
nodeValues.attachments = '<div class="addLink"><div id="addLinkIcon"></div>'
nodeValues.attachments += '<div id="addLinkInput"><input placeholder="Enter or paste a link"></input>'
nodeValues.attachments += '<div id="addLinkReset"></div></div></div>'
} else {
nodeValues.attachmentsHidden = 'hidden'
nodeValues.attachments = ''
}
var inmapsAr = topic.get('inmaps') || []
var inmapsLinks = topic.get('inmapsLinks') || []
nodeValues.inmaps = ''
if (inmapsAr.length < 6) {
for (let i = 0; i < inmapsAr.length; i++) {
const url = '/maps/' + inmapsLinks[i]
nodeValues.inmaps += '<li><a href="' + url + '">' + inmapsAr[i] + '</a></li>'
}
} else {
for (let i = 0; i < 5; i++) {
const url = '/maps/' + inmapsLinks[i]
nodeValues.inmaps += '<li><a href="' + url + '">' + inmapsAr[i] + '</a></li>'
}
const extra = inmapsAr.length - 5
nodeValues.inmaps += '<li><span class="showMore">See ' + extra + ' more...</span></li>'
for (let i = 5; i < inmapsAr.length; i++) {
const url = '/maps/' + inmapsLinks[i]
nodeValues.inmaps += '<li class="hideExtra extraText"><a href="' + url + '">' + inmapsAr[i] + '</a></li>'
}
}
nodeValues.permission = topic.get('permission')
nodeValues.mk_permission = topic.get('permission').substring(0, 2)
nodeValues.map_count = topic.get('map_count').toString()
nodeValues.synapse_count = topic.get('synapse_count').toString()
nodeValues.id = topic.isNew() ? topic.cid : topic.id
nodeValues.metacode = topic.getMetacode().get('name')
nodeValues.metacode_class = 'mbg' + topic.get('metacode_id')
nodeValues.imgsrc = topic.getMetacode().get('icon')
nodeValues.name = topic.get('name')
nodeValues.userid = topic.get('user_id')
nodeValues.username = topic.get('user_name')
nodeValues.date = topic.getDate()
// the code for this is stored in /views/main/_metacodeOptions.html.erb
nodeValues.metacode_select = $('#metacodeOptions').html()
nodeValues.desc_nil = 'Click to add description...'
nodeValues.desc_markdown = (topic.get('desc') === '' && authorized)
? nodeValues.desc_nil
: topic.get('desc')
nodeValues.desc_html = Util.mdToHTML(nodeValues.desc_markdown)
return nodeValues
}
}
export default TopicCard

View file

@ -1,6 +1,6 @@
/* global $ */
import { Parser, HtmlRenderer } from 'commonmark'
import { Parser, HtmlRenderer, Node } from 'commonmark'
import { emojiIndex } from 'emoji-mart'
import { escapeRegExp } from 'lodash'
@ -135,9 +135,26 @@ const Util = {
},
mdToHTML: text => {
const safeText = text || ''
const parsed = new Parser().parse(safeText)
// remove images to avoid http content in https context
const walker = parsed.walker()
for (let event = walker.next(); event = walker.next(); event) {
const node = event.node
if (node.type === 'image') {
const imageAlt = node.firstChild.literal
const imageSrc = node.destination
const textNode = new Node('text', node.sourcepos)
textNode.literal = `![${imageAlt}](${imageSrc})`
node.insertBefore(textNode)
node.unlink() // remove the image, replacing it with markdown
walker.resumeAt(textNode, false)
}
}
// use safe: true to filter xss
return new HtmlRenderer({ safe: true })
.render(new Parser().parse(safeText))
return new HtmlRenderer({ safe: true }).render(parsed)
},
logCanvasAttributes: function(canvas) {
const fakeMgraph = { canvas }

View file

@ -0,0 +1,59 @@
/* global $ */
import React from 'react'
import ReactDOM from 'react-dom'
import Active from '../Active'
import Visualize from '../Visualize'
import ReactTopicCard from '../../components/TopicCard'
const TopicCard = {
openTopicCard: null, // stores the topic that's currently open
metacodeSets: [],
init: function(serverData) {
const self = TopicCard
self.metacodeSets = serverData.metacodeSets
},
populateShowCard: function(topic) {
const self = TopicCard
ReactDOM.render(
React.createElement(ReactTopicCard, {
topic: topic,
ActiveMapper: Active.Mapper,
updateTopic: obj => {
topic.save(obj, { success: topic => self.populateShowCard(topic) })
},
metacodeSets: self.metacodeSets,
redrawCanvas: () => {
Visualize.mGraph.plot()
}
}),
document.getElementById('showcard')
)
// initialize draggability
$('.showcard').draggable({
handle: '.metacodeImage',
stop: function() {
$(this).height('auto')
}
})
},
showCard: function(node, opts) {
var self = TopicCard
if (!opts) opts = {}
var topic = node.getData('topic')
self.openTopicCard = topic
// populate the card that's about to show with the right topics data
self.populateShowCard(topic)
return $('.showcard').fadeIn('fast', () => opts.complete && opts.complete())
},
hideCard: function() {
var self = TopicCard
$('.showcard').fadeOut('fast')
self.openTopicCard = null
}
}
export default TopicCard

View file

@ -4,18 +4,21 @@ import ExploreMaps from './ExploreMaps'
import ChatView from './ChatView'
import VideoView from './VideoView'
import Room from './Room'
import TopicCard from './TopicCard'
import { JUNTO_UPDATED } from '../Realtime/events'
const Views = {
init: (serverData) => {
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
TopicCard.init(serverData)
},
ExploreMaps,
ChatView,
VideoView,
Room
Room,
TopicCard
}
export { ExploreMaps, ChatView, VideoView, Room }
export { ExploreMaps, ChatView, VideoView, Room, TopicCard }
export default Views

View file

@ -9,7 +9,7 @@ import DataModel from './DataModel'
import JIT from './JIT'
import Loading from './Loading'
import Router from './Router'
import TopicCard from './TopicCard'
import TopicCard from './Views/TopicCard'
const Visualize = {
mGraph: null, // a reference to the graph object.

View file

@ -29,7 +29,6 @@ import Settings from './Settings'
import Synapse from './Synapse'
import SynapseCard from './SynapseCard'
import Topic from './Topic'
import TopicCard from './TopicCard'
import Util from './Util'
import Views from './Views'
import Visualize from './Visualize'
@ -71,7 +70,6 @@ Metamaps.Settings = Settings
Metamaps.Synapse = Synapse
Metamaps.SynapseCard = SynapseCard
Metamaps.Topic = Topic
Metamaps.TopicCard = TopicCard
Metamaps.Util = Util
Metamaps.Views = Views
Metamaps.Visualize = Visualize

View file

@ -0,0 +1,53 @@
/* global $ */
/*
* Metacode selector component
*
* This component takes in a callback (onMetacodeSelect; takes one metacode id)
* and a list of metacode sets and renders them. If you click a metacode, it
* passes that metacode's id to the callback.
*/
import React, { PropTypes, Component } from 'react'
class MetacodeSelect extends Component {
render = () => {
return (
<div id="metacodeOptions">
<ul>
{this.props.metacodeSets.map(set => (
<li key={set.name}>
<span>{set.name}</span>
<div className="expandMetacodeSet"></div>
<ul>
{set.metacodes.map(m => (
<li key={m.id}
onClick={() => this.props.onMetacodeSelect(m.id)}
>
<img width="24" height="24" src={m.icon_path} alt={m.name} />
<div className="mSelectName">{m.name}</div>
<div className="clearfloat"></div>
</li>
))}
</ul>
</li>
))}
</ul>
</div>
)
}
}
MetacodeSelect.propTypes = {
onMetacodeClick: PropTypes.func,
metacodeSets: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
metacodes: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number,
icon_path: PropTypes.string, // url
name: PropTypes.string
}))
}))
}
export default MetacodeSelect

View file

@ -0,0 +1,24 @@
import React, { PropTypes, Component } from 'react'
import EmbedlyLink from './EmbedlyLink'
class Attachments extends Component {
render = () => {
const { topic, authorizedToEdit, updateTopic } = this.props
const link = topic.get('link')
return (
<div className="attachments">
<EmbedlyLink link={link} authorizedToEdit={authorizedToEdit} updateTopic={updateTopic} />
</div>
)
}
}
Attachments.propTypes = {
topic: PropTypes.object, // Backbone object
authorizedToEdit: PropTypes.bool,
updateTopic: PropTypes.func
}
export default Attachments

View file

@ -0,0 +1,77 @@
import React, { PropTypes, Component } from 'react'
import { RIETextArea } from 'riek'
import Util from '../../Metamaps/Util'
class MdTextArea extends RIETextArea {
keyDown = (event) => {
// we'll handle Enter on our own, thanks
const ESC = 27
if (event.keyCode === ESC) {
this.cancelEditing()
}
}
renderNormalComponent = () => {
// defaultProps MUST use dangerouslySetInnerHTML
return <span tabIndex="0"
className={this.makeClassString()}
onFocus={this.startEditing}
onClick={this.startEditing}
{...this.props.defaultProps}
/>
}
}
class Desc extends Component {
render = () => {
const descHTML = (!this.props.desc && this.props.authorizedToEdit)
? '<p>Click to add description...</p>'
: Util.mdToHTML(this.props.desc)
if (this.props.authorizedToEdit) {
return (
<div className="scroll">
<div className="desc">
<MdTextArea value={this.props.desc}
propName="desc"
change={this.props.onChange}
className="riek_desc"
classEditing="riek-editing"
editProps={{
onKeyPress: e => {
const ENTER = 13
if (!e.shiftKey && e.which === ENTER) {
e.preventDefault()
this.props.onChange({ desc: e.target.value })
}
}
}}
defaultProps={{
dangerouslySetInnerHTML: { __html: descHTML }
}}
/>
<div className="clearfloat"></div>
</div>
</div>
)
} else {
return (
<div className="scroll">
<div className="desc">
<span className="riek_desc">
{this.props.desc}
</span>
</div>
</div>
)
}
}
}
Desc.propTypes = {
desc: PropTypes.string, // markdown
authorizedToEdit: PropTypes.bool,
onChange: PropTypes.func
}
export default Desc

View file

@ -0,0 +1,65 @@
/* global $, embedly */
import React, { PropTypes, Component } from 'react'
class EmbedlyCard extends Component {
constructor(props) {
super(props)
this.state = {
embedlyLinkStarted: false,
embedlyLinkLoaded: false,
embedlyLinkError: false
}
}
componentDidMount = () => {
embedly('on', 'card.rendered', this.embedlyCardRendered)
if (this.props.link) this.loadLink()
}
componentWillUnmount = () => {
embedly('off')
}
componentDidUpdate = () => {
const { embedlyLinkStarted } = this.state
!embedlyLinkStarted && this.props.link && this.loadLink()
}
embedlyCardRendered = (iframe, test) => {
this.setState({embedlyLinkLoaded: true, embedlyLinkError: false})
}
loadLink = () => {
this.setState({ embedlyLinkStarted: true })
var e = embedly('card', document.getElementById('embedlyLink'))
if (e && e.type === 'error') this.setState({embedlyLinkError: true})
}
render = () => {
const { link } = this.props
const { embedlyLinkLoaded, embedlyLinkStarted, embedlyLinkError } = this.state
const notReady = embedlyLinkStarted && !embedlyLinkLoaded && !embedlyLinkError
return (
<div>
<a style={{ display: notReady ? 'none' : 'block' }}
href={link}
id="embedlyLink"
target="_blank"
data-card-description="0"
>
{link}
</a>
{notReady && <div id="embedlyLinkLoader">loading...</div>}
</div>
)
}
}
EmbedlyCard.propTypes = {
link: PropTypes.string
}
export default EmbedlyCard

View file

@ -0,0 +1,76 @@
/* global embedly */
import React, { PropTypes, Component } from 'react'
import Card from './Card'
class EmbedlyLink extends Component {
constructor(props) {
super(props)
this.state = {
linkEdit: ''
}
}
removeLink = () => {
this.props.updateTopic({ link: null })
}
resetLink = () => {
this.setState({ linkEdit: '' })
}
onLinkChangeHandler = e => {
this.setState({ linkEdit: e.target.value })
}
onLinkKeyUpHandler = e => {
const ENTER_KEY = 13
if (e.which === ENTER_KEY) {
const { linkEdit } = this.state
this.setState({ linkEdit: '' })
this.props.updateTopic({ link: linkEdit })
}
}
render = () => {
const { link, authorizedToEdit } = this.props
const { linkEdit } = this.state
const hasAttachment = !!link
if (!hasAttachment && !authorizedToEdit) return null
return (
<div className={hasAttachment ? 'embeds' : 'link-adder'}>
<div className="addLink"
style={{ display: hasAttachment ? 'none' : 'block' }}
>
<div id="addLinkIcon"></div>
<div id="addLinkInput">
<input ref={input => (this.linkInput = input)}
placeholder="Enter or paste a link"
value={linkEdit}
onChange={this.onLinkChangeHandler}
onKeyUp={this.onLinkKeyUpHandler}></input>
{linkEdit && <div id="addLinkReset" onClick={this.resetLink}></div>}
</div>
</div>
{link && <Card link={link} />}
{authorizedToEdit && (
<div id="linkremove"
style={{ display: hasAttachment ? 'block' : 'none' }}
onClick={this.removeLink}
/>
)}
</div>
)
}
}
EmbedlyLink.propTypes = {
link: PropTypes.string,
authorizedToEdit: PropTypes.bool,
updateTopic: PropTypes.func
}
export default EmbedlyLink

View file

@ -0,0 +1,161 @@
/* global $ */
import React, { PropTypes, Component } from 'react'
import MetacodeSelect from '../MetacodeSelect'
import Permission from './Permission'
class Links extends Component {
constructor(props) {
super(props)
this.state = {
showMetacodeTitle: false,
showMetacodeSelect: false,
showInMaps: false,
showMoreMaps: false,
hoveringMapCount: false,
hoveringSynapseCount: false
}
}
handleMetacodeSelect = metacodeId => {
this.setState({ showMetacodeSelect: false })
this.props.updateTopic({
metacode_id: metacodeId
})
this.props.redrawCanvas()
}
toggleShowMoreMaps = e => {
e.stopPropagation()
e.preventDefault()
this.setState({ showMoreMaps: !this.state.showMoreMaps })
}
updateState = (key, value) => () => {
this.setState({ [key]: value })
}
inMaps = (topic) => {
const inmapsArray = topic.get('inmaps') || []
const inmapsLinks = topic.get('inmapsLinks') || []
let firstFiveLinks = []
let extraLinks = []
for (let i = 0; i < inmapsArray.length; i ++) {
if (i < 5) {
firstFiveLinks.push({ mapName: inmapsArray[i], mapId: inmapsLinks[i] })
} else {
extraLinks.push({ mapName: inmapsArray[i], mapId: inmapsLinks[i] })
}
}
let output = []
firstFiveLinks.forEach(obj => {
output.push(<li key={obj.mapId}><a href={`/maps/${obj.mapId}`}>{obj.mapName}</a></li>)
})
if (extraLinks.length > 0) {
if (this.state.showMoreMaps) {
extraLinks.forEach(obj => {
output.push(<li key={obj.mapId} className="hideExtra extraText"><a href={`/maps/${obj.mapId}`}>{obj.mapName}</a></li>)
})
}
const text = this.state.showMoreMaps ? 'See less...' : `See ${extraLinks.length} more...`
output.push(<li key="showMore"><span className="showMore" onClick={this.toggleShowMoreMaps}>{text}</span></li>)
}
return output
}
handleMetacodeBarClick = () => {
if (this.state.showMetacodeTitle) {
this.setState({ showMetacodeSelect: !this.state.showMetacodeSelect })
}
}
render = () => {
const { topic, ActiveMapper } = this.props
const authorizedToEdit = topic.authorizeToEdit(ActiveMapper)
const authorizedPermissionChange = topic.authorizePermissionChange(ActiveMapper)
const metacode = topic.getMetacode()
return (
<div className="links">
<div className="linkItem icon metacodeItem"
style={{ zIndex: this.state.showMetacodeTitle ? 4 : 1 }}
onMouseLeave={() => this.setState({ showMetacodeTitle: false, showMetacodeSelect: false })}
onClick={this.handleMetacodeBarClick}
>
<div className={`metacodeTitle mbg${metacode.get('id')}`}
style={{ display: this.state.showMetacodeTitle ? 'block' : 'none' }}
>
{metacode.get('name')}
<div className="expandMetacodeSelect"/>
</div>
<div className="metacodeImage"
style={{backgroundImage: `url(${metacode.get('icon')})`}}
title="click and drag to move card"
onMouseEnter={() => this.setState({ showMetacodeTitle: true })}
/>
<div className="metacodeSelect"
style={{ display: this.state.showMetacodeSelect ? 'block' : 'none' }}
>
<MetacodeSelect onMetacodeSelect={this.handleMetacodeSelect} metacodeSets={this.props.metacodeSets} />
</div>
</div>
<div className="linkItem contributor">
<a href={`/explore/mapper/${topic.get('user_id')}`} target="_blank"><img src={topic.get('user_image')} className="contributorIcon" width="32" height="32" /></a>
<div className="contributorName">{topic.get('user_name')}</div>
</div>
<div className="linkItem mapCount"
onMouseOver={this.updateState('hoveringMapCount', true)}
onMouseOut={this.updateState('hoveringMapCount', false)}
onClick={this.updateState('showInMaps', !this.state.showInMaps)}
>
<div className="mapCountIcon"></div>
{topic.get('map_count').toString()}
{!this.state.showInMaps && this.state.hoveringMapCount && (
<div className="hoverTip">Click to see which maps topic appears on</div>
)}
{this.state.showInMaps && <div className="tip"><ul>{this.inMaps(topic)}</ul></div>}
</div>
<a href={`/topics/${topic.id}`}
target="_blank"
className="linkItem synapseCount"
onMouseOver={this.updateState('hoveringSynapseCount', true)}
onMouseOut={this.updateState('hoveringSynapseCount', false)}
>
<div className="synapseCountIcon"></div>
{topic.get('synapse_count').toString()}
{this.state.hoveringSynapseCount && <div className="tip">Click to see this topics synapses</div>}
</a>
<Permission
permission={topic.get('permission')}
authorizedToEdit={authorizedPermissionChange}
updateTopic={this.props.updateTopic}
/>
<div className="clearfloat"></div>
</div>
)
}
}
Links.propTypes = {
topic: PropTypes.object, // backbone object
ActiveMapper: PropTypes.object,
updateTopic: PropTypes.func,
metacodeSets: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
metacodes: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number,
icon_path: PropTypes.string, // url
name: PropTypes.string
}))
})),
redrawCanvas: PropTypes.func
}
export default Links

View file

@ -0,0 +1,69 @@
import React, { PropTypes, Component } from 'react'
import onClickOutsideAddon from 'react-onclickoutside'
class Permission extends Component {
constructor(props) {
super(props)
this.state = {
selectingPermission: false
}
}
togglePermissionSelect = () => {
this.setState({selectingPermission: !this.state.selectingPermission})
}
openPermissionSelect = () => {
this.setState({selectingPermission: true})
}
closePermissionSelect = () => {
this.setState({selectingPermission: false})
}
handleClickOutside = instance => {
this.closePermissionSelect()
}
liClick = value => event => {
this.closePermissionSelect()
this.props.updateTopic({
permission: value,
defer_to_map_id: null
})
// prevents it from also firing the event listener on the parent
event.preventDefault()
}
render = () => {
const { permission, authorizedToEdit } = this.props
const { selectingPermission } = this.state
let classes = `linkItem mapPerm ${permission.substring(0, 2)}`
if (selectingPermission) classes += ' minimize'
return (
<div className={classes}
title={permission}
onClick={authorizedToEdit ? this.togglePermissionSelect : null}
>
<ul className="permissionSelect"
style={{ display: selectingPermission ? 'block' : 'none' }}
>
{permission !== 'commons' && <li className='commons' onClick={this.liClick('commons')}></li>}
{permission !== 'public' && <li className='public' onClick={this.liClick('public')}></li>}
{permission !== 'private' && <li className='private' onClick={this.liClick('private')}></li>}
</ul>
</div>
)
}
}
Permission.propTypes = {
permission: PropTypes.string, // 'co', 'pu', or 'pr'
authorizedToEdit: PropTypes.bool,
updateTopic: PropTypes.func
}
export default onClickOutsideAddon(Permission)

View file

@ -0,0 +1,62 @@
import React, { Component, PropTypes } from 'react'
import { RIETextArea } from 'riek'
const maxTitleLength = 140
class Title extends Component {
nameCounterText() {
// for some reason, there's an error if this isn't inside a function
return `${this.props.name.length}/${maxTitleLength.toString()}`
}
render() {
if (this.props.authorizedToEdit) {
return (
<span className="title">
<RIETextArea value={this.props.name}
ref={textarea => { this.textarea = textarea }}
propName="name"
change={this.props.onChange}
className="titleWrapper"
id="titleActivator"
classEditing="riek-editing"
editProps={{
maxLength: maxTitleLength,
onKeyPress: e => {
const ENTER = 13
if (e.which === ENTER) {
e.preventDefault()
this.props.onChange({ name: e.target.value })
}
},
onChange: e => {
if (!this.nameCounter) return
this.nameCounter.innerHTML = `${e.target.value.length}/140`
}
}}
/>
<span className="nameCounter" ref={span => { this.nameCounter = span }}>
{this.nameCounterText()}
</span>
</span>
)
} else {
return (
<span className="title">
<span className="titleWrapper">
{this.props.name}
</span>
</span>
)
}
}
}
Title.propTypes = {
name: PropTypes.string,
onChange: PropTypes.func,
authorizedToEdit: PropTypes.bool
}
export default Title

View file

@ -0,0 +1,65 @@
import React, { PropTypes, Component } from 'react'
import Title from './Title'
import Links from './Links'
import Desc from './Desc'
import Attachments from './Attachments'
class ReactTopicCard extends Component {
render = () => {
const { topic, ActiveMapper } = this.props
const authorizedToEdit = topic.authorizeToEdit(ActiveMapper)
const hasAttachment = topic.get('link') && topic.get('link') !== ''
let classname = 'permission'
if (authorizedToEdit) {
classname += ' canEdit'
} else {
classname += ' cannotEdit'
}
if (topic.authorizePermissionChange(ActiveMapper)) classname += ' yourTopic'
return (
<div className={classname}>
<div className={`CardOnGraph ${hasAttachment ? 'hasAttachment' : ''}`} id={`topic_${topic.id}`}>
<Title name={topic.get('name')}
authorizedToEdit={authorizedToEdit}
onChange={this.props.updateTopic}
/>
<Links topic={topic}
ActiveMapper={this.props.ActiveMapper}
updateTopic={this.props.updateTopic}
metacodeSets={this.props.metacodeSets}
redrawCanvas={this.props.redrawCanvas}
/>
<Desc desc={topic.get('desc')}
authorizedToEdit={authorizedToEdit}
onChange={this.props.updateTopic}
/>
<Attachments topic={topic}
authorizedToEdit={authorizedToEdit}
updateTopic={this.props.updateTopic}
/>
<div className="clearfloat"></div>
</div>
</div>
)
}
}
ReactTopicCard.propTypes = {
topic: PropTypes.object,
ActiveMapper: PropTypes.object,
updateTopic: PropTypes.func,
metacodeSets: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
metacodes: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number,
icon_path: PropTypes.string, // url
name: PropTypes.string
}))
})),
redrawCanvas: PropTypes.func
}
export default ReactTopicCard

View file

@ -113,9 +113,15 @@ describe('Metamaps.Util.js', function() {
expect(Util.mdToHTML(md).trim()).to.equal(html)
})
it('links and images', function() {
const md = '[Link](https://metamaps.cc) ![Image](https://example.org/image.png)'
const html = '<p><a href="https://metamaps.cc">Link</a> <img src="https://example.org/image.png" alt="Image" /></p>'
it('links', function() {
const md = '[Link](https://metamaps.cc)'
const html = '<p><a href="https://metamaps.cc">Link</a></p>'
expect(Util.mdToHTML(md).trim()).to.equal(html)
})
it('images are not rendered', function() {
const md = '![Image](https://example.org/image.png)'
const html = '<p>![Image](https://example.org/image.png)</p>'
expect(Util.mdToHTML(md).trim()).to.equal(html)
})
})

View file

@ -45,7 +45,9 @@
"react": "15.4.2",
"react-dom": "15.4.2",
"react-dropzone": "3.9.1",
"react-onclickoutside": "^5.9.0",
"redux": "3.6.0",
"riek": "^1.0.7",
"simplewebrtc": "2.2.2",
"socket.io": "1.3.7",
"webpack": "1.14.0"