diff --git a/.agignore b/.agignore
new file mode 100644
index 00000000..a6d6b86c
--- /dev/null
+++ b/.agignore
@@ -0,0 +1 @@
+app/assets/javascripts/metamaps.secret.bundle.js
diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb
index 3b2b0fd6..b16983df 100644
--- a/app/assets/stylesheets/application.scss.erb
+++ b/app/assets/stylesheets/application.scss.erb
@@ -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;
diff --git a/app/assets/stylesheets/base.scss.erb b/app/assets/stylesheets/base.scss.erb
index 283e1db0..355f5652 100644
--- a/app/assets/stylesheets/base.scss.erb
+++ b/app/assets/stylesheets/base.scss.erb
@@ -6,6 +6,11 @@
font-family: helvetica;
color: #727272;
line-height: 11px;
+ display: none;
+}
+
+.riek-editing + .nameCounter {
+ display: block;
}
.nameCounter.forMap {
@@ -14,14 +19,14 @@
}
.nameCounter.forTopic {
-
+
}
#center-container {
position:relative;
height:100%;
width:100%;
-
+
/* background-color:#031924; */
color:#444;
}
@@ -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;
+ 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,155 +196,218 @@
}
.CardOnGraph .links {
- position:relative;
+ position: relative;
border-bottom: 1px solid #BDBDBD;
border-top: 1px solid #BDBDBD;
background-color: #e0e0e0;
-}
-.linkItem {
- float:left;
- height:46px;
- z-index: 1;
- position: relative;
- color: #424242;
- font-size: 14px;
- line-height:14px;
- height:12px;
- padding:17px 0;
-}
-.linkItem a {
- color: #424242;
-}
+ .linkItem {
+ float: left;
+ z-index: 1;
+ position: relative;
+ color: #424242;
+ font-size: 14px;
+ line-height: 14px;
-.CardOnGraph .icon {
- position:absolute;
- width:100%;
- z-index:1;
- padding: 0;
- height: 48px;
-}
-.linkItem.contributor {
- margin-left:40px;
- z-index:1;
- padding:17px 16px 17px 30px;
- position: relative;
-}
-.contributor .contributorIcon {
- position: absolute;
- top: 8px;
- left: 0;
- border-radius: 16px;
-}
+ a {
+ color: #424242;
+ }
+ }
-.contributor:hover .contributorName {
- display: block;
-}
+ .icon {
+ position: absolute;
+ z-index: 1;
+ padding: 0;
+ height: 48px;
+ width: 100%;
-.contributorName {
- display: none;
- position: absolute;
- background: black;
- text-align: center;
- color: white;
- border-radius: 2px;
- font-family: din-regular;
- line-height: 15px;
- font-size: 12px;
- padding: 3px 5px 2px;
- white-space: nowrap;
- margin-top: 36px;
- margin-left: -32px;
-}
+ .metacodeImage {
+ cursor: move;
+ position: relative;
+ 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;
-.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;
-}
+ .contributorIcon {
+ position: relative;
+ vertical-align: middle;
+ border-radius: 16px;
+ margin: 5px;
+ top: 8px;
+ left: 0;
+ border-radius: 16px;
+ }
-.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;
-}
+ span {
+ font-family: 'din-regular', sans-serif;
+ font-size: 14px;
+ }
-.linkItem.mapCount:hover .hoverTip {
- display: block;
-}
-.CardOnGraph .mapCount .tip, .CardonGraph .mapCount .hoverTip {
- top: 44px;
- left: 0px;
- font-size: 12px !important;
-}
+ .contributorName {
+ display: none;
+ position: absolute;
+ background: black;
+ text-align: center;
+ color: white;
+ border-radius: 2px;
+ font-family: din-regular;
+ line-height: 15px;
+ font-size: 12px;
+ padding: 3px 5px 2px;
+ white-space: nowrap;
+ margin-top: 8px;
-.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;
-}
+ &: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;
+ }
+ }
+ &:hover .contributorName {
+ display: block;
+ }
+ }
-.CardOnGraph .mapCount .tip:before, .CardOnGraph .mapCount .hoverTip: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;
-}
+ .mapCount {
+ padding:17px 0 17px 36px;
+ margin-left: 12px;
-.CardOnGraph .mapCount .tip li {
- list-style-type: none;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- padding: 6px 10px;
- display: block;
- height: 14px;
- font-family: 'din-regular', helvetica, sans-serif;
- font-size: 14px;
- line-height: 14px;
- position: relative;
-}
+ .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;
+ }
-.CardOnGraph .mapCount li.hideExtra {
- display: none;
+ &: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;
+ text-overflow: ellipsis;
+ padding: 6px 10px;
+ display: block;
+ height: 14px;
+ font-family: 'din-regular', helvetica, sans-serif;
+ font-size: 14px;
+ line-height: 14px;
+ position: relative;
+ }
+ }
+
+ .synapseCount {
+ margin-left: 26px;
+ width: 24px;
+ padding:17px 0 17px 32px;
+
+ .synapseCountIcon {
+ position: absolute;
+ top: 8px;
+ left: 0;
+ width: 32px;
+ height: 32px;
+ background-image: url(<%= asset_data_uri('synapse32_sprite.png') %>);
+ background-repeat: no-repeat;
+ background-position: 0 0;
+ }
+ hover .synapseCountIcon {
+ background-position: 0 -32px;
+ }
+ .tip {
+ position: absolute;
+ background: black;
+ width: auto;
+ top: 44px;
+ color: white;
+ white-space: nowrap;
+ border-radius: 2px;
+ font-size: 12px !important;
+ font-family: 'din-regular';
+ line-height: 12px;
+ padding: 4px 4px 4px;
+ z-index: 100;
+ }
+
+ .tip:before {
+ content: '';
+ position: absolute;
+ margin-top: -8px;
+ margin-left: 6px;
+ width: 0;
+ height: 0;
+ border-bottom: 4px solid black;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ }
+ }
}
.showMore {
@@ -341,66 +415,6 @@
color: #4FC059;
}
-.mapCount .tip a {
- color: white;
-}
-
-.mapCount .tip a:hover {
- color: #757575;
-}
-
-
-.linkItem.synapseCount {
- margin-left: 2px;
- width: 24px;
- padding:17px 0 17px 32px;
-}
-.linkItem.synapseCount .synapseCountIcon {
- position: absolute;
- top: 8px;
- left: 0;
- width: 32px;
- height: 32px;
- background-image: url(<%= asset_data_uri('synapse32_sprite.png') %>);
- background-repeat: no-repeat;
- background-position: 0 0;
-}
-.linkItem.synapseCount:hover .synapseCountIcon {
- background-position: 0 -32px;
-}
-
-.CardOnGraph .synapseCount .tip {
- position: absolute;
- background: black;
- width: auto;
- top: 44px;
- color: white;
- white-space: nowrap;
- border-radius: 2px;
- font-size: 12px !important;
- font-family: 'din-regular';
- line-height: 12px;
- padding: 4px 4px 4px;
- z-index: 100;
-}
-
-.CardOnGraph .synapseCount:hover .tip {
- display: block;
-}
-
-.CardOnGraph .synapseCount .tip:before {
- content: '';
- position: absolute;
- margin-top: -8px;
- margin-left: 6px;
- width: 0;
- height: 0;
- border-bottom: 4px solid black;
- border-left: 5px solid transparent;
- border-right: 5px solid transparent;
-}
-
-
.mapPerm {
width: 32px;
height: 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%;
@@ -493,35 +507,25 @@ cursor: pointer;
background-position: 0 -32px;
}
.permission.canEdit .minimize .expandMetacodeSelect {
-
+
}
-.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;
+ position: relative;
line-height: 14px;
font-size: 14px;
font-family: helvetica, sans-serif;
@@ -610,7 +614,6 @@ background-color: #E0E0E0;
display:block;
}
.CardOnGraph .tip {
- display:none;
position: absolute;
background: black;
top: 35px;
@@ -623,26 +626,24 @@ 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;
display: block;
- margin-left: 40px;
+ margin-left: 40px;
padding-top:9px;
font-size: 16px;
line-height: 16px;
@@ -652,7 +653,7 @@ background-color: #E0E0E0;
display: inline-block;
width: 102px;
height: 12px;
- text-align: left;
+ text-align: left;
padding: 18px 0 18px 48px;
font-size: 12px;
color: #9e9e9e;
@@ -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;
}
@@ -950,11 +950,11 @@ font-family: 'din-regular', helvetica, sans-serif;
}
#edit_synapse_right {
background-image: url(<%= asset_data_uri('synapsedirectionright_sprite.png') %>);
- right: 16px;
+ right: 16px;
}
#edit_synapse_left {
background-image: url(<%= asset_data_uri('synapsedirectionleft_sprite.png') %>);
- right: 56px;
+ right: 56px;
}
#edit_synapse_left.checked, #edit_synapse_right.checked {
background-position: 0 -48px;
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7d0f4f68..2bb1c925 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/helpers/metacode_sets_helper.rb b/app/helpers/metacode_sets_helper.rb
deleted file mode 100644
index 9a6e09c7..00000000
--- a/app/helpers/metacode_sets_helper.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-module MetacodeSetsHelper
-end
diff --git a/app/helpers/metacodes_helper.rb b/app/helpers/metacodes_helper.rb
index d00f1ef5..bcdd8dd2 100644
--- a/app/helpers/metacodes_helper.rb
+++ b/app/helpers/metacodes_helper.rb
@@ -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
diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb
index 4fbbd12d..dd0b8c7c 100644
--- a/app/views/layouts/_templates.html.erb
+++ b/app/views/layouts/_templates.html.erb
@@ -7,7 +7,7 @@
-
+
-
-
diff --git a/app/views/shared/_metacodeoptions.html.erb b/app/views/shared/_metacodeoptions.html.erb
index 54fb9e48..3cf9604e 100644
--- a/app/views/shared/_metacodeoptions.html.erb
+++ b/app/views/shared/_metacodeoptions.html.erb
@@ -3,61 +3,7 @@
# this code generates the list of icons that will drop down in the metacode select list on the topic card
#%>
-
+
diff --git a/frontend/src/Metamaps/DataModel/Topic.js b/frontend/src/Metamaps/DataModel/Topic.js
index 8eb09fdf..e8025a7d 100644
--- a/frontend/src/Metamaps/DataModel/Topic.js
+++ b/frontend/src/Metamaps/DataModel/Topic.js
@@ -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'
diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js
index d91a2a93..bd952c21 100644
--- a/frontend/src/Metamaps/JIT.js
+++ b/frontend/src/Metamaps/JIT.js
@@ -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 = {
`
- const metacodeOptions = $('#metacodeOptions').html()
-
- menustring += 'Change metacode' + metacodeOptions + ''
+ menustring += 'Change metacode'
}
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() {
diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js
index 3df5a3e2..0b36a68d 100644
--- a/frontend/src/Metamaps/Map/index.js
+++ b/frontend/src/Metamaps/Map/index.js
@@ -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'
diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js
index 7cdcf3a7..b16da5da 100644
--- a/frontend/src/Metamaps/Topic.js
+++ b/frontend/src/Metamaps/Topic.js
@@ -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'
diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js
deleted file mode 100644
index 3fa9a999..00000000
--- a/frontend/src/Metamaps/TopicCard.js
+++ /dev/null
@@ -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('')
- $('#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 = $('', {
- id: 'embedlyLink',
- 'data-card-description': '0',
- href: text
- }).html(text)
- $('.attachments').addClass('hidden')
- $('.embeds').append(embedlyEl)
- $('.embeds').append('')
-
- 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('')
- .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('')
-
- 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('')
- } else if ($(this).hasClass('pu')) {
- $(this).append('')
- } else if ($(this).hasClass('pr')) {
- $(this).append('')
- }
- $('.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 = ''
- nodeValues.embeds += topic.get('link')
- nodeValues.embeds += ''
- nodeValues.attachmentsHidden = 'hidden'
- nodeValues.hasAttachment = 'hasAttachment'
- } else {
- nodeValues.embeds = ''
- nodeValues.hasAttachment = ''
- }
-
- if (authorized) {
- nodeValues.attachments = ''
- nodeValues.attachments += '
'
- } 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 += '' + inmapsAr[i] + ''
- }
- } else {
- for (let i = 0; i < 5; i++) {
- const url = '/maps/' + inmapsLinks[i]
- nodeValues.inmaps += '' + inmapsAr[i] + ''
- }
- const extra = inmapsAr.length - 5
- nodeValues.inmaps += 'See ' + extra + ' more...'
- for (let i = 5; i < inmapsAr.length; i++) {
- const url = '/maps/' + inmapsLinks[i]
- nodeValues.inmaps += ''
- }
- }
- 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
diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js
index c0c3d7ce..ed9783c3 100644
--- a/frontend/src/Metamaps/Util.js
+++ b/frontend/src/Metamaps/Util.js
@@ -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 }
diff --git a/frontend/src/Metamaps/Views/TopicCard.js b/frontend/src/Metamaps/Views/TopicCard.js
new file mode 100644
index 00000000..51036685
--- /dev/null
+++ b/frontend/src/Metamaps/Views/TopicCard.js
@@ -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
diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js
index c496b3b0..85a710c3 100644
--- a/frontend/src/Metamaps/Views/index.js
+++ b/frontend/src/Metamaps/Views/index.js
@@ -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
diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js
index 2ccb08ed..43f6071b 100644
--- a/frontend/src/Metamaps/Visualize.js
+++ b/frontend/src/Metamaps/Visualize.js
@@ -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.
diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js
index 61f5e18a..eb9969b0 100644
--- a/frontend/src/Metamaps/index.js
+++ b/frontend/src/Metamaps/index.js
@@ -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
diff --git a/frontend/src/components/MetacodeSelect.js b/frontend/src/components/MetacodeSelect.js
new file mode 100644
index 00000000..68da09e8
--- /dev/null
+++ b/frontend/src/components/MetacodeSelect.js
@@ -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 (
+
+ )
+ }
+}
+
+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
diff --git a/frontend/src/components/TopicCard/Attachments.js b/frontend/src/components/TopicCard/Attachments.js
new file mode 100644
index 00000000..3e04dfbc
--- /dev/null
+++ b/frontend/src/components/TopicCard/Attachments.js
@@ -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 (
+
+
+
+ )
+ }
+}
+
+Attachments.propTypes = {
+ topic: PropTypes.object, // Backbone object
+ authorizedToEdit: PropTypes.bool,
+ updateTopic: PropTypes.func
+}
+
+export default Attachments
diff --git a/frontend/src/components/TopicCard/Desc.js b/frontend/src/components/TopicCard/Desc.js
new file mode 100644
index 00000000..c94f30fb
--- /dev/null
+++ b/frontend/src/components/TopicCard/Desc.js
@@ -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
+ }
+}
+
+class Desc extends Component {
+ render = () => {
+ const descHTML = (!this.props.desc && this.props.authorizedToEdit)
+ ? 'Click to add description...
'
+ : Util.mdToHTML(this.props.desc)
+
+ if (this.props.authorizedToEdit) {
+ return (
+
+
+
{
+ const ENTER = 13
+ if (!e.shiftKey && e.which === ENTER) {
+ e.preventDefault()
+ this.props.onChange({ desc: e.target.value })
+ }
+ }
+ }}
+ defaultProps={{
+ dangerouslySetInnerHTML: { __html: descHTML }
+ }}
+ />
+
+
+
+ )
+ } else {
+ return (
+
+
+
+ {this.props.desc}
+
+
+
+ )
+ }
+ }
+}
+
+Desc.propTypes = {
+ desc: PropTypes.string, // markdown
+ authorizedToEdit: PropTypes.bool,
+ onChange: PropTypes.func
+}
+
+export default Desc
diff --git a/frontend/src/components/TopicCard/EmbedlyLink/Card.js b/frontend/src/components/TopicCard/EmbedlyLink/Card.js
new file mode 100644
index 00000000..6d251310
--- /dev/null
+++ b/frontend/src/components/TopicCard/EmbedlyLink/Card.js
@@ -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 (
+
+ )
+ }
+}
+
+EmbedlyCard.propTypes = {
+ link: PropTypes.string
+}
+
+export default EmbedlyCard
diff --git a/frontend/src/components/TopicCard/EmbedlyLink/index.js b/frontend/src/components/TopicCard/EmbedlyLink/index.js
new file mode 100644
index 00000000..1775ab03
--- /dev/null
+++ b/frontend/src/components/TopicCard/EmbedlyLink/index.js
@@ -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 (
+
+
+ {link &&
}
+ {authorizedToEdit && (
+
+ )}
+
+ )
+ }
+}
+
+EmbedlyLink.propTypes = {
+ link: PropTypes.string,
+ authorizedToEdit: PropTypes.bool,
+ updateTopic: PropTypes.func
+}
+
+export default EmbedlyLink
diff --git a/frontend/src/components/TopicCard/Links.js b/frontend/src/components/TopicCard/Links.js
new file mode 100644
index 00000000..ad1758d3
--- /dev/null
+++ b/frontend/src/components/TopicCard/Links.js
@@ -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({obj.mapName})
+ })
+
+ if (extraLinks.length > 0) {
+ if (this.state.showMoreMaps) {
+ extraLinks.forEach(obj => {
+ output.push({obj.mapName})
+ })
+ }
+ const text = this.state.showMoreMaps ? 'See less...' : `See ${extraLinks.length} more...`
+ output.push({text})
+ }
+
+ 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 (
+
+
+ )
+ }
+}
+
+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
diff --git a/frontend/src/components/TopicCard/Permission.js b/frontend/src/components/TopicCard/Permission.js
new file mode 100644
index 00000000..bceb2d4c
--- /dev/null
+++ b/frontend/src/components/TopicCard/Permission.js
@@ -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 (
+
+
+ {permission !== 'commons' && }
+ {permission !== 'public' && }
+ {permission !== 'private' && }
+
+
+ )
+ }
+}
+
+Permission.propTypes = {
+ permission: PropTypes.string, // 'co', 'pu', or 'pr'
+ authorizedToEdit: PropTypes.bool,
+ updateTopic: PropTypes.func
+}
+
+export default onClickOutsideAddon(Permission)
diff --git a/frontend/src/components/TopicCard/Title.js b/frontend/src/components/TopicCard/Title.js
new file mode 100644
index 00000000..1eca527b
--- /dev/null
+++ b/frontend/src/components/TopicCard/Title.js
@@ -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 (
+
+ { 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`
+ }
+ }}
+ />
+ { this.nameCounter = span }}>
+ {this.nameCounterText()}
+
+
+ )
+ } else {
+ return (
+
+
+ {this.props.name}
+
+
+ )
+ }
+ }
+}
+
+
+Title.propTypes = {
+ name: PropTypes.string,
+ onChange: PropTypes.func,
+ authorizedToEdit: PropTypes.bool
+}
+
+export default Title
diff --git a/frontend/src/components/TopicCard/index.js b/frontend/src/components/TopicCard/index.js
new file mode 100644
index 00000000..3ebe700a
--- /dev/null
+++ b/frontend/src/components/TopicCard/index.js
@@ -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 (
+
+ )
+ }
+}
+
+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
diff --git a/frontend/test/Metamaps.Util.spec.js b/frontend/test/Metamaps.Util.spec.js
index e0366bd9..80108ee2 100644
--- a/frontend/test/Metamaps.Util.spec.js
+++ b/frontend/test/Metamaps.Util.spec.js
@@ -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 = '
Link
'
+ it('links', function() {
+ const md = '[Link](https://metamaps.cc)'
+ const html = '
Link
'
+ expect(Util.mdToHTML(md).trim()).to.equal(html)
+ })
+
+ it('images are not rendered', function() {
+ const md = '![Image](https://example.org/image.png)'
+ const html = '
![Image](https://example.org/image.png)
'
expect(Util.mdToHTML(md).trim()).to.equal(html)
})
})
diff --git a/package.json b/package.json
index 104bcc5e..6db62390 100644
--- a/package.json
+++ b/package.json
@@ -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"