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 = '' - } 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 += '
  • ' + inmapsAr[i] + '
  • ' - } - } - 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 ( +
    + + {link} + + {notReady &&
    loading...
    } +
    + ) + } +} + +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 ( +
    +
    +
    +
    + (this.linkInput = input)} + placeholder="Enter or paste a link" + value={linkEdit} + onChange={this.onLinkChangeHandler} + onKeyUp={this.onLinkKeyUpHandler}> + {linkEdit &&
    } +
    +
    + {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 ( +
    +
    this.setState({ showMetacodeTitle: false, showMetacodeSelect: false })} + onClick={this.handleMetacodeBarClick} + > +
    + {metacode.get('name')} +
    +
    +
    this.setState({ showMetacodeTitle: true })} + /> +
    + +
    +
    +
    + +
    {topic.get('user_name')}
    +
    +
    +
    + {topic.get('map_count').toString()} + {!this.state.showInMaps && this.state.hoveringMapCount && ( +
    Click to see which maps topic appears on
    + )} + {this.state.showInMaps &&
      {this.inMaps(topic)}
    } +
    + +
    + {topic.get('synapse_count').toString()} + {this.state.hoveringSynapseCount &&
    Click to see this topics synapses
    } +
    + +
    +
    + ) + } +} + +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 ( +
    +
    + + <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 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 = '<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) }) }) 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"